Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_armature_utils.hh"
#include "usd_utils.hh"
#include "ANIM_action.hh"
#include "ANIM_fcurve.hh"
#include "BKE_armature.hh"
#include "BKE_fcurve.hh"
#include "BKE_modifier.hh"
#include "BLI_listbase.h"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_action_types.h"
#include "DNA_armature_types.h"
namespace blender::io::usd {
/* Utility: create new fcurve and add it as a channel to a group. */
FCurve *create_fcurve(animrig::Channelbag &channelbag,
const animrig::FCurveDescriptor &fcurve_descriptor,
const int sample_count)
{
FCurve *fcurve = channelbag.fcurve_create_unique(nullptr, fcurve_descriptor);
BLI_assert_msg(fcurve, "The same F-Curve is being created twice, this is unexpected.");
if (fcurve) {
BKE_fcurve_bezt_resize(*fcurve, sample_count);
}
return fcurve;
}
/* Utility: fill in a single fcurve sample at the provided index. */
void set_fcurve_sample(FCurve *fcu, int64_t sample_index, const float frame, const float value)
{
BLI_assert(sample_index >= 0 && sample_index < fcu->totvert);
BezTriple &bez = fcu->bezt[sample_index];
bez.vec[1][0] = frame;
bez.vec[1][1] = value;
bez.ipo = BEZT_IPO_LIN;
bez.f1 = bez.f2 = bez.f3 = BEZT_FLAG_SELECT;
bez.h1 = bez.h2 = HD_AUTO;
}
/* Recursively invoke the 'visitor' function on the given bone and its children. */
static void visit_bones(const Bone *bone, FunctionRef<void(const Bone *)> visitor)
{
if (!(bone && visitor)) {
return;
}
visitor(bone);
for (const Bone &child : bone->childbase) {
visit_bones(&child, visitor);
}
}
const ModifierData *get_enabled_modifier(const Object &obj,
ModifierType type,
const Depsgraph *depsgraph)
{
BLI_assert(depsgraph);
const Scene *scene = DEG_get_input_scene(depsgraph);
eEvaluationMode mode = DEG_get_mode(depsgraph);
for (ModifierData &md : obj.modifiers) {
if (!BKE_modifier_is_enabled(scene, &md, mode)) {
continue;
}
if (md.type == type) {
return &md;
}
}
return nullptr;
}
/* Return the armature modifier on the given object. Return null if no armature modifier
* can be found. */
static const ArmatureModifierData *get_armature_modifier(const Object &obj,
const Depsgraph *depsgraph)
{
const ArmatureModifierData *mod = reinterpret_cast<const ArmatureModifierData *>(
get_enabled_modifier(obj, eModifierType_Armature, depsgraph));
return mod;
}
void visit_bones(const Object *ob_arm, FunctionRef<void(const Bone *)> visitor)
{
if (!(ob_arm && ob_arm->type == OB_ARMATURE && ob_arm->data)) {
return;
}
const bArmature *armature = id_cast<bArmature *>(ob_arm->data);
for (const Bone &bone : armature->bonebase) {
visit_bones(&bone, visitor);
}
}
void get_armature_bone_names(const Object *ob_arm,
const bool use_deform,
Vector<StringRef> &r_names)
{
Map<StringRef, const Bone *> deform_map;
if (use_deform) {
init_deform_bones_map(ob_arm, &deform_map);
}
auto visitor = [&](const Bone *bone) {
const StringRef bone_name(bone->name);
if (use_deform && !deform_map.contains(bone_name)) {
return;
}
r_names.append(bone_name);
};
visit_bones(ob_arm, visitor);
}
pxr::TfToken build_usd_joint_path(const Bone *bone, bool allow_unicode)
{
std::string path(make_safe_name(bone->name, allow_unicode));
const Bone *parent = bone->parent;
while (parent) {
path = make_safe_name(parent->name, allow_unicode) + '/' + path;
parent = parent->parent;
}
return pxr::TfToken(path);
}
void create_pose_joints(pxr::UsdSkelAnimation &skel_anim,
const Object &obj,
const Map<StringRef, const Bone *> *deform_map,
bool allow_unicode)
{
BLI_assert(obj.pose);
pxr::VtTokenArray joints;
const bPose *pose = obj.pose;
const bArmature &arm = *id_cast<bArmature *>(obj.data);
BKE_pose_ensure_bone_indices(obj);
for (const bPoseChannel &pchan : pose->chanbase) {
const Bone *pchan_bone = pchan.bone_get(arm);
if (pchan_bone) {
if (deform_map && !deform_map->contains(pchan.name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
continue;
}
joints.push_back(build_usd_joint_path(pchan_bone, allow_unicode));
}
}
skel_anim.GetJointsAttr().Set(joints);
}
const Object *get_armature_modifier_obj(const Object &obj, const Depsgraph *depsgraph)
{
const ArmatureModifierData *mod = get_armature_modifier(obj, depsgraph);
return mod ? mod->object : nullptr;
}
bool is_armature_modifier_bone_name(const Object &obj,
const StringRefNull name,
const Depsgraph *depsgraph)
{
const ArmatureModifierData *arm_mod = get_armature_modifier(obj, depsgraph);
if (!arm_mod || !arm_mod->object || !arm_mod->object->data) {
return false;
}
bArmature *arm = id_cast<bArmature *>(arm_mod->object->data);
return BKE_armature_find_bone_name(arm, name.c_str());
}
bool can_export_skinned_mesh(const Object &obj, const Depsgraph *depsgraph)
{
return get_enabled_modifier(obj, eModifierType_Armature, depsgraph) != nullptr;
}
void init_deform_bones_map(const Object *obj, Map<StringRef, const Bone *> *deform_map)
{
if (!deform_map) {
return;
}
deform_map->clear();
auto deform_visitor = [&](const Bone *bone) {
if (!bone) {
return;
}
const bool deform = !(bone->flag & BONE_NO_DEFORM);
if (deform) {
deform_map->add(bone->name, bone);
}
};
visit_bones(obj, deform_visitor);
/* Get deform parents */
for (const auto &item : deform_map->items()) {
BLI_assert(item.value);
for (const Bone *parent = item.value->parent; parent; parent = parent->parent) {
deform_map->add(parent->name, parent);
}
}
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,148 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_modifier_types.h"
#include <pxr/base/tf/token.h>
#include <pxr/usd/usdSkel/animation.h>
namespace blender {
struct Bone;
struct Depsgraph;
struct FCurve;
struct ModifierData;
struct Object;
namespace animrig {
class Channelbag;
struct FCurveDescriptor;
} // namespace animrig
namespace io::usd {
/* Custom Blender Primvar name used for storing armature bone lengths. */
inline const pxr::TfToken BlenderBoneLengths("blender:bone_lengths", pxr::TfToken::Immortal);
/* Utility: create new fcurve and add it as a channel to a group. */
FCurve *create_fcurve(animrig::Channelbag &channelbag,
const animrig::FCurveDescriptor &fcurve_descriptor,
const int sample_count);
/* Utility: fill in a single fcurve sample at the provided index. */
void set_fcurve_sample(FCurve *fcu, int64_t sample_index, const float frame, const float value);
/**
* Recursively invoke the given function on the given armature object's bones.
* This function is a no-op if the object isn't an armature.
*
* \param ob_arm: The armature object
* \param visitor: The function to invoke on each bone
*/
void visit_bones(const Object *ob_arm, FunctionRef<void(const Bone *)> visitor);
/**
* Return in 'r_names' the names of the given armature object's bones.
*
* \param ob_arm: The armature object
* \param use_deform: If true, use only deform bone names, including their parents, to match
* armature export joint indices
* \param r_names: The returned list of bone names
*/
void get_armature_bone_names(const Object *ob_arm, bool use_deform, Vector<StringRef> &r_names);
/**
* Return the USD joint path corresponding to the given bone. For example, for the bone
* "Hand", this function might return the full path "Shoulder/Elbow/Hand" of the joint
* in the hierarchy.
*
* \param bone: The bone whose path will be queried.
* \param allow_unicode: Whether to allow unicode bone names to be used
* \return The path to the joint.
*/
pxr::TfToken build_usd_joint_path(const Bone *bone, bool allow_unicode);
/**
* Sets the USD joint paths as an attribute on the given USD animation,
* where the paths correspond to the bones of the given armature.
*
* \param skel_anim: The animation whose joints attribute will be set
* \param obj: The armature object
* \param deform_map: A pointer to a map associating bone names with
* deform bones and their parents. If the pointer
* is not null, assume only deform bones are to be
* exported and bones not found in this map will be
* skipped
* \param allow_unicode: Whether to allow unicode bone names to be used
*/
void create_pose_joints(pxr::UsdSkelAnimation &skel_anim,
const Object &obj,
const Map<StringRef, const Bone *> *deform_map,
bool allow_unicode);
/**
* Return the modifier of the given type enabled for the given dependency graph's
* evaluation mode (viewport or render).
*
* \param obj: Object to query for the modifier
* \param depsgraph: The dependency graph where the object was evaluated
* \return The modifier.
*/
const ModifierData *get_enabled_modifier(const Object &obj,
ModifierType type,
const Depsgraph *depsgraph);
/**
* If the given object has an enabled armature modifier, return the
* armature object bound to the modifier.
*
* \param: Object to check for the modifier
* \param depsgraph: The dependency graph where the object was evaluated
* \return The armature object.
*/
const Object *get_armature_modifier_obj(const Object &obj, const Depsgraph *depsgraph);
/**
* If the given object has an armature modifier, query whether the given
* name matches the name of a bone on the armature referenced by the modifier.
*
* \param obj: Object to query for the modifier
* \param name: Name to check
* \param depsgraph: The dependency graph where the object was evaluated
* \return True if the name matches a bone name. Return false if no matching
* bone name is found or if the object does not have an armature modifier
*/
bool is_armature_modifier_bone_name(const Object &obj,
StringRefNull name,
const Depsgraph *depsgraph);
/**
* Query whether exporting a skinned mesh is supported for the given object.
* Currently, the object can be exported as a skinned mesh if it has an enabled
* armature modifier and no other enabled modifiers.
*
* \param obj: Object to query
* \param depsgraph: The dependency graph where the object was evaluated
* \return True if skinned mesh export is supported, false otherwise.
*/
bool can_export_skinned_mesh(const Object &obj, const Depsgraph *depsgraph);
/**
* Initialize the deform bones map:
* - First: grab all bones marked for deforming and store them.
* - Second: loop the deform bones you found and recursively walk up their parent
* hierarchies, marking those bones as deform as well.
* \param obj: Object to query
* \param deform_map: A pointer to the deform_map to fill with deform bones and
* their parents found on the object
*/
void init_deform_bones_map(const Object *obj, Map<StringRef, const Bone *> *deform_map);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,653 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_asset_utils.hh"
#include "usd.hh"
#include <pxr/usd/ar/asset.h>
#include <pxr/usd/ar/packageUtils.h>
#include <pxr/usd/ar/resolver.h>
#include <pxr/usd/ar/writableAsset.h>
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usd/stage.h>
#include "BKE_appdir.hh"
#include "BKE_idprop.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "BLI_fileops.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "WM_api.hh"
#include <string_view>
namespace blender::io::usd {
constexpr char UDIM_PATTERN[] = "<UDIM>";
constexpr char UDIM_PATTERN2[] = "%3CUDIM%3E";
/* Maximum range of UDIM tiles, per the
* UsdPreviewSurface specifications. See
* https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html#texture-reader
*/
constexpr int UDIM_START_TILE = 1001;
constexpr int UDIM_END_TILE = 1100;
/**
* The following is copied from `_SplitUdimPattern()` in
* USD library source file `materialParamsUtils.cpp`.
* Split a UDIM file path such as `/someDir/myFile.<UDIM>.exr` into a
* prefix `/someDir/myFile.` and suffix `.exr`.
*/
static std::pair<std::string, std::string> split_udim_pattern(const std::string &path)
{
std::string_view patterns[]{UDIM_PATTERN, UDIM_PATTERN2};
for (const std::string_view pattern : patterns) {
const std::string::size_type pos = path.find(pattern);
if (pos != std::string::npos) {
return {path.substr(0, pos), path.substr(pos + pattern.size())};
}
}
return {};
}
/* Return the asset file base name, with special handling of
* package relative paths. */
static std::string get_asset_base_name(const std::string &src_path, ReportList *reports)
{
char base_name[FILE_MAXFILE];
if (pxr::ArIsPackageRelativePath(src_path)) {
std::pair<std::string, std::string> split = pxr::ArSplitPackageRelativePathInner(src_path);
if (split.second.empty()) {
BKE_reportf(reports,
RPT_WARNING,
"%s: Couldn't determine package-relative file name from path %s",
__func__,
src_path.c_str());
return src_path;
}
BLI_path_split_file_part(split.second.c_str(), base_name, sizeof(base_name));
}
else {
BLI_path_split_file_part(src_path.c_str(), base_name, sizeof(base_name));
}
return base_name;
}
/* Copy an asset to a destination directory. */
static std::string copy_asset_to_directory(const std::string &src_path,
const char *dest_dir_path,
TexNameCollisionMode name_collision_mode,
ReportList *reports)
{
std::string base_name = get_asset_base_name(src_path, reports);
char dest_file_path[FILE_MAX];
BLI_path_join(dest_file_path, sizeof(dest_file_path), dest_dir_path, base_name.c_str());
BLI_path_normalize(dest_file_path);
if (name_collision_mode == TexNameCollisionMode::UseExisting && BLI_is_file(dest_file_path)) {
return dest_file_path;
}
if (!copy_asset(src_path, dest_file_path, name_collision_mode, reports)) {
BKE_reportf(reports,
RPT_WARNING,
"%s: Couldn't copy file %s to %s",
__func__,
src_path.c_str(),
dest_file_path);
return src_path;
}
return dest_file_path;
}
static std::string copy_udim_asset_to_directory(const std::string &src_path,
const char *dest_dir_path,
TexNameCollisionMode name_collision_mode,
ReportList *reports)
{
/* Get prefix and suffix from udim pattern. */
std::pair<std::string, std::string> splitPath = split_udim_pattern(src_path);
if (splitPath.first.empty() || splitPath.second.empty()) {
BKE_reportf(
reports, RPT_ERROR, "%s: Couldn't split UDIM pattern %s", __func__, src_path.c_str());
return src_path;
}
/* Copy the individual UDIM tiles. Since there is currently no way to query the contents
* of a directory using the USD resolver, we must take a brute force approach. We iterate
* over the allowed range of tile indices and copy any tiles that exist. The USDPreviewSurface
* specification stipulates "a maximum of ten tiles in the U direction" and that
* "the tiles must be within the range [1001, 1100] (as of specification version 2.5)". See
* https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html#texture-reader
*/
for (int i = UDIM_START_TILE; i <= UDIM_END_TILE; ++i) {
const std::string src_udim = splitPath.first + std::to_string(i) + splitPath.second;
if (asset_exists(src_udim)) {
copy_asset_to_directory(src_udim, dest_dir_path, name_collision_mode, reports);
}
}
const std::string src_file_name = get_asset_base_name(src_path, reports);
char ret_udim_path[FILE_MAX];
BLI_path_join(ret_udim_path, sizeof(ret_udim_path), dest_dir_path, src_file_name.c_str());
/* Blender only recognizes the <UDIM> pattern, not the
* alternative UDIM_PATTERN2, so we make sure the returned
* path has the former. */
splitPath = split_udim_pattern(ret_udim_path);
if (splitPath.first.empty() || splitPath.second.empty()) {
BKE_reportf(reports, RPT_ERROR, "%s: Couldn't split UDIM pattern %s", __func__, ret_udim_path);
return ret_udim_path;
}
return splitPath.first + UDIM_PATTERN + splitPath.second;
}
bool copy_asset(const std::string &src,
const std::string &dst,
TexNameCollisionMode name_collision_mode,
ReportList *reports)
{
const pxr::ArResolver &ar = pxr::ArGetResolver();
if (name_collision_mode != TexNameCollisionMode::Overwrite) {
if (!ar.Resolve(dst).IsEmpty()) {
/* The asset exists, so this is a no-op. */
BKE_reportf(
reports, RPT_INFO, "%s: Will not overwrite existing asset %s", __func__, dst.c_str());
return true;
}
}
pxr::ArResolvedPath src_path = ar.Resolve(src);
if (src_path.IsEmpty()) {
BKE_reportf(reports, RPT_ERROR, "%s: Cannot resolve path %s", __func__, src.c_str());
return false;
}
pxr::ArResolvedPath dst_path = ar.ResolveForNewAsset(dst);
if (dst_path.IsEmpty()) {
BKE_reportf(
reports, RPT_ERROR, "%s: Cannot resolve path %s for writing", __func__, dst.c_str());
return false;
}
if (src_path == dst_path) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Cannot copy %s. The source and destination paths are the same",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::string why_not;
if (!ar.CanWriteAssetToPath(dst_path, &why_not)) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Cannot write to asset %s: %s",
__func__,
dst_path.GetPathString().c_str(),
why_not.c_str());
return false;
}
std::shared_ptr<pxr::ArAsset> src_asset = ar.OpenAsset(src_path);
if (!src_asset) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Cannot open source asset %s",
__func__,
src_path.GetPathString().c_str());
return false;
}
const size_t size = src_asset->GetSize();
if (size == 0) {
BKE_reportf(reports,
RPT_WARNING,
"%s: Will not copy zero size source asset %s",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::shared_ptr<const char> buf = src_asset->GetBuffer();
if (!buf) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Null buffer for source asset %s",
__func__,
src_path.GetPathString().c_str());
return false;
}
std::shared_ptr<pxr::ArWritableAsset> dst_asset = ar.OpenAssetForWrite(
dst_path, pxr::ArResolver::WriteMode::Replace);
if (!dst_asset) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Cannot open destination asset %s for writing",
__func__,
src_path.GetPathString().c_str());
return false;
}
size_t bytes_written = dst_asset->Write(src_asset->GetBuffer().get(), src_asset->GetSize(), 0);
if (bytes_written == 0) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Error writing to destination asset %s",
__func__,
dst_path.GetPathString().c_str());
}
if (!dst_asset->Close()) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Couldn't close destination asset %s",
__func__,
dst_path.GetPathString().c_str());
return false;
}
return bytes_written > 0;
}
bool asset_exists(const std::string &path)
{
return !pxr::ArGetResolver().Resolve(path).IsEmpty();
}
std::string import_asset(const std::string &src,
const char *import_dir,
TexNameCollisionMode name_collision_mode,
ReportList *reports)
{
if (import_dir[0] == '\0') {
BKE_reportf(reports,
RPT_ERROR,
"%s: Texture import directory path empty, couldn't import %s",
__func__,
src.c_str());
return src;
}
char dest_dir_path[FILE_MAXDIR];
STRNCPY(dest_dir_path, import_dir);
if (BLI_path_is_rel(import_dir)) {
const char *basepath = BKE_main_blendfile_path_from_global();
if (basepath[0] == '\0') {
BKE_reportf(reports,
RPT_ERROR,
"%s: import directory is relative "
"but the blend file path is empty. "
"Please save the blend file before importing the USD "
"or provide an absolute import directory path. "
"Cannot import %s",
__func__,
src.c_str());
return src;
}
char path_temp[FILE_MAX];
STRNCPY(path_temp, dest_dir_path);
BLI_path_abs(path_temp, basepath);
STRNCPY(dest_dir_path, path_temp);
}
BLI_path_normalize(dest_dir_path);
if (!BLI_dir_create_recursive(dest_dir_path)) {
BKE_reportf(reports,
RPT_ERROR,
"%s: Couldn't create texture import directory %s",
__func__,
dest_dir_path);
return src;
}
if (is_udim_path(src)) {
return copy_udim_asset_to_directory(src, dest_dir_path, name_collision_mode, reports);
}
return copy_asset_to_directory(src, dest_dir_path, name_collision_mode, reports);
}
/**
* Returns true if the parent directory of the given path exists on the
* file system.
*
* \param path: input file path
* \return true if the parent directory exists
*/
static bool parent_dir_exists_on_file_system(const std::string &path)
{
char dir_path[FILE_MAX];
BLI_path_split_dir_part(path.c_str(), dir_path, FILE_MAX);
return BLI_is_dir(dir_path);
}
bool is_udim_path(const std::string &path)
{
return path.find(UDIM_PATTERN) != std::string::npos ||
path.find(UDIM_PATTERN2) != std::string::npos;
}
std::string get_export_textures_dir(const pxr::UsdStageRefPtr stage)
{
pxr::SdfLayerHandle layer = stage->GetRootLayer();
if (layer->IsAnonymous()) {
WM_global_reportf(RPT_WARNING,
"%s: Cannot generate a textures directory path for anonymous stage",
__func__);
return "";
}
const pxr::ArResolvedPath &stage_path = layer->GetResolvedPath();
if (stage_path.empty()) {
WM_global_reportf(RPT_WARNING, "%s: Cannot get resolved path for stage", __func__);
return "";
}
const pxr::ArResolver &ar = pxr::ArGetResolver();
/* Resolve the `./textures` relative path, with the stage path as an anchor. */
std::string textures_dir = ar.CreateIdentifierForNewAsset("./textures", stage_path);
/* If parent of the stage path exists as a file system directory, try to create the
* textures directory. */
if (parent_dir_exists_on_file_system(stage_path.GetPathString())) {
BLI_dir_create_recursive(textures_dir.c_str());
}
return textures_dir;
}
bool should_import_asset(const std::string &path)
{
if (path.empty()) {
return false;
}
if (BLI_path_is_rel(path.c_str())) {
return false;
}
if (pxr::ArIsPackageRelativePath(path)) {
return true;
}
if (is_udim_path(path) && parent_dir_exists_on_file_system(path)) {
return false;
}
return !BLI_is_file(path.c_str()) && asset_exists(path);
}
bool paths_equal(const std::string &path1, const std::string &path2)
{
BLI_assert_msg(!BLI_path_is_rel(path1.c_str()) && !BLI_path_is_rel(path2.c_str()),
"Paths arguments must be absolute");
const pxr::ArResolver &ar = pxr::ArGetResolver();
return ar.ResolveForNewAsset(path1) == ar.ResolveForNewAsset(path2);
}
const char *temp_textures_dir()
{
static bool inited = false;
static char temp_dir[FILE_MAXDIR] = {'\0'};
if (!inited) {
BLI_path_join(temp_dir, sizeof(temp_dir), BKE_tempdir_session(), "usd_textures_tmp", SEP_STR);
inited = true;
}
return temp_dir;
}
bool write_to_path(const void *data, size_t size, const std::string &path, ReportList *reports)
{
BLI_assert(data);
if (size == 0) {
return false;
}
const pxr::ArResolver &ar = pxr::ArGetResolver();
pxr::ArResolvedPath resolved_path = ar.ResolveForNewAsset(path);
if (resolved_path.IsEmpty()) {
BKE_reportf(reports, RPT_ERROR, "Cannot resolve path %s for writing", path.c_str());
return false;
}
std::string why_not;
if (!ar.CanWriteAssetToPath(resolved_path, &why_not)) {
BKE_reportf(reports,
RPT_ERROR,
"Cannot write to asset %s: %s",
resolved_path.GetPathString().c_str(),
why_not.c_str());
return false;
}
std::shared_ptr<pxr::ArWritableAsset> dst_asset = ar.OpenAssetForWrite(
resolved_path, pxr::ArResolver::WriteMode::Replace);
if (!dst_asset) {
BKE_reportf(reports,
RPT_ERROR,
"Cannot open destination asset %s for writing",
resolved_path.GetPathString().c_str());
return false;
}
size_t bytes_written = dst_asset->Write(data, size, 0);
if (bytes_written == 0) {
BKE_reportf(reports,
RPT_ERROR,
"Error writing to destination asset %s",
resolved_path.GetPathString().c_str());
}
if (!dst_asset->Close()) {
BKE_reportf(reports,
RPT_ERROR,
"Couldn't close destination asset %s",
resolved_path.GetPathString().c_str());
return false;
}
return bytes_written > 0;
}
void ensure_usd_source_path_prop(const std::string &path, ID *id)
{
if (!id || path.empty()) {
return;
}
if (pxr::ArIsPackageRelativePath(path)) {
/* Don't record package-relative paths (e.g., images in USDZ
* archives). */
return;
}
IDProperty *idgroup = IDP_EnsureProperties(id);
if (!idgroup) {
return;
}
const StringRef prop_name = "usd_source_path";
if (IDP_GetPropertyFromGroup(idgroup, prop_name)) {
return;
}
IDPropertyTemplate val = {0};
val.string.str = path.c_str();
/* Note length includes null terminator. */
val.string.len = path.size() + 1;
val.string.subtype = IDP_STRING_SUB_UTF8;
IDProperty *prop = IDP_New(IDP_STRING, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
std::string get_usd_source_path(ID *id)
{
if (!id) {
return "";
}
const IDProperty *idgroup = IDP_EnsureProperties(id);
if (!idgroup) {
return "";
}
const StringRef prop_name = "usd_source_path";
const IDProperty *prop = IDP_GetPropertyFromGroup(idgroup, prop_name);
if (!prop) {
return "";
}
return static_cast<const char *>(prop->data.pointer);
}
std::string get_relative_path(const std::string &path, const std::string &anchor)
{
if (path.empty() || anchor.empty()) {
return path;
}
if (path == anchor) {
return path;
}
if (BLI_path_is_rel(path.c_str())) {
return path;
}
if (pxr::ArIsPackageRelativePath(path)) {
return path;
}
if (BLI_is_file(path.c_str()) && BLI_is_file(anchor.c_str())) {
/* Treat the paths as standard files. */
char rel_path[FILE_MAX];
STRNCPY(rel_path, path.c_str());
BLI_path_rel(rel_path, anchor.c_str());
if (!BLI_path_is_rel(rel_path)) {
return path;
}
BLI_string_replace_char(rel_path, '\\', '/');
return rel_path + 2;
}
/* If we got here, the paths may be URIs or files on the file system. */
/* We don't have a library to compute relative paths for URIs
* so we use the standard file-system calls to do so. This
* may not work for all URIs in theory, but is probably sufficient
* for the subset of URIs we are likely to encounter in practice
* currently.
* TODO(makowalski): provide better utilities for this. */
const pxr::ArResolver &ar = pxr::ArGetResolver();
std::string resolved_path = ar.Resolve(path);
std::string resolved_anchor = ar.Resolve(anchor);
if (resolved_path.empty() || resolved_anchor.empty()) {
return path;
}
std::string prefix = pxr::TfStringGetCommonPrefix(path, anchor);
if (prefix.empty()) {
return path;
}
std::replace(prefix.begin(), prefix.end(), '\\', '/');
size_t last_slash_pos = prefix.find_last_of('/');
if (last_slash_pos == std::string::npos) {
/* Unexpected: The prefix doesn't contain a slash,
* so this was not an absolute path. */
return path;
}
/* Replace the common prefix up to the last slash with
* a fake root directory to allow computing the relative path
* excluding the URI. We omit the URI because it might not
* be handled correctly by the standard file-system path computations. */
resolved_path = "/root" + resolved_path.substr(last_slash_pos);
resolved_anchor = "/root" + resolved_anchor.substr(last_slash_pos);
char anchor_parent_dir[FILE_MAX];
BLI_path_split_dir_part(resolved_anchor.c_str(), anchor_parent_dir, FILE_MAX);
if (anchor_parent_dir[0] == '\0') {
return path;
}
char result_path[FILE_MAX];
STRNCPY(result_path, resolved_path.c_str());
BLI_path_rel(result_path, anchor_parent_dir);
if (BLI_path_is_rel(result_path)) {
/* Strip the Blender relative path marker, and set paths to Unix-style. */
BLI_string_replace_char(result_path, '\\', '/');
return std::string(result_path + 2);
}
return path;
}
void USD_path_abs(char *path, const char *basepath, bool for_import)
{
if (!BLI_path_is_rel(path)) {
pxr::ArResolvedPath resolved_path = for_import ? pxr::ArGetResolver().Resolve(path) :
pxr::ArGetResolver().ResolveForNewAsset(path);
const std::string &path_str = resolved_path.GetPathString();
if (!path_str.empty()) {
if (path_str.length() < FILE_MAX) {
BLI_strncpy(path, path_str.c_str(), FILE_MAX);
return;
}
WM_global_reportf(RPT_ERROR,
"In %s: resolved path %s exceeds path buffer length.",
__func__,
path_str.c_str());
}
}
/* If we got here, the path couldn't be resolved by the ArResolver, so we
* fall back on the standard Blender absolute path resolution. */
BLI_path_abs(path, basepath);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,160 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include <string>
#include <pxr/usd/usd/common.h>
namespace blender::io::usd {
/**
* Invoke the USD asset resolver to copy an asset.
*
* \param src: source path of the asset to copy
* \param dst: destination path of the copy
* \param name_collision_mode: behavior when `dst` already exists
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
* \return true if the copy succeeded, false otherwise
*/
bool copy_asset(const std::string &src,
const std::string &dst,
TexNameCollisionMode name_collision_mode,
ReportList *reports);
/**
* Invoke the USD asset resolver to determine if the
* asset with the given path exists.
*
* \param path: the path to resolve
* \return true if the asset exists, false otherwise
*/
bool asset_exists(const std::string &path);
/**
* Invoke the USD asset resolver to copy an asset to a destination
* directory and return the path to the copied file. This function may
* be used to copy textures from a USDZ archive to a directory on disk.
* The destination directory will be created if it doesn't already exist.
* If the copy was unsuccessful, this function will log an error and
* return the original source file path unmodified.
*
* \param src: source path of the asset to import
* \param import_dir: path to the destination directory
* \param name_collision_mode: behavior when a file of the same name already exists
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
* \return path to copied file or the original `src` path if there was an error
*/
std::string import_asset(const std::string &src,
const char *import_dir,
TexNameCollisionMode name_collision_mode,
ReportList *reports);
/**
* Check if the given path contains a UDIM token.
*
* \param path: the path to check
* \return true if the path contains a UDIM token, false otherwise
*/
bool is_udim_path(const std::string &path);
/**
* Invoke the USD asset resolver to return an identifier for a 'textures' directory
* which is a sibling of the given stage. The resulting path is created by
* resolving the './textures' relative path with the stage's root layer path as
* the anchor. If the parent of the stage root layer path resolves to a file
* system path, the textures directory will be created, if it doesn't exist.
*
* \param stage: The stage whose root layer is a sibling of the 'textures'
* directory
* \return the path to the 'textures' directory
*/
std::string get_export_textures_dir(const pxr::UsdStageRefPtr stage);
/**
* Return true if the asset at the given path is a candidate for importing
* with the USD asset resolver. The following heuristics are currently
* applied for this test:
* - Returns false if it's a Blender relative path.
* - Returns true if the path is package-relative.
* - Returns true is the path doesn't exist on the file system but can
* nonetheless be resolved by the USD asset resolver.
* - Returns false otherwise.
*
* TODO(makowalski): the test currently requires a file-system stat.
* Consider possible ways around this, e.g., by determining if the
* path is a supported URI.
*
* \param path: input file path
* \return true if the path should be imported, false otherwise
*/
bool should_import_asset(const std::string &path);
/**
* Invokes the USD asset resolver to resolve the given paths and
* returns true if the resolved paths are equal.
*
* \param path1: first path to compare
* \param path2: second path to compare
* \return true if the resolved input paths are equal, returns
* false otherwise.
*
*/
bool paths_equal(const std::string &path1, const std::string &path2);
/**
* Returns path to temporary folder for saving imported textures prior to packing.
* CAUTION: this directory is recursively deleted after material import.
*/
const char *temp_textures_dir();
/**
* Invokes the USD asset resolver to write data to the given path.
*
* \param data: pointer to data to write
* \param size: number of bytes to write
* \param path: path of asset to be written
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
* \return true if the data was written, returns
* false otherwise.
*
*/
bool write_to_path(const void *data, size_t size, const std::string &path, ReportList *reports);
/**
* Add the given path as a custom property "usd_source_path" on the given id.
* If the path is a package-relative path (i.e., is relative to a USDZ archive)
* it will not be added a a property. If custom property "usd_source_path"
* already exists, this function does nothing.
*
* \param path: path to record as a custom property
* \param id: id for which to create the custom property
*/
void ensure_usd_source_path_prop(const std::string &path, ID *id);
/**
* Return the value of the "usd_source_path" custom property on the given id.
* Return an empty string if the property does not exist.
*/
std::string get_usd_source_path(ID *id);
/**
* Return the given path as a relative path with respect to the given anchor
* path.
*
* \param path: path to make relative with respect to the anchor path
* \param anchor: the anchor path
* \return the relative path string; return the input path unchanged if it can't
* be made relative, is already a relative path or is a package-relative
* path
*
*/
std::string get_relative_path(const std::string &path, const std::string &anchor);
} // namespace blender::io::usd

View File

@@ -0,0 +1,215 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_attribute_utils.hh"
#include "usd_colorspace_utils.hh"
#include "usd_hash_types.hh"
#include "BLI_map.hh"
#include "BLI_offset_indices.hh"
#include "BLI_sys_types.h"
#include "BKE_attribute.hh"
#include <pxr/usd/sdf/valueTypeName.h>
#include <optional>
namespace blender::io::usd {
std::optional<pxr::SdfValueTypeName> convert_blender_type_to_usd(const bke::AttrType blender_type,
bool use_color3f_type)
{
switch (blender_type) {
case bke::AttrType::Float:
return pxr::SdfValueTypeNames->FloatArray;
case bke::AttrType::Int8:
return pxr::SdfValueTypeNames->UCharArray;
case bke::AttrType::Int32:
return pxr::SdfValueTypeNames->IntArray;
case bke::AttrType::Float2:
return pxr::SdfValueTypeNames->Float2Array;
case bke::AttrType::Float3:
return pxr::SdfValueTypeNames->Float3Array;
case bke::AttrType::String:
return pxr::SdfValueTypeNames->StringArray;
case bke::AttrType::Bool:
return pxr::SdfValueTypeNames->BoolArray;
case bke::AttrType::ColorFloat:
case bke::AttrType::ColorByte:
return use_color3f_type ? pxr::SdfValueTypeNames->Color3fArray :
pxr::SdfValueTypeNames->Color4fArray;
case bke::AttrType::Quaternion:
return pxr::SdfValueTypeNames->QuatfArray;
default:
return std::nullopt;
}
}
std::optional<bke::AttrType> convert_usd_type_to_blender(const pxr::SdfValueTypeName usd_type)
{
static const Map<pxr::SdfValueTypeName, bke::AttrType> type_map = []() {
Map<pxr::SdfValueTypeName, bke::AttrType> map;
map.add_new(pxr::SdfValueTypeNames->FloatArray, bke::AttrType::Float);
map.add_new(pxr::SdfValueTypeNames->Double, bke::AttrType::Float);
map.add_new(pxr::SdfValueTypeNames->UCharArray, bke::AttrType::Int8);
map.add_new(pxr::SdfValueTypeNames->IntArray, bke::AttrType::Int32);
map.add_new(pxr::SdfValueTypeNames->Float2Array, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord2dArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord2fArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord2hArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord3dArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord3fArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->TexCoord3hArray, bke::AttrType::Float2);
map.add_new(pxr::SdfValueTypeNames->Float3Array, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Point3fArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Point3dArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Point3hArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Normal3fArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Normal3dArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Normal3hArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Vector3fArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Vector3hArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Vector3dArray, bke::AttrType::Float3);
map.add_new(pxr::SdfValueTypeNames->Color3fArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->Color3hArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->Color3dArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->Color4fArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->Color4hArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->Color4dArray, bke::AttrType::ColorFloat);
map.add_new(pxr::SdfValueTypeNames->BoolArray, bke::AttrType::Bool);
map.add_new(pxr::SdfValueTypeNames->QuatfArray, bke::AttrType::Quaternion);
map.add_new(pxr::SdfValueTypeNames->QuatdArray, bke::AttrType::Quaternion);
map.add_new(pxr::SdfValueTypeNames->QuathArray, bke::AttrType::Quaternion);
return map;
}();
const bke::AttrType *value = type_map.lookup_ptr(usd_type);
if (value == nullptr) {
return std::nullopt;
}
return *value;
}
void copy_primvar_to_blender_attribute(const pxr::UsdGeomPrimvar &primvar,
const pxr::UsdTimeCode time,
const bke::AttrType data_type,
const bke::AttrDomain domain,
const OffsetIndices<int> face_indices,
bke::MutableAttributeAccessor attributes)
{
switch (data_type) {
case bke::AttrType::Float:
copy_primvar_to_blender_buffer<float, float>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::Int8:
copy_primvar_to_blender_buffer<uchar, int8_t>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::Int32:
copy_primvar_to_blender_buffer<int32_t, int>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::Float2:
copy_primvar_to_blender_buffer<pxr::GfVec2f, float2>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::Float3:
copy_primvar_to_blender_buffer<pxr::GfVec3f, float3>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::ColorFloat: {
const pxr::SdfValueTypeName pv_type = primvar.GetTypeName();
if (ELEM(pv_type,
pxr::SdfValueTypeNames->Color3fArray,
pxr::SdfValueTypeNames->Color3hArray,
pxr::SdfValueTypeNames->Color3dArray))
{
copy_primvar_to_blender_buffer<pxr::GfVec3f, ColorGeometry4f>(
primvar, time, data_type, domain, face_indices, attributes);
}
else {
copy_primvar_to_blender_buffer<pxr::GfVec4f, ColorGeometry4f>(
primvar, time, data_type, domain, face_indices, attributes);
}
} break;
case bke::AttrType::Bool:
copy_primvar_to_blender_buffer<bool, bool>(
primvar, time, data_type, domain, face_indices, attributes);
break;
case bke::AttrType::Quaternion:
copy_primvar_to_blender_buffer<pxr::GfQuatf, math::Quaternion>(
primvar, time, data_type, domain, face_indices, attributes);
break;
default:
BLI_assert_unreachable();
}
}
void copy_blender_attribute_to_primvar(const GVArray &attribute,
const bke::AttrType data_type,
const pxr::UsdTimeCode time,
const pxr::UsdGeomPrimvar &primvar,
pxr::UsdUtilsSparseValueWriter &value_writer)
{
switch (data_type) {
case bke::AttrType::Float:
copy_blender_buffer_to_primvar<float, float>(
attribute.typed<float>(), time, primvar, value_writer);
break;
case bke::AttrType::Int8:
copy_blender_buffer_to_primvar<int8_t, uchar>(
attribute.typed<int8_t>(), time, primvar, value_writer);
break;
case bke::AttrType::Int32:
copy_blender_buffer_to_primvar<int, int32_t>(
attribute.typed<int>(), time, primvar, value_writer);
break;
case bke::AttrType::Float2:
copy_blender_buffer_to_primvar<float2, pxr::GfVec2f>(
attribute.typed<float2>(), time, primvar, value_writer);
break;
case bke::AttrType::Float3:
copy_blender_buffer_to_primvar<float3, pxr::GfVec3f>(
attribute.typed<float3>(), time, primvar, value_writer);
break;
case bke::AttrType::Bool:
copy_blender_buffer_to_primvar<bool, bool>(
attribute.typed<bool>(), time, primvar, value_writer);
break;
case bke::AttrType::ColorFloat:
if (primvar.GetTypeName() == pxr::SdfValueTypeNames->Color3fArray) {
copy_blender_buffer_to_primvar<ColorGeometry4f, pxr::GfVec3f>(
attribute.typed<ColorGeometry4f>(), time, primvar, value_writer);
}
else {
copy_blender_buffer_to_primvar<ColorGeometry4f, pxr::GfVec4f>(
attribute.typed<ColorGeometry4f>(), time, primvar, value_writer);
}
colorspace_apply_to_prim(primvar.GetAttr().GetPrim());
break;
case bke::AttrType::ColorByte:
if (primvar.GetTypeName() == pxr::SdfValueTypeNames->Color3fArray) {
copy_blender_buffer_to_primvar<ColorGeometry4b, pxr::GfVec3f>(
attribute.typed<ColorGeometry4b>(), time, primvar, value_writer);
}
else {
copy_blender_buffer_to_primvar<ColorGeometry4b, pxr::GfVec4f>(
attribute.typed<ColorGeometry4b>(), time, primvar, value_writer);
}
colorspace_apply_to_prim(primvar.GetAttr().GetPrim());
break;
case bke::AttrType::Quaternion:
copy_blender_buffer_to_primvar<math::Quaternion, pxr::GfQuatf>(
attribute.typed<math::Quaternion>(), time, primvar, value_writer);
break;
default:
BLI_assert_unreachable();
}
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,320 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_colorspace_utils.hh"
#include "BLI_color.hh"
#include "BLI_generic_virtual_array.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_span.hh"
#include "BLI_virtual_array.hh"
#include "BKE_attribute.hh"
#include "IO_validate.hh"
#include <pxr/base/gf/quatf.h>
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/vt/array.h>
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/sdf/valueTypeName.h>
#include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/usdGeom/primvar.h>
#include <pxr/usd/usdUtils/sparseValueWriter.h>
#include <cstdint>
#include <optional>
#include <type_traits>
namespace blender {
namespace usdtokens {
inline const pxr::TfToken displayColor("displayColor", pxr::TfToken::Immortal);
}
namespace io::usd {
namespace detail {
/* Until we can use C++20, implement our own version of std::is_layout_compatible.
* Types with compatible layouts can be exchanged much more efficiently than otherwise.
*/
template<class T, class U> struct is_layout_compatible : std::false_type {};
template<> struct is_layout_compatible<float2, pxr::GfVec2f> : std::true_type {};
template<> struct is_layout_compatible<float3, pxr::GfVec3f> : std::true_type {};
template<> struct is_layout_compatible<pxr::GfVec2f, float2> : std::true_type {};
template<> struct is_layout_compatible<pxr::GfVec3f, float3> : std::true_type {};
/* Conversion utilities to convert a Blender type to an USD type. */
template<typename From, typename To> inline To convert_value(const From value)
{
return value;
}
template<> inline pxr::GfVec2f convert_value(const float2 value)
{
return pxr::GfVec2f(value[0], value[1]);
}
template<> inline pxr::GfVec3f convert_value(const float3 value)
{
return pxr::GfVec3f(value[0], value[1], value[2]);
}
template<> inline pxr::GfVec3f convert_value(const ColorGeometry4f value)
{
return pxr::GfVec3f(value.r, value.g, value.b);
}
template<> inline pxr::GfVec4f convert_value(const ColorGeometry4f value)
{
return pxr::GfVec4f(value.r, value.g, value.b, value.a);
}
template<> inline pxr::GfVec3f convert_value(const ColorGeometry4b value)
{
ColorGeometry4f color4f = color::decode(value);
return pxr::GfVec3f(color4f.r, color4f.g, color4f.b);
}
template<> inline pxr::GfVec4f convert_value(const ColorGeometry4b value)
{
ColorGeometry4f color4f = color::decode(value);
return pxr::GfVec4f(color4f.r, color4f.g, color4f.b, color4f.a);
}
template<> inline pxr::GfQuatf convert_value(const math::Quaternion value)
{
return pxr::GfQuatf(value.w, value.x, value.y, value.z);
}
template<> inline float2 convert_value(const pxr::GfVec2f value)
{
return float2(value[0], value[1]);
}
template<> inline float3 convert_value(const pxr::GfVec3f value)
{
return float3(value[0], value[1], value[2]);
}
template<> inline ColorGeometry4f convert_value(const pxr::GfVec3f value)
{
return ColorGeometry4f(value[0], value[1], value[2], 1.0f);
}
template<> inline ColorGeometry4f convert_value(const pxr::GfVec4f value)
{
return ColorGeometry4f(value[0], value[1], value[2], value[3]);
}
template<> inline math::Quaternion convert_value(const pxr::GfQuatf value)
{
const pxr::GfVec3f &img = value.GetImaginary();
return math::Quaternion(value.GetReal(), img[0], img[1], img[2]);
}
template<class T> struct is_vt_array : std::false_type {};
template<class T> struct is_vt_array<pxr::VtArray<T>> : std::true_type {};
} // namespace detail
std::optional<pxr::SdfValueTypeName> convert_blender_type_to_usd(const bke::AttrType blender_type,
bool use_color3f_type = false);
std::optional<bke::AttrType> convert_usd_type_to_blender(const pxr::SdfValueTypeName usd_type);
/**
* Set the USD attribute to the provided value at the given time. The value will be written
* sparsely.
*/
template<typename USDT>
void set_attribute(const pxr::UsdAttribute &attr,
const USDT value,
pxr::UsdTimeCode time,
pxr::UsdUtilsSparseValueWriter &value_writer)
{
/* This overload should only be use with non-VtArray types. If it is not, then that indicates
* an issue on the caller side, usually because of using a const reference rather than non-const
* for the `value` parameter. */
static_assert(!detail::is_vt_array<USDT>::value, "Wrong set_attribute overload selected.");
if (!attr.HasValue()) {
attr.Set(value, pxr::UsdTimeCode::Default());
}
value_writer.SetAttribute(attr, pxr::VtValue(value), time);
}
/**
* Set the USD attribute to the provided array value at the given time. The value will be written
* sparsely. For efficiency, this function swaps out the given value, leaving it empty, so it can
* leverage the USD API where no additional copy of the data is required. */
template<typename USDT>
void set_attribute(const pxr::UsdAttribute &attr,
pxr::VtArray<USDT> &value,
pxr::UsdTimeCode time,
pxr::UsdUtilsSparseValueWriter &value_writer)
{
if (!attr.HasValue()) {
attr.Set(value, pxr::UsdTimeCode::Default());
}
pxr::VtValue val = pxr::VtValue::Take(value);
value_writer.SetAttribute(attr, &val, time);
}
/* Copy a typed Blender attribute array into a typed USD primvar attribute. */
template<typename BlenderT, typename USDT>
void copy_blender_buffer_to_primvar(const VArray<BlenderT> &buffer,
const pxr::UsdTimeCode time,
const pxr::UsdGeomPrimvar &primvar,
pxr::UsdUtilsSparseValueWriter &value_writer)
{
constexpr bool is_same = std::is_same_v<BlenderT, USDT>;
constexpr bool is_compatible = detail::is_layout_compatible<BlenderT, USDT>::value;
pxr::VtArray<USDT> usd_data;
if (const std::optional<BlenderT> value = buffer.get_if_single()) {
usd_data.assign(buffer.size(), detail::convert_value<BlenderT, USDT>(*value));
}
else {
const VArraySpan<BlenderT> data(buffer);
if constexpr (is_same || is_compatible) {
usd_data.assign(data.template cast<USDT>().begin(), data.template cast<USDT>().end());
}
else {
usd_data.resize(data.size());
for (const int i : data.index_range()) {
usd_data[i] = detail::convert_value<BlenderT, USDT>(data[i]);
}
}
}
set_attribute(primvar, usd_data, time, value_writer);
}
void copy_blender_attribute_to_primvar(const GVArray &attribute,
const bke::AttrType data_type,
const pxr::UsdTimeCode time,
const pxr::UsdGeomPrimvar &primvar,
pxr::UsdUtilsSparseValueWriter &value_writer);
template<typename T>
pxr::VtArray<T> get_primvar_array(const pxr::UsdGeomPrimvar &primvar, const pxr::UsdTimeCode time)
{
pxr::VtValue primvar_val;
if (!primvar.ComputeFlattened(&primvar_val, time)) {
return {};
}
if (!primvar_val.CanCast<pxr::VtArray<T>>()) {
return {};
}
return primvar_val.Cast<pxr::VtArray<T>>().template UncheckedGet<pxr::VtArray<T>>();
}
inline void set_single_value(bke::MutableAttributeAccessor attributes,
const StringRef attr_name,
const bke::AttrDomain domain,
const bke::AttrType data_type,
const bke::AttributeInit &value)
{
if (!attributes.contains(attr_name)) {
attributes.add(attr_name, domain, data_type, value);
}
else {
attributes.assign_data(attr_name, value);
}
}
template<typename USDT, typename BlenderT>
void copy_primvar_to_blender_buffer(const pxr::UsdGeomPrimvar &primvar,
const pxr::UsdTimeCode time,
const bke::AttrType data_type,
const bke::AttrDomain domain,
const OffsetIndices<int> faces,
bke::MutableAttributeAccessor attributes)
{
const pxr::VtArray<USDT> usd_data = get_primvar_array<USDT>(primvar, time);
if (usd_data.empty()) {
return;
}
constexpr bool is_same = std::is_same_v<USDT, BlenderT>;
constexpr bool is_compatible = detail::is_layout_compatible<USDT, BlenderT>::value;
constexpr bool is_color = std::is_same_v<BlenderT, ColorGeometry4f>;
const pxr::TfToken pv_interp = primvar.GetInterpolation();
const pxr::TfToken pv_name = pxr::UsdGeomPrimvar::StripPrimvarsName(primvar.GetPrimvarName());
const StringRef attr_name = pv_name.GetText();
/* Map constant interpolation to single-value attributes. */
if (pv_interp == pxr::UsdGeomTokens->constant) {
BlenderT value = detail::convert_value<USDT, BlenderT>(usd_data[0]);
if constexpr (is_color) {
colorspace_attr_to_scene_linear(primvar.GetAttr(), value);
}
set_single_value(attributes, attr_name, domain, data_type, bke::AttributeInitValue(value));
return;
}
bke::SpanAttributeWriter<BlenderT> attribute_writer =
attributes.lookup_or_add_for_write_span<BlenderT>(pv_name.GetText(), domain);
MutableSpan<BlenderT> attribute = attribute_writer.span;
if (pv_interp == pxr::UsdGeomTokens->faceVarying) {
if (!faces.is_empty()) {
/* Reverse the index order. */
for (const int i : faces.index_range()) {
const IndexRange face = faces[i];
for (int j : face.index_range()) {
const int rev_index = face.last(j);
attribute[face.start() + j] = validate::index_in_range(rev_index, usd_data.size()) ?
detail::convert_value<USDT, BlenderT>(
usd_data[rev_index]) :
BlenderT();
}
}
}
else {
if constexpr (is_same || is_compatible) {
const Span<USDT> src(usd_data.data(), usd_data.size());
attribute.copy_from(src.template cast<BlenderT>());
}
else {
for (const int64_t i : attribute.index_range()) {
attribute[i] = detail::convert_value<USDT, BlenderT>(usd_data[i]);
}
}
}
}
else {
/* Assume direct one-to-one mapping. */
if (usd_data.size() == attribute.size()) {
if constexpr (is_same || is_compatible) {
const Span<USDT> src(usd_data.data(), usd_data.size());
attribute.copy_from(src.template cast<BlenderT>());
}
else {
for (const int64_t i : attribute.index_range()) {
attribute[i] = detail::convert_value<USDT, BlenderT>(usd_data[i]);
}
}
}
}
if constexpr (is_color) {
colorspace_attr_to_scene_linear(primvar.GetAttr(), attribute);
}
attribute_writer.finish();
}
void copy_primvar_to_blender_attribute(const pxr::UsdGeomPrimvar &primvar,
const pxr::UsdTimeCode time,
const bke::AttrType data_type,
const bke::AttrDomain domain,
const OffsetIndices<int> face_indices,
bke::MutableAttributeAccessor attributes);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,467 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_blend_shape_utils.hh"
#include "usd_utils.hh"
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdSkel/animMapper.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/blendShape.h>
#include "DNA_key_types.h"
#include "DNA_mesh_types.h"
#include "BKE_key.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_math_vector.h"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include <string>
#include <vector>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace usdtokens {
static const pxr::TfToken Anim("Anim", pxr::TfToken::Immortal);
static const pxr::TfToken joint1("joint1", pxr::TfToken::Immortal);
static const pxr::TfToken Skel("Skel", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace {
/* Helper struct to facilitate merging blend shape weights time
* samples from multiple meshes to a single skeleton animation. */
struct BlendShapeMergeInfo {
pxr::VtTokenArray src_blend_shapes;
pxr::UsdAttribute src_weights_attr;
/* Remap blend shape weight array from the
* source order to the destination order. */
pxr::UsdSkelAnimMapper anim_map;
void init_anim_map(const pxr::VtTokenArray &dst_blend_shapes)
{
anim_map = pxr::UsdSkelAnimMapper(src_blend_shapes, dst_blend_shapes);
}
};
/* Helper function to avoid name collisions when merging blend shape names from
* multiple meshes to a single skeleton.
*
* Attempt to add the given name to the 'names' set as a unique entry, modifying
* the name with a numerical suffix if necessary, and return the unique name that
* was added to the set. */
std::string add_unique_name(Set<std::string> &names, const std::string &name)
{
std::string unique_name = name;
int suffix = 2;
while (names.contains(unique_name)) {
unique_name = name + std::to_string(suffix++);
}
names.add(unique_name);
return unique_name;
}
} // namespace
namespace io::usd {
pxr::TfToken TempBlendShapeWeightsPrimvarName("temp:weights", pxr::TfToken::Immortal);
void ensure_blend_shape_skeleton(pxr::UsdStageRefPtr stage, pxr::UsdPrim &mesh_prim)
{
if (!stage || !mesh_prim) {
return;
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
pxr::UsdSkelSkeleton skel;
if (!skel_api.GetSkeleton(&skel)) {
pxr::SdfPath skel_path = mesh_prim.GetParent().GetPath().AppendChild(usdtokens::Skel);
skel = pxr::UsdSkelSkeleton::Define(stage, skel_path);
if (!skel) {
CLOG_WARN(&LOG,
"Couldn't find or create skeleton bound to mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
skel_api.CreateSkeletonRel().AddTarget(skel.GetPath());
/* Initialize the skeleton. */
pxr::VtMatrix4dArray bind_transforms(1, pxr::GfMatrix4d(1.0));
pxr::VtMatrix4dArray rest_transforms(1, pxr::GfMatrix4d(1.0));
skel.CreateBindTransformsAttr().Set(bind_transforms);
skel.GetRestTransformsAttr().Set(rest_transforms);
/* Some DCCs seem to require joint names to bind the
* skeleton to blend-shapes. */
pxr::VtTokenArray joints({usdtokens::joint1});
skel.CreateJointsAttr().Set(joints);
}
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).GetPrimvar(
TempBlendShapeWeightsPrimvarName);
if (!temp_weights_attr) {
/* No need to create the animation. */
return;
}
pxr::SdfPath anim_path = skel.GetPath().AppendChild(usdtokens::Anim);
pxr::UsdSkelAnimation anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
if (!anim) {
CLOG_WARN(&LOG, "Couldn't define animation at path %s", anim_path.GetAsString().c_str());
return;
}
pxr::VtTokenArray blendshape_names;
skel_api.GetBlendShapesAttr().Get(&blendshape_names);
anim.CreateBlendShapesAttr().Set(blendshape_names);
std::vector<double> times;
temp_weights_attr.GetTimeSamples(&times);
pxr::UsdAttribute anim_weights_attr = anim.CreateBlendShapeWeightsAttr();
pxr::VtFloatArray weights;
for (const double time : times) {
if (temp_weights_attr.Get(&weights, time)) {
anim_weights_attr.Set(weights, time);
}
}
/* Next, set the animation source on the skeleton. */
skel_api = pxr::UsdSkelBindingAPI::Apply(skel.GetPrim());
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to skeleton prim %s",
skel.GetPath().GetAsString().c_str());
return;
}
if (!skel_api.CreateAnimationSourceRel().AddTarget(pxr::SdfPath(usdtokens::Anim))) {
CLOG_WARN(&LOG,
"Couldn't set animation source on skeleton %s",
skel.GetPath().GetAsString().c_str());
}
pxr::UsdGeomPrimvarsAPI(mesh_prim).RemovePrimvar(TempBlendShapeWeightsPrimvarName);
}
const Key *get_mesh_shape_key(const Object *obj)
{
BLI_assert(obj);
if (!obj->data || obj->type != OB_MESH) {
return nullptr;
}
const Mesh *mesh = id_cast<const Mesh *>(obj->data);
return mesh->key;
}
bool is_mesh_with_shape_keys(const Object *obj)
{
const Key *key = get_mesh_shape_key(obj);
return key && key->totkey > 0 && key->type == KEY_RELATIVE;
}
void create_blend_shapes(pxr::UsdStageRefPtr stage,
const Object *obj,
const pxr::UsdPrim &mesh_prim,
bool allow_unicode)
{
const Key *key = get_mesh_shape_key(obj);
if (!(key && mesh_prim)) {
return;
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
pxr::VtTokenArray blendshape_names;
std::vector<pxr::SdfPath> blendshape_paths;
/* Get the basis, which we'll use to calculate offsets. */
KeyBlock *basis_key = static_cast<KeyBlock *>(key->block.first);
if (!basis_key) {
return;
}
int basis_totelem = basis_key->totelem;
for (KeyBlock &kb : key->block) {
if (&kb == basis_key) {
/* Skip the basis. */
continue;
}
pxr::TfToken name(make_safe_name(kb.name, allow_unicode));
blendshape_names.push_back(name);
pxr::SdfPath path = mesh_prim.GetPath().AppendChild(name);
blendshape_paths.push_back(path);
pxr::UsdSkelBlendShape blendshape = pxr::UsdSkelBlendShape::Define(stage, path);
pxr::UsdAttribute offsets_attr = blendshape.CreateOffsetsAttr();
/* Some applications, like Houdini, don't render blend shapes unless the point
* indices are set, so we always create this attribute, even when every index
* is included. */
pxr::UsdAttribute point_indices_attr = blendshape.CreatePointIndicesAttr();
pxr::VtVec3fArray offsets(kb.totelem);
pxr::VtIntArray indices(kb.totelem);
std::iota(indices.begin(), indices.end(), 0);
const float (*fp)[3] = static_cast<float (*)[3]>(kb.data);
const float (*basis_fp)[3] = static_cast<float (*)[3]>(basis_key->data);
for (int i = 0; i < kb.totelem; ++i) {
/* Subtract the key positions from the
* basis positions to get the offsets. */
sub_v3_v3v3(offsets[i].data(), fp[i], basis_fp[i]);
}
offsets_attr.Set(offsets);
point_indices_attr.Set(indices);
}
/* Set the blend-shape names and targets on the shape. */
pxr::UsdAttribute blendshape_attr = skel_api.CreateBlendShapesAttr();
blendshape_attr.Set(blendshape_names);
skel_api.CreateBlendShapeTargetsRel().SetTargets(blendshape_paths);
/* Some DCCs seem to require joint indices and weights to
* bind the skeleton for blend-shapes, so we create these primvars, if needed. */
if (!skel_api.GetJointIndicesAttr().HasAuthoredValue()) {
pxr::VtArray<int> joint_indices(basis_totelem, 0);
skel_api.CreateJointIndicesPrimvar(false, 1).GetAttr().Set(joint_indices);
}
if (!skel_api.GetJointWeightsAttr().HasAuthoredValue()) {
pxr::VtArray<float> joint_weights(basis_totelem, 1.0f);
skel_api.CreateJointWeightsPrimvar(false, 1).GetAttr().Set(joint_weights);
}
}
pxr::VtFloatArray get_blendshape_weights(const Key *key)
{
BLI_assert(key);
pxr::VtFloatArray weights;
for (KeyBlock &kb : key->block) {
if (&kb == key->block.first) {
/* Skip the first key, which is the basis. */
continue;
}
weights.push_back(kb.curval);
}
return weights;
}
void remap_blend_shape_anim(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &skel_path,
const pxr::SdfPathSet &mesh_paths)
{
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Get(stage, skel_path);
if (!skel_api) {
CLOG_WARN(&LOG, "Couldn't get skeleton from path %s", skel_path.GetAsString().c_str());
return;
}
/* Use existing animation if possible, otherwise create a new one. */
pxr::UsdPrim anim_prim;
pxr::UsdSkelAnimation anim;
if (skel_api.GetAnimationSource(&anim_prim)) {
anim = pxr::UsdSkelAnimation(anim_prim);
}
else {
pxr::SdfPath anim_path = skel_path.AppendChild(usdtokens::Anim);
anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
}
if (!anim) {
CLOG_WARN(&LOG, "Couldn't get animation under skeleton %s", skel_path.GetAsString().c_str());
return;
}
Vector<BlendShapeMergeInfo> merge_info;
/* We are merging blend shape names and weights from multiple
* meshes to a single animation. In case of name collisions,
* we must generate unique blend shape names for the merged
* result. This set keeps track of the unique names that will
* be combined on the animation. */
Set<std::string> merged_names;
/* Iterate over all the meshes, generate unique blend shape names in case of name
* collisions and set up the information we will need to merge the results. */
for (const pxr::SdfPath &mesh_path : mesh_paths) {
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(mesh_path);
pxr::UsdSkelBindingAPI mesh_skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!mesh_skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
mesh_path.GetAsString().c_str());
continue;
}
/* Get the blend shape names for this mesh. */
pxr::UsdAttribute blend_shapes_attr = mesh_skel_api.GetBlendShapesAttr();
if (!blend_shapes_attr) {
continue;
}
pxr::VtTokenArray names;
if (!mesh_skel_api.GetBlendShapesAttr().Get(&names)) {
continue;
}
/* Ensure the names are unique. */
pxr::VtTokenArray unique_names;
for (const pxr::TfToken &name : names.AsConst()) {
std::string unique = add_unique_name(merged_names, name.GetString());
unique_names.push_back(pxr::TfToken(unique));
}
/* Set the unique names back on the mesh. */
mesh_skel_api.GetBlendShapesAttr().Set(unique_names);
/* Look up the temporary weights time sample we wrote to the mesh. */
const pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).GetPrimvar(
TempBlendShapeWeightsPrimvarName);
if (!temp_weights_attr) {
/* No need to create the animation. Shouldn't usually happen. */
return;
}
/* Generate information we will need to merge the weight samples below. */
merge_info.append(BlendShapeMergeInfo());
merge_info.last().src_blend_shapes = unique_names;
merge_info.last().src_weights_attr = temp_weights_attr;
}
if (merged_names.is_empty()) {
/* No blend shape names were collected. Shouldn't usually happen. */
return;
}
/* Copy the list of name strings to a list of tokens, since we need to work with tokens. */
pxr::VtTokenArray skel_blend_shape_names;
for (const std::string &name : merged_names) {
skel_blend_shape_names.push_back(pxr::TfToken(name));
}
/* Initialize the merge info structs with the list of names on the merged animation. */
for (BlendShapeMergeInfo &info : merge_info) {
info.init_anim_map(skel_blend_shape_names);
}
/* Set the names on the animation prim. */
anim.CreateBlendShapesAttr().Set(skel_blend_shape_names);
pxr::UsdAttribute dst_weights_attr = anim.CreateBlendShapeWeightsAttr();
/* Merge the weight time samples. */
std::vector<double> times;
merge_info.first().src_weights_attr.GetTimeSamples(&times);
if (times.empty()) {
/* Times may be empty if there is only a default value for the weights,
* so we read the default. */
times.push_back(pxr::UsdTimeCode::Default().GetValue());
}
pxr::VtFloatArray dst_weights;
for (const double time : times) {
for (const BlendShapeMergeInfo &info : merge_info) {
pxr::VtFloatArray src_weights;
if (info.src_weights_attr.Get(&src_weights, time)) {
if (!info.anim_map.Remap(src_weights.AsConst(), &dst_weights)) {
CLOG_WARN(&LOG, "Failed remapping blend shape weights");
}
}
}
/* Set the merged weights on the animation. */
dst_weights_attr.Set(dst_weights, time);
}
}
Mesh *get_shape_key_basis_mesh(Object *obj)
{
if (!obj || !obj->data || obj->type != OB_MESH) {
return nullptr;
}
/* If we're exporting blend shapes, we export the unmodified mesh with
* the verts in the basis key positions. */
const Mesh *mesh = BKE_object_get_pre_modified_mesh(obj);
if (!mesh || !mesh->key || !mesh->key->block.first) {
return nullptr;
}
const KeyBlock *basis = reinterpret_cast<KeyBlock *>(mesh->key->block.first);
if (mesh->verts_num != basis->totelem) {
CLOG_WARN(&LOG, "Vertex and shape key element count mismatch for mesh %s", obj->id.name + 2);
return nullptr;
}
/* Make a copy of the mesh so we can update the verts to the basis shape. */
Mesh *temp_mesh = BKE_mesh_copy_for_eval(*mesh);
/* Update the verts. */
BKE_keyblock_convert_to_mesh(basis, temp_mesh->vert_positions_for_write());
return temp_mesh;
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/usd/prim.h>
namespace blender {
struct Key;
struct Mesh;
struct Object;
namespace io::usd {
/* Name of the temporary USD primvar for storing blend shape
* weight time samples on the mesh before they are copied
* to the bound skeleton. */
extern pxr::TfToken TempBlendShapeWeightsPrimvarName;
/**
* Return the shape key on the given mesh object.
*
* \param obj: The mesh object
* \return The shape key on the given object's mesh data, or
* null if the object isn't a mesh.
*/
const Key *get_mesh_shape_key(const Object *obj);
/**
* Query whether the given object is a mesh with relative
* shape keys.
*
* \param obj: The mesh object
* \return True if the object is a mesh with shape keys, false otherwise
*/
bool is_mesh_with_shape_keys(const Object *obj);
/**
* Convert shape keys on the given object to USD blend shapes. The blend-shapes
* will be added to the stage as children of the given USD mesh prim. The blend-shape
* names and targets will also be set as properties on the primitive.
*
* \param stage: The stage
* \param obj: The mesh object whose shape keys will be converted to blend shapes
* \param mesh_prim: The USD mesh that will be assigned the blend shape targets
* \param allow_unicode: Whether to allow unicode encoded characters in the blend shape name
*/
void create_blend_shapes(pxr::UsdStageRefPtr stage,
const Object *obj,
const pxr::UsdPrim &mesh_prim,
bool allow_unicode);
/**
* Return the current weight values of the given key.
*
* \param key: The key whose values will be queried
* \return The array of key values.
*/
pxr::VtFloatArray get_blendshape_weights(const Key *key);
/**
* USD implementations expect that a mesh with blend shape targets
* be bound to a skeleton with an animation that provides the blend
* shape weights. If the given mesh is not already bound to a skeleton
* this function will create a dummy skeleton with a single joint and
* will bind it to the mesh. This is typically required if the source
* Blender mesh has shape keys but not an armature deformer.
*
* This function will also create a skel animation prim as a child of
* the skeleton and will copy the weight time samples from a temporary
* primvar on the mesh to the animation prim.
*
* \param stage: The stage
* \param mesh_prim: The USD mesh to which the skeleton will be bound
*/
void ensure_blend_shape_skeleton(pxr::UsdStageRefPtr stage, pxr::UsdPrim &mesh_prim);
/**
* When multiple meshes with blend shape animations are bound to one skeleton, USD implementations
* typically expect these animations to be combined in a single animation on the skeleton. This
* function creates an animation prim as a child of the skeleton and merges the blend shape time
* samples from multiple meshes in a single attribute on the animation. Merging the weight samples
* requires handling blend shape name collisions by generating unique names for the combined
* result.
*
* \param stage: The stage
* \param skel_path: Path to the skeleton
* \param mesh_paths: Paths to one or more mesh primitives bound to the skeleton
*/
void remap_blend_shape_anim(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &skel_path,
const pxr::SdfPathSet &mesh_paths);
/**
* If the given object is a mesh with shape keys, return a copy of the object's pre-modified mesh
* with its verts in the shape key basis positions. The returned mesh must be freed by the caller.
*
* \param obj: The mesh object with shape keys
* \return A new mesh corresponding to the shape key basis shape, or null if the object
* isn't a mesh or has no shape keys.
*/
Mesh *get_shape_key_basis_mesh(Object *obj);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,938 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/core.h>
#include "IO_subdiv_disabler.hh"
#include "usd.hh"
#include "usd_colorspace_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_hook.hh"
#include "usd_instancing_utils.hh"
#include "usd_light_convert.hh"
#include "usd_private.hh"
#include <pxr/base/tf/token.h>
#include <pxr/pxr.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/pointInstancer.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdUI/accessibilityAPI.h>
#include <pxr/usd/usdUtils/usdzPackage.h>
#include "MEM_guardedalloc.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_collection_types.h"
#include "DNA_scene_types.h"
#include "BKE_appdir.hh"
#include "BKE_blender_version.h"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_image.hh"
#include "BKE_image_save.hh"
#include "BKE_lib_id.hh"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "BLI_fileops.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_timeit.hh"
#include "ED_util.hh"
#include <IMB_imbuf.hh>
#include <IMB_imbuf_types.hh>
#include "WM_api.hh"
#include "WM_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
struct ExportJobData {
Main *bmain = nullptr;
Depsgraph *depsgraph = nullptr;
wmWindowManager *wm = nullptr;
Scene *scene = nullptr;
/** Unarchived_filepath is used for USDA/USDC/USD export. */
char unarchived_filepath[FILE_MAX] = {};
char usdz_filepath[FILE_MAX] = {};
USDExportParams params = {};
bool export_ok = false;
timeit::TimePoint start_time = {};
bool targets_usdz() const
{
return usdz_filepath[0] != '\0';
}
const char *export_filepath() const
{
if (targets_usdz()) {
return usdz_filepath;
}
return unarchived_filepath;
}
};
/* Returns true if the given prim path is valid, per
* the requirements of the prim path manipulation logic
* of the exporter. Also returns true if the path is
* the empty string. Returns false otherwise. */
static bool prim_path_valid(const std::string &path)
{
if (path.empty()) {
/* Empty paths are ignored in the code,
* so they can be passed through. */
return true;
}
/* Check path syntax. */
std::string errMsg;
if (!pxr::SdfPath::IsValidPathString(path, &errMsg)) {
WM_global_reportf(
RPT_ERROR, "USD Export: invalid path string '%s': %s", path.c_str(), errMsg.c_str());
return false;
}
/* Verify that an absolute prim path can be constructed
* from this path string. */
pxr::SdfPath sdf_path(path);
if (!sdf_path.IsAbsolutePath()) {
WM_global_reportf(RPT_ERROR, "USD Export: path '%s' is not an absolute path", path.c_str());
return false;
}
if (!sdf_path.IsPrimPath()) {
WM_global_reportf(RPT_ERROR, "USD Export: path string '%s' is not a prim path", path.c_str());
return false;
}
return true;
}
/**
* Perform validation of export parameter settings.
* \return true if the parameters are valid; returns false otherwise.
*
* \warning Do not call from worker thread, only from main thread (i.e. before starting the wmJob).
*/
static bool export_params_valid(const USDExportParams &params)
{
bool valid = true;
if (!prim_path_valid(params.root_prim_path)) {
valid = false;
}
return valid;
}
/**
* Create the root Xform primitive, if the Root Prim path has been set
* in the export options. In the future, this function can be extended
* to author transforms and additional schema data (e.g., model Kind)
* on the root prim.
*/
static void ensure_root_prim(pxr::UsdStageRefPtr stage, const USDExportParams &params)
{
if (params.root_prim_path.empty()) {
return;
}
pxr::UsdGeomXform root_xf = pxr::UsdGeomXform::Define(stage,
pxr::SdfPath(params.root_prim_path));
if (!root_xf) {
return;
}
pxr::UsdGeomXformCommonAPI xf_api(root_xf.GetPrim());
if (!xf_api) {
return;
}
if (params.convert_scene_units != SceneUnits::Meters) {
xf_api.SetScale(pxr::GfVec3f(float(1.0 / get_meters_per_unit(params))));
}
if (params.convert_orientation) {
float3x3 mrot;
mat3_from_axis_conversion(
IO_AXIS_Y, IO_AXIS_Z, params.forward_axis, params.up_axis, mrot.ptr());
const math::EulerXYZ eul = math::to_euler(math::transpose(mrot));
xf_api.SetRotate(pxr::GfVec3f(eul.x().degree(), eul.y().degree(), eul.z().degree()));
}
/* Color-space on the root prim. It's also applied on all individual prims that need
* it, but perhaps this is useful to signal the overall color-space of the file. */
colorspace_apply_to_prim(root_xf.GetPrim());
for (const auto &path : pxr::SdfPath(params.root_prim_path).GetPrefixes()) {
auto xform = pxr::UsdGeomXform::Define(stage, path);
/* Tag generated primitives to allow filtering on import. */
xform.GetPrim().SetCustomDataByKey(pxr::TfToken("Blender:generated"), pxr::VtValue(true));
}
}
/**
* If the user has provided an accessibility label and description for the export,
* write that information to the exported stage's default prim. This information
* will be written with the `UsdUIAccessibilityAPI` under the `default`
* namespace. Note: The information will only be added if the label is non-empty.
*/
static void write_root_accessibility_information(pxr::UsdStageRefPtr stage,
const USDExportParams &params)
{
/* Don't apply the API if both the label and description are empty. */
if (params.accessibility_label.empty() && params.accessibility_description.empty()) {
return;
}
pxr::UsdUIAccessibilityAPI accessibility_api = pxr::UsdUIAccessibilityAPI::ApplyDefaultAPI(
stage->GetDefaultPrim());
if (!accessibility_api) {
return;
}
if (!params.accessibility_label.empty()) {
accessibility_api.CreateLabelAttr().Set(params.accessibility_label);
}
if (!params.accessibility_description.empty()) {
accessibility_api.CreateDescriptionAttr().Set(params.accessibility_description);
}
}
static void report_job_duration(const ExportJobData *data)
{
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
const char *export_filepath = data->export_filepath();
fmt::print("USD export of '{}' took ", export_filepath);
timeit::print_duration(duration);
fmt::print("\n");
}
static void process_usdz_textures(const ExportJobData *data, const char *path)
{
const TextureDownscaleSize enum_value = data->params.usdz_downscale_size;
if (enum_value == TextureDownscaleSize::Keep) {
return;
}
const int image_size = (enum_value == TextureDownscaleSize::Custom) ?
data->params.usdz_downscale_custom_size :
int(enum_value);
char texture_path[FILE_MAX];
STRNCPY(texture_path, path);
BLI_path_append(texture_path, FILE_MAX, "textures");
BLI_path_slash_ensure(texture_path, sizeof(texture_path));
direntry *entries;
uint num_files = BLI_filelist_dir_contents(texture_path, &entries);
for (int index = 0; index < num_files; index++) {
/* We can skip checking extensions as this folder is only created
* when we're doing a USDZ export. */
if (!BLI_is_dir(entries[index].path)) {
Image *im = BKE_image_load(data->bmain, entries[index].path);
if (!im) {
CLOG_WARN(&LOG, "Unable to open file for downscaling: %s", entries[index].path);
continue;
}
int width, height;
BKE_image_get_size(im, nullptr, &width, &height);
const int longest = width >= height ? width : height;
const float scale = 1.0 / (float(longest) / float(image_size));
if (longest > image_size) {
const int width_adjusted = float(width) * scale;
const int height_adjusted = float(height) * scale;
BKE_image_scale(im, width_adjusted, height_adjusted, nullptr);
ImageSaveOptions opts;
if (BKE_image_save_options_init(
&opts, data->bmain, data->scene, im, nullptr, false, false))
{
bool result = BKE_image_save(nullptr, data->bmain, im, nullptr, &opts);
if (!result) {
CLOG_ERROR(&LOG,
"Unable to resave '%s' (new size: %dx%d)",
data->usdz_filepath,
width_adjusted,
height_adjusted);
}
else {
CLOG_DEBUG(&LOG,
"Downscaled '%s' to %dx%d",
entries[index].path,
width_adjusted,
height_adjusted);
}
}
BKE_image_save_options_free(&opts);
}
/* Make sure to free the image so it doesn't stick
* around in the library of the open file. */
BKE_id_free(data->bmain, static_cast<void *>(im));
}
}
BLI_filelist_free(entries, num_files);
}
/**
* For usdz export, we must first create a usd/a/c file and then covert it to usdz. In Blender's
* case, we first create a usdc file in Blender's temporary working directory, and store the path
* to the usdc file in `unarchived_filepath`. This function then does the conversion of that usdc
* file into usdz.
*
* \return true when the conversion from usdc to usdz is successful.
*/
static bool perform_usdz_conversion(const ExportJobData *data)
{
char usdc_temp_dir[FILE_MAX], usdc_file[FILE_MAX];
BLI_path_split_dir_file(data->unarchived_filepath,
usdc_temp_dir,
sizeof(usdc_temp_dir),
usdc_file,
sizeof(usdc_file));
char usdz_file[FILE_MAX];
BLI_path_split_file_part(data->usdz_filepath, usdz_file, FILE_MAX);
char original_working_dir_buff[FILE_MAX];
const char *original_working_dir = BLI_current_working_dir(original_working_dir_buff,
sizeof(original_working_dir_buff));
/* Buffer is expected to be returned by #BLI_current_working_dir, although in theory other
* returns are possible on some platforms, this is not handled by this code. */
BLI_assert(original_working_dir == original_working_dir_buff);
BLI_change_working_dir(usdc_temp_dir);
process_usdz_textures(data, usdc_temp_dir);
pxr::UsdUtilsCreateNewUsdzPackage(pxr::SdfAssetPath(usdc_file), usdz_file);
BLI_change_working_dir(original_working_dir);
char usdz_temp_full_path[FILE_MAX];
BLI_path_join(usdz_temp_full_path, FILE_MAX, usdc_temp_dir, usdz_file);
int result = 0;
if (BLI_exists(data->usdz_filepath)) {
result = BLI_delete(data->usdz_filepath, false, false);
if (result != 0) {
BKE_reportf(data->params.worker_status->reports,
RPT_ERROR,
"USD Export: Unable to delete existing usdz file %s",
data->usdz_filepath);
return false;
}
}
result = BLI_path_move(usdz_temp_full_path, data->usdz_filepath);
if (result != 0) {
BKE_reportf(data->params.worker_status->reports,
RPT_ERROR,
"USD Export: Couldn't move new usdz file from temporary location %s to %s",
usdz_temp_full_path,
data->usdz_filepath);
return false;
}
return true;
}
std::string image_cache_file_path()
{
char dir_path[FILE_MAX];
BLI_path_join(dir_path, sizeof(dir_path), BKE_tempdir_session(), "usd", "image_cache");
return dir_path;
}
std::string get_image_cache_file(const std::string &file_name, bool mkdir)
{
std::string dir_path = image_cache_file_path();
if (mkdir) {
BLI_dir_create_recursive(dir_path.c_str());
}
char file_path[FILE_MAX];
BLI_path_join(file_path, sizeof(file_path), dir_path.c_str(), file_name.c_str());
return file_path;
}
std::string cache_image_color(const float color[4])
{
std::string name = fmt::format("color_{:02X}{:02X}{:02X}.exr",
int(color[0] * 255),
int(color[1] * 255),
int(color[2] * 255));
std::string file_path = get_image_cache_file(name);
if (BLI_exists(file_path.c_str())) {
return file_path;
}
ImBuf *ibuf = IMB_allocImBuf(1, 1, ImBufFlags::FloatData);
IMB_rectfill(ibuf, color);
ibuf->ftype = IMB_FTYPE_OPENEXR;
ibuf->foptions.flag = R_IMF_EXR_CODEC_RLE;
if (IMB_save_image(ibuf, file_path.c_str(), ImBufFlags::FloatData)) {
CLOG_INFO(&LOG, "%s", file_path.c_str());
}
else {
CLOG_ERROR(&LOG, "Can't save %s", file_path.c_str());
file_path = "";
}
IMB_freeImBuf(ibuf);
return file_path;
}
static void collect_point_instancer_prototypes_and_set_extent(
pxr::UsdGeomPointInstancer instancer,
const pxr::UsdStageRefPtr &stage,
const pxr::SdfPath &wrapper_path,
std::vector<pxr::UsdPrim> &proto_list)
{
/* Compute extent of the current point instancer. */
pxr::VtArray<pxr::GfVec3f> extent;
instancer.ComputeExtentAtTime(&extent, pxr::UsdTimeCode::Default(), pxr::UsdTimeCode::Default());
instancer.CreateExtentAttr().Set(extent);
pxr::UsdPrim wrapper_prim = stage->GetPrimAtPath(wrapper_path);
if (!wrapper_prim || !wrapper_prim.IsValid()) {
return;
}
std::string real_path_str;
for (const pxr::SdfPrimSpecHandle &primSpec : wrapper_prim.GetPrimStack()) {
if (!primSpec || !primSpec->HasReferences()) {
continue;
}
for (const pxr::SdfReference &ref : primSpec->GetReferenceList().GetPrependedItems()) {
if (ref.GetAssetPath().empty() && !ref.GetPrimPath().IsEmpty()) {
real_path_str = ref.GetPrimPath().GetString();
break;
}
}
if (!real_path_str.empty()) {
break;
}
}
if (real_path_str.empty()) {
CLOG_WARN(&LOG, "No prototype reference found for: %s", wrapper_path.GetText());
return;
}
const pxr::SdfPath real_path(real_path_str);
pxr::UsdPrim proto_prim = stage->GetPrimAtPath(real_path);
if (!proto_prim || !proto_prim.IsValid()) {
CLOG_WARN(&LOG, "Referenced prototype not found at: %s", real_path.GetText());
return;
}
proto_list.push_back(proto_prim);
proto_list.push_back(wrapper_prim.GetParent());
std::string doc_message = fmt::format(
"This prim is used as a prototype by the PointInstancer \"{}\" so we override the def "
"with an \"over\" so that it isn't imaged in the scene, but is available as a prototype "
"that can be referenced.",
wrapper_prim.GetName().GetString());
proto_prim.SetDocumentation(doc_message);
/* Check if the proto prim itself is a PointInstancer. */
if (proto_prim.IsA<pxr::UsdGeomPointInstancer>()) {
pxr::UsdGeomPointInstancer nested_instancer(proto_prim);
pxr::SdfPathVector nested_targets;
if (nested_instancer.GetPrototypesRel().GetTargets(&nested_targets)) {
for (const pxr::SdfPath &nested_wrapper_path : nested_targets) {
collect_point_instancer_prototypes_and_set_extent(
nested_instancer, stage, nested_wrapper_path, proto_list);
}
}
}
/* Also check all children of the proto prim for nested PointInstancers. */
for (const pxr::UsdPrim &child : proto_prim.GetAllChildren()) {
if (child.IsA<pxr::UsdGeomPointInstancer>()) {
pxr::UsdGeomPointInstancer nested_instancer(child);
pxr::SdfPathVector nested_targets;
if (nested_instancer.GetPrototypesRel().GetTargets(&nested_targets)) {
for (const pxr::SdfPath &nested_wrapper_path : nested_targets) {
collect_point_instancer_prototypes_and_set_extent(
nested_instancer, stage, nested_wrapper_path, proto_list);
}
}
}
}
}
pxr::UsdStageRefPtr export_to_stage(const USDExportParams &params,
Depsgraph *depsgraph,
const char *filepath)
{
pxr::UsdStageRefPtr usd_stage = pxr::UsdStage::CreateNew(filepath);
if (!usd_stage) {
return usd_stage;
}
wmJobWorkerStatus *worker_status = params.worker_status;
Scene *scene = DEG_get_input_scene(depsgraph);
Main *bmain = DEG_get_bmain(depsgraph);
SubdivModifierDisabler mod_disabler(depsgraph);
/* If we want to set the subdiv scheme, then we need to the export the mesh
* without the subdiv modifier applied. */
if (ELEM(params.export_subdiv, SubdivExportMode::Match, SubdivExportMode::Ignore)) {
mod_disabler.disable_modifiers();
BKE_scene_graph_update_tagged(depsgraph, bmain);
}
/* This whole `export_to_stage` function is assumed to cover about 80% of the whole export
* process, from 0.1f to 0.9f. */
worker_status->progress = 0.10f;
worker_status->do_update = true;
usd_stage->SetMetadata(pxr::UsdGeomTokens->metersPerUnit, double(scene->unit.scale_length));
usd_stage->GetRootLayer()->SetDocumentation(std::string("Blender v") +
BKE_blender_version_string());
/* Set up the stage for animated data. */
if (params.export_animation) {
usd_stage->SetTimeCodesPerSecond(scene->frames_per_second());
usd_stage->SetStartTimeCode(scene->r.sfra);
usd_stage->SetEndTimeCode(scene->r.efra);
}
/* For restoring the current frame after exporting animation is done. */
const int orig_frame = scene->r.cfra;
/* Ensure Python types for invoking hooks are registered. */
register_hook_converters();
pxr::VtValue upAxis = pxr::VtValue(pxr::UsdGeomTokens->z);
if (params.convert_orientation) {
if (params.up_axis == IO_AXIS_X) {
upAxis = pxr::VtValue(pxr::UsdGeomTokens->x);
}
else if (params.up_axis == IO_AXIS_Y) {
upAxis = pxr::VtValue(pxr::UsdGeomTokens->y);
}
}
usd_stage->SetMetadata(pxr::UsdGeomTokens->upAxis, upAxis);
const double meters_per_unit = get_meters_per_unit(params);
pxr::UsdGeomSetStageMetersPerUnit(usd_stage, meters_per_unit);
ensure_root_prim(usd_stage, params);
USDHierarchyIterator iter(bmain, depsgraph, usd_stage, params);
worker_status->progress = 0.11f;
worker_status->do_update = true;
if (params.export_animation) {
/* Writing the animated frames is not 100% of the work, here it's assumed to be 75% of it. */
float progress_per_frame = 0.75f / std::max(1, (scene->r.efra - scene->r.sfra + 1));
int exported_frame_count = 0;
for (float frame = scene->r.sfra; frame <= scene->r.efra; frame++) {
if (G.is_break || worker_status->stop) {
break;
}
/* Update the scene for the next frame to render. */
scene->r.cfra = int(frame);
scene->r.subframe = frame - scene->r.cfra;
BKE_scene_graph_update_for_newframe(depsgraph);
iter.set_export_frame(frame);
iter.iterate_and_write();
/* Check if we need to perform an incremental save. A value of 0 will never trigger. */
exported_frame_count++;
if (exported_frame_count == params.incremental_frames) {
usd_stage->GetRootLayer()->Save();
exported_frame_count = 0;
}
worker_status->progress += progress_per_frame;
worker_status->do_update = true;
}
}
else {
/* If we're not animating, a single iteration over all objects is enough. */
iter.iterate_and_write();
}
worker_status->progress = 0.86f;
worker_status->do_update = true;
iter.release_writers();
if (params.export_shapekeys || params.export_armatures) {
iter.process_usd_skel();
}
/* Creating dome lights should be called after writers have
* completed, to avoid a name collision when creating the light
* prim. */
if (params.convert_world_material) {
world_material_to_dome_light(params, scene, usd_stage);
}
/* Set the default prim if it doesn't exist */
if (!usd_stage->GetDefaultPrim()) {
/* Use TraverseAll since it's guaranteed to be depth first and will get the first top level
* prim, and is less verbose than getting the PseudoRoot + iterating its children. */
for (auto prim : usd_stage->TraverseAll()) {
usd_stage->SetDefaultPrim(prim);
break;
}
}
/* Write accessibility information to the default prim. */
write_root_accessibility_information(usd_stage, params);
if (params.use_instancing) {
process_scene_graph_instances(params, usd_stage);
}
call_export_hooks(depsgraph, &iter, params.worker_status->reports);
worker_status->progress = 0.88f;
worker_status->do_update = true;
/* Finish up by going back to the keyframe that was current before we started. */
if (scene->r.cfra != orig_frame) {
scene->r.cfra = orig_frame;
BKE_scene_graph_update_for_newframe(depsgraph);
}
worker_status->progress = 0.9f;
worker_status->do_update = true;
return usd_stage;
}
static void export_startjob(void *customdata, wmJobWorkerStatus *worker_status)
{
ExportJobData *data = static_cast<ExportJobData *>(customdata);
data->export_ok = false;
data->start_time = timeit::Clock::now();
G.is_rendering = true;
if (data->wm) {
WM_locked_interface_set(data->wm, true);
}
G.is_break = false;
worker_status->progress = 0.01f;
worker_status->do_update = true;
/* Evaluate the depsgraph for exporting.
*
* Note that, unlike with its building, this is expected to be safe to perform from worker
* thread, since UI is locked during export, so there should not be any more changes in the Main
* original data concurrently done from the main thread at this point. All necessary (deferred)
* changes are expected to have been triggered and processed during depsgraph building in
* #USD_export. */
BKE_scene_graph_update_tagged(data->depsgraph, data->bmain);
worker_status->progress = 0.1f;
worker_status->do_update = true;
data->params.worker_status = worker_status;
pxr::UsdStageRefPtr usd_stage = export_to_stage(
data->params, data->depsgraph, data->unarchived_filepath);
if (!usd_stage) {
/* This happens when the USD JSON files cannot be found. When that happens,
* the USD library doesn't know it has the functionality to write USDA and
* USDC files, and creating a new UsdStage fails. */
BKE_reportf(worker_status->reports,
RPT_ERROR,
"USD Export: unable to find suitable USD plugin to write %s",
data->unarchived_filepath);
return;
}
/* Traverse the point instancer to make sure the prototype referenced by nested point instancers
* are also marked as over. */
std::vector<pxr::UsdPrim> proto_list;
for (const pxr::UsdPrim &prim : usd_stage->Traverse()) {
if (!prim.IsA<pxr::UsdGeomPointInstancer>()) {
continue;
}
pxr::UsdGeomPointInstancer instancer(prim);
pxr::SdfPathVector targets;
if (instancer.GetPrototypesRel().GetTargets(&targets)) {
for (const pxr::SdfPath &wrapper_path : targets) {
collect_point_instancer_prototypes_and_set_extent(
instancer, usd_stage, wrapper_path, proto_list);
}
}
}
/* The standard way is to mark the point instancer's prototypes as over. Reference in OpenUSD:
* https://openusd.org/docs/api/class_usd_geom_point_instancer.html#:~:text=place%20them%20under%20a%20prim%20that%20is%20just%20an%20%22over%22
*/
for (pxr::UsdPrim &proto : proto_list) {
proto.SetSpecifier(pxr::SdfSpecifierOver);
}
usd_stage->GetRootLayer()->Save();
data->export_ok = true;
worker_status->progress = 1.0f;
worker_status->do_update = true;
}
static void export_endjob_usdz_cleanup(const ExportJobData *data)
{
if (!BLI_exists(data->unarchived_filepath)) {
return;
}
char dir[FILE_MAX];
BLI_path_split_dir_part(data->unarchived_filepath, dir, FILE_MAX);
char usdc_temp_dir[FILE_MAX];
BLI_path_join(usdc_temp_dir, FILE_MAX, BKE_tempdir_session(), "USDZ", SEP_STR);
BLI_assert_msg(BLI_strcasecmp(dir, usdc_temp_dir) == 0,
"USD Export: Attempting to delete directory that doesn't match the expected "
"temporary directory for usdz export.");
BLI_delete(usdc_temp_dir, true, true);
}
static void export_endjob(void *customdata)
{
ExportJobData *data = static_cast<ExportJobData *>(customdata);
DEG_graph_free(data->depsgraph);
if (data->targets_usdz()) {
/* NOTE: call to #perform_usdz_conversion has to be done here instead of the main threaded
* worker callback (#export_startjob) because USDZ conversion requires changing the current
* working directory. This is not safe to do from a non-main thread. Once the USD library fix
* this weird requirement, this call can be moved back at the end of #export_startjob, and not
* block the main user interface anymore. */
bool usd_conversion_success = perform_usdz_conversion(data);
if (!usd_conversion_success) {
data->export_ok = false;
}
export_endjob_usdz_cleanup(data);
}
if (!data->export_ok && BLI_exists(data->unarchived_filepath)) {
BLI_delete(data->unarchived_filepath, false, false);
}
G.is_rendering = false;
if (data->wm) {
WM_locked_interface_set(data->wm, false);
}
report_job_duration(data);
}
/**
* To create a USDZ file, we must first create a `.usd/a/c` file and then covert it to `.usdz`.
* The temporary files will be created in Blender's temporary session storage.
* The `.usdz` file will then be moved to `job->usdz_filepath`.
*/
static void create_temp_path_for_usdz_export(const char *filepath, io::usd::ExportJobData *job)
{
char usdc_file[FILE_MAX];
STRNCPY(usdc_file, BLI_path_basename(filepath));
if (BLI_path_extension_check(usdc_file, ".usdz")) {
BLI_path_extension_replace(usdc_file, sizeof(usdc_file), ".usdc");
}
char usdc_temp_filepath[FILE_MAX];
BLI_path_join(usdc_temp_filepath, FILE_MAX, BKE_tempdir_session(), "USDZ", usdc_file);
STRNCPY(job->unarchived_filepath, usdc_temp_filepath);
STRNCPY(job->usdz_filepath, filepath);
}
static void set_job_filepath(io::usd::ExportJobData *job, const char *filepath)
{
if (BLI_path_extension_check_n(filepath, ".usdz", nullptr)) {
create_temp_path_for_usdz_export(filepath, job);
return;
}
STRNCPY(job->unarchived_filepath, filepath);
job->usdz_filepath[0] = '\0';
}
bool USD_export(const bContext *C,
const char *filepath,
const USDExportParams *params,
bool as_background_job,
ReportList *reports)
{
if (!io::usd::export_params_valid(*params)) {
return false;
}
ViewLayer *view_layer = CTX_data_view_layer(C);
Scene *scene = CTX_data_scene(C);
io::usd::ExportJobData *job = MEM_new<io::usd::ExportJobData>("ExportJobData");
job->bmain = CTX_data_main(C);
job->wm = CTX_wm_manager(C);
job->scene = scene;
job->export_ok = false;
set_job_filepath(job, filepath);
ED_editors_flush_edits(job->bmain);
job->depsgraph = DEG_graph_new(job->bmain, scene, view_layer, params->evaluation_mode);
job->params = *params;
/* Construct the depsgraph for exporting.
*
* Has to be done from main thread currently, as it may affect Main original data (e.g. when
* doing deferred update of the view-layers, see #112534 for details). */
if (job->params.collection[0]) {
Collection *collection = reinterpret_cast<Collection *>(
BKE_libblock_find_name(job->bmain, ID_GR, job->params.collection));
if (!collection) {
BKE_reportf(reports,
RPT_ERROR,
"USD Export: Unable to find collection '%s'",
job->params.collection);
return false;
}
DEG_graph_build_from_collection(job->depsgraph, collection);
}
else {
DEG_graph_build_from_view_layer(job->depsgraph);
}
bool export_ok = false;
if (as_background_job) {
wmJob *wm_job = WM_jobs_get(job->wm,
CTX_wm_window(C),
scene,
"Exporting USD...",
WM_JOB_PROGRESS,
WM_JOB_TYPE_USD_EXPORT);
/* setup job */
WM_jobs_customdata_set(
wm_job, job, [](void *j) { MEM_delete(static_cast<io::usd::ExportJobData *>(j)); });
WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_FRAME, NC_SCENE | ND_FRAME);
WM_jobs_callbacks(wm_job, io::usd::export_startjob, nullptr, nullptr, io::usd::export_endjob);
WM_jobs_start(CTX_wm_manager(C), wm_job);
}
else {
wmJobWorkerStatus worker_status = {};
/* Use the operator's reports in non-background case. */
worker_status.reports = reports;
io::usd::export_startjob(job, &worker_status);
io::usd::export_endjob(job);
export_ok = job->export_ok;
MEM_delete(job);
}
return export_ok;
}
int USD_get_version()
{
/* USD 19.11 defines:
*
* #define PXR_MAJOR_VERSION 0
* #define PXR_MINOR_VERSION 19
* #define PXR_PATCH_VERSION 11
* #define PXR_VERSION 1911
*
* So the major version is implicit/invisible in the public version number.
*/
return PXR_VERSION;
}
double get_meters_per_unit(const USDExportParams &params)
{
double result;
switch (params.convert_scene_units) {
case SceneUnits::Centimeters:
result = 0.01;
break;
case SceneUnits::Millimeters:
result = 0.001;
break;
case SceneUnits::Kilometers:
result = 1000.0;
break;
case SceneUnits::Inches:
result = 0.0254;
break;
case SceneUnits::Feet:
result = 0.3048;
break;
case SceneUnits::Yards:
result = 0.9144;
break;
case SceneUnits::Custom:
result = double(params.custom_meters_per_unit);
break;
default:
result = 1.0;
break;
}
return result;
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,669 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "IO_types.hh"
#include "usd.hh"
#include "usd_hook.hh"
#include "usd_reader_domelight.hh"
#include "usd_reader_geom.hh"
#include "usd_reader_prim.hh"
#include "usd_reader_stage.hh"
#include "BKE_cachefile.hh"
#include "BKE_collection.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_timeit.hh"
#include "BLT_translation.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DNA_cachefile_types.h"
#include "DNA_collection_types.h"
#include "DNA_layer_types.h"
#include "DNA_listBase.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_windowmanager_types.h"
#include "ED_undo.hh"
#include "MEM_guardedalloc.h"
#include "WM_api.hh"
#include "WM_types.hh"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <fmt/core.h>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
static CacheArchiveHandle *handle_from_stage_reader(USDStageReader *reader)
{
return reinterpret_cast<CacheArchiveHandle *>(reader);
}
static USDStageReader *stage_reader_from_handle(CacheArchiveHandle *handle)
{
return reinterpret_cast<USDStageReader *>(handle);
}
static bool gather_objects_paths(const pxr::UsdPrim &object,
ListBaseT<CacheObjectPath> *object_paths)
{
if (!object.IsValid()) {
return false;
}
for (const pxr::UsdPrim &childPrim : object.GetChildren()) {
gather_objects_paths(childPrim, object_paths);
}
CacheObjectPath *usd_path = MEM_new<CacheObjectPath>("CacheObjectPath");
STRNCPY(usd_path->path, object.GetPrimPath().GetString().c_str());
BLI_addtail(object_paths, usd_path);
return true;
}
enum {
USD_NO_ERROR = 0,
USD_ARCHIVE_FAIL,
};
struct ImportJobData {
bContext *C;
Main *bmain;
Scene *scene;
wmWindowManager *wm;
char filepath[FILE_MAX];
USDImportParams params;
USDStageReader *archive;
char error_code;
bool was_canceled;
bool import_ok;
bool is_background_job;
timeit::TimePoint start_time;
CacheFile *cache_file;
};
static void report_job_duration(const ImportJobData *data)
{
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
fmt::print("USD import of '{}' took ", data->filepath);
timeit::print_duration(duration);
fmt::print("\n");
}
static void import_startjob(void *customdata, wmJobWorkerStatus *worker_status)
{
ImportJobData *data = static_cast<ImportJobData *>(customdata);
data->was_canceled = false;
data->archive = nullptr;
data->start_time = timeit::Clock::now();
data->cache_file = nullptr;
data->params.worker_status = worker_status;
if (data->wm) {
WM_locked_interface_set(data->wm, true);
}
G.is_break = false;
BLI_path_abs(data->filepath, BKE_main_blendfile_path_from_global());
pxr::UsdStagePopulationMask pop_mask;
for (const std::string &mask_token : pxr::TfStringTokenize(data->params.prim_path_mask, ",;")) {
pxr::SdfPath prim_path(mask_token);
if (!prim_path.IsEmpty()) {
pop_mask.Add(prim_path);
}
}
pxr::UsdStageRefPtr stage = pop_mask.IsEmpty() ?
pxr::UsdStage::Open(data->filepath) :
pxr::UsdStage::OpenMasked(data->filepath, pop_mask);
if (!stage) {
BKE_reportf(worker_status->reports,
RPT_ERROR,
"USD Import: unable to open stage to read %s",
data->filepath);
data->import_ok = false;
data->error_code = USD_ARCHIVE_FAIL;
return;
}
worker_status->progress = 0.05f;
worker_status->do_update = true;
if (G.is_break) {
data->was_canceled = true;
return;
}
double scene_scale = data->params.scale;
if (data->params.apply_unit_conversion_scale) {
scene_scale *= pxr::UsdGeomGetStageMetersPerUnit(stage);
}
/* Callback function to lazily create a cache file when converting
* time varying data. */
auto get_cache_file = [data, scene_scale]() {
if (!data->cache_file) {
data->cache_file = static_cast<CacheFile *>(
BKE_cachefile_add(data->bmain, BLI_path_basename(data->filepath)));
/* Decrement the ID ref-count because it is going to be incremented for each
* modifier and constraint that it will be attached to, so since currently
* it is not used by anyone, its use count will off by one. */
id_us_min(&data->cache_file->id);
data->cache_file->is_sequence = data->params.is_sequence;
data->cache_file->scale = scene_scale;
STRNCPY(data->cache_file->filepath, data->filepath);
if (data->params.relative_path && !BLI_path_is_rel(data->cache_file->filepath)) {
BLI_path_rel(data->cache_file->filepath, BKE_main_blendfile_path_from_global());
}
}
return data->cache_file;
};
USDStageReader *archive = new USDStageReader(stage, data->params, get_cache_file);
data->archive = archive;
/* Ensure Python types for invoking hooks are registered. */
register_hook_converters();
archive->find_material_import_hook_sources();
worker_status->progress = 0.1f;
worker_status->do_update = true;
if (G.is_break) {
data->was_canceled = true;
return;
}
archive->collect_readers();
worker_status->progress = 0.15f;
worker_status->do_update = true;
if (G.is_break) {
data->was_canceled = true;
return;
}
if (data->params.import_lights && data->params.create_world_material &&
!archive->dome_light_readers().is_empty())
{
/* NOTE: Since Blender does not have dome light objects, we need to modify the Scene directly
* in order to setup environment lighting. */
if (data->scene) {
USDDomeLightReader *dome_light_reader = archive->dome_light_readers().first();
dome_light_reader->create_object(data->scene, data->bmain);
}
}
if (data->params.import_materials && data->params.import_all_materials) {
archive->import_all_materials(data->bmain);
}
worker_status->progress = 0.2f;
worker_status->do_update = true;
/* Sort readers by name: when creating a lot of objects in Blender,
* it is much faster if the order is sorted by name. */
archive->sort_readers();
worker_status->progress = 0.25f;
worker_status->do_update = true;
const float size = float(archive->readers().size());
size_t i = 0;
/* Create blender objects. */
for (USDPrimReader *reader : archive->readers()) {
reader->create_object(data->bmain);
worker_status->progress = 0.25f + 0.25f * (++i / size);
worker_status->do_update = true;
if (G.is_break) {
data->was_canceled = true;
return;
}
}
/* Setup parenthood and read actual object data. */
i = 0;
for (USDPrimReader *reader : archive->readers()) {
Object *ob = reader->object();
reader->read_object_data(data->bmain, 0.0);
USDPrimReader *parent = reader->parent();
if (parent == nullptr) {
ob->parent = nullptr;
}
else {
ob->parent = parent->object();
}
worker_status->progress = 0.5f + 0.5f * (++i / size);
worker_status->do_update = true;
if (G.is_break) {
data->was_canceled = true;
return;
}
}
if (data->params.import_skeletons) {
archive->process_armature_modifiers();
}
data->import_ok = !data->was_canceled;
worker_status->progress = 1.0f;
worker_status->do_update = true;
}
static void import_endjob(void *customdata)
{
ImportJobData *data = static_cast<ImportJobData *>(customdata);
/* Delete objects on cancellation. */
if (data->was_canceled && data->archive) {
for (const USDPrimReader *reader : data->archive->readers()) {
/* It's possible that cancellation occurred between the creation of
* the reader and the creation of the Blender object. */
if (Object *ob = reader->object()) {
BKE_id_free_us(data->bmain, ob);
}
}
}
else if (data->archive) {
Collection *collection_dst = nullptr;
if (data->scene) {
/* Set scene animation range. */
if (data->params.set_frame_range) {
const pxr::UsdStageRefPtr stage = data->archive->stage();
data->scene->r.sfra = stage->GetStartTimeCode();
data->scene->r.efra = stage->GetEndTimeCode();
}
ViewLayer *view_layer = CTX_data_view_layer(data->C);
/* Create a new collection if required. */
if (data->params.create_collection) {
char display_name[MAX_ID_NAME - 2];
BLI_path_to_display_name(
display_name, sizeof(display_name), BLI_path_basename(data->filepath));
Collection *import_collection = BKE_collection_add(
data->bmain, data->scene->master_collection, display_name);
DEG_id_tag_update(&import_collection->id, ID_RECALC_SYNC_TO_EVAL);
DEG_relations_tag_update(data->bmain);
BKE_view_layer_synced_ensure(*data->bmain, data->scene, view_layer);
view_layer->active_collection = BKE_layer_collection_first_from_scene_collection(
view_layer, import_collection);
}
BKE_view_layer_base_deselect_all(*data->bmain, data->scene, view_layer);
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
if (!ID_IS_EDITABLE(lc->collection)) {
BKE_report(
data->params.worker_status->reports,
RPT_WARNING,
"Could not find an editable collection in current scene, imported data will not "
"be instantiated");
}
collection_dst = lc->collection;
}
/* Create prototype collections for instancing. */
data->archive->create_proto_collections(data->bmain, collection_dst);
/* Add all objects to the collection. */
for (const USDPrimReader *reader : data->archive->readers()) {
if (reader->is_in_proto()) {
/* Skip prototype prims, as these are added to prototype collections. */
continue;
}
Object *ob = reader->object();
if (!ob) {
continue;
}
BKE_collection_object_add(data->bmain, collection_dst, ob);
}
/* Sync and do the view layer operations. */
if (data->scene) {
ViewLayer *view_layer = CTX_data_view_layer(data->C);
BKE_view_layer_synced_ensure(*data->bmain, data->scene, view_layer);
bool has_instantiated_object = false;
bool has_uninstantiated_object = false;
for (const USDPrimReader *reader : data->archive->readers()) {
Object *ob = reader->object();
if (!ob) {
continue;
}
Base *base = BKE_view_layer_base_find(view_layer, ob);
if (!base) {
/* Object not instantiated in current viewlayer. */
has_uninstantiated_object = true;
continue;
}
has_instantiated_object = true;
/* TODO: is setting active needed? */
BKE_view_layer_base_select_and_set_active(view_layer, base);
DEG_id_tag_update(&collection_dst->id, ID_RECALC_SYNC_TO_EVAL);
DEG_id_tag_update_ex(data->bmain,
&ob->id,
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS);
}
if (has_instantiated_object && has_uninstantiated_object) {
CLOG_ERROR(&LOG, "Some imported objects were not instantiated, while others were");
}
DEG_id_tag_update(&data->scene->id, ID_RECALC_BASE_FLAGS);
}
DEG_relations_tag_update(data->bmain);
if (data->params.import_materials && data->params.import_all_materials) {
data->archive->fake_users_for_unused_materials();
}
data->archive->call_material_import_hooks(data->bmain);
call_import_hooks(data->archive, data->params.worker_status->reports);
if (data->is_background_job) {
/* Blender already returned from the import operator, so we need to store our own extra undo
* step. */
ED_undo_push(data->C, "USD Import Finished");
}
}
if (data->wm) {
WM_locked_interface_set(data->wm, false);
}
switch (data->error_code) {
default:
case USD_NO_ERROR:
data->import_ok = !data->was_canceled;
break;
case USD_ARCHIVE_FAIL:
BKE_report(data->params.worker_status->reports,
RPT_ERROR,
"Could not open USD archive for reading, see console for detail");
break;
}
WM_main_add_notifier(NC_ID | NA_ADDED, nullptr);
report_job_duration(data);
}
static void import_freejob(void *user_data)
{
ImportJobData *data = static_cast<ImportJobData *>(user_data);
delete data->archive;
delete data;
}
bool USD_import(bContext *C,
const char *filepath,
const USDImportParams *params,
bool as_background_job,
ReportList *reports)
{
/* Using new here since `MEM_*` functions do not call constructor to properly initialize data. */
ImportJobData *job = new ImportJobData();
job->C = C;
job->bmain = CTX_data_main(C);
job->scene = CTX_data_scene(C); // May be null
job->wm = CTX_wm_manager(C); // May be null
job->import_ok = false;
job->is_background_job = as_background_job;
STRNCPY(job->filepath, filepath);
job->error_code = USD_NO_ERROR;
job->was_canceled = false;
job->archive = nullptr;
job->params = *params;
G.is_break = false;
bool import_ok = false;
if (as_background_job) {
wmJob *wm_job = WM_jobs_get(CTX_wm_manager(C),
CTX_wm_window(C),
CTX_data_scene(C),
"Importing USD...",
WM_JOB_PROGRESS,
WM_JOB_TYPE_USD_IMPORT);
/* setup job */
WM_jobs_customdata_set(wm_job, job, import_freejob);
WM_jobs_timer(wm_job, 0.1, NC_SCENE, NC_SCENE);
WM_jobs_callbacks(wm_job, import_startjob, nullptr, nullptr, import_endjob);
WM_jobs_start(CTX_wm_manager(C), wm_job);
}
else {
wmJobWorkerStatus worker_status = {};
/* Use the operator's reports in non-background case. */
worker_status.reports = reports;
import_startjob(job, &worker_status);
import_endjob(job);
import_ok = job->import_ok;
import_freejob(job);
}
return import_ok;
}
/* TODO(makowalski): Extend this function with basic validation that the
* USD reader is compatible with the type of the given (currently unused) 'ob'
* Object parameter, similar to the logic in get_abc_reader() in the
* Alembic importer code. */
static USDPrimReader *get_usd_reader(CacheReader *reader,
const Object * /*ob*/,
const char **r_err_str)
{
USDPrimReader *usd_reader = reinterpret_cast<USDPrimReader *>(reader);
pxr::UsdPrim iobject = usd_reader->prim();
if (!iobject.IsValid()) {
*r_err_str = RPT_("Invalid object: verify object path");
return nullptr;
}
return usd_reader;
}
USDMeshReadParams create_mesh_read_params(const double motion_sample_time, const int read_flags)
{
USDMeshReadParams params = {};
params.motion_sample_time = motion_sample_time;
params.read_flags = read_flags;
return params;
}
void USD_read_geometry(CacheReader *reader,
const Object *ob,
bke::GeometrySet &geometry_set,
const USDMeshReadParams params,
const char **r_err_str)
{
USDGeomReader *usd_reader = dynamic_cast<USDGeomReader *>(get_usd_reader(reader, ob, r_err_str));
if (usd_reader == nullptr) {
return;
}
usd_reader->read_geometry(geometry_set, params, r_err_str);
}
bool USD_mesh_topology_changed(CacheReader *reader,
const Object *ob,
const Mesh *existing_mesh,
const double time,
const char **r_err_str)
{
USDGeomReader *usd_reader = dynamic_cast<USDGeomReader *>(get_usd_reader(reader, ob, r_err_str));
if (usd_reader == nullptr) {
return false;
}
return usd_reader->topology_changed(existing_mesh, time);
}
CacheReader *CacheReader_open_usd_object(CacheArchiveHandle *handle,
CacheReader *reader,
Object *object,
const char *object_path)
{
if (object_path[0] == '\0') {
return reader;
}
USDStageReader *archive = stage_reader_from_handle(handle);
if (!archive || !archive->valid()) {
return reader;
}
if (reader) {
USD_CacheReader_free(reader);
}
pxr::UsdPrim prim = archive->stage()->GetPrimAtPath(pxr::SdfPath(object_path));
if (!prim) {
return nullptr;
}
/* TODO(makowalski): The handle does not have the proper import params or settings. */
USDPrimReader *usd_reader = archive->create_reader(prim);
if (usd_reader == nullptr) {
/* This object is not supported. */
return nullptr;
}
if (!usd_reader->valid()) {
/* This object is invalid for some reason. */
return nullptr;
}
usd_reader->object(object);
usd_reader->incref();
return reinterpret_cast<CacheReader *>(usd_reader);
}
void USD_CacheReader_free(CacheReader *reader)
{
USDPrimReader *usd_reader = reinterpret_cast<USDPrimReader *>(reader);
usd_reader->decref();
if (usd_reader->refcount() == 0) {
delete usd_reader;
}
}
CacheArchiveHandle *USD_create_handle(Main * /*bmain*/,
const char *filepath,
ListBaseT<CacheObjectPath> *object_paths)
{
pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(filepath);
if (!stage) {
return nullptr;
}
USDImportParams params{};
USDStageReader *stage_reader = new USDStageReader(stage, params);
if (object_paths) {
gather_objects_paths(stage->GetPseudoRoot(), object_paths);
}
return handle_from_stage_reader(stage_reader);
}
void USD_free_handle(CacheArchiveHandle *handle)
{
USDStageReader *stage_reader = stage_reader_from_handle(handle);
delete stage_reader;
}
void USD_get_transform(CacheReader *reader, float4x4 &r_mat_world, float time, float scale)
{
if (!reader) {
return;
}
const USDXformReader *usd_reader = reinterpret_cast<USDXformReader *>(reader);
bool is_constant = false;
/* Convert from the local matrix we obtain from USD to world coordinates
* for Blender. This conversion is done here rather than by Blender due to
* work around the non-standard interpretation of CONSTRAINT_SPACE_LOCAL in
* BKE_constraint_mat_convertspace(). */
Object *object = usd_reader->object();
if (object->parent == nullptr) {
/* No parent, so local space is the same as world space. */
usd_reader->read_matrix(r_mat_world, time, scale, &is_constant);
return;
}
float4x4 mat_parent;
BKE_object_get_parent_matrix(object, object->parent, mat_parent.ptr());
float4x4 mat_local;
usd_reader->read_matrix(mat_local, time, scale, &is_constant);
r_mat_world = mat_parent * float4x4(object->parentinv);
r_mat_world *= mat_local;
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_colorspace_utils.hh"
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/usd/colorSpaceAPI.h>
#include "BLI_string_utf8.h"
#include "DNA_image_types.h"
#include "IMB_colormanagement.hh"
namespace blender::io::usd {
namespace usdtokens {
static const pxr::TfToken sourceColorSpace("sourceColorSpace", pxr::TfToken::Immortal);
static const pxr::TfToken auto_("auto", pxr::TfToken::Immortal);
static const pxr::TfToken sRGB("sRGB", pxr::TfToken::Immortal);
static const pxr::TfToken data("data", pxr::TfToken::Immortal);
static const pxr::TfToken raw("raw", pxr::TfToken::Immortal);
static const pxr::TfToken RAW("RAW", pxr::TfToken::Immortal);
} // namespace usdtokens
pxr::TfToken colorspace_scene_linear_interop_id()
{
const char *scene_linear_name = IMB_colormanagement_role_colorspace_name_get(
COLOR_ROLE_SCENE_LINEAR);
const ColorSpace *cs = IMB_colormanagement_space_get_named(scene_linear_name);
StringRefNull interop_id = (cs) ? IMB_colormanagement_space_get_interop_id(cs) : "";
return (interop_id.is_empty()) ? pxr::TfToken() : pxr::TfToken(interop_id);
}
void colorspace_apply_to_prim(const pxr::UsdPrim &prim)
{
const pxr::TfToken interop_id = colorspace_scene_linear_interop_id();
if (interop_id.IsEmpty()) {
return;
}
pxr::UsdColorSpaceAPI cs_api = pxr::UsdColorSpaceAPI::Apply(prim);
cs_api.CreateColorSpaceNameAttr(pxr::VtValue(interop_id));
}
static const ColorSpace *colorspace_from_attr(const pxr::UsdAttribute &attr)
{
pxr::TfToken cs_name = pxr::UsdColorSpaceAPI::ComputeColorSpaceName(attr);
if (cs_name.IsEmpty()) {
return nullptr;
}
const ColorSpace *cs = IMB_colormanagement_space_get_named(cs_name.GetText());
if (!cs || IMB_colormanagement_space_is_scene_linear(cs) ||
IMB_colormanagement_space_is_data(cs))
{
return nullptr;
}
return cs;
}
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr, pxr::GfVec3f &color)
{
const ColorSpace *cs = colorspace_from_attr(attr);
if (cs) {
IMB_colormanagement_colorspace_to_scene_linear_v3(color.data(), cs);
}
}
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr, ColorGeometry4f &color)
{
const ColorSpace *cs = colorspace_from_attr(attr);
if (cs) {
IMB_colormanagement_colorspace_to_scene_linear_v4(&color.r, false, cs);
}
}
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr,
MutableSpan<ColorGeometry4f> colors)
{
const ColorSpace *cs = colorspace_from_attr(attr);
if (cs && !colors.is_empty()) {
IMB_colormanagement_colorspace_to_scene_linear(&colors[0].r, colors.size(), 1, 4, cs, false);
}
}
static pxr::TfToken get_source_color_space(const pxr::UsdShadeShader &usd_shader)
{
if (!usd_shader) {
return pxr::TfToken();
}
pxr::UsdShadeInput color_space_input = usd_shader.GetInput(usdtokens::sourceColorSpace);
if (!color_space_input) {
return pxr::TfToken();
}
pxr::VtValue color_space_val;
if (color_space_input.Get(&color_space_val) && color_space_val.IsHolding<pxr::TfToken>()) {
return color_space_val.UncheckedGet<pxr::TfToken>();
}
return pxr::TfToken();
}
void colorspace_to_image_texture(const pxr::UsdShadeShader &usd_shader,
const pxr::UsdShadeInput &file_input,
const bool is_data,
Image *image)
{
/* Set texture color space.
* TODO(makowalski): For now, just checking for RAW color space,
* assuming sRGB otherwise, but more complex logic might be
* required if the color space is "auto". */
pxr::TfToken color_space = get_source_color_space(usd_shader);
if (color_space.IsEmpty()) {
color_space = file_input.GetAttr().GetColorSpace();
}
if (color_space.IsEmpty()) {
color_space = pxr::UsdColorSpaceAPI::ComputeColorSpaceName(usd_shader.GetPrim());
}
if (color_space.IsEmpty()) {
/* At this point, assume the "auto" space and translate accordingly. */
color_space = usdtokens::auto_;
}
if (color_space == usdtokens::auto_) {
/* If it's auto, determine whether to apply color correction based
* on incoming connection (passed in from outer functions). */
STRNCPY_UTF8(image->colorspace_settings.name,
IMB_colormanagement_role_colorspace_name_get(is_data ? COLOR_ROLE_DATA :
COLOR_ROLE_DEFAULT_BYTE));
}
else if (color_space == usdtokens::sRGB) {
STRNCPY_UTF8(image->colorspace_settings.name, IMB_colormanagement_srgb_colorspace_name_get());
}
/* Due to there being a lot of non-compliant USD assets out there, this is
* a special case where we need to check for different spellings here.
* On write, we are *only* using the correct, lower-case "raw" token. */
else if (ELEM(color_space, usdtokens::data, usdtokens::RAW, usdtokens::raw)) {
STRNCPY_UTF8(image->colorspace_settings.name,
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_DATA));
}
else {
const ColorSpace *cs = IMB_colormanagement_space_get_named(color_space.GetText());
if (cs) {
STRNCPY_UTF8(image->colorspace_settings.name, IMB_colormanagement_colorspace_get_name(cs));
}
}
}
void colorspace_from_image_texture(const Image *image, pxr::UsdShadeShader &shader)
{
if (!image) {
return;
}
/* Write sourceColorSpace input for backward compatibility with older USD readers. */
if (IMB_colormanagement_space_name_is_data(image->colorspace_settings.name)) {
shader.CreateInput(usdtokens::sourceColorSpace, pxr::SdfValueTypeNames->Token)
.Set(usdtokens::raw);
}
else if (IMB_colormanagement_space_name_is_srgb(image->colorspace_settings.name)) {
shader.CreateInput(usdtokens::sourceColorSpace, pxr::SdfValueTypeNames->Token)
.Set(usdtokens::sRGB);
}
/* Write ColorSpaceAPI with the interop ID, which supports any colorspace. */
const ColorSpace *cs = IMB_colormanagement_space_get_named(image->colorspace_settings.name);
if (cs) {
StringRefNull interop_id = IMB_colormanagement_space_get_interop_id(cs);
if (!interop_id.is_empty()) {
pxr::UsdColorSpaceAPI cs_api = pxr::UsdColorSpaceAPI::Apply(shader.GetPrim());
cs_api.CreateColorSpaceNameAttr(pxr::VtValue(pxr::TfToken(interop_id)));
}
}
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_color.hh"
#include "BLI_span.hh"
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/tf/token.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdShade/shader.h>
namespace blender {
struct Image;
}
namespace blender::io::usd {
/** Get the interop ID for tagging exported USD stages. */
pxr::TfToken colorspace_scene_linear_interop_id();
/** Tag a prim with the scene linear color space. */
void colorspace_apply_to_prim(const pxr::UsdPrim &prim);
/** Convert an imported USD color to scene linear. */
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr, pxr::GfVec3f &color);
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr, ColorGeometry4f &color);
/** Convert imported USD color array to scene linear. */
void colorspace_attr_to_scene_linear(const pxr::UsdAttribute &attr,
MutableSpan<ColorGeometry4f> colors);
/** Set the colorspace on an exported USD texture shader from a Blender image. */
void colorspace_from_image_texture(const Image *image, pxr::UsdShadeShader &shader);
/** Set the Blender image colorspace from an imported USD texture shader. */
void colorspace_to_image_texture(const pxr::UsdShadeShader &usd_shader,
const pxr::UsdShadeInput &file_input,
bool is_data,
Image *image);
} // namespace blender::io::usd

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/common.h>
#include <functional>
namespace blender {
struct Depsgraph;
struct Main;
struct Image;
struct ImageUser;
struct Scene;
namespace io::usd {
class USDHierarchyIterator;
struct USDExporterContext {
Main *bmain;
Depsgraph *depsgraph;
const pxr::UsdStageRefPtr stage;
const pxr::SdfPath usd_path;
/**
* Wrap a function which returns the current time code
* for export. This is necessary since the context
* may be used for exporting an animation over a sequence
* of frames.
*/
std::function<pxr::UsdTimeCode()> get_time_code;
const USDExportParams &export_params;
std::string export_file_path;
std::function<std::string(Main *, Scene *, Image *, ImageUser *)> export_image_fn;
/** Optional callback for skel/shape-key path registration (used by USDPointInstancerWriter). */
std::function<void(const Object *, const pxr::SdfPath &)> add_skel_mapping_fn;
USDHierarchyIterator *hierarchy_iterator;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,36 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_hash.hh"
#include <pxr/base/tf/token.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/valueTypeName.h>
namespace blender {
template<> struct DefaultHash<pxr::SdfValueTypeName> {
uint64_t operator()(const pxr::SdfValueTypeName &value) const
{
return value.GetHash();
}
};
template<> struct DefaultHash<pxr::TfToken> {
uint64_t operator()(const pxr::TfToken &value) const
{
return value.Hash();
}
};
template<> struct DefaultHash<pxr::SdfPath> {
uint64_t operator()(const pxr::SdfPath &value) const
{
return value.GetHash();
}
};
} // namespace blender

View File

@@ -0,0 +1,505 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd.hh"
#include "usd_armature_utils.hh"
#include "usd_blend_shape_utils.hh"
#include "usd_hash_types.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_skel_convert.hh"
#include "usd_skel_root_utils.hh"
#include "usd_utils.hh"
#include "usd_writer_abstract.hh"
#include "usd_writer_armature.hh"
#include "usd_writer_camera.hh"
#include "usd_writer_curves.hh"
#include "usd_writer_hair.hh"
#include "usd_writer_light.hh"
#include "usd_writer_mesh.hh"
#include "usd_writer_metaball.hh"
#include "usd_writer_pointinstancer.hh"
#include "usd_writer_points.hh"
#include "usd_writer_text.hh"
#include "usd_writer_transform.hh"
#include "usd_writer_volume.hh"
#include <string>
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "BLI_assert.h"
#include "DNA_layer_types.h"
#include "DNA_object_types.h"
#include "WM_types.hh"
namespace blender::io::usd {
USDHierarchyIterator::USDHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
pxr::UsdStageRefPtr stage,
const USDExportParams &params)
: AbstractHierarchyIterator(bmain, depsgraph), stage_(stage), params_(params)
{
}
bool USDHierarchyIterator::mark_as_weak_export(const Object *object) const
{
if (params_.selected_objects_only && (object->base_flag & BASE_SELECTED) == 0) {
return true;
}
switch (object->type) {
case OB_EMPTY:
/* Always assume empties are being exported intentionally. */
return false;
case OB_MESH:
case OB_MBALL:
case OB_FONT:
case OB_SURF:
return !params_.export_meshes;
case OB_CAMERA:
return !params_.export_cameras;
case OB_LAMP:
return !params_.export_lights;
case OB_CURVES_LEGACY:
case OB_CURVES:
return !params_.export_curves;
case OB_VOLUME:
return !params_.export_volumes;
case OB_ARMATURE:
return !params_.export_armatures;
case OB_POINTCLOUD:
return !params_.export_points;
default:
/* Assume weak for all other types. */
return true;
}
}
void USDHierarchyIterator::release_writer(AbstractHierarchyWriter *writer)
{
delete static_cast<USDAbstractWriter *>(writer);
}
std::string USDHierarchyIterator::make_valid_name(const std::string &name) const
{
return make_safe_name(name, params_.allow_unicode);
}
void USDHierarchyIterator::process_usd_skel() const
{
skel_export_chaser(stage_,
armature_export_map_,
skinned_mesh_export_map_,
shape_key_mesh_export_map_,
depsgraph_);
create_skel_roots(stage_, params_);
}
void USDHierarchyIterator::set_export_frame(float frame_nr)
{
/* The USD stage is already set up to have FPS time-codes per frame. */
export_time_ = pxr::UsdTimeCode(frame_nr);
}
USDExporterContext USDHierarchyIterator::create_usd_export_context(const HierarchyContext *context)
{
pxr::SdfPath path;
if (!params_.root_prim_path.empty()) {
path = pxr::SdfPath(params_.root_prim_path + context->export_path);
}
else {
path = pxr::SdfPath(context->export_path);
}
if (params_.merge_parent_xform && context->is_object_data_context && !context->is_parent) {
bool can_merge_with_xform = true;
if (params_.export_shapekeys && is_mesh_with_shape_keys(context->object)) {
can_merge_with_xform = false;
}
if (params_.use_instancing && (context->is_prototype() || context->is_instance())) {
can_merge_with_xform = false;
}
if (can_merge_with_xform) {
path = path.GetParentPath();
}
}
/* Returns the same path that was passed to `stage_` object during it's creation (via
* `pxr::UsdStage::CreateNew` function). */
const pxr::SdfLayerHandle root_layer = stage_->GetRootLayer();
const std::string export_file_path = root_layer->GetRealPath();
auto get_time_code = [this]() { return this->export_time_; };
USDExporterContext exporter_context = USDExporterContext{bmain_,
depsgraph_,
stage_,
path,
get_time_code,
params_,
export_file_path,
nullptr,
nullptr,
this};
/* Provides optional skel mapping hook. Now it's been used in USDPointInstancerWriter for write
* base layer. */
exporter_context.add_skel_mapping_fn = [this](const Object *obj, const pxr::SdfPath &usd_path) {
this->add_usd_skel_export_mapping(obj, usd_path);
};
return exporter_context;
}
bool USDHierarchyIterator::determine_point_instancers(const HierarchyContext *context)
{
if (!context) {
return true;
}
if (context->object->type == OB_ARMATURE) {
return true;
}
bool is_referencing_self = false;
if (context->is_point_instancer()) {
/* Mark the point instancer's children as a point instance. */
USDExporterContext usd_export_context = create_usd_export_context(context);
const ExportChildren *children = graph_children(context);
pxr::SdfPath instancer_path;
if (!params_.root_prim_path.empty()) {
instancer_path = pxr::SdfPath(params_.root_prim_path + context->export_path);
}
else {
instancer_path = pxr::SdfPath(context->export_path);
}
if (children != nullptr) {
for (HierarchyContext *child_context : *children) {
if (!child_context->original_export_path.empty()) {
const pxr::SdfPath parent_export_path(context->export_path);
const pxr::SdfPath children_original_export_path(child_context->original_export_path);
/* Detect if the parent is referencing itself via a prototype. */
if (parent_export_path.HasPrefix(children_original_export_path)) {
is_referencing_self = true;
break;
}
}
pxr::SdfPath prototype_path;
if (child_context->is_instance() && child_context->duplicator != nullptr) {
/* When the current child context is point instancer's instance, use reference path
* (original_export_path) as the prototype path. */
if (!params_.root_prim_path.empty()) {
prototype_path = pxr::SdfPath(params_.root_prim_path +
child_context->original_export_path);
}
else {
prototype_path = pxr::SdfPath(child_context->original_export_path);
}
prototype_paths_.lookup_or_add(instancer_path, {})
.add(std::make_pair(prototype_path, child_context->object));
child_context->is_point_instance = true;
}
else {
/* When the current child context is point instancer's prototype, use its own export path
* (export_path) as the prototype path. */
if (!params_.root_prim_path.empty()) {
prototype_path = pxr::SdfPath(params_.root_prim_path + child_context->export_path);
}
else {
prototype_path = pxr::SdfPath(child_context->export_path);
}
prototype_paths_.lookup_or_add(instancer_path, {})
.add(std::make_pair(prototype_path, child_context->object));
child_context->is_point_proto = true;
}
}
}
/* MARK: If the "Instance on Points" node uses an Object as a prototype,
* but the "Object Info" node has not enabled the "As Instance" option,
* then the generated reference path is incorrect and refers to itself. */
if (is_referencing_self) {
BKE_reportf(
params_.worker_status->reports,
RPT_WARNING,
"One or more objects used as prototypes in 'Instance on Points' nodes either do not "
"have 'As Instance' enabled in their 'Object Info' nodes, or the prototype is the "
"base geometry input itself. Both cases prevent valid point instancer export. If it's "
"the former, enable 'As Instance' to avoid incorrect self-referencing.");
/* Clear any paths which had already been accumulated. */
Set<std::pair<pxr::SdfPath, Object *>> *paths = prototype_paths_.lookup_ptr(instancer_path);
if (paths) {
paths->clear();
}
for (HierarchyContext *child_context : *children) {
child_context->is_point_instance = false;
child_context->is_point_proto = false;
}
}
}
return !is_referencing_self;
}
AbstractHierarchyWriter *USDHierarchyIterator::create_transform_writer(
const HierarchyContext *context)
{
/* The transform writer is always called before data writers,
* so determine if the #Xform's children is a point instancer before writing data. */
if (params_.use_instancing) {
if (!determine_point_instancers(context)) {
/* If we could not determine that our point instancing setup is safe, we should not continue
* writing. Continuing would result in enormous amounts of USD warnings about cyclic
* references. */
return nullptr;
}
}
return new USDTransformWriter(create_usd_export_context(context));
}
AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const HierarchyContext *context)
{
USDExporterContext usd_export_context = create_usd_export_context(context);
USDAbstractWriter *data_writer = nullptr;
const Set<std::pair<pxr::SdfPath, Object *>> *proto_paths = prototype_paths_.lookup_ptr(
usd_export_context.usd_path.GetParentPath());
const bool use_point_instancing = context->is_point_instancer() &&
(proto_paths && !proto_paths->is_empty());
switch (context->object->type) {
case OB_MESH:
case OB_SURF:
/* NURBS surfaces are tessellated to a mesh during depsgraph evaluation,
* so they can be written through the standard mesh writer. */
if (usd_export_context.export_params.export_meshes) {
if (params_.use_instancing && use_point_instancing) {
USDExporterContext mesh_context = create_point_instancer_context(context,
usd_export_context);
std::unique_ptr<USDMeshWriter> mesh_writer = std::make_unique<USDMeshWriter>(
mesh_context);
data_writer = new USDPointInstancerWriter(
usd_export_context, *proto_paths, std::move(mesh_writer));
}
else {
data_writer = new USDMeshWriter(usd_export_context);
}
}
else {
return nullptr;
}
break;
case OB_CAMERA:
if (usd_export_context.export_params.export_cameras) {
data_writer = new USDCameraWriter(usd_export_context);
}
else {
return nullptr;
}
break;
case OB_LAMP:
if (usd_export_context.export_params.export_lights) {
data_writer = new USDLightWriter(usd_export_context);
}
else {
return nullptr;
}
break;
case OB_MBALL:
data_writer = new USDMetaballWriter(usd_export_context);
break;
case OB_FONT:
data_writer = new USDTextWriter(usd_export_context);
break;
case OB_CURVES_LEGACY:
case OB_CURVES:
if (usd_export_context.export_params.export_curves) {
if (params_.use_instancing && use_point_instancing) {
USDExporterContext curves_context = create_point_instancer_context(context,
usd_export_context);
std::unique_ptr<USDCurvesWriter> curves_writer = std::make_unique<USDCurvesWriter>(
curves_context);
data_writer = new USDPointInstancerWriter(
usd_export_context, *proto_paths, std::move(curves_writer));
}
else {
data_writer = new USDCurvesWriter(usd_export_context);
}
}
else {
return nullptr;
}
break;
case OB_VOLUME:
if (usd_export_context.export_params.export_volumes) {
data_writer = new USDVolumeWriter(usd_export_context);
}
else {
return nullptr;
}
break;
case OB_ARMATURE:
if (usd_export_context.export_params.export_armatures) {
data_writer = new USDArmatureWriter(usd_export_context);
}
else {
return nullptr;
}
break;
case OB_POINTCLOUD:
if (usd_export_context.export_params.export_points) {
if (params_.use_instancing && use_point_instancing) {
USDExporterContext point_cloud_context = create_point_instancer_context(
context, usd_export_context);
std::unique_ptr<USDPointsWriter> point_cloud_writer = std::make_unique<USDPointsWriter>(
point_cloud_context);
data_writer = new USDPointInstancerWriter(
usd_export_context, *proto_paths, std::move(point_cloud_writer));
}
else {
data_writer = new USDPointsWriter(usd_export_context);
}
}
else {
return nullptr;
}
break;
case OB_EMPTY:
case OB_SPEAKER:
case OB_LIGHTPROBE:
case OB_LATTICE:
case OB_GREASE_PENCIL:
return nullptr;
case OB_TYPE_MAX:
BLI_assert_msg(0, "OB_TYPE_MAX should not be used");
return nullptr;
default:
BLI_assert_unreachable();
return nullptr;
}
if (data_writer && !data_writer->is_supported(context)) {
delete data_writer;
return nullptr;
}
if (data_writer && (params_.export_armatures || params_.export_shapekeys)) {
add_usd_skel_export_mapping(context->object, data_writer->usd_path());
}
return data_writer;
}
AbstractHierarchyWriter *USDHierarchyIterator::create_hair_writer(const HierarchyContext *context)
{
if (!params_.export_hair) {
return nullptr;
}
return new USDHairWriter(create_usd_export_context(context));
}
AbstractHierarchyWriter *USDHierarchyIterator::create_particle_writer(
const HierarchyContext * /*context*/)
{
return nullptr;
}
bool USDHierarchyIterator::include_data_writers(const HierarchyContext *context) const
{
/* Don't generate data writers for instances. */
return !(params_.use_instancing && context->is_instance());
}
bool USDHierarchyIterator::include_child_writers(const HierarchyContext *context) const
{
/* Don't generate writers for children of instances. */
return !(params_.use_instancing && context->is_instance());
}
void USDHierarchyIterator::add_usd_skel_export_mapping(const Object *obj, const pxr::SdfPath &path)
{
if (params_.export_shapekeys && is_mesh_with_shape_keys(obj)) {
shape_key_mesh_export_map_.add(obj, path);
}
if (params_.export_armatures && obj->type == OB_ARMATURE) {
armature_export_map_.add(obj, path);
}
if (params_.export_armatures && obj->type == OB_MESH &&
can_export_skinned_mesh(*obj, depsgraph_))
{
skinned_mesh_export_map_.add(obj, path);
}
}
const Map<pxr::SdfPath, Vector<ID *>> &USDHierarchyIterator::get_exported_prim_map() const
{
return exported_prim_map_;
}
pxr::UsdStageRefPtr USDHierarchyIterator::get_stage() const
{
return stage_;
}
void USDHierarchyIterator::add_to_prim_map(const pxr::SdfPath &usd_path, const ID *id) const
{
if (!id) {
return;
}
ID *local_id = BKE_libblock_find_name(bmain_, GS(id->name), id->name + 2);
if (local_id) {
Vector<ID *> &id_list = exported_prim_map_.lookup_or_add_default(usd_path);
if (!id_list.contains(local_id)) {
id_list.append(local_id);
}
}
}
USDExporterContext USDHierarchyIterator::create_point_instancer_context(
const HierarchyContext *context, const USDExporterContext &export_context) const
{
BLI_assert(context && context->object);
std::string base_name = std::string(BKE_id_name(context->object->id)).append("_base");
std::string safe_name = make_safe_name(base_name, export_context.export_params.allow_unicode);
pxr::SdfPath base_path = export_context.usd_path.GetParentPath().AppendChild(
pxr::TfToken(safe_name));
return {export_context.bmain,
export_context.depsgraph,
export_context.stage,
base_path,
export_context.get_time_code,
export_context.export_params,
export_context.export_file_path,
export_context.export_image_fn,
export_context.add_skel_mapping_fn,
export_context.hierarchy_iterator};
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,96 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "IO_abstract_hierarchy_iterator.h"
#include "usd.hh"
#include "usd_exporter_context.hh"
#include "usd_skel_convert.hh"
#include <string>
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usd/timeCode.h>
namespace blender {
struct Depsgraph;
struct Main;
struct Object;
namespace io::usd {
using io::AbstractHierarchyIterator;
using io::AbstractHierarchyWriter;
using io::HierarchyContext;
class USDHierarchyIterator : public AbstractHierarchyIterator {
private:
const pxr::UsdStageRefPtr stage_;
pxr::UsdTimeCode export_time_;
const USDExportParams &params_;
ObjExportMap armature_export_map_;
ObjExportMap skinned_mesh_export_map_;
ObjExportMap shape_key_mesh_export_map_;
/*
* The field below is mutable because it is used to keep track
* of what the exporter is doing. This is necessary even when all
* the other export settings are to remain const.
*/
/* Map a USD prim path to a list of Blender IDs associated with that prim.
* This map is updated by writers during stage export. */
mutable Map<pxr::SdfPath, Vector<ID *>> exported_prim_map_;
/* Map prototype_paths[instancer path] = [
* (proto_path_1, proto_object_1), (proto_path_2, proto_object_2), ... ] */
Map<pxr::SdfPath, Set<std::pair<pxr::SdfPath, Object *>>> prototype_paths_;
public:
USDHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
pxr::UsdStageRefPtr stage,
const USDExportParams &params);
void set_export_frame(float frame_nr);
std::string make_valid_name(const std::string &name) const override;
void process_usd_skel() const;
/* Get the USD stage being exported to. */
pxr::UsdStageRefPtr get_stage() const;
/* Get the mapping of exported objects to their USD prim paths. */
const Map<pxr::SdfPath, Vector<ID *>> &get_exported_prim_map() const;
/* Add an ID to the prim map for a given USD path. */
void add_to_prim_map(const pxr::SdfPath &usd_path, const ID *id) const;
protected:
bool mark_as_weak_export(const Object *object) const override;
bool determine_point_instancers(const HierarchyContext *context);
AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) override;
void release_writer(AbstractHierarchyWriter *writer) override;
bool include_data_writers(const HierarchyContext *context) const override;
bool include_child_writers(const HierarchyContext *context) const override;
private:
USDExporterContext create_usd_export_context(const HierarchyContext *context);
USDExporterContext create_point_instancer_context(
const HierarchyContext *context, const USDExporterContext &usd_export_context) const;
void add_usd_skel_export_mapping(const Object *obj, const pxr::SdfPath &usd_path);
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,684 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_hook.hh"
#include "usd.hh"
#include "usd_api_hook.hh"
#include "usd_asset_utils.hh"
#include "usd_hash_types.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_reader_prim.hh"
#include "usd_reader_stage.hh"
#include "usd_writer_material.hh"
#include "BLI_map.hh"
#include "BLI_utildefines.h"
#include "BLI_vector.hh"
#include "BKE_lib_id.hh"
#include "BKE_report.hh"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "RNA_types.hh"
#include "bpy_rna.hh"
#include <list>
#include <memory>
#include <string>
#include <pxr/external/boost/python/call_method.hpp>
#include <pxr/external/boost/python/class.hpp>
#include <pxr/external/boost/python/dict.hpp>
#include <pxr/external/boost/python/import.hpp>
#include <pxr/external/boost/python/list.hpp>
#include <pxr/external/boost/python/ref.hpp>
#include <pxr/external/boost/python/return_value_policy.hpp>
#include <pxr/external/boost/python/to_python_converter.hpp>
#include <pxr/external/boost/python/tuple.hpp>
namespace blender {
using namespace pxr::pxr_boost;
namespace io::usd {
using USDHookList = std::list<std::unique_ptr<USDHook>>;
using ImportedPrimMap = Map<pxr::SdfPath, Vector<PointerRNA>>;
/* USD hook type declarations */
static USDHookList &hook_list()
{
static USDHookList hooks{};
return hooks;
}
void USD_register_hook(std::unique_ptr<USDHook> hook)
{
if (USD_find_hook_name(hook->idname)) {
/* The hook is already in the list. */
return;
}
/* Add hook type to the list. */
hook_list().push_back(std::move(hook));
}
void USD_unregister_hook(const USDHook *hook)
{
hook_list().remove_if(
[hook](const std::unique_ptr<USDHook> &item) { return item.get() == hook; });
}
USDHook *USD_find_hook_name(const char idname[])
{
/* sanity checks */
if (hook_list().empty() || (idname == nullptr) || (idname[0] == 0)) {
return nullptr;
}
USDHookList::iterator hook_iter = std::find_if(
hook_list().begin(), hook_list().end(), [idname](const std::unique_ptr<USDHook> &item) {
return STREQ(item->idname, idname);
});
return (hook_iter == hook_list().end()) ? nullptr : hook_iter->get();
}
/* Convert PointerRNA to a PyObject*. */
struct PointerRNAToPython {
/* We pass the argument by value because we need
* to obtain a non-const pointer to it. */
static PyObject *convert(PointerRNA ptr)
{
return pyrna_struct_CreatePyObject(&ptr);
}
};
/* Encapsulate arguments for scene export. */
class USDSceneExportContext {
private:
pxr::UsdStageRefPtr stage_;
PointerRNA depsgraph_ptr_;
const USDHierarchyIterator *hierarchy_iterator_ = nullptr;
public:
USDSceneExportContext(const USDHierarchyIterator *iter, Depsgraph *depsgraph)
: stage_(iter->get_stage()), hierarchy_iterator_(iter)
{
depsgraph_ptr_ = RNA_pointer_create_discrete(nullptr, RNA_Depsgraph, depsgraph);
}
pxr::UsdStageRefPtr get_stage() const
{
return stage_;
}
const PointerRNA &get_depsgraph() const
{
return depsgraph_ptr_;
}
python::dict get_prim_map() const
{
python::dict result;
const auto &exported_prim_map = hierarchy_iterator_->get_exported_prim_map();
exported_prim_map.foreach_item([&](const pxr::SdfPath &path, const Vector<ID *> &ids) {
python::list id_list;
for (ID *id : ids) {
if (id) {
PointerRNA ptr_rna = RNA_id_pointer_create(id);
id_list.append(ptr_rna);
}
}
result[path] = id_list;
});
return result;
}
};
/* Encapsulate arguments for scene import. */
class USDSceneImportContext {
private:
pxr::UsdStageRefPtr stage_;
ImportedPrimMap prim_map_;
python::dict *prim_map_dict_ = nullptr;
public:
USDSceneImportContext(pxr::UsdStageRefPtr in_stage, const ImportedPrimMap &in_prim_map)
: stage_(in_stage), prim_map_(in_prim_map)
{
}
void release()
{
delete prim_map_dict_;
}
pxr::UsdStageRefPtr get_stage() const
{
return stage_;
}
python::dict get_prim_map()
{
if (!prim_map_dict_) {
prim_map_dict_ = new python::dict;
prim_map_.foreach_item([&](const pxr::SdfPath &path, const Vector<PointerRNA> &ids) {
if (!prim_map_dict_->has_key(path)) {
(*prim_map_dict_)[path] = python::list();
}
python::list list = python::extract<python::list>((*prim_map_dict_)[path]);
for (const auto &ptr_rna : ids) {
list.append(ptr_rna);
}
});
}
return *prim_map_dict_;
}
};
/* Encapsulate arguments for material export. */
class USDMaterialExportContext {
private:
pxr::UsdStageRefPtr stage_;
USDExportParams params_ = {};
ReportList *reports_ = nullptr;
public:
USDMaterialExportContext(pxr::UsdStageRefPtr stage,
const USDExportParams &params,
ReportList *reports)
: stage_(stage), params_(params), reports_(reports)
{
}
pxr::UsdStageRefPtr get_stage() const
{
return stage_;
}
/**
* Returns the USD asset export path for the given texture image. The image will be copied
* to the export directory if exporting textures is enabled in the export options. The
* function may return an empty string in case of an error.
*/
std::string export_texture(python::object obj) const
{
ID *id;
if (!pyrna_id_FromPyObject(obj.ptr(), &id)) {
return "";
}
if (!id) {
return "";
}
if (GS(id->name) != ID_IM) {
return "";
}
Image *ima = reinterpret_cast<Image *>(id);
std::string asset_path = get_tex_image_asset_filepath(ima, stage_, params_);
if (params_.export_textures) {
io::usd::export_texture(ima, stage_, params_.overwrite_textures, reports_);
}
return asset_path;
}
};
/* Encapsulate arguments for material import. */
class USDMaterialImportContext {
private:
pxr::UsdStageRefPtr stage_;
USDImportParams params_ = {};
ReportList *reports_ = nullptr;
public:
USDMaterialImportContext(pxr::UsdStageRefPtr stage,
const USDImportParams &params,
ReportList *reports)
: stage_(stage), params_(params), reports_(reports)
{
}
pxr::UsdStageRefPtr get_stage() const
{
return stage_;
}
/**
* If the given texture asset path is a URI or is relative to a USDZ archive,
* attempt to copy the texture to the local file system and returns a `tuple[str, bool]`,
* containing the asset's local path and a boolean indicating whether the path references
* a temporary file (in the case where imported textures should be packed).
* The original asset path will be returned unchanged if it's already a local file
* or if it could not be copied to a local destination.
*/
python::tuple import_texture(const std::string &asset_path) const
{
if (!should_import_asset(asset_path)) {
/* This path does not need to be imported, so return it unchanged. */
return python::make_tuple(asset_path, false);
}
const char *textures_dir = params_.import_textures_mode == TexImportMode::Pack ?
temp_textures_dir() :
params_.import_textures_dir;
const TexNameCollisionMode name_collision_mode = params_.import_textures_mode ==
TexImportMode::Pack ?
TexNameCollisionMode::Overwrite :
params_.tex_name_collision_mode;
std::string import_path = import_asset(
asset_path, textures_dir, name_collision_mode, reports_);
if (import_path == asset_path) {
/* Path is unchanged. */
return python::make_tuple(asset_path, false);
}
const bool is_temporary = params_.import_textures_mode == TexImportMode::Pack;
return python::make_tuple(import_path, is_temporary);
}
};
void register_hook_converters()
{
static bool registered = false;
/* No need to register if there are no hooks. */
if (hook_list().empty()) {
return;
}
if (registered) {
return;
}
registered = true;
PyGILState_STATE gilstate = PyGILState_Ensure();
/* We must import these modules for the USD type converters to work. */
python::import("pxr.Usd");
python::import("pxr.UsdShade");
/* Register converter from PoinerRNA to a PyObject*. */
python::to_python_converter<PointerRNA, PointerRNAToPython>();
/* Register context class converters. */
python::class_<USDSceneExportContext>("USDSceneExportContext", python::no_init)
.def("get_stage", &USDSceneExportContext::get_stage)
.def("get_depsgraph",
&USDSceneExportContext::get_depsgraph,
python::return_value_policy<python::return_by_value>())
.def("get_prim_map", &USDSceneExportContext::get_prim_map);
python::class_<USDMaterialExportContext>("USDMaterialExportContext", python::no_init)
.def("get_stage", &USDMaterialExportContext::get_stage)
.def("export_texture", &USDMaterialExportContext::export_texture);
python::class_<USDSceneImportContext>("USDSceneImportContext", python::no_init)
.def("get_stage", &USDSceneImportContext::get_stage)
.def("get_prim_map", &USDSceneImportContext::get_prim_map);
python::class_<USDMaterialImportContext>("USDMaterialImportContext", python::no_init)
.def("get_stage", &USDMaterialImportContext::get_stage)
.def("import_texture", &USDMaterialImportContext::import_texture);
PyGILState_Release(gilstate);
}
/* Retrieve and report the current Python error. */
static void handle_python_error(USDHook *hook, ReportList *reports)
{
if (!PyErr_Occurred()) {
return;
}
PyErr_Print();
BKE_reportf(reports,
RPT_ERROR,
"An exception occurred invoking USD hook '%s'. Please see the console for details",
hook->name);
}
/* Abstract base class to facilitate calling a function with a given
* signature defined by the registered USDHook classes. Subclasses
* override virtual methods to specify the hook function name and to
* call the hook with the required arguments.
*/
class USDHookInvoker {
private:
ReportList *reports_;
public:
explicit USDHookInvoker(ReportList *reports) : reports_(reports) {}
virtual ~USDHookInvoker() = default;
/* Attempt to call the function, if defined by the registered hooks. */
void call()
{
if (hook_list().empty()) {
return;
}
PyGILState_STATE gilstate = PyGILState_Ensure();
init_in_gil();
/* Iterate over the hooks and invoke the hook function, if it's defined. */
USDHookList::const_iterator hook_iter = hook_list().begin();
while (hook_iter != hook_list().end()) {
/* XXX: Not sure if this is necessary:
* Advance the iterator before invoking the callback, to guard
* against the unlikely error where the hook is de-registered in
* the callback. This would prevent a crash due to the iterator
* getting invalidated. */
USDHook *hook = hook_iter->get();
++hook_iter;
if (!hook->rna_ext.data) {
continue;
}
try {
PyObject *hook_obj = static_cast<PyObject *>(hook->rna_ext.data);
if (!PyObject_HasAttrString(hook_obj, function_name())) {
continue;
}
call_hook(hook_obj);
}
catch (python::error_already_set const &) {
handle_python_error(hook, reports_);
}
catch (...) {
BKE_reportf(
reports_, RPT_ERROR, "An exception occurred invoking USD hook '%s'", hook->name);
}
}
release_in_gil();
PyGILState_Release(gilstate);
}
protected:
/* Override to specify the name of the function to be called. */
virtual const char *function_name() const = 0;
/* Override to call the function of the given object with the
* required arguments, e.g.,
*
* python::call_method<void>(hook_obj, function_name(), arg1, arg2); */
virtual void call_hook(PyObject *hook_obj) = 0;
virtual void init_in_gil() {};
virtual void release_in_gil() {};
};
class OnExportInvoker final : public USDHookInvoker {
private:
USDSceneExportContext hook_context_;
public:
OnExportInvoker(const USDHierarchyIterator *iter, Depsgraph *depsgraph, ReportList *reports)
: USDHookInvoker(reports), hook_context_(iter, depsgraph)
{
}
private:
const char *function_name() const override
{
return "on_export";
}
void call_hook(PyObject *hook_obj) override
{
python::call_method<bool>(hook_obj, function_name(), python::ref(hook_context_));
}
};
class OnMaterialExportInvoker final : public USDHookInvoker {
private:
USDMaterialExportContext hook_context_;
pxr::UsdShadeMaterial usd_material_;
PointerRNA material_ptr_;
public:
OnMaterialExportInvoker(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDExportParams &export_params,
ReportList *reports)
: USDHookInvoker(reports),
hook_context_(stage, export_params, reports),
usd_material_(usd_material)
{
material_ptr_ = RNA_pointer_create_discrete(nullptr, RNA_Material, material);
}
private:
const char *function_name() const override
{
return "on_material_export";
}
void call_hook(PyObject *hook_obj) override
{
python::call_method<bool>(
hook_obj, function_name(), python::ref(hook_context_), material_ptr_, usd_material_);
}
};
class OnImportInvoker final : public USDHookInvoker {
private:
USDSceneImportContext hook_context_;
public:
OnImportInvoker(pxr::UsdStageRefPtr stage, const ImportedPrimMap &prim_map, ReportList *reports)
: USDHookInvoker(reports), hook_context_(stage, prim_map)
{
}
private:
const char *function_name() const override
{
return "on_import";
}
void call_hook(PyObject *hook_obj) override
{
python::call_method<bool>(hook_obj, function_name(), python::ref(hook_context_));
}
void release_in_gil() override
{
hook_context_.release();
}
};
class MaterialImportPollInvoker final : public USDHookInvoker {
private:
USDMaterialImportContext hook_context_;
pxr::UsdShadeMaterial usd_material_;
bool result_ = false;
public:
MaterialImportPollInvoker(pxr::UsdStageRefPtr stage,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports)
: USDHookInvoker(reports),
hook_context_(stage, import_params, reports),
usd_material_(usd_material)
{
}
bool result() const
{
return result_;
}
private:
const char *function_name() const override
{
return "material_import_poll";
}
void call_hook(PyObject *hook_obj) override
{
/* If we already know that one of the registered hook classes can import the material
* because it returned true in a previous invocation of the callback, we skip the call. */
if (!result_) {
result_ = python::call_method<bool>(
hook_obj, function_name(), python::ref(hook_context_), usd_material_);
}
}
};
class OnMaterialImportInvoker final : public USDHookInvoker {
private:
USDMaterialImportContext hook_context_;
pxr::UsdShadeMaterial usd_material_;
PointerRNA material_ptr_;
bool result_ = false;
public:
OnMaterialImportInvoker(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports)
: USDHookInvoker(reports),
hook_context_(stage, import_params, reports),
usd_material_(usd_material)
{
material_ptr_ = RNA_pointer_create_discrete(nullptr, RNA_Material, material);
}
bool result() const
{
return result_;
}
private:
const char *function_name() const override
{
return "on_material_import";
}
void call_hook(PyObject *hook_obj) override
{
result_ |= python::call_method<bool>(
hook_obj, function_name(), python::ref(hook_context_), material_ptr_, usd_material_);
}
};
void call_export_hooks(Depsgraph *depsgraph, const USDHierarchyIterator *iter, ReportList *reports)
{
if (hook_list().empty()) {
return;
}
OnExportInvoker on_export(iter, depsgraph, reports);
on_export.call();
}
void call_material_export_hooks(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDExportParams &export_params,
ReportList *reports)
{
if (hook_list().empty()) {
return;
}
OnMaterialExportInvoker on_material_export(
stage, material, usd_material, export_params, reports);
on_material_export.call();
}
void call_import_hooks(USDStageReader *archive, ReportList *reports)
{
if (hook_list().empty()) {
return;
}
const Vector<USDPrimReader *> &readers = archive->readers();
const ImportSettings &settings = archive->settings();
ImportedPrimMap prim_map;
/* Resize based on the typical scenario where there will be both Object and Data entries
* in the map in addition to each material. */
prim_map.reserve((readers.size() * 2) + settings.usd_path_to_mat.size());
for (const USDPrimReader *reader : readers) {
Object *ob = reader->object();
prim_map.lookup_or_add_default(reader->object_prim_path())
.append(RNA_id_pointer_create(&ob->id));
if (ob->data) {
prim_map.lookup_or_add_default(reader->data_prim_path())
.append(RNA_id_pointer_create(ob->data));
}
}
settings.usd_path_to_mat.foreach_item([&prim_map](const pxr::SdfPath &path, Material *mat) {
prim_map.lookup_or_add_default(path).append(RNA_id_pointer_create(&mat->id));
});
OnImportInvoker on_import(archive->stage(), prim_map, reports);
on_import.call();
}
bool have_material_import_hook(pxr::UsdStageRefPtr stage,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports)
{
if (hook_list().empty()) {
return false;
}
MaterialImportPollInvoker poll(stage, usd_material, import_params, reports);
poll.call();
return poll.result();
}
bool call_material_import_hooks(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports)
{
if (hook_list().empty()) {
return false;
}
OnMaterialImportInvoker on_material_import(
stage, material, usd_material, import_params, reports);
on_material_import.call();
return on_material_import.result();
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/usd/common.h>
#include <pxr/usd/usdShade/material.h>
namespace blender {
struct Depsgraph;
struct Material;
struct ReportList;
namespace io::usd {
struct USDExportParams;
class USDHierarchyIterator;
struct USDImportParams;
class USDStageReader;
/** Ensure classes and type converters necessary for invoking import and export hooks
* are registered. */
void register_hook_converters();
/** Call the 'on_export' chaser function defined in the registered #USDHook classes. */
void call_export_hooks(Depsgraph *depsgraph,
const USDHierarchyIterator *iter,
ReportList *reports);
/** Call the 'on_material_export' hook functions defined in the registered #USDHook classes. */
void call_material_export_hooks(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDExportParams &export_params,
ReportList *reports);
/** Call the 'on_import' chaser function defined in the registered USDHook classes. */
void call_import_hooks(USDStageReader *archive, ReportList *reports);
/** Returns true if there is a registered #USDHook class that can convert the given material. */
bool have_material_import_hook(pxr::UsdStageRefPtr stage,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports);
/** Call the 'on_material_import' hook functions defined in the registered #USDHook classes.
* Returns true if any of the hooks were successful, false otherwise. */
bool call_material_import_hooks(pxr::UsdStageRefPtr stage,
Material *material,
const pxr::UsdShadeMaterial &usd_material,
const USDImportParams &import_params,
ReportList *reports);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,192 @@
/* SPDX-FileCopyrightText: 2025 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_instancing_utils.hh"
#include "usd.hh"
#include "usd_hash_types.hh"
#include "usd_utils.hh"
#include "BLI_map.hh"
#include "BLI_set.hh"
#include <pxr/usd/sdf/copyUtils.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/primCompositionQuery.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/references.h>
#include <string>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
/* We need an ordered map so we use std::map. */
using PathMap = std::map<pxr::SdfPath, pxr::SdfPath>;
using PathSet = Set<pxr::SdfPath>;
/* Map an instanceable prim path to a list of prototype prim paths. */
using ReferencesMap = Map<pxr::SdfPath, Vector<pxr::SdfPath>>;
/* Convert the given prototype prim to an instance by deleting its children and making
* it an instanceable reference to the prim at ref_path. */
static void convert_proto_to_instance(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &proto_path,
const pxr::SdfPath &ref_path)
{
pxr::UsdPrim proto_prim = stage->GetPrimAtPath(proto_path);
if (!proto_prim) {
CLOG_ERROR(&LOG, "Couldn't find prototype prim %s", proto_path.GetAsString().c_str());
return;
}
/* Collect child paths. */
pxr::SdfPathVector child_paths;
pxr::UsdPrimSiblingRange children = proto_prim.GetFilteredChildren(
pxr::Usd_PrimFlagsPredicate());
for (const auto &child_prim : children) {
child_paths.push_back(child_prim.GetPath());
}
/* Remove children from the sage. */
for (const pxr::SdfPath &child_path : child_paths) {
stage->RemovePrim(child_path);
}
proto_prim.GetReferences().AddInternalReference(pxr::SdfPath(ref_path));
proto_prim.SetInstanceable(true);
}
void process_scene_graph_instances(const USDExportParams &export_params, pxr::UsdStageRefPtr stage)
{
if (!stage) {
return;
}
/* Collect paths to instanceable references and prototypes. */
PathSet protos;
/* Map an instance to the prototypes it references. */
ReferencesMap references_map;
pxr::UsdPrimRange range(stage->GetPseudoRoot());
for (pxr::UsdPrim prim : range) {
if (prim.IsInstanceable()) {
/* Get the prototypes referenced by this prim. */
pxr::UsdPrimCompositionQuery query = pxr::UsdPrimCompositionQuery::GetDirectReferences(prim);
Vector<pxr::SdfPath> references;
for (const auto &arc : query.GetCompositionArcs()) {
pxr::SdfPath target_prim_path = arc.GetTargetPrimPath();
protos.add(target_prim_path);
references.append(target_prim_path);
}
references_map.add(prim.GetPath(), references);
}
}
if (protos.is_empty()) {
/* No prototypes to move. */
return;
}
/* Map an original prototype path to the location where it will be copied. */
PathMap proto_to_copy_map;
std::string protos_root_str(export_params.root_prim_path);
protos_root_str += "/prototypes";
pxr::SdfPath protos_root_path = get_unique_path(stage, protos_root_str);
/* Create the abstract prim under which prototypes will be copied. */
if (!stage->CreateClassPrim(protos_root_path)) {
CLOG_ERROR(&LOG, "Couldn't create class prim %s.", protos_root_path.GetAsString().c_str());
return;
}
/*
* For each original prototype, create a placeholder Xform prim under the protos root
* which will be the new location where the prototype will be copied.
*/
for (const pxr::SdfPath &proto_path : protos) {
pxr::SdfPath copy_path = protos_root_path;
copy_path = copy_path.AppendChild(proto_path.GetNameToken());
copy_path = get_unique_path(stage, copy_path.GetAsString());
/* Create the placeholder prim. */
static pxr::TfToken xform_type_tok("Xform");
pxr::UsdPrim dest_prim = stage->DefinePrim(copy_path, xform_type_tok);
if (!dest_prim) {
CLOG_ERROR(&LOG,
"Couldn't create destination prim %s for copying prototype %s",
copy_path.GetAsString().c_str(),
proto_path.GetAsString().c_str());
continue;
}
/* Record where original prototype path will be copied. */
proto_to_copy_map.insert(std::make_pair(proto_path, dest_prim.GetPath()));
}
/* Update all references to point to new prototype locations. */
for (const auto item : references_map.items()) {
pxr::SdfPath inst_path = item.key;
pxr::UsdPrim inst_prim = stage->GetPrimAtPath(item.key);
if (!inst_prim) {
CLOG_ERROR(&LOG, "Couldn't get prim for instance %s.", inst_path.GetAsString().c_str());
continue;
}
/* Updated references pointing to new prototype locations. */
Vector<pxr::SdfPath> new_ref_targets;
const Vector<pxr::SdfPath> &ref_targets = item.value;
for (const pxr::SdfPath &target_path : ref_targets) {
PathMap::const_iterator iter = proto_to_copy_map.find(target_path);
if (iter != proto_to_copy_map.end()) {
new_ref_targets.append(iter->second);
}
}
/* Replace existing references with the updated ones. */
if (!new_ref_targets.is_empty()) {
pxr::UsdReferences refs = inst_prim.GetReferences();
refs.ClearReferences();
for (const pxr::SdfPath &target : new_ref_targets) {
refs.AddInternalReference(target);
}
}
}
/*
* Copy the original prototypes to their new locations and update
* the original prototype roots to be references to the new locations.
* Since prototypes may be nested, we must copy the most nested prototypes
* first by iterating backwards through the sorted prototype map.
*/
for (PathMap::reverse_iterator riter = proto_to_copy_map.rbegin();
riter != proto_to_copy_map.rend();
++riter)
{
const pxr::SdfPath &src_path = riter->first;
const pxr::SdfPath &dst_path = riter->second;
if (!pxr::SdfCopySpec(
stage->GetRootLayer(), riter->first, stage->GetRootLayer(), riter->second))
{
CLOG_WARN(&LOG,
"Couldn't copy prim %s to %s",
src_path.GetAsString().c_str(),
dst_path.GetAsString().c_str());
continue;
}
convert_proto_to_instance(stage, src_path, dst_path);
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2025 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/usd/common.h>
namespace blender::io::usd {
struct USDExportParams;
/**
* This function processes the USD stage generated by the USD hierarchy iterator to
* change scene graph instancing prototypes from defined USD prims to abstract prims.
*
* In the following example, instance /root/proto/Plane_0 references prototype prim
* /root/proto_001/Plane_0:
*
* def Xform "root"
* {
* def Xform "proto_001"
* {
* def Xform "Plane_0"
* {
* def Mesh "Plane"
* {
* }
* }
* }
*
* def Xform "proto"
* {
* def Xform "Plane_0" (
* instanceable = true
* prepend references = </root/proto_001/Plane_0>
* )
* {
* }
* }
* }
*
* The function will copy prototype /root/proto_001/Plane_0 under a new class prim
* named /root/prototypes and convert prim /root/proto_001/Plane_0 to be an instance referencing
* the copied abstract prim /root/prototypes/Plane_0.
*
* def Xform "root"
* {
* def Xform "proto"
* {
* def Xform "Plane_0" (
* instanceable = true
* prepend references = </root/prototypes/Plane_0>
* )
* {
* }
* }
*
* def Xform "proto_001"
* {
* def Xform "Plane_0" (
* instanceable = true
* references = </root/prototypes/Plane_0>
* )
* {
* }
* }
*
* class "prototypes"
* {
* def Xform "Plane_0"
* {
* def Mesh "Plane"
* {
* }
* }
* }
* }
*
* \param export_params: The export parameters
*
* \param stage: Pointer to the stage to process
*
*/
void process_scene_graph_instances(const USDExportParams &export_params,
pxr::UsdStageRefPtr stage);
} // namespace blender::io::usd

View File

@@ -0,0 +1,489 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_light_convert.hh"
#include "usd.hh"
#include "usd_asset_utils.hh"
#include "usd_colorspace_utils.hh"
#include "usd_private.hh"
#include "usd_utils.hh"
#include "usd_writer_material.hh"
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/tokens.h>
#include "BKE_image.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
#include "BLI_fileops.h"
#include "BLI_math_vector.h"
#include "BLI_path_utils.hh"
#include "BLI_span.hh"
#include "BLI_string_ref.hh"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"
#include "DNA_image_types.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "DNA_world_types.h"
#include <cmath>
#include <string>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace usdtokens {
// Attribute values.
static const pxr::TfToken pole_axis_z("Z", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace {
struct WorldNtreeSearchPayload {
const io::usd::USDExportParams &params;
pxr::UsdStageRefPtr stage;
WorldNtreeSearchPayload(const io::usd::USDExportParams &in_params, pxr::UsdStageRefPtr in_stage)
: params(in_params), stage(in_stage)
{
}
};
} // End anonymous namespace.
namespace io::usd {
/**
* Load the image at the given path. Handle packing and copying based in the import options.
* Return the opened image on success or a nullptr on failure.
*/
static Image *load_image(std::string tex_path, Main *bmain, const USDImportParams &params)
{
/* Optionally copy the asset if it's inside a USDZ package. */
const bool import_textures = params.import_textures_mode != TexImportMode::None &&
should_import_asset(tex_path);
std::string imported_file_source_path = tex_path;
if (import_textures) {
/* If we are packing the imported textures, we first write them
* to a temporary directory. */
const char *textures_dir = params.import_textures_mode == TexImportMode::Pack ?
temp_textures_dir() :
params.import_textures_dir;
const TexNameCollisionMode name_collision_mode = params.import_textures_mode ==
TexImportMode::Pack ?
TexNameCollisionMode::Overwrite :
params.tex_name_collision_mode;
tex_path = import_asset(tex_path, textures_dir, name_collision_mode, nullptr);
}
Image *image = BKE_image_load_exists(bmain, tex_path.c_str());
if (!image) {
return nullptr;
}
if (import_textures && imported_file_source_path != tex_path) {
ensure_usd_source_path_prop(imported_file_source_path, &image->id);
}
if (import_textures && params.import_textures_mode == TexImportMode::Pack &&
!BKE_image_has_packedfile(image))
{
BKE_image_packfiles(nullptr, image, ID_BLEND_PATH(bmain, &image->id));
if (BLI_is_dir(temp_textures_dir())) {
BLI_delete(temp_textures_dir(), true, true);
}
}
return image;
}
/* Create a new node of type 'new_node_type' and connect it
* as an upstream source to 'dst_node' with the given sockets. */
static bNode *append_node(bNode *dst_node,
int new_node_type,
const StringRef out_sock,
const StringRef in_sock,
bNodeTree *ntree,
float offset)
{
bNode *src_node = bke::node_add_static_node(nullptr, *ntree, new_node_type);
bke::node_add_link(*ntree,
*src_node,
*bke::node_find_socket(*src_node, SOCK_OUT, UString(out_sock)),
*dst_node,
*bke::node_find_socket(*dst_node, SOCK_IN, UString(in_sock)));
src_node->location[0] = dst_node->location[0] - offset;
src_node->location[1] = dst_node->location[1];
return src_node;
}
void world_material_to_dome_light(const USDExportParams &params,
const Scene *scene,
pxr::UsdStageRefPtr stage)
{
if (!(stage && scene && scene->world)) {
return;
}
WorldToDomeLight res;
world_material_to_dome_light(scene, res);
if (!(res.color_found || res.image)) {
/* No nodes to convert */
return;
}
std::string image_filepath;
if (res.image) {
/* Compute image filepath and export if needed. */
image_filepath = get_tex_image_asset_filepath(res.image, stage, params);
if (image_filepath.empty()) {
return;
}
if (params.export_textures) {
export_texture(res.image, stage, params.overwrite_textures);
}
}
/* Create USD dome light. */
pxr::SdfPath env_light_path = get_unique_path(stage, params.root_prim_path + "/env_light");
pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight::Define(stage, env_light_path);
colorspace_apply_to_prim(dome_light.GetPrim());
if (res.image) {
/* Use existing image texture file. */
dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(image_filepath));
/* Set optional color multiplication. */
if (res.mult_found) {
pxr::GfVec3f color_val(res.color_mult[0], res.color_mult[1], res.color_mult[2]);
dome_light.CreateColorAttr().Set(color_val);
}
/* Set transform. */
pxr::GfVec3d angles = res.transform.DecomposeRotation(
pxr::GfVec3d::ZAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::XAxis());
pxr::GfVec3f rot_vec(angles[2], angles[1], angles[0]);
pxr::UsdGeomXformCommonAPI xform_api(dome_light);
xform_api.SetRotate(rot_vec, pxr::UsdGeomXformCommonAPI::RotationOrderXYZ);
}
else if (res.color_found) {
/* If no texture is found export a solid color texture as a stand-in so that Hydra
* renderers don't throw errors. */
dome_light.CreateIntensityAttr().Set(res.intensity);
std::string source_path = cache_image_color(res.color);
const std::string base_path = stage->GetRootLayer()->GetRealPath();
char file_name[FILE_MAX];
BLI_path_split_file_part(source_path.c_str(), file_name, FILE_MAX);
char dest_path[FILE_MAX];
BLI_path_split_dir_part(base_path.c_str(), dest_path, FILE_MAX);
BLI_path_append_dir(dest_path, FILE_MAX, "textures");
BLI_dir_create_recursive(dest_path);
BLI_path_append(dest_path, FILE_MAX, file_name);
if (BLI_copy(source_path.c_str(), dest_path) != 0) {
CLOG_WARN(&LOG, "USD Export: Couldn't write world color image to %s", dest_path);
}
else {
BLI_path_join(dest_path, FILE_MAX, ".", "textures", file_name);
BLI_string_replace_char(dest_path, '\\', '/');
dome_light.CreateTextureFileAttr().Set(pxr::SdfAssetPath(dest_path));
}
}
}
/* Import the dome light as a world material. */
void dome_light_to_world_material(const USDImportParams &params,
Scene *scene,
Main *bmain,
const USDImportDomeLightData &dome_light_data,
const pxr::UsdPrim &prim,
const pxr::UsdTimeCode time)
{
if (!(scene && scene->world && prim)) {
return;
}
bNodeTree *ntree = scene->world->nodetree;
BLI_assert(ntree != nullptr);
bNode *output = nullptr;
bNode *bgshader = nullptr;
/* We never delete existing nodes, but we might disconnect them
* and move them out of the way. */
/* Look for the output and background shader nodes, which we will reuse. */
for (bNode *node : ntree->all_nodes()) {
if (node->type_legacy == SH_NODE_OUTPUT_WORLD) {
output = node;
}
else if (node->type_legacy == SH_NODE_BACKGROUND) {
bgshader = node;
}
else {
/* Move existing node out of the way. */
node->location[1] += 300;
}
}
/* Create the output and background shader nodes, if they don't exist. */
if (!output) {
output = bke::node_add_static_node(nullptr, *ntree, SH_NODE_OUTPUT_WORLD);
output->location[0] = 300.0f;
output->location[1] = 300.0f;
}
if (!bgshader) {
bgshader = append_node(output, SH_NODE_BACKGROUND, "Background", "Surface", ntree, 200);
/* Set the default background color. */
bNodeSocket *color_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Color"_ustr);
copy_v3_v3(color_sock->default_value_typed<bNodeSocketValueRGBA>()->value,
&scene->world->horr);
}
/* Make sure the first input to the shader node is disconnected. */
bNodeSocket *shader_input = bke::node_find_socket(*bgshader, SOCK_IN, "Color"_ustr);
if (shader_input && shader_input->link) {
bke::node_remove_link(ntree, *shader_input->link);
}
/* Set the background shader intensity. */
float intensity = dome_light_data.intensity * params.light_intensity_scale;
bNodeSocket *strength_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Strength"_ustr);
strength_sock->default_value_typed<bNodeSocketValueFloat>()->value = intensity;
if (!dome_light_data.has_tex) {
/* No texture file is authored on the dome light. Set the color, if it was authored,
* and return early. */
if (dome_light_data.has_color) {
bNodeSocket *color_sock = bke::node_find_socket(*bgshader, SOCK_IN, "Color"_ustr);
copy_v3_v3(color_sock->default_value_typed<bNodeSocketValueRGBA>()->value,
dome_light_data.color.data());
}
bke::node_set_active(*ntree, *output);
BKE_ntree_update_after_single_tree_change(*bmain, *ntree);
return;
}
/* If the light has authored color, create a color multiply node for the environment
* texture output. */
bNode *mult = nullptr;
if (dome_light_data.has_color) {
mult = append_node(bgshader, SH_NODE_VECTOR_MATH, "Vector", "Color", ntree, 200);
mult->custom1 = NODE_VECTOR_MATH_MULTIPLY;
/* Set the color in the vector math node's second socket. */
bNodeSocket *vec_sock = bke::node_find_socket(*mult, SOCK_IN, "Vector"_ustr);
if (vec_sock) {
vec_sock = vec_sock->next;
}
if (vec_sock) {
copy_v3_v3(vec_sock->default_value_typed<bNodeSocketValueVector>()->value,
dome_light_data.color.data());
}
else {
CLOG_WARN(&LOG, "Couldn't find vector multiply second vector socket");
}
}
bNode *tex = nullptr;
/* Append an environment texture node to the mult node, if it was created, or directly to
* the background shader. */
if (mult) {
tex = append_node(mult, SH_NODE_TEX_ENVIRONMENT, "Color", "Vector", ntree, 400);
}
else {
tex = append_node(bgshader, SH_NODE_TEX_ENVIRONMENT, "Color", "Color", ntree, 400);
}
bNode *mapping = append_node(tex, SH_NODE_MAPPING, "Vector", "Vector", ntree, 200);
append_node(mapping, SH_NODE_TEX_COORD, "Generated", "Vector", ntree, 200);
/* Load the texture image. */
const std::string &resolved_path = dome_light_data.tex_path.GetResolvedPath();
if (resolved_path.empty()) {
CLOG_WARN(&LOG,
"Couldn't get resolved path for asset %s",
dome_light_data.tex_path.GetAssetPath().c_str());
return;
}
Image *image = load_image(resolved_path, bmain, params);
if (!image) {
CLOG_WARN(&LOG, "Couldn't load image file %s", resolved_path.c_str());
return;
}
tex->id = &image->id;
/* Set the transform. */
pxr::UsdGeomXformCache xf_cache(time);
pxr::GfMatrix4d xf = xf_cache.GetLocalToWorldTransform(prim);
pxr::UsdStageRefPtr stage = prim.GetStage();
if (!stage) {
CLOG_WARN(&LOG, "Couldn't get stage for dome light %s", prim.GetPath().GetText());
return;
}
/* Note: This logic tries to produce identical results to `usdview` as of USD 25.05.
* However, `usdview` seems to handle Y-Up stages differently; some scenes match while others
* do not unless we keep the second conditional below (+90 on x-axis). */
const pxr::TfToken stage_up = pxr::UsdGeomGetStageUpAxis(stage);
const bool needs_stage_z_adjust = stage_up == pxr::UsdGeomTokens->z &&
ELEM(dome_light_data.pole_axis,
pxr::UsdLuxTokens->Z,
pxr::UsdLuxTokens->scene);
const bool needs_stage_y_adjust = stage_up == pxr::UsdGeomTokens->y &&
ELEM(dome_light_data.pole_axis, pxr::UsdLuxTokens->Z);
if (needs_stage_z_adjust || needs_stage_y_adjust) {
xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), 90.0));
}
else if (stage_up == pxr::UsdGeomTokens->y) {
/* Convert from Y-up to Z-up with a 90 degree rotation about the X-axis. */
xf *= pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0));
}
/* Rotate into Blender's frame of reference. */
xf = pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -90.0)) *
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -90.0)) * xf;
pxr::GfVec3d angles = xf.DecomposeRotation(
pxr::GfVec3d::XAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::ZAxis());
pxr::GfVec3f rot_vec(-angles[0], -angles[1], -angles[2]);
/* Convert degrees to radians. */
rot_vec *= M_PI / 180.0f;
if (bNodeSocket *socket = bke::node_find_socket(*mapping, SOCK_IN, "Rotation"_ustr)) {
bNodeSocketValueVector *rot_value = static_cast<bNodeSocketValueVector *>(
socket->default_value);
copy_v3_v3(rot_value->value, rot_vec.data());
}
bke::node_set_active(*ntree, *output);
DEG_id_tag_update(&ntree->id, ID_RECALC_NTREE_OUTPUT);
BKE_ntree_update_after_single_tree_change(*bmain, *ntree);
}
static bool node_search(bNode *fromnode, bNode * /*tonode*/, void *userdata, bool /*reversed*/)
{
if (!(userdata && fromnode)) {
return true;
}
WorldToDomeLight &res = *static_cast<WorldToDomeLight *>(userdata);
if (!res.color_found && fromnode->type_legacy == SH_NODE_BACKGROUND) {
/* Get light color and intensity */
const bNodeSocketValueRGBA *color_data = bke::node_find_socket(
*fromnode, SOCK_IN, "Color"_ustr)
->default_value_typed<bNodeSocketValueRGBA>();
const bNodeSocketValueFloat *strength_data =
bke::node_find_socket(*fromnode, SOCK_IN, "Strength"_ustr)
->default_value_typed<bNodeSocketValueFloat>();
res.color_found = true;
res.intensity = strength_data->value;
res.color[0] = color_data->value[0];
res.color[1] = color_data->value[1];
res.color[2] = color_data->value[2];
res.color[3] = 1.0f;
}
else if (!res.image && fromnode->type_legacy == SH_NODE_TEX_ENVIRONMENT) {
NodeTexImage *tex = static_cast<NodeTexImage *>(fromnode->storage);
res.image = reinterpret_cast<Image *>(fromnode->id);
res.iuser = &tex->iuser;
/* Always adjust for rotational differences between Blender and USD. */
res.transform =
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), 90.0)) *
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), 90.0));
}
else if (!res.image && !res.mult_found && fromnode->type_legacy == SH_NODE_VECTOR_MATH) {
if (fromnode->custom1 == NODE_VECTOR_MATH_MULTIPLY) {
res.mult_found = true;
bNodeSocket *vec_sock = bke::node_find_socket(*fromnode, SOCK_IN, "Vector"_ustr);
if (vec_sock) {
vec_sock = vec_sock->next;
}
if (vec_sock) {
copy_v3_v3(res.color_mult, vec_sock->default_value_typed<bNodeSocketValueVector>()->value);
}
}
}
else if (res.image && fromnode->type_legacy == SH_NODE_MAPPING) {
if (bNodeSocket *socket = bke::node_find_socket(*fromnode, SOCK_IN, "Rotation"_ustr)) {
const bNodeSocketValueVector *rot_value =
socket->default_value_typed<bNodeSocketValueVector>();
/* Convert radians to degrees. */
pxr::GfVec3f rot(rot_value->value);
rot *= 180.0f / M_PI;
res.transform *=
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 0.0, 1.0), -rot[2])) *
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(0.0, 1.0, 0.0), -rot[1])) *
pxr::GfMatrix4d().SetRotate(pxr::GfRotation(pxr::GfVec3d(1.0, 0.0, 0.0), -rot[0]));
}
}
return true;
}
void world_material_to_dome_light(const Scene *scene, WorldToDomeLight &res)
{
/* Find the world output. */
scene->world->nodetree->ensure_topology_cache();
const Span<const bNode *> bsdf_nodes = scene->world->nodetree->nodes_by_type(
"ShaderNodeOutputWorld"_ustr);
for (const bNode *node : bsdf_nodes) {
if (node->flag & NODE_DO_OUTPUT) {
bke::node_chain_iterator(scene->world->nodetree, node, node_search, &res, true);
break;
}
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/usd/common.h>
namespace blender {
struct bNode;
struct bNodeTree;
struct Main;
struct Scene;
namespace io::usd {
struct USDExportParams;
struct USDImportParams;
/* This struct contains all DomeLight attribute needed to
* create a world environment */
struct USDImportDomeLightData {
float intensity;
pxr::GfVec3f color;
pxr::SdfAssetPath tex_path;
pxr::TfToken pole_axis;
bool has_color;
bool has_tex;
};
/**
* If the Blender scene has an environment texture,
* export it as a USD dome light.
*/
void world_material_to_dome_light(const USDExportParams &params,
const Scene *scene,
pxr::UsdStageRefPtr stage);
void dome_light_to_world_material(const USDImportParams &params,
Scene *scene,
Main *bmain,
const USDImportDomeLightData &dome_light_data,
const pxr::UsdPrim &prim,
const pxr::UsdTimeCode time = 0.0);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,150 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_mesh_utils.hh"
#include "usd_attribute_utils.hh"
#include "usd_colorspace_utils.hh"
#include "usd_hash_types.hh"
#include "BKE_attribute.hh"
#include "DNA_mesh_types.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
template<typename USDT>
static void read_face_display_color(Mesh *mesh,
const pxr::UsdGeomPrimvar &primvar,
const pxr::TfToken &pv_name,
const pxr::UsdTimeCode time)
{
const pxr::VtArray<USDT> usd_colors = get_primvar_array<USDT>(primvar, time);
if (usd_colors.empty()) {
return;
}
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
const bke::AttrDomain color_domain = bke::AttrDomain::Corner;
const StringRef attr_name(pv_name.GetString());
if (primvar.GetInterpolation() == pxr::UsdGeomTokens->constant) {
ColorGeometry4f value = detail::convert_value<USDT, ColorGeometry4f>(usd_colors[0]);
colorspace_attr_to_scene_linear(primvar.GetAttr(), value);
set_single_value(attributes,
attr_name,
color_domain,
bke::AttrType::ColorFloat,
bke::AttributeInitValue(value));
return;
}
bke::SpanAttributeWriter<ColorGeometry4f> color_data =
attributes.lookup_or_add_for_write_only_span<ColorGeometry4f>(attr_name, color_domain);
if (!color_data) {
CLOG_WARN(&LOG, "Primvar '%s' could not be added to Blender", primvar.GetBaseName().GetText());
return;
}
const OffsetIndices faces = mesh->faces();
for (const int i : faces.index_range()) {
if (i >= usd_colors.size()) {
break;
}
/* Take the per-face USD color and place it on each face-corner. */
const IndexRange face = faces[i];
for (const int j : face.index_range()) {
const int corner = face.start() + j;
color_data.span[corner] = detail::convert_value<USDT, ColorGeometry4f>(usd_colors[i]);
}
}
colorspace_attr_to_scene_linear(primvar.GetAttr(), color_data.span);
color_data.finish();
}
static std::optional<bke::AttrDomain> convert_usd_varying_to_blender(const pxr::TfToken usd_domain)
{
static const Map<pxr::TfToken, bke::AttrDomain> domain_map = []() {
Map<pxr::TfToken, bke::AttrDomain> map;
map.add_new(pxr::UsdGeomTokens->faceVarying, bke::AttrDomain::Corner);
map.add_new(pxr::UsdGeomTokens->vertex, bke::AttrDomain::Point);
map.add_new(pxr::UsdGeomTokens->varying, bke::AttrDomain::Point);
map.add_new(pxr::UsdGeomTokens->face, bke::AttrDomain::Face);
/* Since there's no "domain" concept in USD, promote prim-level "constant" primvars to the
* Point domain in Blender. */
map.add_new(pxr::UsdGeomTokens->constant, bke::AttrDomain::Point);
map.add_new(pxr::UsdGeomTokens->uniform, bke::AttrDomain::Face);
/* Notice: Edge types are not supported! */
return map;
}();
const bke::AttrDomain *value = domain_map.lookup_ptr(usd_domain);
if (value == nullptr) {
return std::nullopt;
}
return *value;
}
void read_generic_mesh_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &primvar,
const pxr::UsdTimeCode time,
const bool is_left_handed)
{
const pxr::SdfValueTypeName pv_type = primvar.GetTypeName();
const pxr::TfToken pv_interp = primvar.GetInterpolation();
const pxr::TfToken pv_name = pxr::UsdGeomPrimvar::StripPrimvarsName(primvar.GetPrimvarName());
const std::optional<bke::AttrDomain> domain = convert_usd_varying_to_blender(pv_interp);
const std::optional<bke::AttrType> type = convert_usd_type_to_blender(pv_type);
if (!domain.has_value() || !type.has_value()) {
CLOG_WARN(&LOG,
"Primvar '%s' (interpolation %s, type %s) cannot be converted to Blender",
pv_name.GetText(),
pv_interp.GetText(),
pv_type.GetAsToken().GetText());
return;
}
/* Blender does not currently support displaying Face colors with the Viewport Shading
* "Attribute" color type. Make a special case for "displayColor" primvars and put them on
* the Corner domain instead. */
if (pv_name == usdtokens::displayColor &&
ELEM(pv_interp, pxr::UsdGeomTokens->uniform, pxr::UsdGeomTokens->constant))
{
if (ELEM(pv_type,
pxr::SdfValueTypeNames->Color3fArray,
pxr::SdfValueTypeNames->Color3hArray,
pxr::SdfValueTypeNames->Color3dArray))
{
read_face_display_color<pxr::GfVec3f>(mesh, primvar, pv_name, time);
}
else {
read_face_display_color<pxr::GfVec4f>(mesh, primvar, pv_name, time);
}
return;
}
OffsetIndices<int> faces;
if (is_left_handed) {
faces = mesh->faces();
}
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
copy_primvar_to_blender_attribute(primvar, time, *type, *domain, faces, attributes);
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,19 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <pxr/usd/usdGeom/primvar.h>
namespace blender {
struct Mesh;
namespace io::usd {
void read_generic_mesh_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &primvar,
pxr::UsdTimeCode time,
bool is_left_handed);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,13 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <pxr/pxr.h>
#include <pxr/usd/usd/object.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primData.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/imaging/hd/sceneDelegate.h>
#include <pxr/imaging/hd/tokens.h>

View File

@@ -0,0 +1,349 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#include "usd_reader_camera.hh"
#include "usd_armature_utils.hh"
#include "ANIM_action.hh"
#include "ANIM_animdata.hh"
#include "BLI_math_base.h"
#include "BKE_camera.h"
#include "BKE_fcurve.hh"
#include "BKE_object.hh"
#include "DNA_camera_types.h"
#include "DNA_object_types.h"
#include <pxr/usd/usdGeom/camera.h>
#include <array>
#include <optional>
namespace blender::io::usd {
namespace {
template<typename T> struct SampleData {
float frame;
T value;
};
template<typename T> struct AttributeData {
std::optional<T> initial_value = std::nullopt;
Vector<SampleData<T>> samples;
void reset()
{
initial_value = std::nullopt;
samples.clear();
}
};
template<typename T>
bool read_attribute_values(const pxr::UsdAttribute &attr,
const pxr::UsdTimeCode initial_time,
AttributeData<T> &data)
{
data.reset(); /* Clear any prior data. */
T value{};
if (attr.Get(&value, initial_time)) {
data.initial_value = value;
}
else {
data.initial_value = std::nullopt;
}
if (attr.ValueMightBeTimeVarying()) {
std::vector<double> times;
attr.GetTimeSamples(&times);
data.samples.resize(times.size());
for (int64_t i = 0; i < times.size(); i++) {
data.samples[i].frame = float(times[i]);
attr.Get(&data.samples[i].value, times[i]);
}
}
return data.initial_value.has_value() || !data.samples.is_empty();
}
void read_aperture_data(Camera *camera,
const pxr::UsdAttribute &usd_horiz_aperture,
const pxr::UsdAttribute &usd_vert_aperture,
const pxr::UsdAttribute &usd_horiz_offset,
const pxr::UsdAttribute &usd_vert_offset,
const pxr::UsdTimeCode initial_time,
const double tenth_unit_to_millimeters,
animrig::Channelbag &channelbag)
{
/* If the Aperture values are changing, that effects the sensor_fit and shift_x|y values as
* well. We need to put animation data on all of them. */
if (usd_horiz_aperture.ValueMightBeTimeVarying() || usd_vert_aperture.ValueMightBeTimeVarying())
{
std::vector<double> times;
pxr::UsdAttribute::GetUnionedTimeSamples(
{usd_horiz_aperture, usd_vert_aperture, usd_horiz_offset, usd_vert_offset}, &times);
std::array<FCurve *, 5> curves = {
create_fcurve(channelbag, {"sensor_width", 0}, times.size()),
create_fcurve(channelbag, {"sensor_height", 0}, times.size()),
create_fcurve(channelbag, {"sensor_fit", 0}, times.size()),
create_fcurve(channelbag, {"shift_x", 0}, times.size()),
create_fcurve(channelbag, {"shift_y", 0}, times.size())};
for (int64_t i = 0; i < times.size(); i++) {
const double time = times[i];
float horiz_aperture, vert_aperture;
float shift_x, shift_y;
usd_horiz_aperture.Get(&horiz_aperture, time);
usd_vert_aperture.Get(&vert_aperture, time);
usd_horiz_offset.Get(&shift_x, time);
usd_vert_offset.Get(&shift_y, time);
const float sensor_x = horiz_aperture * tenth_unit_to_millimeters;
const float sensor_y = vert_aperture * tenth_unit_to_millimeters;
const char sensor_fit = horiz_aperture >= vert_aperture ? CAMERA_SENSOR_FIT_HOR :
CAMERA_SENSOR_FIT_VERT;
const float sensor_size = sensor_x >= sensor_y ? sensor_x : sensor_y;
shift_x = (shift_x * tenth_unit_to_millimeters) / sensor_size;
shift_y = (shift_y * tenth_unit_to_millimeters) / sensor_size;
set_fcurve_sample(curves[0], i, float(time), sensor_x);
set_fcurve_sample(curves[1], i, float(time), sensor_y);
set_fcurve_sample(curves[2], i, float(time), sensor_fit);
set_fcurve_sample(curves[3], i, float(time), shift_x);
set_fcurve_sample(curves[4], i, float(time), shift_y);
}
}
else if (usd_horiz_offset.ValueMightBeTimeVarying() || usd_vert_offset.ValueMightBeTimeVarying())
{
/* Only the shift_x|y values are changing. Load in the initial values for aperture and
* sensor_fit and use those when setting the shift_x|y curves. */
float horiz_aperture, vert_aperture;
usd_horiz_aperture.Get(&horiz_aperture, initial_time);
usd_vert_aperture.Get(&vert_aperture, initial_time);
camera->sensor_x = horiz_aperture * tenth_unit_to_millimeters;
camera->sensor_y = vert_aperture * tenth_unit_to_millimeters;
camera->sensor_fit = camera->sensor_x >= camera->sensor_y ? CAMERA_SENSOR_FIT_HOR :
CAMERA_SENSOR_FIT_VERT;
const float sensor_size = camera->sensor_x >= camera->sensor_y ? camera->sensor_x :
camera->sensor_y;
std::vector<double> times;
if (usd_horiz_offset.GetTimeSamples(&times)) {
FCurve *fcu = create_fcurve(channelbag, {"shift_x", 0}, times.size());
for (int64_t i = 0; i < times.size(); i++) {
const double time = times[i];
float shift;
usd_horiz_offset.Get(&shift, time);
shift = (shift * tenth_unit_to_millimeters) / sensor_size;
set_fcurve_sample(fcu, i, float(time), shift);
}
}
if (usd_vert_offset.GetTimeSamples(&times)) {
FCurve *fcu = create_fcurve(channelbag, {"shift_y", 0}, times.size());
for (int64_t i = 0; i < times.size(); i++) {
const double time = times[i];
float shift;
usd_vert_offset.Get(&shift, time);
shift = (shift * tenth_unit_to_millimeters) / sensor_size;
set_fcurve_sample(fcu, i, float(time), shift);
}
}
}
else {
/* No animation data. */
float horiz_aperture, vert_aperture;
float shift_x, shift_y;
usd_horiz_aperture.Get(&horiz_aperture, initial_time);
usd_vert_aperture.Get(&vert_aperture, initial_time);
usd_horiz_offset.Get(&shift_x, initial_time);
usd_vert_offset.Get(&shift_y, initial_time);
camera->sensor_x = horiz_aperture * tenth_unit_to_millimeters;
camera->sensor_y = vert_aperture * tenth_unit_to_millimeters;
camera->sensor_fit = camera->sensor_x >= camera->sensor_y ? CAMERA_SENSOR_FIT_HOR :
CAMERA_SENSOR_FIT_VERT;
const float sensor_size = camera->sensor_x >= camera->sensor_y ? camera->sensor_x :
camera->sensor_y;
camera->shiftx = (shift_x * tenth_unit_to_millimeters) / sensor_size;
camera->shifty = (shift_y * tenth_unit_to_millimeters) / sensor_size;
}
}
} // namespace
void USDCameraReader::create_object(Main *bmain)
{
Camera *camera = BKE_camera_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_CAMERA, name_.c_str());
object_->data = id_cast<ID *>(camera);
}
void USDCameraReader::read_object_data(Main *bmain, const pxr::UsdTimeCode time)
{
pxr::UsdAttribute usd_focal_length = cam_prim_.GetFocalLengthAttr();
pxr::UsdAttribute usd_focus_dist = cam_prim_.GetFocusDistanceAttr();
pxr::UsdAttribute usd_fstop = cam_prim_.GetFStopAttr();
pxr::UsdAttribute usd_clipping_range = cam_prim_.GetClippingRangeAttr();
pxr::UsdAttribute usd_horiz_aperture = cam_prim_.GetHorizontalApertureAttr();
pxr::UsdAttribute usd_vert_aperture = cam_prim_.GetVerticalApertureAttr();
pxr::UsdAttribute usd_horiz_offset = cam_prim_.GetHorizontalApertureOffsetAttr();
pxr::UsdAttribute usd_vert_offset = cam_prim_.GetVerticalApertureOffsetAttr();
/* If any of the camera attributes are time varying, then prepare the animation data. */
const bool is_time_varying = usd_focal_length.ValueMightBeTimeVarying() ||
usd_focus_dist.ValueMightBeTimeVarying() ||
usd_fstop.ValueMightBeTimeVarying() ||
usd_clipping_range.ValueMightBeTimeVarying() ||
usd_horiz_aperture.ValueMightBeTimeVarying() ||
usd_vert_aperture.ValueMightBeTimeVarying() ||
usd_horiz_offset.ValueMightBeTimeVarying() ||
usd_vert_offset.ValueMightBeTimeVarying();
Camera *camera = id_cast<Camera *>(object_->data);
bAction *action = nullptr;
if (is_time_varying) {
action = animrig::id_action_ensure(bmain, &camera->id);
}
animrig::Channelbag empty{};
animrig::Channelbag &channelbag = is_time_varying ?
animrig::action_channelbag_ensure(*action, camera->id) :
empty;
/*
* In USD, some camera properties are in tenths of a world unit.
* https://graphics.pixar.com/usd/release/api/class_usd_geom_camera.html#UsdGeom_CameraUnits
*
* tenth_unit_to_meters = stage_meters_per_unit / 10
* tenth_unit_to_millimeters = 1000 * tenth_unit_to_meters
* = 100 * stage_meters_per_unit
*/
const double tenth_unit_to_millimeters = 100.0 * settings_->stage_meters_per_unit;
auto scale_default = [](std::optional<float> input, double scale, float default_value) {
return input.has_value() ? input.value() * scale : default_value;
};
AttributeData<float> data;
if (read_attribute_values(usd_focal_length, time, data)) {
camera->lens = scale_default(data.initial_value, tenth_unit_to_millimeters, camera->lens);
if (!data.samples.is_empty()) {
FCurve *fcu = create_fcurve(channelbag, {"lens", 0}, data.samples.size());
for (int64_t i = 0; i < data.samples.size(); i++) {
const SampleData<float> &sample = data.samples[i];
set_fcurve_sample(fcu, i, sample.frame, sample.value * tenth_unit_to_millimeters);
}
}
}
if (read_attribute_values(usd_focus_dist, time, data)) {
camera->dof.focus_distance = scale_default(
data.initial_value, this->settings_->scene_scale, camera->dof.focus_distance);
if (!data.samples.is_empty()) {
FCurve *fcu = create_fcurve(channelbag, {"dof.focus_distance", 0}, data.samples.size());
for (int64_t i = 0; i < data.samples.size(); i++) {
const SampleData<float> &sample = data.samples[i];
set_fcurve_sample(fcu, i, sample.frame, sample.value * this->settings_->scene_scale);
}
}
}
/* The FStop controls camera focusing and values of 0.0 should disable DOF.
* https://openusd.org/release/api/class_usd_geom_camera.html#a335e1647b730a575e3c0565e91eb8d49
*/
if (read_attribute_values(usd_fstop, time, data)) {
camera->dof.aperture_fstop = scale_default(data.initial_value, 1, camera->dof.aperture_fstop);
camera->dof.flag |= data.initial_value.value_or(0.0f) != 0.0f ? CAM_DOF_ENABLED :
eCamera_DOF_Flag{};
if (!data.samples.is_empty()) {
FCurve *curve1 = create_fcurve(channelbag, {"dof.aperture_fstop", 0}, data.samples.size());
FCurve *curve2 = create_fcurve(channelbag, {"dof.use_dof", 0}, data.samples.size());
for (int64_t i = 0; i < data.samples.size(); i++) {
const SampleData<float> &sample = data.samples[i];
const bool use_dof = sample.value != 0.0f;
set_fcurve_sample(curve1, i, sample.frame, sample.value);
set_fcurve_sample(curve2, i, sample.frame, use_dof);
}
}
}
AttributeData<pxr::GfVec2f> clip_data;
if (read_attribute_values(usd_clipping_range, time, clip_data)) {
auto clamp_clip = [this](pxr::GfVec2f value) {
/* Clamp the value for clip-start, matching the range defined in RNA. */
return pxr::GfVec2f(max_ff(1e-6f, value[0] * settings_->scene_scale),
value[1] * settings_->scene_scale);
};
pxr::GfVec2f clip_range = clip_data.initial_value.has_value() ?
clamp_clip(clip_data.initial_value.value()) :
pxr::GfVec2f(camera->clip_start, camera->clip_end);
camera->clip_start = clip_range[0];
camera->clip_end = clip_range[1];
if (!clip_data.samples.is_empty()) {
std::array<FCurve *, 2> curves = {
create_fcurve(channelbag, {"clip_start", 0}, clip_data.samples.size()),
create_fcurve(channelbag, {"clip_end", 0}, clip_data.samples.size())};
for (int64_t i = 0; i < clip_data.samples.size(); i++) {
const SampleData<pxr::GfVec2f> &sample = clip_data.samples[i];
clip_range = clamp_clip(sample.value);
set_fcurve_sample(curves[0], i, sample.frame, clip_range[0]);
set_fcurve_sample(curves[1], i, sample.frame, clip_range[1]);
}
}
}
/* Aperture data impacts sensor size, sensor fit, and shift values simultaneously. */
read_aperture_data(camera,
usd_horiz_aperture,
usd_vert_aperture,
usd_horiz_offset,
usd_vert_offset,
time,
tenth_unit_to_millimeters,
channelbag);
/* USD Orthographic cameras have very limited support. Support a basic, non-animated, translation
* between USD and Blender. */
pxr::TfToken projection;
cam_prim_.GetProjectionAttr().Get(&projection, time);
camera->type = (projection.GetString() == "perspective") ? CAM_PERSP : CAM_ORTHO;
if (camera->type == CAM_ORTHO) {
float horiz_aperture, vert_aperture;
usd_horiz_aperture.Get(&horiz_aperture, time);
usd_vert_aperture.Get(&vert_aperture, time);
camera->ortho_scale = max_ff(vert_aperture, horiz_aperture);
}
/* Recalculate any animation curve handles. */
for (FCurve *fcu : channelbag.fcurves()) {
if (fcu) {
BKE_fcurve_handles_recalc(*fcu);
}
}
USDXformReader::read_object_data(bmain, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#pragma once
#include "usd.hh"
#include "usd_reader_xform.hh"
#include <pxr/usd/usdGeom/camera.h>
namespace blender {
struct Main;
namespace io::usd {
class USDCameraReader : public USDXformReader {
private:
pxr::UsdGeomCamera cam_prim_;
public:
USDCameraReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings), cam_prim_(prim)
{
}
bool valid() const override
{
return bool(cam_prim_);
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,414 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
* Adapted from the Blender Alembic importer implementation. Copyright 2016 Kévin Dietrich.
* Modifications Copyright 2021 Tangent Animation. All rights reserved. */
#include "usd_reader_curve.hh"
#include "usd.hh"
#include "usd_attribute_utils.hh"
#include "usd_hash_types.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_geometry_set.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "BLI_index_range.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_curves_types.h"
#include "DNA_object_types.h"
#include <pxr/base/vt/types.h>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
namespace blender::io::usd {
static inline float3 to_float3(pxr::GfVec3f vec3f)
{
return float3(vec3f.data());
}
static inline int bezier_point_count(int usd_count, bool is_cyclic)
{
return is_cyclic ? (usd_count / 3) : ((usd_count / 3) + 1);
}
static int point_count(int usdCount, CurveType curve_type, bool is_cyclic)
{
if (curve_type == CURVE_TYPE_BEZIER) {
return bezier_point_count(usdCount, is_cyclic);
}
return usdCount;
}
static Array<int> calc_curve_offsets(const pxr::VtIntArray &usdCounts,
const CurveType curve_type,
bool is_cyclic)
{
Array<int> offsets(usdCounts.size() + 1);
threading::parallel_for(IndexRange(usdCounts.size()), 4096, [&](const IndexRange range) {
for (const int i : range) {
offsets[i] = point_count(usdCounts[i], curve_type, is_cyclic);
}
});
offset_indices::accumulate_counts_to_offsets(offsets);
return offsets;
}
static void add_bezier_control_point(int cp,
int offset,
MutableSpan<float3> positions,
MutableSpan<float3> handles_left,
MutableSpan<float3> handles_right,
const Span<pxr::GfVec3f> usdPoints)
{
if (offset == 0) {
positions[cp] = to_float3(usdPoints[offset]);
handles_right[cp] = to_float3(usdPoints[offset + 1]);
handles_left[cp] = 2.0f * positions[cp] - handles_right[cp];
}
else if (offset >= usdPoints.size() - 1) {
positions[cp] = to_float3(usdPoints.last());
handles_left[cp] = to_float3(usdPoints.last(1));
handles_right[cp] = 2.0f * positions[cp] - handles_left[cp];
}
else {
positions[cp] = to_float3(usdPoints[offset]);
handles_left[cp] = to_float3(usdPoints[offset - 1]);
handles_right[cp] = to_float3(usdPoints[offset + 1]);
}
}
/** Returns true if the number of curves or the number of curve points in each curve differ. */
static bool curves_topology_changed(const bke::CurvesGeometry &curves, const Span<int> usd_offsets)
{
if (curves.offsets() != usd_offsets) {
return true;
}
return false;
}
static CurveType get_curve_type(pxr::TfToken type, pxr::TfToken basis)
{
if (type == pxr::UsdGeomTokens->cubic) {
if (basis == pxr::UsdGeomTokens->bezier) {
return CURVE_TYPE_BEZIER;
}
if (basis == pxr::UsdGeomTokens->bspline) {
return CURVE_TYPE_NURBS;
}
if (basis == pxr::UsdGeomTokens->catmullRom) {
return CURVE_TYPE_CATMULL_ROM;
}
}
return CURVE_TYPE_POLY;
}
static std::optional<bke::AttrDomain> convert_usd_interp_to_blender(const pxr::TfToken usd_domain)
{
static const Map<pxr::TfToken, bke::AttrDomain> domain_map = []() {
Map<pxr::TfToken, bke::AttrDomain> map;
map.add_new(pxr::UsdGeomTokens->vertex, bke::AttrDomain::Point);
map.add_new(pxr::UsdGeomTokens->varying, bke::AttrDomain::Point);
map.add_new(pxr::UsdGeomTokens->constant, bke::AttrDomain::Curve);
map.add_new(pxr::UsdGeomTokens->uniform, bke::AttrDomain::Curve);
return map;
}();
const bke::AttrDomain *value = domain_map.lookup_ptr(usd_domain);
if (value == nullptr) {
return std::nullopt;
}
return *value;
}
void USDCurvesReader::create_object(Main *bmain)
{
Curves *curve = BKE_curves_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_CURVES, name_.c_str());
object_->data = id_cast<ID *>(curve);
}
void USDCurvesReader::read_object_data(Main *bmain, pxr::UsdTimeCode time)
{
Curves *cu = id_cast<Curves *>(object_->data);
this->read_curve_sample(cu, time);
if (this->is_animated()) {
this->add_cache_modifier();
}
USDXformReader::read_object_data(bmain, time);
}
void USDCurvesReader::read_velocities(bke::CurvesGeometry &curves,
const pxr::UsdGeomCurves &usd_curves,
const pxr::UsdTimeCode time) const
{
pxr::VtVec3fArray velocities;
usd_curves.GetVelocitiesAttr().Get(&velocities, time);
if (!velocities.empty()) {
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::SpanAttributeWriter<float3> velocity =
attributes.lookup_or_add_for_write_only_span<float3>("velocity", bke::AttrDomain::Point);
Span<pxr::GfVec3f> usd_data(velocities.cdata(), velocities.size());
velocity.span.copy_from(usd_data.cast<float3>());
velocity.finish();
}
}
void USDCurvesReader::read_custom_data(bke::CurvesGeometry &curves,
const pxr::UsdTimeCode time) const
{
pxr::UsdGeomPrimvarsAPI pv_api(prim_);
std::vector<pxr::UsdGeomPrimvar> primvars = pv_api.GetPrimvarsWithValues();
for (const pxr::UsdGeomPrimvar &pv : primvars) {
const pxr::SdfValueTypeName pv_type = pv.GetTypeName();
if (!pv_type.IsArray()) {
continue; /* Skip non-array primvar attributes. */
}
const pxr::TfToken pv_interp = pv.GetInterpolation();
const std::optional<bke::AttrDomain> domain = convert_usd_interp_to_blender(pv_interp);
const std::optional<bke::AttrType> type = convert_usd_type_to_blender(pv_type);
if (!domain.has_value() || !type.has_value()) {
const pxr::TfToken pv_name = pxr::UsdGeomPrimvar::StripPrimvarsName(pv.GetPrimvarName());
BKE_reportf(reports(),
RPT_WARNING,
"Primvar '%s' (interpolation %s, type %s) cannot be converted to Blender",
pv_name.GetText(),
pv_interp.GetText(),
pv_type.GetAsToken().GetText());
continue;
}
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
copy_primvar_to_blender_attribute(pv, time, *type, *domain, {}, attributes);
}
}
void USDCurvesReader::read_geometry(bke::GeometrySet &geometry_set,
const USDMeshReadParams params,
const char ** /*r_err_str*/)
{
if (!geometry_set.has_curves()) {
return;
}
Curves *curves = geometry_set.get_curves_for_write();
read_curve_sample(curves, params.motion_sample_time);
}
bool USDBasisCurvesReader::is_animated() const
{
if (curve_prim_.GetPointsAttr().ValueMightBeTimeVarying() ||
curve_prim_.GetWidthsAttr().ValueMightBeTimeVarying() ||
curve_prim_.GetVelocitiesAttr().ValueMightBeTimeVarying())
{
return true;
}
pxr::UsdGeomPrimvarsAPI pv_api(curve_prim_);
for (const pxr::UsdGeomPrimvar &pv : pv_api.GetPrimvarsWithValues()) {
if (pv.ValueMightBeTimeVarying()) {
return true;
}
}
return false;
}
void USDBasisCurvesReader::read_curve_sample(Curves *curves_id, const pxr::UsdTimeCode time)
{
pxr::VtIntArray usd_counts;
pxr::VtVec3fArray usd_points;
pxr::VtFloatArray usd_widths;
pxr::TfToken basis;
pxr::TfToken type;
pxr::TfToken wrap;
curve_prim_.GetCurveVertexCountsAttr().Get(&usd_counts, time);
curve_prim_.GetPointsAttr().Get(&usd_points, time);
curve_prim_.GetWidthsAttr().Get(&usd_widths, time);
curve_prim_.GetBasisAttr().Get(&basis, time);
curve_prim_.GetTypeAttr().Get(&type, time);
curve_prim_.GetWrapAttr().Get(&wrap, time);
const CurveType curve_type = get_curve_type(type, basis);
const bool is_cyclic = wrap == pxr::UsdGeomTokens->periodic;
const int curves_num = usd_counts.size();
const Array<int> new_offsets = calc_curve_offsets(usd_counts, curve_type, is_cyclic);
// Check validity of curve counts
const int min_points = (curve_type == CURVE_TYPE_BEZIER) ? 3 : 1;
const bool all_valid = std::all_of(usd_counts.cbegin(),
usd_counts.cend(),
[min_points](int count) { return count >= min_points; });
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
if (all_valid && curves_topology_changed(curves, new_offsets)) {
curves.resize(new_offsets.last(), curves_num);
}
// Early out if there are no curves to load.
if (curves.is_empty()) {
return;
}
curves.offsets_for_write().copy_from(new_offsets);
curves.fill_curve_types(curve_type);
if (is_cyclic) {
curves.attributes_for_write().add<bool>(
"cyclic", bke::AttrDomain::Curve, bke::AttributeInitValue(true));
}
if (curve_type == CURVE_TYPE_NURBS) {
const int8_t curve_order = type == pxr::UsdGeomTokens->cubic ? 4 : 2;
curves.attributes_for_write().add<int8_t>(
"nurbs_order", bke::AttrDomain::Curve, bke::AttributeInitValue(curve_order));
}
MutableSpan<float3> positions = curves.positions_for_write();
Span<pxr::GfVec3f> points = Span(usd_points.cdata(), usd_points.size());
Span<int> counts = Span(usd_counts.cdata(), usd_counts.size());
/* If there's no points defined, fill positions with default values and exit. */
if (points.is_empty()) {
positions.fill(float3(0.0f, 0.0f, 0.0f));
return;
}
/* Bezier curves require care in filing out their left/right handles. */
if (type == pxr::UsdGeomTokens->cubic && basis == pxr::UsdGeomTokens->bezier) {
curves.handle_types_left_for_write().fill(BEZIER_HANDLE_ALIGN);
curves.handle_types_right_for_write().fill(BEZIER_HANDLE_ALIGN);
MutableSpan<float3> handles_right = curves.handle_positions_right_for_write();
MutableSpan<float3> handles_left = curves.handle_positions_left_for_write();
int usd_point_offset = 0;
int point_offset = 0;
for (const int i : curves.curves_range()) {
const int usd_point_count = counts[i];
const int point_count = bezier_point_count(usd_point_count, is_cyclic);
int cp_offset = 0;
for (const int cp : IndexRange(point_count)) {
add_bezier_control_point(cp,
cp_offset,
positions.slice(point_offset, point_count),
handles_left.slice(point_offset, point_count),
handles_right.slice(point_offset, point_count),
points.slice_safe(usd_point_offset, usd_point_count));
cp_offset += 3;
}
point_offset += point_count;
usd_point_offset += usd_point_count;
}
}
else {
static_assert(sizeof(pxr::GfVec3f) == sizeof(float3));
if (positions.size() != points.size()) {
positions.fill(float3(0.0f, 0.0f, 0.0f));
}
const int copy_size = std::min(positions.size(), points.size());
positions.slice(0, copy_size).copy_from(points.slice(0, copy_size).cast<float3>());
}
if (!usd_widths.empty()) {
Span<float> widths = Span(usd_widths.cdata(), usd_widths.size());
pxr::TfToken widths_interp = curve_prim_.GetWidthsInterpolation();
if (widths_interp == pxr::UsdGeomTokens->constant || widths.size() == 1) {
set_single_value(curves.attributes_for_write(),
"radius",
bke::AttrDomain::Point,
bke::AttrType::Float,
bke::AttributeInitValue(widths[0] / 2.0f));
}
else {
MutableSpan<float> radii = curves.radius_for_write();
const bool is_bezier_vertex_interp = (type == pxr::UsdGeomTokens->cubic &&
basis == pxr::UsdGeomTokens->bezier &&
widths_interp == pxr::UsdGeomTokens->vertex);
const bool is_bspline_varying_interp = (type == pxr::UsdGeomTokens->cubic &&
basis == pxr::UsdGeomTokens->bspline &&
widths_interp == pxr::UsdGeomTokens->varying);
const bool is_catmull_varying_interp = (type == pxr::UsdGeomTokens->cubic &&
basis == pxr::UsdGeomTokens->catmullRom &&
widths_interp == pxr::UsdGeomTokens->varying);
if (is_bezier_vertex_interp) {
/* Blender does not support bezier 'vertex' interpolation.
* Assign the widths as-if it were 'varying' only. */
int usd_point_offset = 0;
int point_offset = 0;
for (const int i : curves.curves_range()) {
const int usd_point_count = counts[i];
const int point_count = bezier_point_count(usd_point_count, is_cyclic);
int cp_offset = 0;
for (const int cp : IndexRange(point_count)) {
const int usd_index = std::min(usd_point_offset + cp_offset, int(widths.size()) - 1);
radii[point_offset + cp] = widths[usd_index] / 2.0f;
cp_offset += 3;
}
point_offset += point_count;
usd_point_offset += usd_point_count;
}
}
else if (!is_cyclic && (is_bspline_varying_interp || is_catmull_varying_interp)) {
/* Blender does not support general cubic 'varying' interpolation. Duplicate the first/last
* radius values as a best-effort solution. */
int radii_offset = 0;
int width_offset = 0;
for (const int i : curves.curves_range()) {
const int radii_count = counts[i];
const int width_count = std::max(2, counts[i] - 2);
Span<float> usd_curve_widths = widths.slice_safe(width_offset, width_count);
MutableSpan<float> curve_radii = radii.slice_safe(radii_offset, radii_count);
if (usd_curve_widths.size() != width_count || curve_radii.size() != radii_count) {
/* Generally unsafe to continue loading data. */
break;
}
curve_radii.first() = usd_curve_widths.first() / 2.0f;
curve_radii.last() = usd_curve_widths.last() / 2.0f;
for (const int i : usd_curve_widths.index_range()) {
curve_radii[i + 1] = usd_curve_widths[i] / 2.0f;
}
radii_offset += radii_count;
width_offset += width_count;
}
}
else {
for (const int i_point : IndexRange(std::min(radii.size(), widths.size()))) {
radii[i_point] = widths[i_point] / 2.0f;
}
}
}
}
this->read_velocities(curves, curve_prim_, time);
this->read_custom_data(curves, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,75 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
* Adapted from the Blender Alembic importer implementation. Copyright 2016 Kévin Dietrich.
* Modifications Copyright 2021 Tangent Animation. All rights reserved. */
#pragma once
#include "usd.hh"
#include "usd_api_modifier.hh"
#include "usd_reader_geom.hh"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
namespace blender {
struct Curves;
struct Main;
namespace bke {
struct GeometrySet;
class CurvesGeometry;
} // namespace bke
namespace io::usd {
class USDCurvesReader : public USDGeomReader {
public:
USDCurvesReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDGeomReader(prim, import_params, settings)
{
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
void read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str) override;
void read_velocities(bke::CurvesGeometry &curves,
const pxr::UsdGeomCurves &usd_curves,
const pxr::UsdTimeCode time) const;
void read_custom_data(bke::CurvesGeometry &curves, const pxr::UsdTimeCode time) const;
virtual bool is_animated() const = 0;
virtual void read_curve_sample(Curves *curves_id, pxr::UsdTimeCode time) = 0;
};
class USDBasisCurvesReader : public USDCurvesReader {
private:
pxr::UsdGeomBasisCurves curve_prim_;
public:
USDBasisCurvesReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDCurvesReader(prim, import_params, settings), curve_prim_(prim)
{
}
bool valid() const override
{
return bool(curve_prim_);
}
bool is_animated() const override;
void read_curve_sample(Curves *curves_id, pxr::UsdTimeCode time) override;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,124 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_domelight.hh"
#include "usd_colorspace_utils.hh"
#include "usd_light_convert.hh"
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/domeLight_1.h>
#include <pxr/usd/usdLux/tokens.h>
namespace blender {
namespace usdtokens {
// Attribute names.
static const pxr::TfToken color("color", pxr::TfToken::Immortal);
static const pxr::TfToken intensity("intensity", pxr::TfToken::Immortal);
static const pxr::TfToken texture_file("texture:file", pxr::TfToken::Immortal);
static const pxr::TfToken pole_axis("poleAxis", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace io::usd {
/**
* If the given attribute has an authored value, return its value in the r_value
* out parameter.
*
* We wish to support older UsdLux APIs in older versions of USD. For example,
* in previous versions of the API, shader input attributes did not have the
* "inputs:" prefix. One can provide the older input attribute name in the
* 'fallback_attr_name' argument, and that attribute will be queried if 'attr'
* doesn't exist or doesn't have an authored value.
*/
template<typename T>
static bool get_authored_value(const pxr::UsdAttribute &attr,
const pxr::UsdTimeCode time,
const pxr::UsdPrim &prim,
const pxr::TfToken fallback_attr_name,
T *r_value)
{
if (attr && attr.HasAuthoredValue()) {
return attr.Get<T>(r_value, time);
}
if (!prim || fallback_attr_name.IsEmpty()) {
return false;
}
pxr::UsdAttribute fallback_attr = prim.GetAttribute(fallback_attr_name);
if (fallback_attr && fallback_attr.HasAuthoredValue()) {
return fallback_attr.Get<T>(r_value, time);
}
return false;
}
template<typename T> static float get_intensity(const T &dome_light, const pxr::UsdTimeCode time)
{
float intensity = 1.0f;
get_authored_value(
dome_light.GetIntensityAttr(), time, dome_light.GetPrim(), usdtokens::intensity, &intensity);
return intensity;
}
template<typename T>
static bool get_tex_path(const T &dome_light,
const pxr::UsdTimeCode time,
pxr::SdfAssetPath *tex_path)
{
bool has_tex = get_authored_value(dome_light.GetTextureFileAttr(),
time,
dome_light.GetPrim(),
usdtokens::texture_file,
tex_path);
return has_tex;
}
template<typename T>
static bool get_color(const T &dome_light, const pxr::UsdTimeCode time, pxr::GfVec3f *color)
{
bool has_color = get_authored_value(
dome_light.GetColorAttr(), time, dome_light.GetPrim(), usdtokens::color, color);
if (has_color) {
colorspace_attr_to_scene_linear(dome_light.GetColorAttr(), *color);
}
return has_color;
}
static pxr::TfToken get_pole_axis(const pxr::UsdLuxDomeLight_1 &dome_light,
const pxr::UsdTimeCode time)
{
pxr::TfToken pole_axis = pxr::UsdLuxTokens->scene;
get_authored_value(dome_light.GetPoleAxisAttr(), time, dome_light.GetPrim(), {}, &pole_axis);
return pole_axis;
}
void USDDomeLightReader::create_object(Scene *scene, Main *bmain)
{
USDImportDomeLightData dome_light_data;
/* Time varying dome lights are not currently supported. */
constexpr pxr::UsdTimeCode time = 0.0;
if (prim_.IsA<pxr::UsdLuxDomeLight>()) {
pxr::UsdLuxDomeLight dome_light = pxr::UsdLuxDomeLight(prim_);
dome_light_data.intensity = get_intensity(dome_light, time);
dome_light_data.has_tex = get_tex_path(dome_light, time, &dome_light_data.tex_path);
dome_light_data.has_color = get_color(dome_light, time, &dome_light_data.color);
dome_light_data.pole_axis = pxr::UsdLuxTokens->Y;
}
else if (prim_.IsA<pxr::UsdLuxDomeLight_1>()) {
pxr::UsdLuxDomeLight_1 dome_light = pxr::UsdLuxDomeLight_1(prim_);
dome_light_data.intensity = get_intensity(dome_light, time);
dome_light_data.has_tex = get_tex_path(dome_light, time, &dome_light_data.tex_path);
dome_light_data.has_color = get_color(dome_light, time, &dome_light_data.color);
dome_light_data.pole_axis = get_pole_axis(dome_light, time);
}
dome_light_to_world_material(import_params_, scene, bmain, dome_light_data, prim_);
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_reader_prim.hh"
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/domeLight_1.h>
namespace blender {
struct Main;
struct Scene;
namespace io::usd {
class USDDomeLightReader : public USDPrimReader {
public:
USDDomeLightReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDPrimReader(prim, import_params, settings)
{
}
bool valid() const override
{
return prim_.IsA<pxr::UsdLuxDomeLight>() || prim_.IsA<pxr::UsdLuxDomeLight_1>();
}
/* Until Blender supports DomeLight objects natively, use a separate create_object overload that
* allows the caller to pass in the required Scene data. */
void create_object(Main * /*bmain*/) override {};
void create_object(Scene *scene, Main *bmain);
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_geom.hh"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "DNA_cachefile_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
namespace blender::io::usd {
void USDGeomReader::add_cache_modifier()
{
if (!settings_->get_cache_file) {
return;
}
ModifierData *md = BKE_modifier_new(eModifierType_MeshSequenceCache);
BLI_addtail(&object_->modifiers, md);
BKE_modifiers_persistent_uid_init(*object_, *md);
MeshSeqCacheModifierData *mcmd = reinterpret_cast<MeshSeqCacheModifierData *>(md);
mcmd->cache_file = settings_->get_cache_file();
id_us_plus(&mcmd->cache_file->id);
mcmd->read_flag = import_params_.mesh_read_flag;
STRNCPY(mcmd->object_path, prim_.GetPath().GetString().c_str());
}
void USDGeomReader::add_subdiv_modifier()
{
ModifierData *md = BKE_modifier_new(eModifierType_Subsurf);
BLI_addtail(&object_->modifiers, md);
BKE_modifiers_persistent_uid_init(*object_, *md);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_api_modifier.hh"
#include "usd_reader_xform.hh"
namespace blender {
struct Mesh;
namespace bke {
struct GeometrySet;
}
namespace io::usd {
class USDGeomReader : public USDXformReader {
public:
USDGeomReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings)
{
}
virtual void read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str) = 0;
virtual bool topology_changed(const Mesh * /*existing_mesh*/, pxr::UsdTimeCode /*time*/)
{
return true;
}
void add_cache_modifier();
void add_subdiv_modifier();
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_instance.hh"
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "DNA_collection_types.h"
#include "DNA_object_types.h"
namespace blender::io::usd {
void USDInstanceReader::create_object(Main *bmain)
{
this->object_ = BKE_object_add_only_object(bmain, OB_EMPTY, name_.c_str());
this->object_->data = nullptr;
this->object_->instance_collection = nullptr;
this->object_->transflag |= OB_DUPLICOLLECTION;
}
void USDInstanceReader::set_instance_collection(Collection *coll)
{
if (this->object_ && this->object_->instance_collection != coll) {
if (this->object_->instance_collection) {
id_us_min(&this->object_->instance_collection->id);
this->object_->instance_collection = nullptr;
}
id_us_plus(&coll->id);
this->object_->instance_collection = coll;
}
}
pxr::SdfPath USDInstanceReader::proto_path() const
{
if (pxr::UsdPrim proto = prim_.GetPrototype()) {
return proto.GetPath();
}
return pxr::SdfPath();
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_reader_xform.hh"
namespace blender {
struct Collection;
struct Main;
namespace io::usd {
/**
* Convert a USD instanced prim to a blender collection instance.
*/
class USDInstanceReader : public USDXformReader {
public:
USDInstanceReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings)
{
}
bool valid() const override
{
return prim_.IsValid() && prim_.IsInstance();
}
/**
* Create an object that instances a collection.
*/
void create_object(Main *bmain) override;
/**
* Assign the given collection to the object.
*/
void set_instance_collection(Collection *coll);
/**
* Get the path of the USD prototype prim.
*/
pxr::SdfPath proto_path() const;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,213 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_light.hh"
#include "usd_colorspace_utils.hh"
#include "BLI_math_rotation.h"
#include "BKE_light.h"
#include "BKE_object.hh"
#include "DNA_light_types.h"
#include "DNA_object_types.h"
#include <pxr/usd/usdLux/diskLight.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/rectLight.h>
#include <pxr/usd/usdLux/shapingAPI.h>
#include <pxr/usd/usdLux/sphereLight.h>
namespace blender::io::usd {
void USDLightReader::create_object(Main *bmain)
{
Light *blight = BKE_light_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_LAMP, name_.c_str());
object_->data = id_cast<ID *>(blight);
}
void USDLightReader::read_object_data(Main *bmain, const pxr::UsdTimeCode time)
{
Light *blight = id_cast<Light *>(object_->data);
if (blight == nullptr) {
return;
}
pxr::UsdLuxLightAPI light_api(prim_);
if (!light_api) {
return;
}
if (prim_.IsA<pxr::UsdLuxDiskLight>()) {
/* Disk area light. */
blight->type = LA_AREA;
blight->area_shape = LA_AREA_DISK;
pxr::UsdLuxDiskLight disk_light(prim_);
if (disk_light) {
if (pxr::UsdAttribute radius_attr = disk_light.GetRadiusAttr()) {
float radius = 0.0f;
if (radius_attr.Get(&radius, time)) {
blight->area_size = radius * 2.0f;
}
}
}
}
else if (prim_.IsA<pxr::UsdLuxRectLight>()) {
/* Rectangular area light. */
blight->type = LA_AREA;
blight->area_shape = LA_AREA_RECT;
pxr::UsdLuxRectLight rect_light(prim_);
if (rect_light) {
if (pxr::UsdAttribute width_attr = rect_light.GetWidthAttr()) {
float width = 0.0f;
if (width_attr.Get(&width, time)) {
blight->area_size = width;
}
}
if (pxr::UsdAttribute height_attr = rect_light.GetHeightAttr()) {
float height = 0.0f;
if (height_attr.Get(&height, time)) {
blight->area_sizey = height;
}
}
}
}
else if (prim_.IsA<pxr::UsdLuxSphereLight>()) {
/* Point and spot light. */
blight->type = LA_LOCAL;
pxr::UsdLuxSphereLight sphere_light(prim_);
if (sphere_light) {
pxr::UsdAttribute treatAsPoint_attr = sphere_light.GetTreatAsPointAttr();
bool treatAsPoint;
if (treatAsPoint_attr && treatAsPoint_attr.Get(&treatAsPoint, time) && treatAsPoint) {
blight->radius = 0.0f;
}
else if (pxr::UsdAttribute radius_attr = sphere_light.GetRadiusAttr()) {
float radius = 0.0f;
if (radius_attr.Get(&radius, time)) {
blight->radius = radius;
}
}
}
pxr::UsdLuxShapingAPI shaping_api = pxr::UsdLuxShapingAPI(prim_);
if (shaping_api && shaping_api.GetShapingConeAngleAttr().IsAuthored()) {
blight->type = LA_SPOT;
if (pxr::UsdAttribute cone_angle_attr = shaping_api.GetShapingConeAngleAttr()) {
float cone_angle = 0.0f;
if (cone_angle_attr.Get(&cone_angle, time)) {
blight->spotsize = DEG2RADF(cone_angle) * 2.0f;
}
}
if (pxr::UsdAttribute cone_softness_attr = shaping_api.GetShapingConeSoftnessAttr()) {
float cone_softness = 0.0f;
if (cone_softness_attr.Get(&cone_softness, time)) {
blight->spotblend = cone_softness;
}
}
}
}
else if (prim_.IsA<pxr::UsdLuxDistantLight>()) {
blight->type = LA_SUN;
pxr::UsdLuxDistantLight distant_light(prim_);
if (distant_light) {
if (pxr::UsdAttribute angle_attr = distant_light.GetAngleAttr()) {
float angle = 0.0f;
if (angle_attr.Get(&angle, time)) {
blight->sun_angle = DEG2RADF(angle * 2.0f);
}
}
}
}
/* Intensity */
if (pxr::UsdAttribute intensity_attr = light_api.GetIntensityAttr()) {
float intensity = 0.0f;
if (intensity_attr.Get(&intensity, time)) {
if (blight->type == LA_SUN) {
/* Unclear why, but approximately matches Karma. */
blight->energy = intensity * 4.0f;
}
else {
/* Convert from intensity to radiant flux. */
blight->energy = intensity * M_PI;
}
blight->energy *= this->import_params_.light_intensity_scale;
}
}
/* Exposure. */
if (pxr::UsdAttribute exposure_attr = light_api.GetExposureAttr()) {
float exposure = 0.0f;
if (exposure_attr.Get(&exposure, time)) {
blight->exposure = exposure;
}
}
/* Color. */
if (pxr::UsdAttribute color_attr = light_api.GetColorAttr()) {
pxr::GfVec3f color;
if (color_attr.Get(&color, time)) {
colorspace_attr_to_scene_linear(color_attr, color);
blight->r = color[0];
blight->g = color[1];
blight->b = color[2];
}
}
/* Temperature */
if (pxr::UsdAttribute enable_temperature_attr = light_api.GetEnableColorTemperatureAttr()) {
bool enable_temperature = false;
if (enable_temperature_attr.Get(&enable_temperature, time)) {
if (enable_temperature) {
blight->mode |= LA_USE_TEMPERATURE;
}
}
}
if (pxr::UsdAttribute color_temperature_attr = light_api.GetColorTemperatureAttr()) {
float color_temperature = 6500.0f;
if (color_temperature_attr.Get(&color_temperature, time)) {
blight->temperature = color_temperature;
}
}
/* Diffuse and Specular. */
if (pxr::UsdAttribute diff_attr = light_api.GetDiffuseAttr()) {
float diff_fac = 1.0f;
if (diff_attr.Get(&diff_fac, time)) {
blight->diff_fac = diff_fac;
}
}
if (pxr::UsdAttribute spec_attr = light_api.GetSpecularAttr()) {
float spec_fac = 1.0f;
if (spec_attr.Get(&spec_fac, time)) {
blight->spec_fac = spec_fac;
}
}
/* Normalize */
if (pxr::UsdAttribute normalize_attr = light_api.GetNormalizeAttr()) {
bool normalize = false;
if (normalize_attr.Get(&normalize, time)) {
if (!normalize) {
blight->mode |= LA_UNNORMALIZED;
}
}
}
USDXformReader::read_object_data(bmain, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_reader_xform.hh"
namespace blender {
struct Main;
namespace io::usd {
class USDLightReader : public USDXformReader {
public:
USDLightReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings)
{
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
};
} // namespace io::usd
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,237 @@
/* SPDX-FileCopyrightText: 2021 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include <pxr/usd/usdShade/material.h>
#include <string>
namespace blender {
struct Main;
struct Material;
struct bNode;
struct bNodeTree;
struct ReportList;
namespace io::usd {
using ShaderToNodeMap = Map<std::string, bNode *>;
/* Helper struct used when arranging nodes in columns, keeping track the
* occupancy information for a given column. I.e., for column n,
* column_offsets[n] is the y-offset (from top to bottom) of the occupied
* region in that column. */
struct NodePlacementContext {
const float origx_;
const float origy_;
const float horizontal_step_;
const float vertical_step_;
Vector<float, 8> column_offsets_ = Vector<float, 8>(8, 0.0f);
/* Map a USD shader prim path to the Blender node converted
* from that shader. This map is updated during shader
* conversion and is used to avoid creating duplicate nodes
* for a given shader. */
ShaderToNodeMap node_cache_;
NodePlacementContext(float origx,
float origy,
float horizontal_step = 300.0f,
float vertical_step = 300.0f)
: origx_(origx),
origy_(origy),
horizontal_step_(horizontal_step),
vertical_step_(vertical_step)
{
}
/* Compute the x- and y-coordinates for placing a new node in an unoccupied region of
* the column with the given index. */
float2 compute_node_loc(int column);
/**
* Generate a key for caching a Blender node created for a given USD shader by returning the
* shader prim path with an optional tag suffix. The tag can be specified in order to generate a
* unique key when more than one Blender node is created for the USD shader. */
std::string get_key(const pxr::UsdShadeShader &usd_shader, const StringRef tag) const;
/* Returns the Blender node previously cached for the given USD shader. Returns null if no cached
* shader was found. */
bNode *get_cached_node(const pxr::UsdShadeShader &usd_shader, const StringRef tag = {}) const;
/* Cache the Blender node translated from the given USD shader. */
void cache_node(const pxr::UsdShadeShader &usd_shader, bNode *node, const StringRef tag = {});
};
/* Helper struct which carries an assortment of optional
* information that is sometimes required when linking
* nodes together. */
struct ExtraLinkInfo {
bool is_color_corrected = false;
/* Is the value inverted with respect to Blender.
* For example: Opacity 0.85 <-> Transmission Weight 0.15 */
bool is_inverted = false;
float opacity_threshold = 0.0f;
};
/* Converts USD materials to Blender representation. */
/**
* By default, the #USDMaterialReader creates a Blender material with
* the same name as the USD material. If the USD material has a
* #UsdPreviewSurface source, the Blender material's viewport display
* color, roughness and metallic properties are set to the corresponding
* #UsdPreoviewSurface inputs.
*
* If the Import USD Preview option is enabled, the current implementation
* converts #UsdPreviewSurface to Blender nodes as follows:
*
* - #UsdPreviewSurface -> Principled BSDF
* - #UsdUVTexture -> Texture Image + Normal Map
* - UsdPrimvarReader_float2 -> UV Map
*
* Limitations: arbitrary primvar readers or UsdTransform2d not yet
* supported. For #UsdUVTexture, only the file, st and #sourceColorSpace
* inputs are handled.
*
* TODO(makowalski): Investigate adding support for converting additional
* shaders and inputs. Supporting certain types of inputs, such as texture
* scale and bias, will probably require creating Blender Group nodes with
* the corresponding inputs.
*/
class USDMaterialReader {
private:
const USDImportParams &params_;
Main &bmain_;
public:
USDMaterialReader(const USDImportParams &params, Main &bmain);
Material *add_material(const pxr::UsdShadeMaterial &usd_material,
bool read_usd_preview = true) const;
void import_usd_preview(Material *mtl, const pxr::UsdShadeMaterial &usd_material) const;
/** Get the wmJobWorkerStatus-provided `reports` list pointer, to use with the BKE_report API. */
ReportList *reports() const;
protected:
/** Create the Principled BSDF shader node network. */
void import_usd_preview_nodes(Material *mtl,
const pxr::UsdShadeMaterial &usd_material,
const pxr::UsdShadeShader &usd_shader) const;
void set_principled_node_inputs(bNode *principled_node,
bNodeTree *ntree,
const pxr::UsdShadeShader &usd_shader) const;
bool set_displacement_node_inputs(bNodeTree *ntree,
bNode *output,
const pxr::UsdShadeShader &usd_shader) const;
/** Convert the given USD shader input to an input on the given Blender node. */
bool set_node_input(const pxr::UsdShadeInput &usd_input,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx,
const ExtraLinkInfo &extra = {}) const;
/**
* Follow the connected source of the USD input to create corresponding inputs
* for the given Blender node.
*/
bool follow_connection(const pxr::UsdShadeInput &usd_input,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx,
const ExtraLinkInfo &extra = {}) const;
void convert_usd_uv_texture(const pxr::UsdShadeShader &usd_shader,
const pxr::TfToken &usd_source_name,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx,
const ExtraLinkInfo &extra = {}) const;
void convert_usd_transform_2d(const pxr::UsdShadeShader &usd_shader,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx) const;
/**
* Load the texture image node's texture from the path given by the USD shader's
* file input value.
*/
void load_tex_image(const pxr::UsdShadeShader &usd_shader,
bNode *tex_image,
const ExtraLinkInfo &extra = {}) const;
/**
* This function creates a Blender UV Map node, under the simplifying assumption that
* UsdPrimvarReader_float2 shaders output UV coordinates.
*/
void convert_usd_primvar_reader_float2(const pxr::UsdShadeShader &usd_shader,
const pxr::TfToken &usd_source_name,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx) const;
void convert_usd_primvar_reader_generic(const pxr::UsdShadeShader &usd_shader,
StringRef output_type,
bNode *dest_node,
const StringRefNull dest_socket_name,
bNodeTree *ntree,
int column,
NodePlacementContext &ctx) const;
};
/* Utility functions. */
/**
* Returns a map containing all the Blender materials which allows a fast
* lookup of the material by name. Note that the material name key
* might be modified to be a valid USD identifier, to match material
* names in the imported USD.
*/
void build_material_map(const Main *bmain, Map<std::string, Material *> &r_mat_map);
/**
* Returns an existing Blender material that corresponds to the USD material with the given path.
* Returns null if no such material exists.
*
* \param mat_map: Map a material name to a Blender material. Note that the name key
* might be the Blender material name modified to be a valid USD identifier,
* to match the material names in the imported USD.
* \param usd_path_to_mat: Map a USD material path to the imported Blender material.
*
* The usd_path_to_mat is needed to determine the name of the Blender
* material imported from a USD path in the case when a unique name was generated
* for the material due to a name collision.
*/
Material *find_existing_material(const pxr::SdfPath &usd_mat_path,
const USDImportParams &params,
const Map<std::string, Material *> &mat_map,
const Map<pxr::SdfPath, Material *> &usd_path_to_mat);
} // namespace io::usd
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
* Adapted from the Blender Alembic importer implementation.
* Modifications Copyright 2021 Tangent Animation and. NVIDIA Corporation. All rights reserved. */
#pragma once
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_span.hh"
#include "usd.hh"
#include "usd_api_modifier.hh"
#include "usd_reader_geom.hh"
#include <pxr/usd/usdGeom/mesh.h>
namespace blender::io::usd {
struct USDMeshReadData {
private:
pxr::VtVec3fArray positions_;
pxr::VtVec3fArray normals_;
pxr::VtIntArray face_indices_;
pxr::VtIntArray face_counts_;
public:
USDMeshReadData(const pxr::UsdGeomMesh &mesh_prim, pxr::UsdTimeCode time);
Span<int> face_indices() const
{
return Span(face_indices_.cdata(), face_indices_.size());
}
Span<int> face_counts() const
{
return Span(face_counts_.cdata(), face_counts_.size());
}
Span<float3> positions() const
{
return Span(positions_.cdata(), positions_.size()).cast<float3>();
}
Span<float3> normals() const
{
return Span(normals_.cdata(), normals_.size()).cast<float3>();
}
MutableSpan<float3> normals_for_write()
{
return MutableSpan(normals_.data(), normals_.size()).cast<float3>();
}
pxr::TfToken normal_interpolation;
pxr::TfToken orientation;
};
class USDMeshReader : public USDGeomReader {
private:
pxr::UsdGeomMesh mesh_prim_;
bool is_left_handed_ = false;
bool is_time_varying_ = false;
/* This is to ensure we load all data once, because we reuse the read_mesh function
* in the mesh seq modifier, and in initial load. Ideally, a better fix would be
* implemented. Note this will break if faces or positions vary. */
bool is_initial_load_ = false;
Map<const pxr::TfToken, bool> primvar_varying_map_;
public:
USDMeshReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDGeomReader(prim, import_params, settings), mesh_prim_(prim)
{
}
bool valid() const override
{
return bool(mesh_prim_);
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
void read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str) override;
bool topology_changed(const Mesh *existing_mesh, pxr::UsdTimeCode time) override;
/**
* If the USD mesh prim has a valid `UsdSkel` schema defined, return the USD path
* string to the bound skeleton, if any. Returns the empty string if no skeleton
* binding is defined.
*
* The returned path is currently used to match armature modifiers with armature
* objects during import.
*/
pxr::SdfPath get_skeleton_path() const;
private:
void process_normals_vertex_varying(Mesh *mesh, MutableSpan<float3> usd_normals) const;
void process_normals_face_varying(Mesh *mesh, Span<float3> usd_normals) const;
/** Set USD uniform (per-face) normals as Blender loop normals. */
void process_normals_uniform(Mesh *mesh, Span<float3> usd_normals) const;
void readFaceSetsSample(Main *bmain, Mesh *mesh, pxr::UsdTimeCode time);
void assign_facesets_to_material_indices(pxr::UsdTimeCode time,
MutableSpan<int> material_indices,
Map<pxr::SdfPath, int> *r_mat_map);
bool read_faces(Mesh *mesh, const USDMeshReadData &usd_data) const;
void read_subdiv();
void read_vertex_creases(Mesh *mesh, pxr::UsdTimeCode time);
void read_edge_creases(Mesh *mesh, pxr::UsdTimeCode time);
void read_velocities(Mesh *mesh, pxr::UsdTimeCode time);
void read_mesh_sample(ImportSettings *settings,
Mesh *mesh,
USDMeshReadData &usd_data,
pxr::UsdTimeCode time,
bool new_mesh);
Mesh *read_mesh(struct Mesh *existing_mesh,
const USDMeshReadParams params,
const char **r_err_str);
void read_custom_data(const ImportSettings *settings,
Mesh *mesh,
pxr::UsdTimeCode time,
bool new_mesh);
void read_uv_data_primvar(Mesh *mesh,
const pxr::UsdGeomPrimvar &primvar,
const pxr::UsdTimeCode time);
bool topology_changed(const Mesh *existing_mesh, const USDMeshReadData &usd_data) const;
/**
* Override transform computation to account for the binding
* transformation for skinned meshes.
*/
std::optional<XformResult> get_local_usd_xform(pxr::UsdTimeCode time) const override;
/**
* A skinned mesh is re-parented to its bound armature during import
* (#USDStageReader::process_armature_modifiers), so it is never the root of a transform
* hierarchy; treating it as one would apply the scene scale / axis conversion to its
* (skeleton-space) local transform on top of the same conversion already carried by the
* armature.
*/
bool is_root_xform_prim() const override;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,474 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#include "usd_reader_nurbs.hh"
#include "BKE_curves.hh"
#include "BLI_offset_indices.hh"
#include "BLI_span.hh"
#include "DNA_curves_types.h"
#include <pxr/base/vt/types.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
/* Store incoming USD data privately and expose Blender-friendly Spans publicly. */
struct USDCurveData {
private:
pxr::VtArray<pxr::GfVec3f> points_;
pxr::VtArray<int> counts_;
pxr::VtArray<int> orders_;
pxr::VtArray<double> knots_;
pxr::VtArray<double> weights_;
pxr::VtArray<float> widths_;
pxr::VtArray<pxr::GfVec3f> velocities_;
public:
Span<float3> points() const
{
return Span(points_.cdata(), points_.size()).cast<float3>();
}
Span<int> counts() const
{
return Span(counts_.cdata(), counts_.size());
}
Span<int> orders() const
{
return Span(orders_.cdata(), orders_.size());
}
Span<double> knots() const
{
return Span(knots_.cdata(), knots_.size());
}
Span<double> weights() const
{
return Span(weights_.cdata(), weights_.size());
}
Span<float> widths() const
{
return Span(widths_.cdata(), widths_.size());
}
Span<float3> velocities() const
{
return Span(velocities_.cdata(), velocities_.size()).cast<float3>();
}
bool load(const pxr::UsdGeomNurbsCurves &curve_prim, const pxr::UsdTimeCode time)
{
curve_prim.GetCurveVertexCountsAttr().Get(&counts_, time);
curve_prim.GetOrderAttr().Get(&orders_, time);
if (counts_.size() != orders_.size()) {
CLOG_WARN(&LOG,
"Curve vertex and order size mismatch for NURBS prim %s",
curve_prim.GetPrim().GetPrimPath().GetAsString().c_str());
return false;
}
if (std::any_of(counts_.cbegin(), counts_.cend(), [](int value) { return value < 0; }) ||
std::any_of(orders_.cbegin(), orders_.cend(), [](int value) { return value < 0; }))
{
CLOG_WARN(&LOG,
"Invalid curve vertex count or order value detected for NURBS prim %s",
curve_prim.GetPrim().GetPrimPath().GetAsString().c_str());
return false;
}
curve_prim.GetPointsAttr().Get(&points_, time);
curve_prim.GetKnotsAttr().Get(&knots_, time);
curve_prim.GetWidthsAttr().Get(&widths_, time);
curve_prim.GetPointWeightsAttr().Get(&weights_, time);
if (!weights_.empty() && points_.size() != weights_.size()) {
CLOG_WARN(&LOG,
"Invalid curve weights count for NURBS prim %s",
curve_prim.GetPrim().GetPrimPath().GetAsString().c_str());
/* Only clear, but continue to load other curve data. */
weights_.clear();
}
curve_prim.GetVelocitiesAttr().Get(&velocities_, time);
if (!velocities_.empty() && points_.size() != velocities_.size()) {
CLOG_WARN(&LOG,
"Invalid curve velocity count for NURBS prim %s",
curve_prim.GetPrim().GetPrimPath().GetAsString().c_str());
/* Only clear, but continue to load other curve data. */
velocities_.clear();
}
return true;
}
};
struct CurveData {
Array<int> blender_offsets;
Array<int> usd_offsets;
Array<int> usd_knot_offsets;
Array<bool> is_cyclic;
};
static KnotsMode determine_knots_mode(const Span<double> usd_knots,
const int order,
const bool is_cyclic)
{
/* TODO: We have to convert knot values to float for usage in Blender APIs. Look into making
* calculate_multiplicity_sequence a template. */
Array<float> blender_knots(usd_knots.size());
for (const int knot_i : usd_knots.index_range()) {
blender_knots[knot_i] = float(usd_knots[knot_i]);
}
Vector<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(blender_knots);
if (multiplicity.size() < 2) {
/* Invalid knot vector. Use normal mode. */
return NURBS_KNOT_MODE_NORMAL;
}
const int head = multiplicity.first();
const int tail = multiplicity.last();
const Span<int> inner = multiplicity.as_span().slice(1, multiplicity.size() - 2);
/* If the knot vector starts and ends with full multiplicity knots, then this is classified as
* Blender's endpoint mode. */
const int degree = order - 1;
const bool is_endpoint = is_cyclic ? (tail >= degree) : (head == order && tail >= order);
/* If all of the inner multiplicities are equal to the degree, then this is a Bezier curve. */
if (degree > 1 &&
std::all_of(inner.begin(), inner.end(), [degree](int value) { return value == degree; }))
{
return is_endpoint ? NURBS_KNOT_MODE_ENDPOINT_BEZIER : NURBS_KNOT_MODE_BEZIER;
}
if (is_endpoint) {
return NURBS_KNOT_MODE_ENDPOINT;
}
/* If all of the inner knot values are equally spaced, then this is a regular/uniform curve and
* we assume that our normal knot mode will match. Use custom knots otherwise. */
const Span<float> inner_values = blender_knots.as_span().drop_front(head).drop_back(tail);
if (inner_values.size() > 2) {
const float delta = inner_values[1] - inner_values[0];
if (delta < 0) {
/* Invalid knot vector. Use normal mode. */
return NURBS_KNOT_MODE_NORMAL;
}
for (int i = 2; i < inner.size(); i++) {
if (inner_values[i] - inner_values[i - 1] != delta) {
/* The knot values are not equally spaced. Use custom knots. */
return NURBS_KNOT_MODE_CUSTOM;
}
}
}
/* Nothing matches. Use normal mode. */
return NURBS_KNOT_MODE_NORMAL;
}
static CurveData calc_curve_offsets(const Span<float3> usd_points,
const Span<int> usd_counts,
const Span<int> usd_orders,
const Span<double> usd_knots)
{
CurveData data;
data.blender_offsets.reinitialize(usd_counts.size() + 1);
data.usd_offsets.reinitialize(usd_counts.size() + 1);
data.usd_knot_offsets.reinitialize(usd_counts.size() + 1);
data.is_cyclic.reinitialize(usd_counts.size());
Span<float3> usd_remaining_points = usd_points;
Span<double> usd_remaining_knots = usd_knots;
for (const int curve_i : usd_counts.index_range()) {
const int points_num = usd_counts[curve_i];
const int knots_num = points_num + usd_orders[curve_i];
const int degree = usd_orders[curve_i] - 1;
const Span<double> usd_current_knots = usd_remaining_knots.take_front(knots_num);
const Span<float3> usd_current_points = usd_remaining_points.take_front(points_num);
if (knots_num < 4 || knots_num != usd_current_knots.size()) {
data.is_cyclic[curve_i] = false;
}
else {
data.is_cyclic[curve_i] = usd_current_points.take_front(degree) ==
usd_current_points.take_back(degree);
}
int blender_count = usd_counts[curve_i];
/* Account for any repeated degree(order - 1) number of points from USD cyclic curves which
* Blender does not use internally. */
if (data.is_cyclic[curve_i]) {
blender_count -= degree;
}
data.blender_offsets[curve_i] = blender_count;
data.usd_offsets[curve_i] = points_num;
data.usd_knot_offsets[curve_i] = knots_num;
/* Move to next sequence of values. */
usd_remaining_points = usd_remaining_points.drop_front(points_num);
usd_remaining_knots = usd_remaining_knots.drop_front(knots_num);
}
offset_indices::accumulate_counts_to_offsets(data.blender_offsets);
offset_indices::accumulate_counts_to_offsets(data.usd_offsets);
offset_indices::accumulate_counts_to_offsets(data.usd_knot_offsets);
return data;
}
/** Returns true if the number of curves or the number of curve points in each curve differ. */
static bool curves_topology_changed(const bke::CurvesGeometry &curves, const Span<int> usd_offsets)
{
if (curves.offsets() != usd_offsets) {
return true;
}
return false;
}
static IndexRange get_usd_points_range_de_dup(IndexRange blender_points_range,
IndexRange usd_points_range)
{
/* Take from the front of USD's range to exclude any duplicates at the end. */
return usd_points_range.take_front(blender_points_range.size());
};
bool USDNurbsReader::is_animated() const
{
if (curve_prim_.GetPointsAttr().ValueMightBeTimeVarying() ||
curve_prim_.GetWidthsAttr().ValueMightBeTimeVarying() ||
curve_prim_.GetPointWeightsAttr().ValueMightBeTimeVarying())
{
return true;
}
pxr::UsdGeomPrimvarsAPI pv_api(curve_prim_);
for (const pxr::UsdGeomPrimvar &pv : pv_api.GetPrimvarsWithValues()) {
if (pv.ValueMightBeTimeVarying()) {
return true;
}
}
return false;
}
void USDNurbsReader::read_curve_sample(Curves *curves_id, const pxr::UsdTimeCode time)
{
USDCurveData usd_data;
if (!usd_data.load(curve_prim_, time)) {
return;
}
const Span<float3> usd_points = usd_data.points();
const Span<int> usd_counts = usd_data.counts();
const Span<int> usd_orders = usd_data.orders();
const Span<double> usd_knots = usd_data.knots();
const Span<double> usd_weights = usd_data.weights();
const Span<float3> usd_velocities = usd_data.velocities();
/* Calculate and set the Curves topology. */
CurveData data = calc_curve_offsets(usd_points, usd_counts, usd_orders, usd_knots);
// Check validity of curve counts
const bool all_valid = std::all_of(usd_counts.begin(), usd_counts.end(), [](int count) {
const int min_points = 2;
return count >= min_points;
});
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
if (all_valid && curves_topology_changed(curves, data.blender_offsets)) {
curves.resize(data.blender_offsets.last(), usd_counts.size());
}
// Early out if there are no curves to load.
if (curves.is_empty()) {
return;
}
curves.offsets_for_write().copy_from(data.blender_offsets);
curves.fill_curve_types(CurveType::CURVE_TYPE_NURBS);
MutableSpan<float3> curves_positions = curves.positions_for_write();
/* If there's no points defined, fill positions with default values and exit. */
if (usd_points.is_empty()) {
curves_positions.fill(float3(0.0f, 0.0f, 0.0f));
return;
}
/* NOTE: USD contains duplicated points for periodic(cyclic) curves. The indices into each curve
* will differ from what Blender expects so we need to maintain and use separate offsets for
* each. A side effect of this dissonance is that all primvar/attribute loading needs to be
* handled in a special manner vs. what might be seen in our other USD readers. */
const OffsetIndices blender_points_by_curve = curves.points_by_curve();
const OffsetIndices usd_points_by_curve = OffsetIndices<int>(data.usd_offsets,
offset_indices::NoSortCheck{});
const OffsetIndices usd_knots_by_curve = OffsetIndices<int>(data.usd_knot_offsets,
offset_indices::NoSortCheck{});
/* TODO: We cannot read custom primvars for cyclic curves at the moment. */
const bool can_read_primvars = std::all_of(
data.is_cyclic.begin(), data.is_cyclic.end(), [](bool item) { return item == false; });
/* Set all curve data. */
for (const int curve_i : blender_points_by_curve.index_range()) {
const IndexRange blender_points_range = blender_points_by_curve[curve_i];
const IndexRange usd_points_range_de_dup = get_usd_points_range_de_dup(
blender_points_range, usd_points_by_curve[curve_i]);
const int copy_size = std::min(
usd_points.size(), std::min(blender_points_range.size(), usd_points_range_de_dup.size()));
curves_positions.slice(blender_points_range.start(), copy_size)
.copy_from(usd_points.slice(usd_points_range_de_dup.start(), copy_size));
/* Fill any missing items with a default value. */
for (int i = copy_size; i < blender_points_range.size(); i++) {
curves_positions[blender_points_range.start() + i] = float3(0.0f, 0.0f, 0.0f);
}
}
MutableSpan<bool> curves_cyclic = curves.cyclic_for_write();
curves_cyclic.copy_from(data.is_cyclic);
MutableSpan<int8_t> curves_nurbs_orders = curves.nurbs_orders_for_write();
for (const int curve_i : blender_points_by_curve.index_range()) {
const int order = usd_orders[curve_i];
curves_nurbs_orders[curve_i] = int8_t(order >= 2 ? order : 2);
}
MutableSpan<int8_t> curves_knots_mode = curves.nurbs_knots_modes_for_write();
for (const int curve_i : blender_points_by_curve.index_range()) {
const IndexRange usd_knots_range = usd_knots_by_curve[curve_i];
curves_knots_mode[curve_i] = determine_knots_mode(
usd_knots.slice_safe(usd_knots_range), usd_orders[curve_i], data.is_cyclic[curve_i]);
}
/* Load in the optional weights. */
if (!usd_weights.is_empty()) {
MutableSpan<float> curves_weights = curves.nurbs_weights_for_write();
for (const int curve_i : blender_points_by_curve.index_range()) {
const IndexRange blender_points_range = blender_points_by_curve[curve_i];
const IndexRange usd_points_range_de_dup = get_usd_points_range_de_dup(
blender_points_range, usd_points_by_curve[curve_i]);
const Span<double> usd_weights_de_dup = usd_weights.slice(usd_points_range_de_dup);
int64_t usd_point_i = 0;
for (const int point_i : blender_points_range) {
curves_weights[point_i] = float(usd_weights_de_dup[usd_point_i]);
usd_point_i++;
}
}
}
/* Load in the optional velocities. */
if (!usd_velocities.is_empty()) {
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::SpanAttributeWriter<float3> curves_velocity =
attributes.lookup_or_add_for_write_only_span<float3>("velocity", bke::AttrDomain::Point);
for (const int curve_i : blender_points_by_curve.index_range()) {
const IndexRange blender_points_range = blender_points_by_curve[curve_i];
const IndexRange usd_points_range_de_dup = get_usd_points_range_de_dup(
blender_points_range, usd_points_by_curve[curve_i]);
curves_velocity.span.slice(blender_points_range)
.copy_from(usd_velocities.slice(usd_points_range_de_dup));
}
curves_velocity.finish();
}
/* Once all of the curves metadata (orders, cyclic, knots_mode) has been set, we can prepare
* Blender for any custom knots that need to be loaded. */
MutableSpan<float> blender_custom_knots;
OffsetIndices<int> blender_knots_by_curve;
for (const int curve_i : curves.curves_range()) {
if (curves_knots_mode[curve_i] != NURBS_KNOT_MODE_CUSTOM) {
continue;
}
/* If this is our first time through, we need to update Blender's topology data to prepare for
* the incoming custom knots. */
if (blender_custom_knots.is_empty()) {
curves.nurbs_custom_knots_update_size();
blender_knots_by_curve = curves.nurbs_custom_knots_by_curve();
blender_custom_knots = curves.nurbs_custom_knots_for_write();
}
const IndexRange blender_knots_range = blender_knots_by_curve[curve_i];
const IndexRange usd_knots_range = usd_knots_by_curve[curve_i];
const Span<double> usd_knots_values = usd_knots.slice(usd_knots_range);
MutableSpan<float> blender_knots = blender_custom_knots.slice(blender_knots_range);
int usd_knot_i = 0;
for (float &blender_knot : blender_knots) {
blender_knot = usd_knots_values[usd_knot_i] > 0.0 ? float(usd_knots_values[usd_knot_i]) : 0;
usd_knot_i++;
}
}
/* Curve widths. */
const Span<float> usd_widths = usd_data.widths();
if (!usd_widths.is_empty()) {
MutableSpan<float> radii = curves.radius_for_write();
const pxr::TfToken widths_interp = curve_prim_.GetWidthsInterpolation();
if (widths_interp == pxr::UsdGeomTokens->constant || usd_widths.size() == 1) {
radii.fill(usd_widths[0] / 2.0f);
}
else if (widths_interp == pxr::UsdGeomTokens->varying) {
int point_offset = 0;
for (const int curve_i : curves.curves_range()) {
const float usd_curve_radius = usd_widths[curve_i] / 2.0f;
int point_count = usd_counts[curve_i];
if (curves_cyclic[curve_i]) {
point_count -= usd_orders[curve_i] - 1;
}
for (const int point : IndexRange(point_count)) {
radii[point_offset + point] = usd_curve_radius;
}
point_offset += point_count;
}
}
else if (widths_interp == pxr::UsdGeomTokens->vertex) {
for (const int curve_i : curves.curves_range()) {
const IndexRange blender_points_range = blender_points_by_curve[curve_i];
const IndexRange usd_points_range = usd_points_by_curve[curve_i];
/* Take from the front of USD's range to exclude any duplicates at the end. */
const IndexRange usd_points_range_de_dup = usd_points_range.take_front(
blender_points_range.size());
const Span<float> usd_widths_de_dup = usd_widths.slice(usd_points_range_de_dup);
int64_t usd_point_i = 0;
for (const int point_i : blender_points_range) {
radii[point_i] = usd_widths_de_dup[usd_point_i] / 2.0f;
usd_point_i++;
}
}
}
}
if (can_read_primvars) {
this->read_custom_data(curves, time);
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,49 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#pragma once
#include "usd.hh"
#include "usd_reader_curve.hh"
#include "usd_reader_prim.hh"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
namespace blender {
struct Curves;
namespace bke {
class CurvesGeometry;
} // namespace bke
namespace io::usd {
class USDNurbsReader : public USDCurvesReader {
private:
pxr::UsdGeomNurbsCurves curve_prim_;
public:
USDNurbsReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDCurvesReader(prim, import_params, settings), curve_prim_(prim)
{
}
bool valid() const override
{
return bool(curve_prim_);
}
void read_curve_sample(Curves *curves_id, pxr::UsdTimeCode time) override;
bool is_animated() const override;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,342 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_pointinstancer.hh"
#include "BKE_attribute.hh"
#include "BKE_geometry_set.hh"
#include "BKE_modifier.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
#include "BKE_object.hh"
#include "BKE_pointcloud.hh"
#include "BLI_listbase.h"
#include "BLI_math_quaternion_types.hh"
#include "BLI_string.h"
#include "DNA_collection_types.h"
#include "DNA_node_types.h"
#include <pxr/usd/usdGeom/pointInstancer.h>
namespace blender::io::usd {
/**
* Create a node to read a geometry attribute of the given name and type.
*/
static bNode *add_input_named_attrib_node(bNodeTree *ntree, const char *name, int8_t prop_type)
{
bNode *node = bke::node_add_static_node(nullptr, *ntree, GEO_NODE_INPUT_NAMED_ATTRIBUTE);
auto *storage = reinterpret_cast<NodeGeometryInputNamedAttribute *>(node->storage);
storage->data_type = prop_type;
bNodeSocket *socket = bke::node_find_socket(*node, SOCK_IN, "Name"_ustr);
bNodeSocketValueString *str_value = static_cast<bNodeSocketValueString *>(socket->default_value);
BLI_strncpy(str_value->value, name, MAX_NAME);
return node;
}
void USDPointInstancerReader::create_object(Main *bmain)
{
PointCloud *pointcloud = BKE_pointcloud_add(bmain, name_.c_str());
this->object_ = BKE_object_add_only_object(bmain, OB_POINTCLOUD, name_.c_str());
this->object_->data = id_cast<ID *>(pointcloud);
}
void USDPointInstancerReader::read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char ** /*r_err_str*/)
{
pxr::VtArray<pxr::GfVec3f> usd_positions;
pxr::VtArray<pxr::GfVec3f> usd_scales;
pxr::VtArray<pxr::GfQuath> usd_orientations;
pxr::VtInt64Array usd_ids;
pxr::VtArray<int> usd_proto_indices;
std::vector<bool> usd_mask = point_instancer_prim_.ComputeMaskAtTime(params.motion_sample_time);
point_instancer_prim_.GetPositionsAttr().Get(&usd_positions, params.motion_sample_time);
point_instancer_prim_.GetScalesAttr().Get(&usd_scales, params.motion_sample_time);
point_instancer_prim_.GetOrientationsAttr().Get(&usd_orientations, params.motion_sample_time);
point_instancer_prim_.GetIdsAttr().Get(&usd_ids, params.motion_sample_time);
point_instancer_prim_.GetProtoIndicesAttr().Get(&usd_proto_indices, params.motion_sample_time);
PointCloud *pointcloud = geometry_set.get_pointcloud_for_write();
if (pointcloud->totpoint != usd_positions.size()) {
/* Size changed so we must reallocate. */
pointcloud = BKE_pointcloud_new_nomain(usd_positions.size());
}
MutableSpan<float3> point_positions = pointcloud->positions_for_write();
point_positions.copy_from(Span(usd_positions.cdata(), usd_positions.size()).cast<float3>());
bke::MutableAttributeAccessor attributes = pointcloud->attributes_for_write();
bke::SpanAttributeWriter<float3> scales_attribute =
attributes.lookup_or_add_for_write_only_span<float3>("scale", bke::AttrDomain::Point);
/* Here and below, handle the case where instancing attributes are empty or
* not of the expected size. */
if (usd_scales.size() < usd_positions.size()) {
scales_attribute.span.fill(float3(1.0f));
}
Span<pxr::GfVec3f> scales = Span(usd_scales.cdata(), usd_scales.size());
for (const int i : IndexRange(std::min(usd_scales.size(), usd_positions.size()))) {
scales_attribute.span[i] = float3(scales[i][0], scales[i][1], scales[i][2]);
}
scales_attribute.finish();
bke::SpanAttributeWriter<math::Quaternion> orientations_attribute =
attributes.lookup_or_add_for_write_only_span<math::Quaternion>("orientation",
bke::AttrDomain::Point);
if (usd_orientations.size() < usd_positions.size()) {
orientations_attribute.span.fill(math::Quaternion::identity());
}
Span<pxr::GfQuath> orientations = Span(usd_orientations.cdata(), usd_orientations.size());
for (const int i : IndexRange(std::min(usd_orientations.size(), usd_positions.size()))) {
orientations_attribute.span[i] = math::Quaternion(orientations[i].GetReal(),
orientations[i].GetImaginary()[0],
orientations[i].GetImaginary()[1],
orientations[i].GetImaginary()[2]);
}
orientations_attribute.finish();
if (!usd_ids.empty()) {
bke::SpanAttributeWriter<int> ids_attribute =
attributes.lookup_or_add_for_write_only_span<int>("id", bke::AttrDomain::Point);
const Span<int64_t> usd_data(usd_ids.cdata(), usd_ids.size());
if (usd_data.size() < ids_attribute.span.size()) {
ids_attribute.span.fill(0);
}
for (const int i : IndexRange(std::min(usd_data.size(), ids_attribute.span.size()))) {
/* Blender only supports int ID attributes so we have to narrow the value. */
ids_attribute.span[i] = int(usd_data[i]);
}
ids_attribute.finish();
}
bke::SpanAttributeWriter<int> proto_indices_attribute =
attributes.lookup_or_add_for_write_only_span<int>("proto_index", bke::AttrDomain::Point);
if (usd_proto_indices.size() < usd_positions.size()) {
proto_indices_attribute.span.fill(0);
}
Span<int> proto_indices = Span(usd_proto_indices.cdata(), usd_proto_indices.size());
for (const int i : IndexRange(std::min(usd_proto_indices.size(), usd_positions.size()))) {
proto_indices_attribute.span[i] = proto_indices[i];
}
proto_indices_attribute.finish();
bke::SpanAttributeWriter<bool> mask_attribute =
attributes.lookup_or_add_for_write_only_span<bool>("mask", bke::AttrDomain::Point);
if (usd_mask.size() < usd_positions.size()) {
mask_attribute.span.fill(true);
}
for (const int i : IndexRange(std::min(usd_mask.size(), usd_positions.size()))) {
mask_attribute.span[i] = usd_mask[i];
}
mask_attribute.finish();
geometry_set.replace_pointcloud(pointcloud);
}
void USDPointInstancerReader::read_object_data(Main *bmain, const pxr::UsdTimeCode time)
{
PointCloud *pointcloud = id_cast<PointCloud *>(object_->data);
bke::GeometrySet geometry_set = bke::GeometrySet::from_pointcloud(
pointcloud, bke::GeometryOwnershipType::Editable);
const USDMeshReadParams params = create_mesh_read_params(time.GetValue(),
import_params_.mesh_read_flag);
read_geometry(geometry_set, params, nullptr);
PointCloud *read_pointcloud =
geometry_set.get_component_for_write<bke::PointCloudComponent>().release();
if (read_pointcloud != pointcloud) {
BKE_pointcloud_nomain_to_pointcloud(read_pointcloud, pointcloud);
}
if (is_animated()) {
/* If the point cloud has time-varying data, we add the cache modifier. */
add_cache_modifier();
}
ModifierData *md = BKE_modifier_new(eModifierType_Nodes);
BLI_addtail(&object_->modifiers, md);
BKE_modifiers_persistent_uid_init(*object_, *md);
NodesModifierData &nmd = *reinterpret_cast<NodesModifierData *>(md);
nmd.node_group = bke::node_tree_add_tree(bmain, "Instances", "GeometryNodeTree");
bNodeTree *ntree = nmd.node_group;
ntree->tree_interface.add_socket(
"Geometry", "", "NodeSocketGeometry", NODE_INTERFACE_SOCKET_OUTPUT, nullptr);
ntree->tree_interface.add_socket(
"Geometry", "", "NodeSocketGeometry", NODE_INTERFACE_SOCKET_INPUT, nullptr);
bNode *group_input = bke::node_add_static_node(nullptr, *ntree, NODE_GROUP_INPUT);
group_input->location[0] = -400.0f;
bNode *group_output = bke::node_add_static_node(nullptr, *ntree, NODE_GROUP_OUTPUT);
group_output->location[0] = 500.0f;
group_output->flag |= NODE_DO_OUTPUT;
bNode *instance_on_points_node = bke::node_add_static_node(
nullptr, *ntree, GEO_NODE_INSTANCE_ON_POINTS);
instance_on_points_node->location[0] = 300.0f;
bNodeSocket *socket = bke::node_find_socket(
*instance_on_points_node, SOCK_IN, "Pick Instance"_ustr);
socket->default_value_typed<bNodeSocketValueBoolean>()->value = true;
bNode *mask_attrib_node = add_input_named_attrib_node(ntree, "mask", CD_PROP_BOOL);
mask_attrib_node->location[0] = 100.0f;
mask_attrib_node->location[1] = -100.0f;
bNode *collection_info_node = bke::node_add_static_node(
nullptr, *ntree, GEO_NODE_COLLECTION_INFO);
collection_info_node->location[0] = 100.0f;
collection_info_node->location[1] = -300.0f;
socket = bke::node_find_socket(*collection_info_node, SOCK_IN, "Separate Children"_ustr);
socket->default_value_typed<bNodeSocketValueBoolean>()->value = true;
bNode *indices_attrib_node = add_input_named_attrib_node(ntree, "proto_index", CD_PROP_INT32);
indices_attrib_node->location[0] = 100.0f;
indices_attrib_node->location[1] = -500.0f;
bNode *rotation_attrib_node = add_input_named_attrib_node(
ntree, "orientation", CD_PROP_QUATERNION);
rotation_attrib_node->location[0] = 100.0f;
rotation_attrib_node->location[1] = -700.0f;
bNode *scale_attrib_node = add_input_named_attrib_node(ntree, "scale", CD_PROP_FLOAT3);
scale_attrib_node->location[0] = 100.0f;
scale_attrib_node->location[1] = -900.0f;
bke::node_add_link(*ntree,
*group_input,
*static_cast<bNodeSocket *>(group_input->outputs.first),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Points"_ustr));
bke::node_add_link(*ntree,
*mask_attrib_node,
*bke::node_find_socket(*mask_attrib_node, SOCK_OUT, "Attribute"_ustr),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Selection"_ustr));
bke::node_add_link(
*ntree,
*indices_attrib_node,
*bke::node_find_socket(*indices_attrib_node, SOCK_OUT, "Attribute"_ustr),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Instance Index"_ustr));
bke::node_add_link(*ntree,
*scale_attrib_node,
*bke::node_find_socket(*scale_attrib_node, SOCK_OUT, "Attribute"_ustr),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Scale"_ustr));
bke::node_add_link(*ntree,
*rotation_attrib_node,
*bke::node_find_socket(*rotation_attrib_node, SOCK_OUT, "Attribute"_ustr),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Rotation"_ustr));
bke::node_add_link(*ntree,
*collection_info_node,
*bke::node_find_socket(*collection_info_node, SOCK_OUT, "Instances"_ustr),
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_IN, "Instance"_ustr));
bke::node_add_link(*ntree,
*instance_on_points_node,
*bke::node_find_socket(*instance_on_points_node, SOCK_OUT, "Instances"_ustr),
*group_output,
*static_cast<bNodeSocket *>(group_output->inputs.first));
BKE_ntree_update_after_single_tree_change(*bmain, *ntree);
BKE_object_modifier_set_active(object_, md);
USDXformReader::read_object_data(bmain, time);
}
pxr::SdfPathVector USDPointInstancerReader::proto_paths() const
{
pxr::SdfPathVector paths;
point_instancer_prim_.GetPrototypesRel().GetTargets(&paths);
return paths;
}
void USDPointInstancerReader::set_collection(Main *bmain, Collection &coll)
{
/* create_object() should have been called already. */
BLI_assert(object_);
ModifierData *md = BKE_modifiers_findby_type(this->object_, eModifierType_Nodes);
if (!md) {
BLI_assert_unreachable();
return;
}
NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
bNodeTree *ntree = nmd->node_group;
if (!ntree) {
BLI_assert_unreachable();
return;
}
bNode *collection_node = bke::node_find_node_by_name(*ntree, "Collection Info");
if (!collection_node) {
BLI_assert_unreachable();
return;
}
bNodeSocket *sock = bke::node_find_socket(*collection_node, SOCK_IN, "Collection"_ustr);
if (!sock) {
BLI_assert_unreachable();
return;
}
bNodeSocketValueCollection *socket_data = static_cast<bNodeSocketValueCollection *>(
sock->default_value);
if (socket_data->value != &coll) {
socket_data->value = &coll;
BKE_ntree_update_tag_socket_property(ntree, sock);
BKE_ntree_update_after_single_tree_change(*bmain, *ntree);
}
}
bool USDPointInstancerReader::is_animated() const
{
bool is_animated = false;
is_animated |= point_instancer_prim_.GetPositionsAttr().ValueMightBeTimeVarying();
is_animated |= point_instancer_prim_.GetScalesAttr().ValueMightBeTimeVarying();
is_animated |= point_instancer_prim_.GetOrientationsAttr().ValueMightBeTimeVarying();
is_animated |= point_instancer_prim_.GetProtoIndicesAttr().ValueMightBeTimeVarying();
return is_animated;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,64 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_api_modifier.hh"
#include "usd_reader_geom.hh"
#include <pxr/usd/usdGeom/pointInstancer.h>
namespace blender {
struct Collection;
namespace io::usd {
/* Wraps the UsdGeomPointInstancer schema. Creates a Blender point cloud object. */
class USDPointInstancerReader : public USDGeomReader {
private:
pxr::UsdGeomPointInstancer point_instancer_prim_;
public:
USDPointInstancerReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDGeomReader(prim, import_params, settings), point_instancer_prim_(prim)
{
}
bool valid() const override
{
return bool(point_instancer_prim_);
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
/* This may be called by the cache modifier to update animated geometry. */
void read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str) override;
pxr::SdfPathVector proto_paths() const;
/**
* Set the given collection on the Collection Info
* node referenced by the geometry nodes modifier
* on the object created by the reader. This assumes
* create_object() and read_object_data() have already
* been called.
*
* \param bmain: Pointer to Main
* \param coll: The collection to set
*/
void set_collection(Main *bmain, Collection &coll);
/* Return true if the USD data may be time varying. */
bool is_animated() const;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,192 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_points.hh"
#include "usd_attribute_utils.hh"
#include "BKE_geometry_set.hh"
#include "BKE_object.hh"
#include "BKE_pointcloud.hh"
#include "BLI_span.hh"
#include "DNA_object_types.h"
#include "DNA_pointcloud_types.h"
#include <pxr/usd/usdGeom/primvar.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
namespace blender::io::usd {
void USDPointsReader::create_object(Main *bmain)
{
PointCloud *pointcloud = BKE_pointcloud_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_POINTCLOUD, name_.c_str());
object_->data = id_cast<ID *>(pointcloud);
}
void USDPointsReader::read_object_data(Main *bmain, pxr::UsdTimeCode time)
{
const USDMeshReadParams params = create_mesh_read_params(time.GetValue(),
import_params_.mesh_read_flag);
PointCloud *pointcloud = id_cast<PointCloud *>(object_->data);
bke::GeometrySet geometry_set = bke::GeometrySet::from_pointcloud(
pointcloud, bke::GeometryOwnershipType::Editable);
read_geometry(geometry_set, params, nullptr);
PointCloud *read_pointcloud =
geometry_set.get_component_for_write<bke::PointCloudComponent>().release();
if (read_pointcloud != pointcloud) {
BKE_pointcloud_nomain_to_pointcloud(read_pointcloud, pointcloud);
}
if (is_animated()) {
/* If the point cloud has animated positions or attributes, we add the cache
* modifier. */
add_cache_modifier();
}
/* Update the transform. */
USDXformReader::read_object_data(bmain, time);
}
void USDPointsReader::read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char ** /*r_err_str*/)
{
PointCloud *pointcloud = geometry_set.get_pointcloud_for_write();
/* Get the existing point cloud data. */
pxr::VtVec3fArray usd_positions;
points_prim_.GetPointsAttr().Get(&usd_positions, params.motion_sample_time);
if (pointcloud->totpoint != usd_positions.size()) {
/* Size changed so we must reallocate. */
pointcloud = BKE_pointcloud_new_nomain(usd_positions.size());
}
/* Update point positions and radii */
static_assert(sizeof(pxr::GfVec3f) == sizeof(float3));
MutableSpan<float3> positions = pointcloud->positions_for_write();
positions.copy_from(Span(usd_positions.cdata(), usd_positions.size()).cast<float3>());
pxr::VtFloatArray usd_widths;
points_prim_.GetWidthsAttr().Get(&usd_widths, params.motion_sample_time);
if (!usd_widths.empty()) {
Span<float> widths = Span(usd_widths.cdata(), usd_widths.size());
const pxr::TfToken widths_interp = points_prim_.GetWidthsInterpolation();
if (widths_interp == pxr::UsdGeomTokens->constant) {
set_single_value(pointcloud->attributes_for_write(),
"radius",
bke::AttrDomain::Point,
bke::AttrType::Float,
bke::AttributeInitValue(widths[0] / 2.0f));
}
else {
MutableSpan<float> radii = pointcloud->radius_for_write();
for (const int i_point : IndexRange(std::min(radii.size(), widths.size()))) {
radii[i_point] = widths[i_point] / 2.0f;
}
}
}
/* TODO: Once Blender supports custom normals for points, we can consider reading in normals.
* See UsdGeomPointBased::GetNormalsAttr */
/* Read in IDs, velocity, and generic data. */
this->read_ids(pointcloud, params.motion_sample_time);
this->read_velocities(pointcloud, params.motion_sample_time);
this->read_custom_data(pointcloud, params.motion_sample_time);
geometry_set.replace_pointcloud(pointcloud);
}
void USDPointsReader::read_ids(PointCloud *pointcloud, const pxr::UsdTimeCode time) const
{
pxr::VtInt64Array usd_ids;
points_prim_.GetIdsAttr().Get(&usd_ids, time);
if (!usd_ids.empty()) {
bke::MutableAttributeAccessor attributes = pointcloud->attributes_for_write();
bke::SpanAttributeWriter<int> ids = attributes.lookup_or_add_for_write_only_span<int>(
"id", bke::AttrDomain::Point);
const Span<int64_t> usd_data(usd_ids.cdata(), usd_ids.size());
for (const int i_point : IndexRange(std::min(ids.span.size(), usd_data.size()))) {
/* Blender only supports int ID attributes so we have to narrow the value. */
ids.span[i_point] = int(usd_ids[i_point]);
}
ids.finish();
}
}
void USDPointsReader::read_velocities(PointCloud *pointcloud, const pxr::UsdTimeCode time) const
{
pxr::VtVec3fArray velocities;
points_prim_.GetVelocitiesAttr().Get(&velocities, time);
if (!velocities.empty()) {
bke::MutableAttributeAccessor attributes = pointcloud->attributes_for_write();
bke::SpanAttributeWriter<float3> velocity =
attributes.lookup_or_add_for_write_only_span<float3>("velocity", bke::AttrDomain::Point);
Span<pxr::GfVec3f> usd_data(velocities.cdata(), velocities.size());
velocity.span.copy_from(usd_data.cast<float3>());
velocity.finish();
}
}
void USDPointsReader::read_custom_data(PointCloud *pointcloud, const pxr::UsdTimeCode time) const
{
pxr::UsdGeomPrimvarsAPI pv_api(points_prim_);
std::vector<pxr::UsdGeomPrimvar> primvars = pv_api.GetPrimvarsWithValues();
for (const pxr::UsdGeomPrimvar &pv : primvars) {
const pxr::SdfValueTypeName pv_type = pv.GetTypeName();
if (!pv_type.IsArray()) {
continue; /* Skip non-array primvar attributes. */
}
const bke::AttrDomain domain = bke::AttrDomain::Point;
const std::optional<bke::AttrType> type = convert_usd_type_to_blender(pv_type);
if (!type.has_value()) {
return;
}
bke::MutableAttributeAccessor attributes = pointcloud->attributes_for_write();
copy_primvar_to_blender_attribute(pv, time, *type, domain, {}, attributes);
}
}
bool USDPointsReader::is_animated() const
{
if (!points_prim_) {
return false;
}
bool is_animated = false;
is_animated |= points_prim_.GetPointsAttr().ValueMightBeTimeVarying();
is_animated |= points_prim_.GetVelocitiesAttr().ValueMightBeTimeVarying();
is_animated |= points_prim_.GetWidthsAttr().ValueMightBeTimeVarying();
pxr::UsdGeomPrimvarsAPI pv_api(points_prim_);
std::vector<pxr::UsdGeomPrimvar> primvars = pv_api.GetPrimvarsWithValues();
for (const pxr::UsdGeomPrimvar &pv : primvars) {
is_animated |= pv.ValueMightBeTimeVarying();
}
return is_animated;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_api_modifier.hh"
#include "usd_reader_geom.hh"
#include <pxr/usd/usdGeom/points.h>
namespace blender {
struct Main;
struct PointCloud;
namespace io::usd {
/*
* Read UsdGeomPoints primitives as Blender point clouds.
*/
class USDPointsReader : public USDGeomReader {
private:
pxr::UsdGeomPoints points_prim_;
public:
USDPointsReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDGeomReader(prim, import_params, settings), points_prim_(prim)
{
}
bool valid() const override
{
return bool(points_prim_);
}
/* Initial object creation. */
void create_object(Main *bmain) override;
/* Initial point cloud data update. */
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
/* Implement point cloud update. This may be called by the cache modifier
* to update animated geometry. */
void read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str) override;
void read_ids(PointCloud *pointcloud, const pxr::UsdTimeCode time) const;
void read_velocities(PointCloud *pointcloud, const pxr::UsdTimeCode time) const;
void read_custom_data(PointCloud *pointcloud, const pxr::UsdTimeCode time) const;
/* Return true if the USD data may be time varying. */
bool is_animated() const;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#include "usd_reader_prim.hh"
#include "usd_reader_utils.hh"
#include "usd.hh"
#include "DNA_object_types.h"
#include <pxr/usd/usd/prim.h>
#include "BLI_assert.h"
#include "WM_types.hh"
namespace blender::io::usd {
ReportList *USDPrimReader::reports() const
{
return import_params_.worker_status ? import_params_.worker_status->reports : nullptr;
}
void USDPrimReader::set_props(const bool merge_with_parent, const pxr::UsdTimeCode time)
{
if (!prim_ || !object_) {
return;
}
PropertyImportMode property_import_mode = this->import_params_.property_import_mode;
if (property_import_mode == PropertyImportMode::None) {
return;
}
if (merge_with_parent) {
/* This object represents a parent Xform merged with its child prim.
* Set the parent prim's custom properties on the Object ID. */
if (const pxr::UsdPrim parent_prim = prim_.GetParent()) {
set_id_props_from_prim(&object_->id, parent_prim, property_import_mode, time);
}
}
if (!object_->data) {
/* If the object has no data, set the prim's custom properties on the object.
* This applies to Xforms that have been converted to Empty objects. */
set_id_props_from_prim(&object_->id, prim_, property_import_mode, time);
}
if (object_->data) {
/* If the object has data, the data represents the USD prim, so set the prim's custom
* properties on the data directly. */
set_id_props_from_prim(object_->data, prim_, property_import_mode, time);
}
}
USDPrimReader::USDPrimReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: name_(prim.GetName().GetString()),
object_(nullptr),
prim_(prim),
parent_reader_(nullptr),
import_params_(import_params),
settings_(&settings),
refcount_(0),
is_in_instancer_proto_(false)
{
}
USDPrimReader::~USDPrimReader() = default;
const pxr::UsdPrim &USDPrimReader::prim() const
{
return prim_;
}
Object *USDPrimReader::object() const
{
return object_;
}
void USDPrimReader::object(Object *ob)
{
object_ = ob;
}
bool USDPrimReader::valid() const
{
return prim_.IsValid();
}
int USDPrimReader::refcount() const
{
return refcount_;
}
void USDPrimReader::incref()
{
refcount_++;
}
void USDPrimReader::decref()
{
refcount_--;
BLI_assert(refcount_ >= 0);
}
bool USDPrimReader::is_in_proto() const
{
return prim_ && (prim_.IsInPrototype() || is_in_instancer_proto_);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,183 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#pragma once
#include "usd.hh"
#include "usd_hash_types.hh"
#include "BLI_map.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_set.hh"
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/prim.h>
#include <string>
namespace blender {
struct CacheFile;
struct Main;
struct Material;
struct Object;
struct ReportList;
namespace io::usd {
struct ImportSettings {
bool blender_stage_version_prior_44 = false;
bool do_convert_mat = false;
float4x4 conversion_mat;
/* From MeshSeqCacheModifierData.read_flag */
int read_flag = 0;
std::function<CacheFile *()> get_cache_file{};
/*
* The fields below are mutable because they are used to keep track
* of what the importer is doing. This is necessary even when all
* the other import settings are to remain const.
*/
/* Map a USD material prim path to a Blender material.
* This map is updated by readers during stage traversal. */
mutable Map<pxr::SdfPath, Material *> usd_path_to_mat{};
/* Map a material name to Blender material.
* This map is updated by readers during stage traversal. */
mutable Map<std::string, Material *> mat_name_to_mat{};
/* Map a USD material prim path to a Blender material to be
* converted by invoking the 'on_material_import' USD hook.
* This map is updated by readers during stage traversal. */
mutable Map<pxr::SdfPath, Material *> usd_path_to_mat_for_hook{};
/* Set of paths to USD material primitives that can be converted by the
* 'on_material_import' USD hook. For efficiency this set should
* be populated prior to stage traversal. */
mutable Set<pxr::SdfPath> mat_import_hook_sources{};
/* We use the stage metersPerUnit to convert camera properties from USD scene units to the
* correct millimeter scale that Blender uses for camera parameters. */
double stage_meters_per_unit = 1.0;
pxr::SdfPath skip_prefix{};
/* Combined user-specified and unit conversion scales. */
double scene_scale = 1.0;
};
/* Most generic USD Reader. */
class USDPrimReader {
protected:
StringRefNull name_;
Object *object_;
pxr::UsdPrim prim_;
USDPrimReader *parent_reader_;
const USDImportParams &import_params_;
const ImportSettings *settings_;
int refcount_;
bool is_in_instancer_proto_;
public:
USDPrimReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings);
virtual ~USDPrimReader();
const pxr::UsdPrim &prim() const;
virtual bool valid() const;
virtual void create_object(Main *bmain) = 0;
virtual void read_object_data(Main * /*bmain*/, pxr::UsdTimeCode /*time*/) {};
Object *object() const;
void object(Object *ob);
USDPrimReader *parent() const
{
return parent_reader_;
}
void parent(USDPrimReader *parent)
{
parent_reader_ = parent;
}
/** Get the wmJobWorkerStatus-provided `reports` list pointer, to use with the BKE_report API. */
ReportList *reports() const;
/* Since readers might be referenced through handles
* maintained by modifiers and constraints, we provide
* a reference count to facilitate managing the object
* lifetime.
* TODO(makowalski): investigate transitioning to using
* smart pointers for readers, or, alternatively look into
* making the lifetime management more robust, e.g., by
* making the destructors protected and implementing deletion
* in decref(), etc. */
int refcount() const;
void incref();
void decref();
StringRefNull name() const
{
return name_;
}
pxr::SdfPath prim_path() const
{
return prim_.GetPrimPath();
}
virtual pxr::SdfPath object_prim_path() const
{
return prim_path();
}
virtual pxr::SdfPath data_prim_path() const
{
return prim_path();
}
void set_is_in_instancer_proto(bool flag)
{
is_in_instancer_proto_ = flag;
}
bool is_in_instancer_proto() const
{
return is_in_instancer_proto_;
}
bool is_in_proto() const;
protected:
/**
* Convert custom attributes on the encapsulated USD prim (or on its parent)
* to custom properties on the generated object and/or data. This function
* assumes create_object() and read_object_data() have been called.
*
* If the generated object has instantiated data, it's assumed that the data
* represents the USD prim, and the prim properties will be set on the data ID.
* If the object data is null (which would be the case when a USD Xform is
* converted to an Empty object), then the prim properties will be set on the
* object ID. Finally, a true value for the 'merge_with_parent' argument indicates
* that the object represents a USD Xform and its child prim that were merged
* on import, and the properties of the prim's parent will be set on the object
* ID.
*
* \param merge_with_parent: If true, set the properties of the prim's parent
* on the object ID
* \param time: The time code for sampling the USD attributes.
*/
void set_props(bool merge_with_parent = false,
pxr::UsdTimeCode time = pxr::UsdTimeCode::Default());
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,337 @@
/* SPDX-FileCopyrightText: 2023 Nvidia. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.h"
#include "BKE_geometry_set.hh"
#include "BKE_lib_id.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "usd_attribute_utils.hh"
#include "usd_mesh_utils.hh"
#include "usd_reader_shape.hh"
#include <pxr/usd/usdGeom/capsule.h>
#include <pxr/usd/usdGeom/capsule_1.h>
#include <pxr/usd/usdGeom/cone.h>
#include <pxr/usd/usdGeom/cube.h>
#include <pxr/usd/usdGeom/cylinder.h>
#include <pxr/usd/usdGeom/cylinder_1.h>
#include <pxr/usd/usdGeom/plane.h>
#include <pxr/usd/usdGeom/sphere.h>
#include <pxr/usdImaging/usdImaging/capsuleAdapter.h>
#include <pxr/usdImaging/usdImaging/coneAdapter.h>
#include <pxr/usdImaging/usdImaging/cubeAdapter.h>
#include <pxr/usdImaging/usdImaging/cylinderAdapter.h>
#include <pxr/usdImaging/usdImaging/planeAdapter.h>
#include <pxr/usdImaging/usdImaging/sphereAdapter.h>
namespace blender::io::usd {
USDShapeReader::USDShapeReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDGeomReader(prim, import_params, settings)
{
}
void USDShapeReader::create_object(Main *bmain)
{
Mesh *mesh = BKE_mesh_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_MESH, name_.c_str());
object_->data = id_cast<ID *>(mesh);
}
void USDShapeReader::read_object_data(Main *bmain, pxr::UsdTimeCode time)
{
const USDMeshReadParams params = create_mesh_read_params(time.GetValue(),
import_params_.mesh_read_flag);
Mesh *mesh = id_cast<Mesh *>(object_->data);
Mesh *read_mesh = this->read_mesh(mesh, params, nullptr);
if (read_mesh != mesh) {
BKE_mesh_nomain_to_mesh(read_mesh, mesh, object_);
if (is_time_varying()) {
USDGeomReader::add_cache_modifier();
}
}
USDXformReader::read_object_data(bmain, time);
}
template<typename Adapter>
void USDShapeReader::read_values(const pxr::UsdTimeCode time,
pxr::VtVec3fArray &positions,
pxr::VtIntArray &face_indices,
pxr::VtIntArray &face_counts) const
{
Adapter adapter;
pxr::VtValue points_val = adapter.GetPoints(prim_, time);
if (points_val.IsHolding<pxr::VtVec3fArray>()) {
positions = points_val.UncheckedGet<pxr::VtVec3fArray>();
}
pxr::VtValue topology_val = adapter.GetTopology(prim_, pxr::SdfPath(), time);
if (topology_val.IsHolding<pxr::HdMeshTopology>()) {
const pxr::HdMeshTopology &topology = topology_val.UncheckedGet<pxr::HdMeshTopology>();
face_counts = topology.GetFaceVertexCounts();
face_indices = topology.GetFaceVertexIndices();
}
}
bool USDShapeReader::read_mesh_values(pxr::UsdTimeCode time,
pxr::VtVec3fArray &positions,
pxr::VtIntArray &face_indices,
pxr::VtIntArray &face_counts) const
{
if (prim_.IsA<pxr::UsdGeomCapsule>() || prim_.IsA<pxr::UsdGeomCapsule_1>()) {
read_values<pxr::UsdImagingCapsuleAdapter>(time, positions, face_indices, face_counts);
return true;
}
if (prim_.IsA<pxr::UsdGeomCylinder>() || prim_.IsA<pxr::UsdGeomCylinder_1>()) {
read_values<pxr::UsdImagingCylinderAdapter>(time, positions, face_indices, face_counts);
return true;
}
if (prim_.IsA<pxr::UsdGeomCone>()) {
read_values<pxr::UsdImagingConeAdapter>(time, positions, face_indices, face_counts);
return true;
}
if (prim_.IsA<pxr::UsdGeomCube>()) {
read_values<pxr::UsdImagingCubeAdapter>(time, positions, face_indices, face_counts);
return true;
}
if (prim_.IsA<pxr::UsdGeomSphere>()) {
read_values<pxr::UsdImagingSphereAdapter>(time, positions, face_indices, face_counts);
return true;
}
if (prim_.IsA<pxr::UsdGeomPlane>()) {
read_values<pxr::UsdImagingPlaneAdapter>(time, positions, face_indices, face_counts);
return true;
}
BKE_reportf(reports(),
RPT_ERROR,
"Unhandled Gprim type: %s (%s)",
prim_.GetTypeName().GetText(),
prim_.GetPath().GetText());
return false;
}
Mesh *USDShapeReader::read_mesh(Mesh *existing_mesh,
const USDMeshReadParams params,
const char ** /*r_err_str*/)
{
if (!prim_) {
return existing_mesh;
}
pxr::VtVec3fArray usd_positions;
pxr::VtIntArray usd_face_indices;
pxr::VtIntArray usd_face_counts;
if (!read_mesh_values(
params.motion_sample_time, usd_positions, usd_face_indices, usd_face_counts))
{
return existing_mesh;
}
/* Build or update the existing mesh. */
Mesh *active_mesh = mesh_from_prim(
existing_mesh, params, usd_positions, usd_face_indices, usd_face_counts);
return active_mesh;
}
void USDShapeReader::read_geometry(bke::GeometrySet &geometry_set,
USDMeshReadParams params,
const char **r_err_str)
{
Mesh *existing_mesh = geometry_set.get_mesh_for_write();
Mesh *new_mesh = read_mesh(existing_mesh, params, r_err_str);
if (new_mesh != existing_mesh) {
geometry_set.replace_mesh(new_mesh);
}
}
void USDShapeReader::apply_primvars_to_mesh(Mesh *mesh, const pxr::UsdTimeCode time) const
{
/* TODO: also handle the displayOpacity primvar. */
if (!mesh || !prim_) {
return;
}
pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(prim_);
std::vector<pxr::UsdGeomPrimvar> primvars = pv_api.GetPrimvarsWithValues();
pxr::TfToken active_color_name;
for (const pxr::UsdGeomPrimvar &pv : primvars) {
const pxr::SdfValueTypeName pv_type = pv.GetTypeName();
if (!pv_type.IsArray()) {
continue; /* Skip non-array primvar attributes. */
}
const pxr::TfToken name = pxr::UsdGeomPrimvar::StripPrimvarsName(pv.GetPrimvarName());
/* Skip reading primvars that have been read before and are not time varying. */
if (primvar_time_varying_map_.contains(name) && !primvar_time_varying_map_.lookup(name)) {
continue;
}
const std::optional<bke::AttrType> type = convert_usd_type_to_blender(pv_type);
if (type == bke::AttrType::ColorFloat) {
/* Set the active color name to 'displayColor', if a color primvar
* with this name exists. Otherwise, use the name of the first
* color primvar we find for the active color. */
if (active_color_name.IsEmpty() || name == usdtokens::displayColor) {
active_color_name = name;
}
}
read_generic_mesh_primvar(mesh, pv, time, false);
/* Record whether the primvar attribute might be time varying. */
if (!primvar_time_varying_map_.contains(name)) {
primvar_time_varying_map_.add(name, pv.ValueMightBeTimeVarying());
}
}
if (!active_color_name.IsEmpty()) {
BKE_id_attributes_default_color_set(&mesh->id, active_color_name.GetText());
BKE_id_attributes_active_color_set(&mesh->id, active_color_name.GetText());
}
}
Mesh *USDShapeReader::mesh_from_prim(Mesh *existing_mesh,
const USDMeshReadParams params,
pxr::VtVec3fArray &usd_positions,
pxr::VtIntArray &usd_face_indices,
pxr::VtIntArray &usd_face_counts) const
{
Span<int> face_indices = Span(usd_face_indices.cdata(), usd_face_indices.size());
Span<int> face_counts = Span(usd_face_counts.cdata(), usd_face_counts.size());
Span<float3> positions = Span(usd_positions.cdata(), usd_positions.size()).cast<float3>();
const bool poly_counts_match = existing_mesh ? face_counts.size() == existing_mesh->faces_num :
false;
const bool position_counts_match = existing_mesh ? positions.size() == existing_mesh->verts_num :
false;
Mesh *active_mesh = nullptr;
if (!position_counts_match || !poly_counts_match) {
active_mesh = BKE_mesh_new_nomain_from_template(
existing_mesh, positions.size(), 0, face_counts.size(), face_indices.size());
}
else {
active_mesh = existing_mesh;
}
MutableSpan<float3> vert_positions = active_mesh->vert_positions_for_write();
vert_positions.copy_from(positions);
MutableSpan<int> face_offsets = active_mesh->face_offsets_for_write();
for (const int i : IndexRange(active_mesh->faces_num)) {
face_offsets[i] = face_counts[i];
}
offset_indices::accumulate_counts_to_offsets(face_offsets);
MutableSpan<int> corner_verts = active_mesh->corner_verts_for_write();
for (const int i : corner_verts.index_range()) {
corner_verts[i] = face_indices[i];
}
bke::mesh_calc_edges(*active_mesh, false, false);
/* Don't smooth-shade cubes; we're not worrying about sharpness for Gprims. */
bke::mesh_smooth_set(*active_mesh, !prim_.IsA<pxr::UsdGeomCube>());
if (params.read_flags & MOD_MESHSEQ_READ_COLOR) {
apply_primvars_to_mesh(active_mesh, params.motion_sample_time);
}
return active_mesh;
}
bool USDShapeReader::is_time_varying()
{
for (const bool animating_flag : primvar_time_varying_map_.values()) {
if (animating_flag) {
return true;
}
}
if (prim_.IsA<pxr::UsdGeomCapsule>()) {
pxr::UsdGeomCapsule geom(prim_);
return (geom.GetAxisAttr().ValueMightBeTimeVarying() ||
geom.GetHeightAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusAttr().ValueMightBeTimeVarying());
}
if (prim_.IsA<pxr::UsdGeomCapsule_1>()) {
pxr::UsdGeomCapsule_1 geom(prim_);
return (geom.GetAxisAttr().ValueMightBeTimeVarying() ||
geom.GetHeightAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusTopAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusBottomAttr().ValueMightBeTimeVarying());
}
if (prim_.IsA<pxr::UsdGeomCylinder>()) {
pxr::UsdGeomCylinder geom(prim_);
return (geom.GetAxisAttr().ValueMightBeTimeVarying() ||
geom.GetHeightAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusAttr().ValueMightBeTimeVarying());
}
if (prim_.IsA<pxr::UsdGeomCylinder_1>()) {
pxr::UsdGeomCylinder_1 geom(prim_);
return (geom.GetAxisAttr().ValueMightBeTimeVarying() ||
geom.GetHeightAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusTopAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusBottomAttr().ValueMightBeTimeVarying());
}
if (prim_.IsA<pxr::UsdGeomCone>()) {
pxr::UsdGeomCone geom(prim_);
return (geom.GetAxisAttr().ValueMightBeTimeVarying() ||
geom.GetHeightAttr().ValueMightBeTimeVarying() ||
geom.GetRadiusAttr().ValueMightBeTimeVarying());
}
if (prim_.IsA<pxr::UsdGeomCube>()) {
pxr::UsdGeomCube geom(prim_);
return geom.GetSizeAttr().ValueMightBeTimeVarying();
}
if (prim_.IsA<pxr::UsdGeomSphere>()) {
pxr::UsdGeomSphere geom(prim_);
return geom.GetRadiusAttr().ValueMightBeTimeVarying();
}
if (prim_.IsA<pxr::UsdGeomPlane>()) {
pxr::UsdGeomPlane geom(prim_);
return (geom.GetWidthAttr().ValueMightBeTimeVarying() ||
geom.GetLengthAttr().ValueMightBeTimeVarying() ||
geom.GetAxisAttr().ValueMightBeTimeVarying());
}
BKE_reportf(reports(),
RPT_ERROR,
"Unhandled Gprim type: %s (%s)",
prim_.GetTypeName().GetText(),
prim_.GetPath().GetText());
return false;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,78 @@
/* SPDX-FileCopyrightText: 2023 Nvidia. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_api_modifier.hh"
#include "usd_hash_types.hh"
#include "usd_reader_geom.hh"
namespace blender {
struct Mesh;
namespace io::usd {
/*
* Read USDGeom primitive shapes as Blender Meshes. This class uses the same adapter functions
* as the GL viewport to generate geometry for each of the supported types.
*/
class USDShapeReader : public USDGeomReader {
/* A cache to record whether a given primvar is time-varying, so that static primvars are not
* read more than once when the mesh is evaluated for animation by the cache file modifier.
* The map is mutable so that it can be updated in const functions. */
mutable Map<const pxr::TfToken, bool> primvar_time_varying_map_;
private:
/* Template required to read mesh information out of Shape prims,
* as each prim type has a separate subclass. */
template<typename Adapter>
void read_values(pxr::UsdTimeCode time,
pxr::VtVec3fArray &positions,
pxr::VtIntArray &face_indices,
pxr::VtIntArray &face_counts) const;
/* Wrapper for the templated method read_values, calling the correct template
* instantiation based on the introspected prim type. */
bool read_mesh_values(pxr::UsdTimeCode time,
pxr::VtVec3fArray &positions,
pxr::VtIntArray &face_indices,
pxr::VtIntArray &face_counts) const;
void apply_primvars_to_mesh(Mesh *mesh, pxr::UsdTimeCode time) const;
/* Read the pxr:UsdGeomMesh values and convert them to a Blender Mesh,
* also returning face_indices and counts for further loop processing. */
Mesh *mesh_from_prim(Mesh *existing_mesh,
USDMeshReadParams params,
pxr::VtVec3fArray &positions,
pxr::VtIntArray &face_indices,
pxr::VtIntArray &face_counts) const;
Mesh *read_mesh(Mesh *existing_mesh, USDMeshReadParams params, const char ** /*r_err_str*/);
public:
USDShapeReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings);
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
void read_geometry(bke::GeometrySet & /*geometry_set*/,
USDMeshReadParams /*params*/,
const char ** /*r_err_str*/) override;
/* Returns the generated mesh might be affected by time-varying attributes.
* This assumes mesh_from_prim() has been called. */
bool is_time_varying();
bool topology_changed(const Mesh * /*existing_mesh*/, pxr::UsdTimeCode /*time*/) override
{
return false;
};
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2021 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_skeleton.hh"
#include "usd_skel_convert.hh"
#include "BKE_armature.hh"
#include "BKE_object.hh"
#include "DNA_armature_types.h"
#include "DNA_object_types.h"
namespace blender::io::usd {
void USDSkeletonReader::create_object(Main *bmain)
{
bArmature *arm = BKE_armature_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_ARMATURE, name_.c_str());
object_->data = id_cast<ID *>(arm);
}
void USDSkeletonReader::read_object_data(Main *bmain, const pxr::UsdTimeCode time)
{
if (!object_ || !object_->data) {
return;
}
import_skeleton(bmain, object_, skel_, reports());
USDXformReader::read_object_data(bmain, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_reader_xform.hh"
#include <pxr/usd/usdSkel/skeleton.h>
namespace blender::io::usd {
class USDSkeletonReader : public USDXformReader {
private:
pxr::UsdSkelSkeleton skel_;
public:
USDSkeletonReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings), skel_(prim)
{
}
bool valid() const override
{
return bool(skel_);
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,999 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation and. NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_stage.hh"
#include "usd_hook.hh"
#include "usd_reader_camera.hh"
#include "usd_reader_curve.hh"
#include "usd_reader_instance.hh"
#include "usd_reader_light.hh"
#include "usd_reader_material.hh"
#include "usd_reader_mesh.hh"
#include "usd_reader_nurbs.hh"
#include "usd_reader_pointinstancer.hh"
#include "usd_reader_points.hh"
#include "usd_reader_prim.hh"
#include "usd_reader_shape.hh"
#include "usd_reader_skeleton.hh"
#include "usd_reader_volume.hh"
#include "usd_reader_xform.hh"
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/capsule.h>
#include <pxr/usd/usdGeom/capsule_1.h>
#include <pxr/usd/usdGeom/cone.h>
#include <pxr/usd/usdGeom/cube.h>
#include <pxr/usd/usdGeom/cylinder.h>
#include <pxr/usd/usdGeom/cylinder_1.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/plane.h>
#include <pxr/usd/usdGeom/pointInstancer.h>
#include <pxr/usd/usdGeom/points.h>
#include <pxr/usd/usdGeom/scope.h>
#include <pxr/usd/usdGeom/sphere.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdLux/boundableLightBase.h>
#include <pxr/usd/usdLux/domeLight.h>
#include <pxr/usd/usdLux/domeLight_1.h>
#include <pxr/usd/usdLux/nonboundableLightBase.h>
#include <pxr/usd/usdShade/material.h>
#include "BLI_map.hh"
#include "BLI_math_base.h"
#include "BLI_math_euler_types.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_sort.hh"
#include "BLI_string.h"
#include "BKE_collection.hh"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "CLG_log.h"
#include "DNA_collection_types.h"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "WM_types.hh"
#include <fmt/core.h>
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
static void decref(USDPrimReader *reader)
{
reader->decref();
if (reader->refcount() == 0) {
delete reader;
}
}
/**
* Create a collection with the given parent and name.
*/
static Collection *create_collection(Main *bmain, Collection *parent, const char *name)
{
if (!bmain) {
return nullptr;
}
return BKE_collection_add(bmain, parent, name);
}
/**
* Set the instance collection on the given instance reader.
* The collection is assigned from the given map based on
* the prototype prim path.
*/
static void set_instance_collection(USDInstanceReader *instance_reader,
const Map<pxr::SdfPath, Collection *> &proto_collection_map)
{
if (!instance_reader) {
return;
}
pxr::SdfPath proto_path = instance_reader->proto_path();
Collection *collection = proto_collection_map.lookup_default(proto_path, nullptr);
if (collection != nullptr) {
instance_reader->set_instance_collection(collection);
}
else {
CLOG_WARN(&LOG,
"Couldn't find prototype collection for %s",
instance_reader->prim_path().GetAsString().c_str());
}
}
/* Update the given import settings with the global rotation matrix to orient
* imported objects with Z-up, if necessary */
static void convert_to_z_up(pxr::UsdStageRefPtr stage, ImportSettings &settings)
{
if (!stage || pxr::UsdGeomGetStageUpAxis(stage) == pxr::UsdGeomTokens->z) {
return;
}
settings.do_convert_mat = true;
/* Rotate 90 degrees about the X-axis. */
settings.conversion_mat = math::from_rotation<float4x4>(math::EulerXYZ(M_PI_2, 0.0f, 0.0f));
}
/**
* Find the lowest level of Blender generated roots
* so that round tripping an export can be more invisible
*/
static void find_prefix_to_skip(pxr::UsdStageRefPtr stage, ImportSettings &settings)
{
if (!stage) {
return;
}
pxr::TfToken generated_key("Blender:generated");
pxr::SdfPath path("/");
auto prim = stage->GetPseudoRoot();
while (true) {
uint32_t child_count = 0;
for (auto child : prim.GetChildren()) {
if (child_count == 0) {
prim = child.GetPrim();
}
++child_count;
}
if (child_count != 1) {
/* Our blender write out only supports a single root chain,
* so whenever we encounter more than one child, we should
* early exit */
break;
}
/* We only care about prims that have the key and the value doesn't matter */
if (!prim.HasCustomDataKey(generated_key)) {
break;
}
path = path.AppendChild(prim.GetName());
}
/* Treat the root as empty */
if (path == pxr::SdfPath("/")) {
path = pxr::SdfPath();
}
settings.skip_prefix = path;
}
/**
* Set compatibility flags if the Stage was written by Blender.
*/
static void determine_blender_compat(pxr::UsdStageRefPtr stage, ImportSettings &settings)
{
const std::string doc = stage->GetRootLayer()->GetDocumentation();
/* Was the incoming Stage written by Blender? If so, set some broad compatibility flags. */
if (doc.find("Blender v", 0) == 0) {
/* Set flag if the Blender Stage was from before version 4.4. */
settings.blender_stage_version_prior_44 = doc < "Blender v4.4";
}
}
USDStageReader::USDStageReader(pxr::UsdStageRefPtr stage,
const USDImportParams &params,
const std::function<CacheFile *()> &get_cache_file_fn)
: stage_(stage), params_(params)
{
determine_blender_compat(stage_, settings_);
convert_to_z_up(stage_, settings_);
find_prefix_to_skip(stage_, settings_);
settings_.get_cache_file = get_cache_file_fn;
settings_.stage_meters_per_unit = pxr::UsdGeomGetStageMetersPerUnit(stage);
settings_.scene_scale = params.scale;
if (params.apply_unit_conversion_scale) {
settings_.scene_scale *= settings_.stage_meters_per_unit;
}
}
USDStageReader::~USDStageReader()
{
clear_readers();
}
bool USDStageReader::valid() const
{
return stage_;
}
bool USDStageReader::is_primitive_prim(const pxr::UsdPrim &prim) const
{
return (prim.IsA<pxr::UsdGeomCapsule>() || prim.IsA<pxr::UsdGeomCapsule_1>() ||
prim.IsA<pxr::UsdGeomCylinder>() || prim.IsA<pxr::UsdGeomCylinder_1>() ||
prim.IsA<pxr::UsdGeomCone>() || prim.IsA<pxr::UsdGeomCube>() ||
prim.IsA<pxr::UsdGeomSphere>() || prim.IsA<pxr::UsdGeomPlane>());
}
ReportList *USDStageReader::reports() const
{
return params_.worker_status ? params_.worker_status->reports : nullptr;
}
USDPrimReader *USDStageReader::create_reader_if_allowed(const pxr::UsdPrim &prim)
{
if (params_.support_scene_instancing && prim.IsInstance()) {
return new USDInstanceReader(prim, params_, settings_);
}
if (params_.import_shapes && is_primitive_prim(prim)) {
return new USDShapeReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomPointInstancer>()) {
return new USDPointInstancerReader(prim, params_, settings_);
}
if (params_.import_cameras && prim.IsA<pxr::UsdGeomCamera>()) {
return new USDCameraReader(prim, params_, settings_);
}
if (params_.import_curves && prim.IsA<pxr::UsdGeomBasisCurves>()) {
return new USDBasisCurvesReader(prim, params_, settings_);
}
if (params_.import_curves && prim.IsA<pxr::UsdGeomNurbsCurves>()) {
return new USDNurbsReader(prim, params_, settings_);
}
if (params_.import_meshes && prim.IsA<pxr::UsdGeomMesh>()) {
return new USDMeshReader(prim, params_, settings_);
}
if (params_.import_lights &&
(prim.IsA<pxr::UsdLuxDomeLight>() || prim.IsA<pxr::UsdLuxDomeLight_1>()))
{
/* Dome lights are handled elsewhere. */
return nullptr;
}
if (params_.import_lights &&
(prim.IsA<pxr::UsdLuxBoundableLightBase>() || prim.IsA<pxr::UsdLuxNonboundableLightBase>()))
{
return new USDLightReader(prim, params_, settings_);
}
if (params_.import_volumes && prim.IsA<pxr::UsdVolVolume>()) {
return new USDVolumeReader(prim, params_, settings_);
}
if (params_.import_skeletons && prim.IsA<pxr::UsdSkelSkeleton>()) {
return new USDSkeletonReader(prim, params_, settings_);
}
if (params_.import_points && prim.IsA<pxr::UsdGeomPoints>()) {
return new USDPointsReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomImageable>()) {
return new USDXformReader(prim, params_, settings_);
}
return nullptr;
}
USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim)
{
if (params_.support_scene_instancing && prim.IsInstance()) {
return new USDInstanceReader(prim, params_, settings_);
}
if (is_primitive_prim(prim)) {
return new USDShapeReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomCamera>()) {
return new USDCameraReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomBasisCurves>()) {
return new USDBasisCurvesReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomNurbsCurves>()) {
return new USDNurbsReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomMesh>()) {
return new USDMeshReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdLuxDomeLight>() || prim.IsA<pxr::UsdLuxDomeLight_1>()) {
/* We don't handle dome lights. */
return nullptr;
}
if (prim.IsA<pxr::UsdLuxBoundableLightBase>() || prim.IsA<pxr::UsdLuxNonboundableLightBase>()) {
return new USDLightReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdVolVolume>()) {
return new USDVolumeReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdSkelSkeleton>()) {
return new USDSkeletonReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomPoints>()) {
return new USDPointsReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomPointInstancer>()) {
return new USDPointInstancerReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomImageable>()) {
return new USDXformReader(prim, params_, settings_);
}
return nullptr;
}
bool USDStageReader::include_by_visibility(const pxr::UsdGeomImageable &imageable) const
{
if (!params_.import_visible_only) {
/* Invisible prims are allowed. */
return true;
}
pxr::UsdAttribute visibility_attr = imageable.GetVisibilityAttr();
if (!visibility_attr) {
/* No visibility attribute, so allow. */
return true;
}
/* Include if the prim has an animating visibility attribute or is not invisible. */
if (visibility_attr.ValueMightBeTimeVarying()) {
return true;
}
pxr::TfToken visibility;
visibility_attr.Get(&visibility);
return visibility != pxr::UsdGeomTokens->invisible;
}
bool USDStageReader::include_by_purpose(const pxr::UsdGeomImageable &imageable) const
{
if (params_.import_skeletons && imageable.GetPrim().IsA<pxr::UsdSkelSkeleton>()) {
/* Always include skeletons, if requested by the user, regardless of purpose. */
return true;
}
if (params_.import_guide && params_.import_proxy && params_.import_render) {
/* The options allow any purpose, so we trivially include the prim. */
return true;
}
pxr::UsdAttribute purpose_attr = imageable.GetPurposeAttr();
if (!purpose_attr) {
/* No purpose attribute, so trivially include the prim. */
return true;
}
pxr::TfToken purpose;
purpose_attr.Get(&purpose);
if (purpose == pxr::UsdGeomTokens->guide) {
return params_.import_guide;
}
if (purpose == pxr::UsdGeomTokens->proxy) {
return params_.import_proxy;
}
if (purpose == pxr::UsdGeomTokens->render) {
return params_.import_render;
}
return true;
}
bool USDStageReader::merge_with_parent(USDPrimReader *reader) const
{
/* Don't merge if the param is set to false */
if (!params_.merge_parent_xform) {
return false;
}
USDXformReader *xform_reader = dynamic_cast<USDXformReader *>(reader);
if (!xform_reader) {
return false;
}
/* Check if the Xform reader is already merged. */
if (xform_reader->use_parent_xform()) {
return false;
}
/* Only merge if the parent is an Xform. */
if (!xform_reader->prim().GetParent().IsA<pxr::UsdGeomXform>()) {
return false;
}
/* Don't merge Xform and Scope prims. */
if (xform_reader->prim().IsA<pxr::UsdGeomXform>() ||
xform_reader->prim().IsA<pxr::UsdGeomScope>())
{
return false;
}
/* Don't merge if the prim has authored transform ops. */
if (xform_reader->prim_has_xform_ops()) {
return false;
}
/* Flag the Xform reader as merged. */
xform_reader->set_use_parent_xform(true);
return true;
}
USDPrimReader *USDStageReader::collect_readers(const pxr::UsdPrim &prim,
const UsdPathSet &pruned_prims,
const bool defined_prims_only,
Vector<USDPrimReader *> &r_readers)
{
if (prim.IsA<pxr::UsdGeomImageable>()) {
pxr::UsdGeomImageable imageable(prim);
if (!include_by_purpose(imageable)) {
return nullptr;
}
if (!include_by_visibility(imageable)) {
return nullptr;
}
}
if (prim.IsA<pxr::UsdLuxDomeLight>() || prim.IsA<pxr::UsdLuxDomeLight_1>()) {
USDDomeLightReader *reader = new USDDomeLightReader(prim, params_, settings_);
reader->incref();
dome_light_readers_.append(reader);
}
pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded &&
!pxr::UsdPrimIsAbstract;
if (defined_prims_only) {
filter_flags &= pxr::UsdPrimIsDefined;
}
pxr::Usd_PrimFlagsPredicate filter_predicate(filter_flags);
if (!params_.support_scene_instancing) {
filter_predicate = pxr::UsdTraverseInstanceProxies(filter_predicate);
}
Vector<USDPrimReader *> child_readers;
pxr::UsdPrimSiblingRange children = prim.GetFilteredChildren(filter_predicate);
for (const auto &child_prim : children) {
if (pruned_prims.contains(child_prim.GetPath())) {
continue;
}
if (USDPrimReader *child_reader = collect_readers(
child_prim, pruned_prims, defined_prims_only, r_readers))
{
child_readers.append(child_reader);
}
}
if (prim.IsPseudoRoot()) {
return nullptr;
}
/* If we find prims that have been auto generated by Blender, we skip them on import
* so that the imported scene can closely match the exported scene */
if (!settings_.skip_prefix.IsEmpty()) {
if (settings_.skip_prefix.HasPrefix(prim.GetPath())) {
return nullptr;
}
}
/* Check if we can merge an Xform with its child prim. */
if (child_readers.size() == 1) {
USDPrimReader *child_reader = child_readers.first();
if (merge_with_parent(child_reader)) {
return child_reader;
}
}
if (prim.IsA<pxr::UsdShadeMaterial>()) {
/* Record material path for later processing, if needed,
* e.g., when importing all materials. */
material_paths_.append(prim.GetPath());
/* We don't create readers for materials, so return early. */
return nullptr;
}
USDPrimReader *reader = create_reader_if_allowed(prim);
if (!reader) {
return nullptr;
}
if (!reader->valid()) {
return nullptr;
}
r_readers.append(reader);
reader->incref();
/* Set each child reader's parent. */
for (USDPrimReader *child_reader : child_readers) {
child_reader->parent(reader);
}
return reader;
}
void USDStageReader::collect_readers()
{
if (!valid()) {
return;
}
clear_readers();
/* Identify paths to point instancer prototypes, as these will be converted
* in a separate pass over the stage. */
UsdPathSet instancer_proto_paths = collect_point_instancer_proto_paths();
/* Iterate through the stage. */
pxr::UsdPrim root = stage_->GetPseudoRoot();
stage_->SetInterpolationType(pxr::UsdInterpolationType::UsdInterpolationTypeHeld);
/* Create readers, skipping over prototype prims in this pass. */
collect_readers(root, instancer_proto_paths, params_.import_defined_only, readers_);
if (params_.support_scene_instancing) {
/* Collect the scene-graph instance prototypes. */
std::vector<pxr::UsdPrim> protos = stage_->GetPrototypes();
for (const pxr::UsdPrim &proto_prim : protos) {
Vector<USDPrimReader *> proto_readers;
collect_readers(proto_prim, instancer_proto_paths, true, proto_readers);
proto_readers_.add(proto_prim.GetPath(), proto_readers);
for (USDPrimReader *reader : proto_readers) {
readers_.append(reader);
reader->incref();
}
}
}
if (!instancer_proto_paths.is_empty()) {
create_point_instancer_proto_readers(instancer_proto_paths);
}
}
void USDStageReader::process_armature_modifiers() const
{
/* Iterate over the skeleton readers to create the
* armature object map, which maps a USD skeleton prim
* path to the corresponding armature object. */
Map<pxr::SdfPath, Object *> usd_path_to_armature;
for (const USDPrimReader *reader : readers_) {
if (dynamic_cast<const USDSkeletonReader *>(reader) && reader->object()) {
usd_path_to_armature.add(reader->prim_path(), reader->object());
}
}
/* Iterate over the mesh readers and set armature objects on armature modifiers. */
for (const USDPrimReader *reader : readers_) {
if (!reader->object()) {
continue;
}
const USDMeshReader *mesh_reader = dynamic_cast<const USDMeshReader *>(reader);
if (!mesh_reader) {
continue;
}
/* Check if the mesh object has an armature modifier. */
ModifierData *md = BKE_modifiers_findby_type(reader->object(), eModifierType_Armature);
if (!md) {
continue;
}
ArmatureModifierData *amd = reinterpret_cast<ArmatureModifierData *>(md);
/* Assign the armature based on the bound USD skeleton path of the skinned mesh. */
pxr::SdfPath skel_path = mesh_reader->get_skeleton_path();
Object *object = usd_path_to_armature.lookup_default(skel_path, nullptr);
if (object == nullptr) {
BKE_reportf(reports(),
RPT_WARNING,
"%s: Couldn't find armature object corresponding to USD skeleton %s",
__func__,
skel_path.GetAsString().c_str());
continue;
}
amd->object = object;
/* Per the UsdSkel spec, a skinned mesh's own and ancestor xformOps below the SkelRoot do not
* position the skinned result: the geometry is placed by the bound Skeleton's world transform,
* with `primvars:skel:geomBindTransform` aligning it (skinned mesh world =
* skeleton world * geomBindTransform). Blender's armature deform reproduces this when the mesh
* object is a child of the armature with its local transform equal to the geomBindTransform.
* USDMeshReader::get_local_usd_xform already set the mesh local transform to the
* geomBindTransform (or the identity when no usable geomBindTransform is authored), so
* re-parent the mesh to the armature (with an identity parent-inverse) here. Otherwise the
* mesh keeps the transform of its USD-hierarchy parent and ends up mis-scaled/rotated whenever
* that differs from the skeleton's transform. */
Object *mesh_object = reader->object();
/* Guard against a parent cycle in the unusual case where the bound Skeleton prim is a USD
* descendant of the skinned mesh, in which case the armature is already parented to the mesh.
*/
if (mesh_object == object || BKE_object_parent_loop_check(object, mesh_object)) {
continue;
}
mesh_object->parent = object;
unit_m4(mesh_object->parentinv);
}
}
void USDStageReader::import_all_materials(Main *bmain)
{
BLI_assert(valid());
/* Build the material name map if it's not built yet. */
if (settings_.mat_name_to_mat.is_empty()) {
build_material_map(bmain, settings_.mat_name_to_mat);
}
USDMaterialReader mtl_reader(params_, *bmain);
for (const pxr::SdfPath &mtl_path : material_paths_) {
pxr::UsdPrim prim = stage_->GetPrimAtPath(mtl_path);
pxr::UsdShadeMaterial usd_mtl(prim);
if (!usd_mtl) {
continue;
}
if (io::usd::find_existing_material(
prim.GetPath(), params_, settings_.mat_name_to_mat, settings_.usd_path_to_mat))
{
/* The material already exists. */
continue;
}
/* Can the material be handled by an import hook? */
const bool have_import_hook = settings_.mat_import_hook_sources.contains(mtl_path);
/* Add the Blender material. If we have an import hook which can handle this material
* we don't import USD Preview Surface shaders. */
Material *new_mtl = mtl_reader.add_material(usd_mtl, !have_import_hook);
BLI_assert_msg(new_mtl, "Failed to create material");
settings_.mat_name_to_mat.add_new(new_mtl->id.name + 2, new_mtl);
if (params_.mtl_name_collision_mode == MtlNameCollisionMode::MakeUnique) {
/* Record the Blender material we created for the USD material with the given path.
* This is to prevent importing the material again when assigning materials to objects
* elsewhere in the code. */
settings_.usd_path_to_mat.add_new(mtl_path, new_mtl);
}
if (have_import_hook) {
/* Defer invoking the hook to convert the material till we can do so from
* the main thread. */
settings_.usd_path_to_mat_for_hook.add_new(mtl_path, new_mtl);
}
}
}
void USDStageReader::fake_users_for_unused_materials()
{
/* Iterate over the imported materials and set a fake user for any unused
* materials. */
for (Material *mat : settings_.usd_path_to_mat.values()) {
if (mat->id.us == 0) {
id_fake_user_set(&mat->id);
}
}
}
void USDStageReader::find_material_import_hook_sources()
{
pxr::UsdPrimRange range = stage_->Traverse();
for (pxr::UsdPrim prim : range) {
if (prim.IsA<pxr::UsdShadeMaterial>()) {
pxr::UsdShadeMaterial usd_mat(prim);
if (have_material_import_hook(stage_, usd_mat, params_, reports())) {
settings_.mat_import_hook_sources.add(prim.GetPath());
}
}
}
}
void USDStageReader::call_material_import_hooks(Main *bmain) const
{
if (settings_.usd_path_to_mat_for_hook.is_empty()) {
/* No materials can be converted by a hook. */
return;
}
for (const auto item : settings_.usd_path_to_mat_for_hook.items()) {
pxr::UsdPrim prim = stage_->GetPrimAtPath(item.key);
pxr::UsdShadeMaterial usd_mtl(prim);
if (!usd_mtl) {
continue;
}
bool success = io::usd::call_material_import_hooks(
stage_, item.value, usd_mtl, params_, reports());
if (!success) {
/* None of the hooks succeeded, so fall back on importing USD Preview Surface if possible. */
CLOG_WARN(&LOG,
"USD hook 'on_material_import' for material %s failed, attempting to convert USD "
"Preview Surface material",
usd_mtl.GetPath().GetAsString().c_str());
USDMaterialReader mat_reader(this->params_, *bmain);
mat_reader.import_usd_preview(item.value, usd_mtl);
}
}
}
void USDStageReader::clear_readers()
{
for (USDPrimReader *reader : readers_) {
decref(reader);
}
readers_.clear();
for (const auto item : proto_readers_.items()) {
for (USDPrimReader *reader : item.value) {
decref(reader);
}
}
proto_readers_.clear();
for (const auto item : instancer_proto_readers_.items()) {
for (USDPrimReader *reader : item.value) {
decref(reader);
}
}
instancer_proto_readers_.clear();
for (USDDomeLightReader *reader : dome_light_readers_) {
decref(reader);
}
dome_light_readers_.clear();
}
void USDStageReader::sort_readers()
{
parallel_sort(
readers_.begin(), readers_.end(), [](const USDPrimReader *a, const USDPrimReader *b) {
int result = BLI_strcasecmp(a->name().c_str(), b->name().c_str());
/* The sorting should be deterministic and consistent regardless of how the list of readers
* was created. Use the original, unique, USD prim path to break ties. */
if (result == 0) {
return a->prim_path() < b->prim_path();
}
return result < 0;
});
}
void USDStageReader::create_proto_collections(Main *bmain, Collection *parent_collection)
{
if (proto_readers_.is_empty() && instancer_proto_readers_.is_empty()) {
return;
}
Collection *all_protos_collection = create_collection(bmain, parent_collection, "prototypes");
if (all_protos_collection) {
all_protos_collection->flag |= COLLECTION_HIDE_VIEWPORT;
all_protos_collection->flag |= COLLECTION_HIDE_RENDER;
if (parent_collection) {
DEG_id_tag_update(&parent_collection->id, ID_RECALC_HIERARCHY);
}
}
Map<pxr::SdfPath, Collection *> proto_collection_map;
for (const pxr::SdfPath &path : proto_readers_.keys()) {
Collection *proto_collection = create_collection(bmain, all_protos_collection, "proto");
proto_collection_map.add(path, proto_collection);
}
/* Set the instance collections on the readers, including the prototype
* readers (which are included in readers_), as instancing may be nested. */
for (USDPrimReader *reader : readers_) {
if (USDInstanceReader *instance_reader = dynamic_cast<USDInstanceReader *>(reader)) {
set_instance_collection(instance_reader, proto_collection_map);
}
}
/* Add the prototype objects to the collections. */
for (const auto &item : proto_readers_.items()) {
Collection *collection = proto_collection_map.lookup_default(item.key, nullptr);
if (collection == nullptr) {
CLOG_WARN(&LOG,
"Couldn't find collection when adding objects for prototype %s",
item.key.GetAsString().c_str());
continue;
}
for (const USDPrimReader *reader : item.value) {
Object *ob = reader->object();
if (!ob) {
continue;
}
BKE_collection_object_add(bmain, collection, ob);
}
}
/* Create collections for the point instancer prototypes. */
/* For every point instancer reader, create a "prototypes" collection and set it
* on the Collection Info node referenced by the geometry nodes modifier created by
* the reader. We also create collections containing prototype geometry as children
* of the "prototypes" collection. These child collections will be indexed for
* instancing by the Instance on Points geometry node.
*
* Note that the prototype collections will be ordered alphabetically by the Collection
* Info node. We must therefore take care to generate collection names that will maintain
* the original prototype order, so that the prototype indices will remain valid. We use
* the naming convention proto_<index>, where the index suffix may be zero padded (e.g.,
* "proto_00", "proto_01", "proto_02", etc.).
*/
for (USDPrimReader *reader : readers_) {
USDPointInstancerReader *instancer_reader = dynamic_cast<USDPointInstancerReader *>(reader);
if (!instancer_reader) {
continue;
}
pxr::SdfPathVector proto_paths = instancer_reader->proto_paths();
const pxr::SdfPath &instancer_path = reader->prim().GetPath();
Collection *instancer_protos_coll = create_collection(
bmain, all_protos_collection, instancer_path.GetName().c_str());
/* Determine the max number of digits we will need for the possibly zero-padded
* string representing the prototype index. */
const int max_index_digits = integer_digits_i(proto_paths.size());
int proto_index = 0;
for (const pxr::SdfPath &proto_path : proto_paths) {
BLI_assert(max_index_digits > 0);
/* Format the collection name to follow the proto_<index> pattern. */
std::string coll_name = fmt::format("proto_{0:0{1}}", proto_index, max_index_digits);
/* Create the collection and populate it with the prototype objects. */
Collection *proto_coll = create_collection(bmain, instancer_protos_coll, coll_name.c_str());
Vector<USDPrimReader *> proto_readers = instancer_proto_readers_.lookup_default(proto_path,
{});
for (const USDPrimReader *proto : proto_readers) {
Object *ob = proto->object();
if (!ob) {
continue;
}
BKE_collection_object_add(bmain, proto_coll, ob);
}
++proto_index;
}
instancer_reader->set_collection(bmain, *instancer_protos_coll);
}
}
void USDStageReader::create_point_instancer_proto_readers(const UsdPathSet &proto_paths)
{
if (proto_paths.is_empty()) {
return;
}
for (const pxr::SdfPath &path : proto_paths) {
pxr::UsdPrim proto_prim = stage_->GetPrimAtPath(path);
if (!proto_prim) {
continue;
}
Vector<USDPrimReader *> proto_readers;
/* Note that point instancer prototypes may be defined as overs, so
* we must call collect readers with argument defined_prims_only = false. */
collect_readers(proto_prim, proto_paths, false /* include undefined prims */, proto_readers);
instancer_proto_readers_.add(path, proto_readers);
for (USDPrimReader *reader : proto_readers) {
reader->set_is_in_instancer_proto(true);
readers_.append(reader);
reader->incref();
}
}
}
void USDStageReader::collect_point_instancer_proto_paths(const pxr::UsdPrim &prim,
UsdPathSet &r_paths) const
{
/* Note that we use custom filter flags to allow traversing undefined prims,
* because prototype prims may be defined as overs which are skipped by the
* default predicate. */
pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded &&
!pxr::UsdPrimIsAbstract;
pxr::UsdPrimSiblingRange children = prim.GetFilteredChildren(filter_flags);
for (const auto &child_prim : children) {
/* Note we allow undefined prims in case prototypes are defined as overs.
* If the prim is defined, we apply additional checks for inclusion. */
if (child_prim.IsDefined()) {
const pxr::UsdGeomImageable imageable = pxr::UsdGeomImageable(child_prim);
if (!imageable) {
continue;
}
/* We should only traverse through a hierarchy, and any potential instancers, if they would
* be included by our purpose and visibility checks, matching what is inside
* #collect_readers. */
if (!include_by_purpose(imageable)) {
continue;
}
if (!include_by_visibility(imageable)) {
continue;
}
}
/* We should only consider potential point instancers if they would be included by the scene
* instancing flags. */
if (!params_.support_scene_instancing && child_prim.IsInPrototype()) {
continue;
}
if (pxr::UsdGeomPointInstancer instancer = pxr::UsdGeomPointInstancer(child_prim)) {
pxr::SdfPathVector paths;
instancer.GetPrototypesRel().GetTargets(&paths);
for (const pxr::SdfPath &path : paths) {
r_paths.add(path);
}
}
collect_point_instancer_proto_paths(child_prim, r_paths);
}
}
UsdPathSet USDStageReader::collect_point_instancer_proto_paths() const
{
UsdPathSet result;
if (!stage_) {
return result;
}
collect_point_instancer_proto_paths(stage_->GetPseudoRoot(), result);
std::vector<pxr::UsdPrim> protos = stage_->GetPrototypes();
for (const pxr::UsdPrim &proto_prim : protos) {
collect_point_instancer_proto_paths(proto_prim, result);
}
return result;
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,216 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation and. NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "usd.hh"
#include "usd_hash_types.hh"
#include "usd_reader_domelight.hh"
#include "usd_reader_prim.hh"
#include <pxr/usd/usdGeom/imageable.h>
struct ImportSettings;
namespace blender {
struct Collection;
struct Main;
struct ReportList;
namespace io::usd {
class USDPointInstancerReader;
/**
* Map a USD prototype prim path to the list of readers that convert
* the prototype data.
*/
using ProtoReaderMap = Map<pxr::SdfPath, Vector<USDPrimReader *>>;
using UsdPathSet = Set<pxr::SdfPath>;
class USDStageReader {
protected:
pxr::UsdStageRefPtr stage_;
USDImportParams params_;
ImportSettings settings_;
Vector<USDPrimReader *> readers_;
/* USD dome lights are converted to a world material,
* rather than light objects, so are handled differently */
Vector<USDDomeLightReader *> dome_light_readers_;
/* USD material prim paths encountered during stage
* traversal, for importing unused materials. */
Vector<pxr::SdfPath> material_paths_;
/* Readers for scene-graph instance prototypes. */
ProtoReaderMap proto_readers_;
/* Readers for point instancer prototypes. */
ProtoReaderMap instancer_proto_readers_;
public:
USDStageReader(pxr::UsdStageRefPtr stage,
const USDImportParams &params,
const std::function<CacheFile *()> &get_cache_file_fn = {});
~USDStageReader();
USDPrimReader *create_reader_if_allowed(const pxr::UsdPrim &prim);
USDPrimReader *create_reader(const pxr::UsdPrim &prim);
void collect_readers();
/**
* Complete setting up the armature modifiers that
* were created for skinned meshes by setting the
* modifier object on the corresponding modifier.
*/
void process_armature_modifiers() const;
/* Convert every material prim on the stage to a Blender
* material, including materials not used by any geometry.
* Note that collect_readers() must be called before calling
* import_all_materials(). */
void import_all_materials(struct Main *bmain);
/* Add fake users for any imported materials with no
* users. This is typically required when importing all
* materials. */
void fake_users_for_unused_materials();
/**
* Discover the USD materials that can be converted
* by material import hook add-ons.
*/
void find_material_import_hook_sources();
/**
* Invoke USD hook add-ons to convert materials. This function
* should be called from the main thread and not from a
* background job.
*/
void call_material_import_hooks(struct Main *bmain) const;
bool valid() const;
pxr::UsdStageRefPtr stage()
{
return stage_;
}
const USDImportParams &params() const
{
return params_;
}
const ImportSettings &settings() const
{
return settings_;
}
/** Get the wmJobWorkerStatus-provided `reports` list pointer, to use with the BKE_report API. */
ReportList *reports() const;
/** Clear all cached reader collections. */
void clear_readers();
const Vector<USDPrimReader *> &readers() const
{
return readers_;
};
const Vector<USDDomeLightReader *> &dome_light_readers() const
{
return dome_light_readers_;
};
void sort_readers();
/**
* Create prototype collections for instancing by the USD instance readers.
*/
void create_proto_collections(Main *bmain, Collection *parent_collection);
private:
/**
* Create readers for the subtree rooted at the given prim and append the
* new readers in r_readers.
*
* \param prim: Root of the subtree to convert to readers
* \param pruned_prims: Set of paths to prune when iterating over the
* stage during conversion. I.e., these prims
* and their descendants will not be converted to
* readers.
* \param defined_prims_only: If true, only defined prims will be converted,
* skipping abstract and over prims. This should
* be set to false when converting point instancer
* prototype prims, which can be declared as overs.
* \param r_readers: Readers created for the prims in the converted subtree.
* \return A pointer to the reader created for the given prim or null if
* the prim cannot be converted.
*/
USDPrimReader *collect_readers(const pxr::UsdPrim &prim,
const UsdPathSet &pruned_prims,
bool defined_prims_only,
Vector<USDPrimReader *> &r_readers);
/**
* Returns true if the given prim should be included in the
* traversal based on the import options and the prim's visibility
* attribute. Note that the prim will be trivially included
* if it has no visibility attribute or if the visibility
* is inherited.
*/
bool include_by_visibility(const pxr::UsdGeomImageable &imageable) const;
/**
* Returns true if the given prim should be included in the
* traversal based on the import options and the prim's purpose
* attribute. E.g., return false (to exclude the prim) if the prim
* represents guide geometry and the 'Import Guide' option is
* toggled off.
*/
bool include_by_purpose(const pxr::UsdGeomImageable &imageable) const;
/**
* Returns true if the given reader can use the parent of the encapsulated USD prim
* to compute the Blender object's transform. If so, the reader is appropriately
* flagged and the function returns true. Otherwise, the function returns false.
*/
bool merge_with_parent(USDPrimReader *reader) const;
/**
* Returns true if the specified UsdPrim is a UsdGeom primitive,
* procedural shape, such as UsdGeomCube.
*/
bool is_primitive_prim(const pxr::UsdPrim &prim) const;
/**
* Iterate over the stage and return the paths of all prototype
* primitives references by point instancers.
*
* \return The prototype paths, or an empty path set if the scene
* does not contain any point instancers.
*/
UsdPathSet collect_point_instancer_proto_paths() const;
void collect_point_instancer_proto_paths(const pxr::UsdPrim &prim, UsdPathSet &r_paths) const;
/**
* Populate the instancer_proto_readers_ map for the prototype prims
* in the given set. For each prototype path, this function will
* create readers for the prims in the subtree rooted at the prototype
* prim.
*/
void create_point_instancer_proto_readers(const UsdPathSet &proto_paths);
};
}; // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,334 @@
/* SPDX-FileCopyrightText: 2024 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_utils.hh"
#include "BLI_string_ref.hh"
#include "BKE_idprop.hh"
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usdUI/accessibilityAPI.h>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace {
template<typename VECT>
void set_array_prop(IDProperty *idgroup,
const StringRefNull prop_name,
const pxr::UsdAttribute &attr,
const pxr::UsdTimeCode time)
{
if (!idgroup || !attr) {
return;
}
VECT vec;
if (!attr.Get<VECT>(&vec, time)) {
return;
}
IDPropertyTemplate val = {0};
val.array.len = int(vec.dimension);
if (val.array.len <= 0) {
CLOG_WARN(&LOG, "Invalid array length for prop %s", prop_name.c_str());
return;
}
if (std::is_same<float, typename VECT::ScalarType>()) {
val.array.type = IDP_FLOAT;
}
else if (std::is_same<pxr::GfHalf, typename VECT::ScalarType>()) {
val.array.type = IDP_FLOAT;
}
else if (std::is_same<double, typename VECT::ScalarType>()) {
val.array.type = IDP_DOUBLE;
}
else if (std::is_same<int, typename VECT::ScalarType>()) {
val.array.type = IDP_INT;
}
else {
CLOG_WARN(&LOG, "Couldn't determine array type for prop %s", prop_name.c_str());
return;
}
IDProperty *prop = IDP_New(IDP_ARRAY, &val, prop_name);
if (!prop) {
CLOG_WARN(&LOG, "Couldn't create array prop %s", prop_name.c_str());
return;
}
if (std::is_same<pxr::GfHalf, typename VECT::ScalarType>()) {
float *prop_data = static_cast<float *>(prop->data.pointer);
for (int i = 0; i < val.array.len; ++i) {
prop_data[i] = vec[i];
}
}
else {
std::memcpy(prop->data.pointer, vec.data(), prop->len * sizeof(typename VECT::ScalarType));
}
IDP_AddToGroup(idgroup, prop);
}
bool equivalent(const pxr::SdfValueTypeName &type_name1, const pxr::SdfValueTypeName &type_name2)
{
return type_name1.GetType().IsA(type_name2.GetType());
}
} // anonymous namespace
namespace io::usd {
static void set_string_prop(IDProperty *idgroup,
const StringRefNull prop_name,
const StringRefNull str_val)
{
if (!idgroup) {
return;
}
IDPropertyTemplate val = {0};
val.string.str = str_val.data();
/* Note length includes null terminator. */
val.string.len = str_val.size() + 1;
val.string.subtype = IDP_STRING_SUB_UTF8;
IDProperty *prop = IDP_New(IDP_STRING, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
static void set_int_prop(IDProperty *idgroup, const StringRefNull prop_name, const int ival)
{
if (!idgroup) {
return;
}
IDPropertyTemplate val = {0};
val.i = ival;
IDProperty *prop = IDP_New(IDP_INT, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
static void set_bool_prop(IDProperty *idgroup, const StringRefNull prop_name, const bool bval)
{
if (!idgroup) {
return;
}
IDPropertyTemplate val = {0};
val.i = bval;
IDProperty *prop = IDP_New(IDP_BOOLEAN, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
static void set_float_prop(IDProperty *idgroup, const StringRefNull prop_name, const float fval)
{
if (!idgroup) {
return;
}
IDPropertyTemplate val = {0};
val.f = fval;
IDProperty *prop = IDP_New(IDP_FLOAT, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
static void set_double_prop(IDProperty *idgroup, const StringRefNull prop_name, const double dval)
{
if (!idgroup) {
return;
}
IDPropertyTemplate val = {0};
val.d = dval;
IDProperty *prop = IDP_New(IDP_DOUBLE, &val, prop_name);
IDP_AddToGroup(idgroup, prop);
}
static void set_accessibility_property(const pxr::UsdAttribute &attr,
IDProperty *idgroup,
const pxr::UsdTimeCode time_code)
{
/* Only set the property if the attribute has an authored value. */
if (!attr.IsAuthored()) {
return;
}
/* Since STRING properties do not support keyframes, if there is already a value written
* for this property, don't try to write it again. */
std::string property_name = attr.GetName().GetString();
if (IDP_GetPropertyFromGroup(idgroup, property_name)) {
return;
}
pxr::SdfValueTypeName type_name = attr.GetTypeName();
if (type_name == pxr::SdfValueTypeNames->String) {
std::string value;
if (attr.Get<std::string>(&value, time_code)) {
set_string_prop(idgroup, property_name, value);
}
}
else if (type_name == pxr::SdfValueTypeNames->Token) {
pxr::TfToken value;
if (attr.Get<pxr::TfToken>(&value, time_code)) {
set_string_prop(idgroup, property_name, value.GetString());
}
}
}
void set_id_props_from_prim(ID *id,
const pxr::UsdPrim &prim,
const PropertyImportMode property_import_mode,
const pxr::UsdTimeCode time_code)
{
for (const auto &api : pxr::UsdUIAccessibilityAPI::GetAll(prim)) {
IDProperty *idgroup = IDP_EnsureProperties(id);
set_accessibility_property(api.GetLabelAttr(), idgroup, time_code);
set_accessibility_property(api.GetDescriptionAttr(), idgroup, time_code);
set_accessibility_property(api.GetPriorityAttr(), idgroup, time_code);
}
pxr::UsdAttributeVector attribs = prim.GetAuthoredAttributes();
if (attribs.empty()) {
return;
}
bool all_custom_attrs = (property_import_mode == PropertyImportMode::All);
for (const pxr::UsdAttribute &attr : attribs) {
if (!attr.IsCustom()) {
continue;
}
std::vector<std::string> attr_names = attr.SplitName();
const bool is_user_prop = attr_names[0] == "userProperties";
if (attr_names.size() > 2 && is_user_prop && attr_names[1] == "blender") {
continue;
}
if (!all_custom_attrs && !is_user_prop) {
continue;
}
IDProperty *idgroup = IDP_EnsureProperties(id);
/* When importing user properties, strip the namespace. */
pxr::TfToken attr_name;
if (is_user_prop) {
/* We strip the userProperties namespace, but leave others in case
* someone's custom attribute namespace is important in their pipeline. */
const StringRefNull token = "userProperties:";
const StringRefNull name = attr.GetName().GetString();
attr_name = pxr::TfToken(name.substr(token.size()));
}
else {
attr_name = attr.GetName();
}
pxr::SdfValueTypeName type_name = attr.GetTypeName();
if (type_name == pxr::SdfValueTypeNames->Int) {
int ival = 0;
if (attr.Get<int>(&ival, time_code)) {
set_int_prop(idgroup, attr_name.GetString(), ival);
}
}
else if (type_name == pxr::SdfValueTypeNames->Float) {
float fval = 0.0f;
if (attr.Get<float>(&fval, time_code)) {
set_float_prop(idgroup, attr_name.GetString(), fval);
}
}
else if (type_name == pxr::SdfValueTypeNames->Double) {
double dval = 0.0;
if (attr.Get<double>(&dval, time_code)) {
set_double_prop(idgroup, attr_name.GetString(), dval);
}
}
else if (type_name == pxr::SdfValueTypeNames->Half) {
pxr::GfHalf hval = 0.0f;
if (attr.Get<pxr::GfHalf>(&hval, time_code)) {
set_float_prop(idgroup, attr_name.GetString(), hval);
}
}
else if (type_name == pxr::SdfValueTypeNames->String) {
std::string sval;
if (attr.Get<std::string>(&sval, time_code)) {
set_string_prop(idgroup, attr_name.GetString(), sval);
}
}
else if (type_name == pxr::SdfValueTypeNames->Token) {
pxr::TfToken tval;
if (attr.Get<pxr::TfToken>(&tval, time_code)) {
set_string_prop(idgroup, attr_name.GetString(), tval.GetString());
}
}
else if (type_name == pxr::SdfValueTypeNames->Asset) {
pxr::SdfAssetPath aval;
if (attr.Get<pxr::SdfAssetPath>(&aval, time_code)) {
set_string_prop(idgroup, attr_name.GetString(), aval.GetAssetPath());
}
}
else if (type_name == pxr::SdfValueTypeNames->Bool) {
bool bval = false;
if (attr.Get<bool>(&bval, time_code)) {
set_bool_prop(idgroup, attr_name.GetString(), bval);
}
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float2)) {
set_array_prop<pxr::GfVec2f>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float3)) {
set_array_prop<pxr::GfVec3f>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float4)) {
set_array_prop<pxr::GfVec4f>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double2)) {
set_array_prop<pxr::GfVec2d>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double3)) {
set_array_prop<pxr::GfVec3d>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double4)) {
set_array_prop<pxr::GfVec4d>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int2)) {
set_array_prop<pxr::GfVec2i>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int3)) {
set_array_prop<pxr::GfVec3i>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int4)) {
set_array_prop<pxr::GfVec4i>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half2)) {
set_array_prop<pxr::GfVec2h>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half3)) {
set_array_prop<pxr::GfVec3h>(idgroup, attr_name.GetString(), attr, time_code);
}
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half4)) {
set_array_prop<pxr::GfVec4h>(idgroup, attr_name.GetString(), attr, time_code);
}
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2024 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/timeCode.h>
namespace blender {
struct ID;
namespace io::usd {
void set_id_props_from_prim(ID *id,
const pxr::UsdPrim &prim,
PropertyImportMode property_import_mode = PropertyImportMode::All,
pxr::UsdTimeCode time_code = pxr::UsdTimeCode::Default());
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,81 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_volume.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "BKE_volume.hh"
#include "DNA_object_types.h"
#include "DNA_volume_types.h"
#include <pxr/usd/usdVol/openVDBAsset.h>
#include <pxr/usd/usdVol/volume.h>
namespace blender::io::usd {
void USDVolumeReader::create_object(Main *bmain)
{
Volume *volume = BKE_volume_add(bmain, name_.c_str());
object_ = BKE_object_add_only_object(bmain, OB_VOLUME, name_.c_str());
object_->data = id_cast<ID *>(volume);
}
void USDVolumeReader::read_object_data(Main *bmain, const pxr::UsdTimeCode time)
{
Volume *volume = id_cast<Volume *>(object_->data);
pxr::UsdVolVolume::FieldMap fields = volume_.GetFieldPaths();
for (pxr::UsdVolVolume::FieldMap::const_iterator it = fields.begin(); it != fields.end(); ++it) {
pxr::UsdPrim fieldPrim = prim_.GetStage()->GetPrimAtPath(it->second);
if (!fieldPrim.IsA<pxr::UsdVolOpenVDBAsset>()) {
continue;
}
pxr::UsdVolOpenVDBAsset fieldBase(fieldPrim);
pxr::UsdAttribute filepathAttr = fieldBase.GetFilePathAttr();
if (filepathAttr.IsAuthored()) {
pxr::SdfAssetPath fp;
filepathAttr.Get(&fp, time);
const std::string filepath = fp.GetResolvedPath();
STRNCPY(volume->filepath, filepath.c_str());
if (import_params_.relative_path && !BLI_path_is_rel(volume->filepath)) {
BLI_path_rel(volume->filepath, BKE_main_blendfile_path_from_global());
}
if (filepathAttr.ValueMightBeTimeVarying()) {
std::vector<double> filePathTimes;
filepathAttr.GetTimeSamples(&filePathTimes);
if (!filePathTimes.empty()) {
const int start = int(filePathTimes.front());
const int end = int(filePathTimes.back());
const int offset = BLI_path_sequence_decode(
volume->filepath, nullptr, 0, nullptr, 0, nullptr);
volume->is_sequence = char(true);
volume->frame_start = start;
volume->frame_duration = (end - start) + 1;
volume->frame_offset = offset - 1;
}
}
}
}
USDXformReader::read_object_data(bmain, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include "usd_reader_xform.hh"
#include <pxr/usd/usdVol/volume.h>
namespace blender::io::usd {
class USDVolumeReader : public USDXformReader {
private:
pxr::UsdVolVolume volume_;
public:
USDVolumeReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings), volume_(prim)
{
}
bool valid() const override
{
return bool(volume_);
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,180 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#include "usd_reader_xform.hh"
#include "BKE_constraint.h"
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_string.h"
#include "DNA_cachefile_types.h"
#include "DNA_constraint_types.h"
#include "DNA_object_types.h"
#include <pxr/base/gf/matrix4f.h>
#include <pxr/usd/usdGeom/xformable.h>
#include <string>
namespace blender::io::usd {
void USDXformReader::create_object(Main *bmain)
{
object_ = BKE_object_add_only_object(bmain, OB_EMPTY, name_.c_str());
object_->empty_drawsize = 0.1f;
object_->data = nullptr;
}
void USDXformReader::read_object_data(Main * /*bmain*/, const pxr::UsdTimeCode time)
{
bool is_constant;
float4x4 transform_from_usd;
read_matrix(transform_from_usd, time, settings_->scene_scale, &is_constant);
if (!is_constant && settings_->get_cache_file) {
bConstraint *con = BKE_constraint_add_for_object(
object_, nullptr, CONSTRAINT_TYPE_TRANSFORM_CACHE);
bTransformCacheConstraint *data = static_cast<bTransformCacheConstraint *>(con->data);
pxr::SdfPath object_path = use_parent_xform_ ? prim_.GetParent().GetPath() : this->prim_path();
STRNCPY(data->object_path, object_path.GetAsString().c_str());
data->cache_file = settings_->get_cache_file();
id_us_plus(&data->cache_file->id);
}
BKE_object_apply_mat4(object_, transform_from_usd.ptr(), true, false);
/* Make sure to collect custom attributes */
set_props(use_parent_xform(), time);
}
pxr::SdfPath USDXformReader::object_prim_path() const
{
return get_xformable().GetPrim().GetPath();
}
void USDXformReader::read_matrix(float4x4 &r_mat /* local matrix */,
const pxr::UsdTimeCode time,
const float scale,
bool *r_is_constant) const
{
BLI_assert(r_is_constant);
*r_is_constant = true;
r_mat = float4x4::identity();
std::optional<XformResult> xf_result = get_local_usd_xform(time);
if (!xf_result) {
return;
}
std::get<0>(*xf_result).Get(r_mat.ptr());
*r_is_constant = std::get<1>(*xf_result);
/* Apply global scaling and rotation only to root objects, parenting
* will propagate it. */
if (is_root_xform_prim()) {
if (scale != 1.0f) {
const float4x4 mat_scale = math::from_scale<float4x4>(float3(scale));
r_mat = mat_scale * r_mat;
}
if (settings_->do_convert_mat) {
r_mat = settings_->conversion_mat * r_mat;
}
}
}
bool USDXformReader::prim_has_xform_ops() const
{
const pxr::UsdGeomXformable xformable(prim_);
if (!xformable) {
/* This might happen if the prim is a Scope. */
return false;
}
bool reset_xform_stack = false;
return !xformable.GetOrderedXformOps(&reset_xform_stack).empty();
}
bool USDXformReader::is_root_xform_prim() const
{
if (!prim_.IsValid()) {
return false;
}
if (is_in_proto()) {
/* We don't consider prototypes to be root prims,
* because we never want to apply global scaling
* or rotations to the prototypes themselves. */
return false;
}
if (prim_.IsA<pxr::UsdGeomXformable>()) {
/* If this prim doesn't have an ancestor that's a
* UsdGeomXformable, then it's a root prim. Note
* that it's not sufficient to only check the immediate
* parent prim, since the immediate parent could be a
* UsdGeomScope that has an xformable ancestor. */
pxr::UsdPrim cur_parent = prim_.GetParent();
if (use_parent_xform_) {
cur_parent = cur_parent.GetParent();
}
while (cur_parent && !cur_parent.IsPseudoRoot()) {
if (cur_parent.IsA<pxr::UsdGeomXformable>()) {
return false;
}
cur_parent = cur_parent.GetParent();
}
/* We didn't find an xformable ancestor. */
return true;
}
return false;
}
std::optional<XformResult> USDXformReader::get_local_usd_xform(const pxr::UsdTimeCode time) const
{
const pxr::UsdGeomXformable xformable = get_xformable();
if (!xformable) {
/* This might happen if the prim is a Scope. */
return std::nullopt;
}
bool is_constant = !xformable.TransformMightBeTimeVarying();
bool reset_xform_stack;
pxr::GfMatrix4d xform;
if (!xformable.GetLocalTransformation(&xform, &reset_xform_stack, time)) {
return std::nullopt;
}
/* The USD bind transform is a matrix of doubles,
* but we cast it to GfMatrix4f because Blender expects
* a matrix of floats. */
return XformResult(pxr::GfMatrix4f(xform), is_constant);
}
pxr::UsdGeomXformable USDXformReader::get_xformable() const
{
pxr::UsdPrim prim = use_parent_xform_ ? prim_.GetParent() : prim_;
return pxr::UsdGeomXformable(prim);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2021 Tangent Animation. All rights reserved.
* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Adapted from the Blender Alembic importer implementation. */
#pragma once
#include "usd.hh"
#include "usd_reader_prim.hh"
/* For #UsdGeomXformable. */
#include <pxr/usd/usdGeom/xformable.h>
namespace blender {
struct Main;
namespace io::usd {
/**
* A transformation matrix and a boolean indicating
* whether the matrix is constant over time.
*/
using XformResult = std::tuple<pxr::GfMatrix4f, bool>;
class USDXformReader : public USDPrimReader {
private:
bool use_parent_xform_ = false;
public:
USDXformReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings)
: USDPrimReader(prim, import_params, settings)
{
}
void create_object(Main *bmain) override;
void read_object_data(Main *bmain, pxr::UsdTimeCode time) override;
pxr::SdfPath object_prim_path() const override;
void read_matrix(float4x4 &r_mat, pxr::UsdTimeCode time, float scale, bool *r_is_constant) const;
bool use_parent_xform() const
{
return use_parent_xform_;
}
void set_use_parent_xform(bool flag)
{
use_parent_xform_ = flag;
}
bool prim_has_xform_ops() const;
protected:
/* Returns true if the contained USD prim is the root of a transform hierarchy. */
virtual bool is_root_xform_prim() const;
/**
* Return the USD prim's local transformation.
*
* \param time: Time code for evaluating the transform.
*
* \return Optional tuple with the following elements:
* - The transform matrix.
* - A boolean flag indicating whether the matrix
* is constant over time.
*/
virtual std::optional<XformResult> get_local_usd_xform(pxr::UsdTimeCode time) const;
private:
pxr::UsdGeomXformable get_xformable() const;
};
} // namespace io::usd
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
namespace blender {
struct Depsgraph;
struct Main;
struct Mesh;
struct Object;
struct ReportList;
namespace io::usd {
/**
* This file contains utilities for converting between `UsdSkel` data and
* Blender armatures and shape keys. The following is a reference on the
* `UsdSkel` API:
*
* https://openusd.org/23.05/api/usd_skel_page_front.html
*/
/**
* Import USD blend shapes from a USD primitive as shape keys on a mesh
* object. Optionally, if the blend shapes have animating weights, the
* time-sampled weights will be imported as shape key animation curves.
* If the USD primitive does not have blend shape targets defined, this
* function is a no-op.
*
* \param bmain: Main pointer
* \param mesh_obj: Mesh object to which imported shape keys will be added
* \param prim: The USD primitive from which blend-shapes will be imported
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
* \param import_anim: Whether to import time-sampled weights as shape key
* animation curves
*/
void import_blendshapes(Main *bmain,
Object *mesh_obj,
const pxr::UsdPrim &prim,
ReportList *reports,
bool import_anim = true);
/**
* Import the given USD skeleton as an armature object. Optionally, if the
* skeleton has an animation defined, the time sampled joint transforms will be
* imported as bone animation curves.
*
* \param bmain: Main pointer
* \param arm_obj: Armature object to which the bone hierarchy will be added
* \param skel: The USD skeleton from which bones and animation will be imported
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
* \param import_anim: Whether to import time-sampled joint transforms as bone
* animation curves
*/
void import_skeleton(Main *bmain,
Object *arm_obj,
const pxr::UsdSkelSkeleton &skel,
ReportList *reports,
bool import_anim = true);
/**
* Import skinning data from a source USD prim as deform groups and an armature
* modifier on the given mesh object. If the USD prim does not have a skeleton
* binding defined, this function is a no-op.
*
* \param mesh_obj: Mesh object to which an armature modifier will be added
* \param prim: The USD primitive from which skinning data will be imported
* \param reports: the storage for potential warning or error reports (generated using BKE_report
* API).
*/
void import_mesh_skel_bindings(Object *mesh_obj, const pxr::UsdPrim &prim, ReportList *reports);
/**
* Map an object to its USD prim export path.
*/
using ObjExportMap = Map<const Object *, pxr::SdfPath>;
/**
* This function is called after the USD writers are invoked, to
* complete the UsdSkel export process, for example, to bind skinned
* meshes to skeletons or to set blend shape animation data.
*
* \param stage: The stage
* \param armature_export_map: Map armature objects to USD skeletons
* \param skinned_mesh_export_map: Map mesh objects to USD skinned meshes
* \param shape_key_mesh_export_map: Map mesh objects with shape-key to USD meshes
* with blend shape targets
* \param depsgraph: The dependency graph in which objects were evaluated
*/
void skel_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
const ObjExportMap &shape_key_mesh_export_map,
const Depsgraph *depsgraph);
/**
* Complete the export process for skinned meshes.
*
* \param stage: The stage
* \param armature_export_map: Map armature objects to USD skeleton paths
* \param skinned_mesh_export_map: Map mesh objects to USD skinned meshes
* \param xf_cache: Cache to speed up USD prim transform computations
* \param depsgraph: The dependency graph in which objects were evaluated
*/
void skinned_mesh_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &armature_export_map,
const ObjExportMap &skinned_mesh_export_map,
pxr::UsdGeomXformCache &xf_cache,
const Depsgraph *depsgraph);
/**
* Complete the export process for shape keys.
*
* \param stage: The stage
* \param shape_key_mesh_export_map: Map mesh objects with shape-key to USD meshes
* with blend shape targets
*/
void shape_key_export_chaser(pxr::UsdStageRefPtr stage,
const ObjExportMap &shape_key_mesh_export_map);
/**
* Convert deform groups on the given mesh to USD joint index and weight attributes.
*
* \param mesh: The source mesh with deform groups to export
* \param skel_api: API for setting the attributes on the USD prim
* \param bone_names: List of armature bone names corresponding to the deform groups
*/
void export_deform_verts(const Mesh *mesh,
const pxr::UsdSkelBindingAPI &skel_api,
Span<StringRef> bone_names);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,145 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_skel_root_utils.hh"
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/root.h>
#include "BKE_report.hh"
#include "WM_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
/* Utility: return the common Xform ancestor of the given prims. Is no such ancestor can
* be found, return an in valid Xform. */
static pxr::UsdGeomXform get_xform_ancestor(const pxr::UsdPrim &prim1, const pxr::UsdPrim &prim2)
{
if (!prim1 || !prim2) {
return pxr::UsdGeomXform();
}
pxr::SdfPath prefix = prim1.GetPath().GetCommonPrefix(prim2.GetPath());
if (prefix.IsEmpty()) {
return pxr::UsdGeomXform();
}
pxr::UsdPrim ancestor = prim1.GetStage()->GetPrimAtPath(prefix);
if (!ancestor) {
return pxr::UsdGeomXform();
}
while (ancestor && !ancestor.IsA<pxr::UsdGeomXform>()) {
ancestor = ancestor.GetParent();
}
if (ancestor && ancestor.IsA<pxr::UsdGeomXform>()) {
return pxr::UsdGeomXform(ancestor);
}
return pxr::UsdGeomXform();
}
namespace io::usd {
void create_skel_roots(pxr::UsdStageRefPtr stage, const USDExportParams &params)
{
if (!stage || !(params.export_armatures || params.export_shapekeys)) {
return;
}
ReportList *reports = params.worker_status ? params.worker_status->reports : nullptr;
/* Whether we converted any prims to UsdSkel. */
bool converted_to_usdskel = false;
pxr::UsdPrimRange it = stage->Traverse();
for (pxr::UsdPrim prim : it) {
if (!prim) {
continue;
}
if (prim.IsA<pxr::UsdSkelSkeleton>() || !prim.HasAPI<pxr::UsdSkelBindingAPI>()) {
continue;
}
pxr::UsdSkelBindingAPI skel_bind_api(prim);
if (!skel_bind_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to prim %s",
prim.GetPath().GetAsString().c_str());
continue;
}
/* If we got here, then this prim has the skel binding API. */
/* Get this prim's bound skeleton. */
pxr::UsdSkelSkeleton skel;
if (!skel_bind_api.GetSkeleton(&skel)) {
continue;
}
if (!skel.GetPrim().IsValid()) {
CLOG_WARN(&LOG, "Invalid skeleton for prim %s", prim.GetPath().GetAsString().c_str());
continue;
}
/* Try to find a common ancestor of the skinned prim and its bound skeleton. */
pxr::UsdSkelRoot prim_skel_root = pxr::UsdSkelRoot::Find(prim);
pxr::UsdSkelRoot skel_skel_root = pxr::UsdSkelRoot::Find(skel.GetPrim());
if (prim_skel_root && skel_skel_root && prim_skel_root.GetPath() == skel_skel_root.GetPath()) {
continue;
}
if (pxr::UsdGeomXform xf = get_xform_ancestor(prim, skel.GetPrim())) {
/* We found a common Xform ancestor, so we set its type to UsdSkelRoot. */
CLOG_DEBUG(
&LOG, "Converting Xform prim %s to a SkelRoot", prim.GetPath().GetAsString().c_str());
pxr::UsdSkelRoot::Define(stage, xf.GetPath());
converted_to_usdskel = true;
}
else {
BKE_reportf(reports,
RPT_WARNING,
"%s: Couldn't find a common Xform ancestor for skinned prim %s "
"and skeleton %s to convert to a USD SkelRoot. "
"This can be addressed by setting a root primitive in the export options",
__func__,
prim.GetPath().GetAsString().c_str(),
skel.GetPath().GetAsString().c_str());
}
}
if (!converted_to_usdskel) {
return;
}
/* Check for nested SkelRoots, i.e., SkelRoots beneath other SkelRoots, which we want to avoid.
*/
it = stage->Traverse();
for (pxr::UsdPrim prim : it) {
if (prim.IsA<pxr::UsdSkelRoot>()) {
if (pxr::UsdSkelRoot root = pxr::UsdSkelRoot::Find(prim.GetParent())) {
/* This is a nested SkelRoot, so convert it to an Xform. */
pxr::UsdGeomXform::Define(stage, prim.GetPath());
}
}
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 NVIDIA Corporation. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd.hh"
#include <pxr/usd/usd/common.h>
namespace blender::io::usd {
/**
* We must structure the scene graph to encapsulate skinned prim under a UsdSkelRoot
* prim. Per the USD documentation, a SkelRoot is a:
*
* "Boundable prim type used to identify a scope beneath which skeletally-posed primitives are
* defined. A SkelRoot must be defined at or above a skinned primitive for any skinning behaviors
* in UsdSkel."
*
* See: https://openusd.org/23.05/api/class_usd_skel_root.html#details
*
* This function attempts to ensure that skinned primitives and skeletons are encapsulated
* under SkelRoots, converting existing Xform primitives to SkelRoots to achieve this,
* if possible. In the case where no common ancestor which can be converted to a SkelRoot
* is found, this function issues a warning. One way to address such a case is by setting a
* root prim in the export options, so that this root prim can be converted to a SkelRoot
* for the entire scene.
*
* \param stage: The stage
* \param params: The export parameters
*/
void create_skel_roots(pxr::UsdStageRefPtr stage, const USDExportParams &params);
} // namespace blender::io::usd

View File

@@ -0,0 +1,120 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_utils.hh"
#include "BLI_array.hh"
#include "BLI_string_ref.hh"
#include "BLI_string_utf8.h"
#include <pxr/base/tf/stringUtils.h>
#include <pxr/base/tf/unicodeUtils.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/stage.h>
namespace blender::io::usd {
static bool is_safe_char(const pxr::TfUtf8CodePoint cp, bool is_first, bool allow_unicode)
{
constexpr pxr::TfUtf8CodePoint cp_underscore = pxr::TfUtf8CodePointFromAscii('_');
if (cp == cp_underscore) {
return true;
}
if (allow_unicode) {
return is_first ? pxr::TfIsUtf8CodePointXidStart(cp) : pxr::TfIsUtf8CodePointXidContinue(cp);
}
constexpr uint32_t cp_A = pxr::TfUtf8CodePointFromAscii('A').AsUInt32();
constexpr uint32_t cp_Z = pxr::TfUtf8CodePointFromAscii('Z').AsUInt32();
constexpr uint32_t cp_a = pxr::TfUtf8CodePointFromAscii('a').AsUInt32();
constexpr uint32_t cp_z = pxr::TfUtf8CodePointFromAscii('z').AsUInt32();
constexpr uint32_t cp_0 = pxr::TfUtf8CodePointFromAscii('0').AsUInt32();
constexpr uint32_t cp_9 = pxr::TfUtf8CodePointFromAscii('9').AsUInt32();
const uint32_t cp_u32 = cp.AsUInt32();
const bool is_letter = (cp_u32 >= cp_A && cp_u32 <= cp_Z) || (cp_u32 >= cp_a && cp_u32 <= cp_z);
const bool is_digit = cp_u32 >= cp_0 && cp_u32 <= cp_9;
return is_first ? is_letter : (is_letter || is_digit);
}
static std::string make_safe_identifier(const StringRef name, bool allow_unicode)
{
if (name.is_empty()) {
return "_";
}
const bool has_leading_digit = std::isdigit(name[0]);
const bool need_leading_underscore = has_leading_digit;
/* Create temporary buffer using the original incoming string size, which can be larger than
* required if unicode characters are converted to '_'. This size serves as the upper limit of
* what might be produced. */
const int64_t adjust = (need_leading_underscore ? 1 : 0);
Array<char, 256> storage(name.size() + adjust);
MutableSpan<char> buf(storage);
/* Insert a leading '_' to account for invalid starting characters. */
size_t offset = 0;
bool first = true;
if (need_leading_underscore) {
buf[0] = '_';
offset = 1;
first = false;
}
for (auto cp : pxr::TfUtf8CodePointView{name}) {
const bool cp_allowed = is_safe_char(cp, first, allow_unicode);
if (!cp_allowed) {
offset += BLI_str_utf8_from_unicode(uint32_t('_'), buf.data() + offset, buf.size() - offset);
}
else {
offset += BLI_str_utf8_from_unicode(cp.AsUInt32(), buf.data() + offset, buf.size() - offset);
}
first = false;
}
return {buf.data(), offset};
}
std::string make_safe_name(const StringRef name, bool allow_unicode)
{
return make_safe_identifier(name, allow_unicode);
}
std::string make_safe_primvar_name(const StringRef name, bool allow_unicode)
{
/* Allow namespaced identifiers, separated by ':'. */
const std::string original(name);
std::vector<std::string> tokens = pxr::TfStringSplit(original, ":");
if (tokens.empty()) {
return "_";
}
std::string safe_name;
for (size_t i = 0; i < tokens.size(); i++) {
const std::string &token = tokens[i];
safe_name += make_safe_identifier(token, allow_unicode);
if (i != tokens.size() - 1) {
safe_name += ":";
}
}
return safe_name;
}
pxr::SdfPath get_unique_path(pxr::UsdStageRefPtr stage, const std::string &path)
{
std::string unique_path = path;
int suffix = 2;
while (stage->GetPrimAtPath(pxr::SdfPath(unique_path)).IsValid()) {
unique_path = path + std::to_string(suffix++);
}
return pxr::SdfPath(unique_path);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/common.h>
#include <string>
namespace blender::io::usd {
/**
* Return a valid USD identifier based on the passed in string.
*
* \param name: Incoming name to sanitize
* \param allow_unicode: Whether to allow unicode encoded characters in the USD identifier
* \return A valid USD identifier
*/
std::string make_safe_name(StringRef name, bool allow_unicode);
/**
* Return a valid USD primvar name based on the passed in string. The name is permitted to contain
* namespaces separated by colons. E.g. "ns1:ns2:primvar_name".
*
* \param name: Incoming name to sanitize
* \param allow_unicode: Whether to allow unicode encoded characters in the USD primvar name
* \return A valid USD primvar name
*/
std::string make_safe_primvar_name(StringRef name, bool allow_unicode);
/**
* Return a unique USD `SdfPath`. If the given path already exists on the given stage, return
* the path with a numerical suffix appended to the name that ensures the path is unique.
* If the path does not exist on the stage, it will be returned unchanged.
*
* \param stage: The stage
* \param path: The original path
* \return A valid, and unique, USD `SdfPath`
*/
pxr::SdfPath get_unique_path(pxr::UsdStageRefPtr stage, const std::string &path);
} // namespace blender::io::usd

View File

@@ -0,0 +1,588 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_abstract.hh"
#include "usd_attribute_utils.hh"
#include "usd_colorspace_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_utils.hh"
#include "usd_writer_material.hh"
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usdGeom/bboxCache.h>
#include <pxr/usd/usdGeom/scope.h>
#include <pxr/usd/usdUI/accessibilityAPI.h>
#include "BLI_assert.h"
#include "BLI_bounds_types.hh"
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "WM_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
/* TfToken objects are not cheap to construct, so we do it once. */
namespace usdtokens {
static const pxr::TfToken blender_ns("userProperties:blender", pxr::TfToken::Immortal);
} // namespace usdtokens
namespace {
struct AccessibilityPropertyName {
pxr::TfToken property_namespace;
pxr::TfToken property_base_name;
};
} // anonymous namespace
static std::optional<AccessibilityPropertyName> parse_accessibility_property_name(
IDProperty *prop, bool allow_unicode)
{
std::vector<std::string> property_tokens = pxr::TfStringTokenize(prop->name, ":");
/* First check if the property name matches the UsdUIAccessibility format exactly. */
if (property_tokens.size() == 3) {
pxr::TfToken accessibility_token(property_tokens[0]);
pxr::TfToken basename(property_tokens[2]);
if (accessibility_token == pxr::UsdUITokens->accessibility &&
pxr::UsdUIAccessibilityAPI::IsSchemaPropertyBaseName(basename))
{
AccessibilityPropertyName property_name;
/* Sanitize the namespace since this is user-generated and might need to be conformed
* to the `allow_unicode` export setting. */
property_name.property_namespace = pxr::TfToken(
io::usd::make_safe_name(property_tokens[1], allow_unicode));
property_name.property_base_name = basename;
return property_name;
}
}
return std::nullopt;
}
static bool is_valid_accessibility_priority(const pxr::TfToken &token)
{
return token == pxr::UsdUITokens->low || token == pxr::UsdUITokens->standard ||
token == pxr::UsdUITokens->high;
}
/**
* Write the accessibility property on the given prim. Note: although the
* UsdUIAccessibilityAPI DOES allow time-sampled data for the `label`
* and `description` properties, Blender does not currently support
* keyframes on string custom properties so time-sample authoring will
* not be done here.
*/
static void write_accessibility_property(const pxr::UsdPrim &prim,
const AccessibilityPropertyName &property_name,
const std::string &value)
{
pxr::UsdUIAccessibilityAPI accessibility_api = pxr::UsdUIAccessibilityAPI::Apply(
prim, property_name.property_namespace);
if (!accessibility_api) {
return;
}
if (property_name.property_base_name == pxr::UsdUITokens->label) {
accessibility_api.CreateLabelAttr().Set(value);
}
else if (property_name.property_base_name == pxr::UsdUITokens->description) {
accessibility_api.CreateDescriptionAttr().Set(value);
}
else if (property_name.property_base_name == pxr::UsdUITokens->priority) {
pxr::TfToken priority(value);
if (is_valid_accessibility_priority(priority)) {
accessibility_api.CreatePriorityAttr().Set(priority);
}
}
}
static std::string get_mesh_active_uvlayer_name(const Object *ob)
{
if (!ob || ob->type != OB_MESH || !ob->data) {
return "";
}
const Mesh *mesh = id_cast<Mesh *>(ob->data);
return mesh->active_uv_map_name();
}
template<typename USDT>
bool set_vec_attrib(const pxr::UsdPrim &prim,
const IDProperty *prop,
const pxr::TfToken &prop_token,
const pxr::SdfValueTypeName &type_name,
const pxr::UsdTimeCode &time)
{
if (!prim || !prop || !prop->data.pointer || prop_token.IsEmpty() || !type_name) {
return false;
}
pxr::UsdAttribute vec_attr = prim.CreateAttribute(prop_token, type_name, true);
if (!vec_attr) {
CLOG_WARN(&LOG,
"Couldn't create USD attribute for array property %s",
prop_token.GetString().c_str());
return false;
}
USDT vec_value(static_cast<typename USDT::ScalarType *>(prop->data.pointer));
return vec_attr.Set(vec_value, time);
}
namespace io::usd {
static void create_vector_attrib(const pxr::UsdPrim &prim,
const IDProperty *prop,
const pxr::TfToken &prop_token,
const pxr::UsdTimeCode &time)
{
if (!prim || !prop || prop_token.IsEmpty()) {
return;
}
if (prop->type != IDP_ARRAY) {
CLOG_WARN(&LOG,
"Property %s is not an array type and can't be converted to a vector attribute",
prop->name);
return;
}
pxr::SdfValueTypeName type_name;
bool success = false;
if (prop->subtype == IDP_FLOAT) {
if (prop->len == 2) {
type_name = pxr::SdfValueTypeNames->Float2;
success = set_vec_attrib<pxr::GfVec2f>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 3) {
type_name = pxr::SdfValueTypeNames->Float3;
success = set_vec_attrib<pxr::GfVec3f>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 4) {
type_name = pxr::SdfValueTypeNames->Float4;
success = set_vec_attrib<pxr::GfVec4f>(prim, prop, prop_token, type_name, time);
}
}
else if (prop->subtype == IDP_DOUBLE) {
if (prop->len == 2) {
type_name = pxr::SdfValueTypeNames->Double2;
success = set_vec_attrib<pxr::GfVec2d>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 3) {
type_name = pxr::SdfValueTypeNames->Double3;
success = set_vec_attrib<pxr::GfVec3d>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 4) {
type_name = pxr::SdfValueTypeNames->Double4;
success = set_vec_attrib<pxr::GfVec4d>(prim, prop, prop_token, type_name, time);
}
}
else if (prop->subtype == IDP_INT) {
if (prop->len == 2) {
type_name = pxr::SdfValueTypeNames->Int2;
success = set_vec_attrib<pxr::GfVec2i>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 3) {
type_name = pxr::SdfValueTypeNames->Int3;
success = set_vec_attrib<pxr::GfVec3i>(prim, prop, prop_token, type_name, time);
}
else if (prop->len == 4) {
type_name = pxr::SdfValueTypeNames->Int4;
success = set_vec_attrib<pxr::GfVec4i>(prim, prop, prop_token, type_name, time);
}
}
if (!type_name) {
CLOG_WARN(&LOG,
"Couldn't determine USD type name for array property %s",
prop_token.GetString().c_str());
return;
}
if (!success) {
CLOG_WARN(
&LOG, "Couldn't set USD attribute from array property %s", prop_token.GetString().c_str());
return;
}
}
USDAbstractWriter::USDAbstractWriter(const USDExporterContext &usd_export_context)
: usd_export_context_(usd_export_context), frame_has_been_written_(false), is_animated_(false)
{
}
bool USDAbstractWriter::is_supported(const HierarchyContext * /*context*/) const
{
return true;
}
std::string USDAbstractWriter::get_export_file_path() const
{
return usd_export_context_.export_file_path;
}
pxr::UsdTimeCode USDAbstractWriter::get_export_time_code() const
{
if (is_animated_) {
BLI_assert(usd_export_context_.get_time_code);
return usd_export_context_.get_time_code();
}
/* By using the default time-code USD won't even write a single `timeSample` for non-animated
* data. Instead, it writes it as non-time-sampled. */
return pxr::UsdTimeCode::Default();
}
ReportList *USDAbstractWriter::reports() const
{
return usd_export_context_.export_params.worker_status->reports;
}
void USDAbstractWriter::write(HierarchyContext &context)
{
if (!frame_has_been_written_) {
is_animated_ = usd_export_context_.export_params.export_animation &&
check_is_animated(context);
}
else if (!is_animated_) {
/* A frame has already been written, and without animation one frame is enough. */
return;
}
do_write(context);
frame_has_been_written_ = true;
}
const pxr::SdfPath &USDAbstractWriter::usd_path() const
{
return usd_export_context_.usd_path;
}
pxr::SdfPath USDAbstractWriter::get_material_library_path() const
{
static std::string material_library_path("/_materials");
const std::string &root_prim_path = usd_export_context_.export_params.root_prim_path;
if (!root_prim_path.empty()) {
return pxr::SdfPath(root_prim_path + material_library_path);
}
return pxr::SdfPath(material_library_path);
}
pxr::SdfPath USDAbstractWriter::get_proto_material_root_path(const HierarchyContext &context) const
{
static std::string material_library_path("/_materials");
std::string path_prefix(usd_export_context_.export_params.root_prim_path);
path_prefix += context.higher_up_export_path;
return pxr::SdfPath(path_prefix + material_library_path);
}
pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material_created(
const HierarchyContext &context, Material *material) const
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
/* Construct the material. */
pxr::TfToken material_name(
make_safe_name(material->id.name + 2, usd_export_context_.export_params.allow_unicode));
pxr::SdfPath usd_path = pxr::UsdGeomScope::Define(stage, get_material_library_path())
.GetPath()
.AppendChild(material_name);
pxr::UsdShadeMaterial usd_material = pxr::UsdShadeMaterial::Get(stage, usd_path);
if (usd_material) {
return usd_material;
}
std::string active_uv = get_mesh_active_uvlayer_name(context.object);
usd_material = create_usd_material(
usd_export_context_, usd_path, material, active_uv, reports());
auto prim = usd_material.GetPrim();
add_to_prim_map(prim.GetPath(), &material->id);
write_id_properties(prim, material->id, get_export_time_code());
colorspace_apply_to_prim(prim);
return usd_material;
}
pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(const HierarchyContext &context,
Material *material) const
{
pxr::UsdShadeMaterial library_material = ensure_usd_material_created(context, material);
/* If instancing is enabled and the object is an instancing prototype, create a material
* under the prototype root referencing the library material. This is considered a best
* practice and is required for certain renderers (e.g., karma). */
if (!(usd_export_context_.export_params.use_instancing && context.is_prototype())) {
/* We don't need to handle the material for the prototype. */
return library_material;
}
/* Create the prototype material. */
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::SdfPath usd_path = pxr::UsdGeomScope::Define(stage, get_proto_material_root_path(context))
.GetPath()
.AppendChild(library_material.GetPath().GetNameToken());
pxr::UsdShadeMaterial proto_material = pxr::UsdShadeMaterial::Define(stage, usd_path);
if (!proto_material.GetPrim().GetReferences().AddInternalReference(library_material.GetPath())) {
CLOG_WARN(&LOG,
"Unable to add a material reference from %s to %s for prototype %s",
proto_material.GetPath().GetAsString().c_str(),
library_material.GetPath().GetAsString().c_str(),
context.export_path.c_str());
return library_material;
}
return proto_material;
}
void USDAbstractWriter::write_visibility(const HierarchyContext &context,
const pxr::UsdTimeCode time,
const pxr::UsdGeomImageable &usd_geometry)
{
pxr::UsdAttribute attr_visibility = usd_geometry.CreateVisibilityAttr(pxr::VtValue(), true);
const bool is_visible = context.is_object_visible(
usd_export_context_.export_params.evaluation_mode);
const pxr::TfToken visibility = is_visible ? pxr::UsdGeomTokens->inherited :
pxr::UsdGeomTokens->invisible;
usd_value_writer_.SetAttribute(attr_visibility, pxr::VtValue(visibility), time);
}
bool USDAbstractWriter::mark_as_instance(const HierarchyContext &context, const pxr::UsdPrim &prim)
{
BLI_assert(context.is_instance());
if (context.export_path == context.original_export_path) {
CLOG_ERROR(&LOG,
"Reference error: export path matches reference path: %s",
context.export_path.c_str());
BLI_assert_msg(0, "USD reference error");
return false;
}
BLI_assert(!context.original_export_path.empty());
BLI_assert(context.original_export_path.front() == '/');
std::string ref_path_str(usd_export_context_.export_params.root_prim_path);
ref_path_str += context.original_export_path;
pxr::SdfPath ref_path(ref_path_str);
/* To avoid USD errors, make sure the referenced path exists. */
usd_export_context_.stage->DefinePrim(ref_path);
if (!prim.GetReferences().AddInternalReference(ref_path)) {
/* See this URL for a description for why referencing may fail:
* https://graphics.pixar.com/usd/docs/api/class_usd_references.html#Usd_Failing_References
*/
CLOG_WARN(&LOG,
"Unable to add reference from %s to %s, not instancing object for export",
context.export_path.c_str(),
context.original_export_path.c_str());
return false;
}
prim.SetInstanceable(true);
return true;
}
void USDAbstractWriter::write_id_properties(const pxr::UsdPrim &prim,
const ID &id,
pxr::UsdTimeCode time) const
{
if (!usd_export_context_.export_params.export_custom_properties) {
return;
}
if (usd_export_context_.export_params.author_blender_name) {
if (GS(id.name) == ID_OB) {
/* Author property of original blender Object name. */
prim.CreateAttribute(pxr::TfToken(usdtokens::blender_ns.GetString() + ":object_name"),
pxr::SdfValueTypeNames->String,
true)
.Set<std::string>(std::string(id.name + 2));
}
else {
prim.CreateAttribute(pxr::TfToken(usdtokens::blender_ns.GetString() + ":data_name"),
pxr::SdfValueTypeNames->String,
true)
.Set<std::string>(std::string(id.name + 2));
}
}
if (id.properties) {
write_user_properties(prim, id.properties, time);
}
}
void USDAbstractWriter::write_user_properties(const pxr::UsdPrim &prim,
IDProperty *properties,
pxr::UsdTimeCode time) const
{
if (properties == nullptr) {
return;
}
if (properties->type != IDP_GROUP) {
return;
}
const StringRef displayName_identifier = "displayName";
const std::string default_namespace(
usd_export_context_.export_params.custom_properties_namespace);
for (IDProperty *prop = static_cast<IDProperty *>(properties->data.group.first); prop;
prop = prop->next)
{
if (displayName_identifier == prop->name) {
if (prop->type == IDP_STRING && prop->data.pointer) {
prim.SetDisplayName(static_cast<char *>(prop->data.pointer));
}
continue;
}
if (auto accessibility_property_name = parse_accessibility_property_name(
prop, usd_export_context_.export_params.allow_unicode))
{
if (prop->type == IDP_STRING && prop->data.pointer) {
write_accessibility_property(
prim, *accessibility_property_name, static_cast<char *>(prop->data.pointer));
}
continue;
}
std::vector<std::string> path_names = pxr::TfStringTokenize(prop->name, ":");
/* If the path does not already have a namespace prefix, prepend the default namespace
* specified by the user, if any. */
if (!default_namespace.empty() && path_names.size() < 2) {
path_names.insert(path_names.begin(), default_namespace);
}
std::vector<std::string> safe_names;
for (const std::string &name : path_names) {
safe_names.push_back(make_safe_name(name, usd_export_context_.export_params.allow_unicode));
}
std::string full_prop_name = pxr::SdfPath::JoinIdentifier(safe_names);
pxr::TfToken prop_token = pxr::TfToken(full_prop_name);
if (prim.HasAttribute(prop_token)) {
/* Don't overwrite existing attributes, as these may have been
* created by the exporter logic and shouldn't be changed. */
continue;
}
switch (prop->type) {
case IDP_INT:
if (pxr::UsdAttribute int_attr = prim.CreateAttribute(
prop_token, pxr::SdfValueTypeNames->Int, true))
{
int_attr.Set<int>(prop->data.val, time);
}
break;
case IDP_FLOAT:
if (pxr::UsdAttribute float_attr = prim.CreateAttribute(
prop_token, pxr::SdfValueTypeNames->Float, true))
{
float_attr.Set<float>(*reinterpret_cast<float *>(&prop->data.val), time);
}
break;
case IDP_DOUBLE:
if (pxr::UsdAttribute double_attr = prim.CreateAttribute(
prop_token, pxr::SdfValueTypeNames->Double, true))
{
double_attr.Set<double>(*reinterpret_cast<double *>(&prop->data.val), time);
}
break;
case IDP_STRING:
if (pxr::UsdAttribute str_attr = prim.CreateAttribute(
prop_token, pxr::SdfValueTypeNames->String, true))
{
str_attr.Set<std::string>(static_cast<const char *>(prop->data.pointer), time);
}
break;
case IDP_BOOLEAN:
if (pxr::UsdAttribute bool_attr = prim.CreateAttribute(
prop_token, pxr::SdfValueTypeNames->Bool, true))
{
bool_attr.Set<bool>(prop->data.val, time);
}
break;
case IDP_ARRAY:
create_vector_attrib(prim, prop, prop_token, time);
break;
case IDP_GROUP:
case IDP_ID:
case IDP_IDPARRAY:
/* Not supported. */
break;
}
}
}
void USDAbstractWriter::author_extent(const pxr::UsdGeomBoundable &boundable,
const pxr::UsdTimeCode time)
{
/* Do not use any existing `extentsHint` that may be authored, instead recompute the extent when
* authoring it. */
const bool useExtentsHint = false;
const pxr::TfTokenVector includedPurposes{pxr::UsdGeomTokens->default_};
pxr::UsdGeomBBoxCache bboxCache(time, includedPurposes, useExtentsHint);
pxr::GfBBox3d bounds = bboxCache.ComputeLocalBound(boundable.GetPrim());
/* Note: An empty 'bounds' is still valid (e.g. a mesh with no vertices). */
pxr::VtArray<pxr::GfVec3f> extent{pxr::GfVec3f(bounds.GetRange().GetMin()),
pxr::GfVec3f(bounds.GetRange().GetMax())};
pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true);
set_attribute(attr_extent, extent, time, usd_value_writer_);
}
void USDAbstractWriter::author_extent(const pxr::UsdGeomBoundable &boundable,
const std::optional<Bounds<float3>> &bounds,
const pxr::UsdTimeCode time)
{
pxr::VtArray<pxr::GfVec3f> extent(2);
if (bounds) {
extent[0].Set(bounds->min);
extent[1].Set(bounds->max);
}
pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true);
set_attribute(attr_extent, extent, time, usd_value_writer_);
}
void USDAbstractWriter::add_to_prim_map(const pxr::SdfPath &usd_path, const ID *id) const
{
if (usd_export_context_.hierarchy_iterator) {
usd_export_context_.hierarchy_iterator->add_to_prim_map(usd_path, id);
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,126 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "IO_abstract_hierarchy_iterator.h"
#include "usd_exporter_context.hh"
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usdGeom/boundable.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdUtils/sparseValueWriter.h>
#include <string>
namespace blender {
struct ID;
struct IDProperty;
struct Material;
struct ReportList;
template<typename T> struct Bounds;
namespace io::usd {
using io::AbstractHierarchyWriter;
using io::HierarchyContext;
class USDAbstractWriter : public AbstractHierarchyWriter {
protected:
const USDExporterContext usd_export_context_;
pxr::UsdUtilsSparseValueWriter usd_value_writer_;
bool frame_has_been_written_;
bool is_animated_;
public:
USDAbstractWriter(const USDExporterContext &usd_export_context);
void write(HierarchyContext &context) override;
/**
* Returns true if the data to be written is actually supported. This would, for example, allow a
* hypothetical camera writer accept a perspective camera but reject an orthogonal one.
*
* Returning false from a transform writer will prevent the object and all its descendants from
* being exported. Returning false from a data writer (object data, hair, or particles) will
* only prevent that data from being written (and thus cause the object to be exported as an
* Empty).
*/
virtual bool is_supported(const HierarchyContext *context) const;
const pxr::SdfPath &usd_path() const;
/** Get the wmJobWorkerStatus-provided `reports` list pointer, to use with the BKE_report API. */
ReportList *reports() const;
protected:
virtual void do_write(HierarchyContext &context) = 0;
std::string get_export_file_path() const;
pxr::UsdTimeCode get_export_time_code() const;
/* Returns the parent path of exported materials. */
pxr::SdfPath get_material_library_path() const;
/* Returns the parent path of exported materials for instance prototypes. */
pxr::SdfPath get_proto_material_root_path(const HierarchyContext &context) const;
/* Ensure the USD material is created in the default material library folder. */
pxr::UsdShadeMaterial ensure_usd_material_created(const HierarchyContext &context,
Material *material) const;
/* Calls ensure_usd_material_created(). Additionally, if the context is an
* instancing prototype, creates a reference to the library material under the
* prototype root. */
pxr::UsdShadeMaterial ensure_usd_material(const HierarchyContext &context,
Material *material) const;
void write_id_properties(const pxr::UsdPrim &prim,
const ID &id,
pxr::UsdTimeCode = pxr::UsdTimeCode::Default()) const;
void write_user_properties(const pxr::UsdPrim &prim,
IDProperty *properties,
pxr::UsdTimeCode = pxr::UsdTimeCode::Default()) const;
void write_visibility(const HierarchyContext &context,
const pxr::UsdTimeCode time,
const pxr::UsdGeomImageable &usd_geometry);
/**
* Turn `prim` into an instance referencing `context.original_export_path`.
* Return true when the instancing was successful, false otherwise.
*
* Reference the original data instead of writing a copy.
*/
virtual bool mark_as_instance(const HierarchyContext &context, const pxr::UsdPrim &prim);
/**
* Compute the bounds for a boundable prim, and author the result as the `extent` attribute.
*
* Although this method works for any boundable prim, it is preferred to use Blender's own
* cached bounds when possible.
*
* This method does not author the `extentsHint` attribute, which is also important to provide.
* Whereas the `extent` attribute can only be authored on prims inheriting from
* `UsdGeomBoundable`, an `extentsHint` can be provided on any prim, including scopes. This
* `extentsHint` should be authored on every prim in a hierarchy being exported.
*
* Note that this hint is only useful when importing or inspecting layers, and should not be
* taken into account when computing extents during export.
*
* TODO: also provide method for authoring extentsHint on every prim in a hierarchy.
*/
void author_extent(const pxr::UsdGeomBoundable &boundable, const pxr::UsdTimeCode time);
/**
* Author the `extent` attribute for a boundable prim given the Blender `bounds`.
*/
void author_extent(const pxr::UsdGeomBoundable &boundable,
const std::optional<Bounds<float3>> &bounds,
const pxr::UsdTimeCode time);
void add_to_prim_map(const pxr::SdfPath &usd_path, const ID *id) const;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,233 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_armature.hh"
#include "usd_armature_utils.hh"
#include "usd_attribute_utils.hh"
#include "usd_utils.hh"
#include "ANIM_action.hh"
#include "BLI_listbase.h"
#include "BKE_action.hh"
#include "DNA_armature_types.h"
#include "DNA_object_types.h"
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdSkel/animation.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include <pxr/usd/usdSkel/skeleton.h>
#include <pxr/usd/usdSkel/utils.h>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
/**
* Get the pose matrix for the given channel.
* The matrix is computed relative to its parent, if a parent exists.
* The returned matrix corresponds to the USD joint-local transform.
*/
static pxr::GfMatrix4d parent_relative_pose_mat(const bPoseChannel *pchan)
{
/* Note that the float matrix will be returned as GfMatrix4d, because
* USD requires doubles. */
const pxr::GfMatrix4f pose_mat(pchan->pose_mat);
if (pchan->parent) {
const pxr::GfMatrix4f parent_pose_mat(pchan->parent->pose_mat);
const pxr::GfMatrix4f xf = pose_mat * parent_pose_mat.GetInverse();
return pxr::GfMatrix4d(xf);
}
/* No parent, so return the pose matrix directly. */
return pxr::GfMatrix4d(pose_mat);
}
/* Initialize the given skeleton and animation from
* the given armature object. */
static void initialize(const Object *obj,
pxr::UsdSkelSkeleton &skel,
pxr::UsdSkelAnimation &skel_anim,
const Map<StringRef, const Bone *> *deform_bones,
bool allow_unicode)
{
using namespace blender::io::usd;
pxr::VtTokenArray joints;
pxr::VtArray<float> bone_lengths;
pxr::VtArray<pxr::GfMatrix4d> bind_xforms;
pxr::VtArray<pxr::GfMatrix4d> rest_xforms;
/* Function to collect the bind and rest transforms from each bone. */
auto visitor = [&](const Bone *bone) {
if (!bone) {
return;
}
if (deform_bones && !deform_bones->contains(bone->name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
return;
}
/* Store Blender bone lengths to facilitate better round-tripping. */
bone_lengths.push_back(bone->length);
joints.push_back(build_usd_joint_path(bone, allow_unicode));
const pxr::GfMatrix4f arm_mat(bone->arm_mat);
bind_xforms.push_back(pxr::GfMatrix4d(arm_mat));
/* Set the rest transform to the parent-relative pose matrix, or the parent-relative
* armature matrix, if no pose channel exists. */
if (const bPoseChannel *pchan = BKE_pose_channel_find_name(obj->pose, bone->name)) {
rest_xforms.push_back(parent_relative_pose_mat(pchan));
}
else if (bone->parent) {
pxr::GfMatrix4f parent_arm_mat(bone->parent->arm_mat);
const pxr::GfMatrix4f rest_mat = arm_mat * parent_arm_mat.GetInverse();
rest_xforms.push_back(pxr::GfMatrix4d(rest_mat));
}
else {
rest_xforms.push_back(pxr::GfMatrix4d(arm_mat));
}
};
visit_bones(obj, visitor);
skel.GetJointsAttr().Set(joints);
skel.GetBindTransformsAttr().Set(bind_xforms);
skel.GetRestTransformsAttr().Set(rest_xforms);
const pxr::UsdPrim skel_prim = skel.GetPrim();
/* Store the custom bone lengths as just a regular Primvar attached to the Skeleton. */
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(skel_prim);
pxr::UsdGeomPrimvar pv_lengths = pv_api.CreatePrimvar(
BlenderBoneLengths, pxr::SdfValueTypeNames->FloatArray, pxr::UsdGeomTokens->uniform);
pv_lengths.Set(bone_lengths);
pxr::UsdSkelBindingAPI usd_skel_api = pxr::UsdSkelBindingAPI::Apply(skel_prim);
if (skel_anim) {
usd_skel_api.CreateAnimationSourceRel().SetTargets(
pxr::SdfPathVector({pxr::SdfPath(skel_anim.GetPath().GetName())}));
create_pose_joints(skel_anim, *obj, deform_bones, allow_unicode);
}
}
namespace io::usd {
/* Add skeleton transform samples from the armature pose channels. */
static void add_anim_sample(pxr::UsdSkelAnimation &skel_anim,
const Object *obj,
const pxr::UsdTimeCode time,
const Map<StringRef, const Bone *> *deform_map,
pxr::UsdUtilsSparseValueWriter &value_writer)
{
if (!(skel_anim && obj && obj->pose)) {
return;
}
pxr::VtArray<pxr::GfMatrix4d> xforms;
const bPose *pose = obj->pose;
for (const bPoseChannel &pchan : pose->chanbase) {
if (deform_map && !deform_map->contains(pchan.name)) {
/* If deform_map is passed in, assume we're going deform-only.
* Bones not found in the map should be skipped. */
continue;
}
xforms.push_back(parent_relative_pose_mat(&pchan));
}
/* Perform the same steps as UsdSkelAnimation::SetTransforms but write data out sparsely. */
pxr::VtArray<pxr::GfVec3f> translations;
pxr::VtArray<pxr::GfQuatf> rotations;
pxr::VtArray<pxr::GfVec3h> scales;
if (pxr::UsdSkelDecomposeTransforms(xforms, &translations, &rotations, &scales)) {
set_attribute(skel_anim.GetTranslationsAttr(), translations, time, value_writer);
set_attribute(skel_anim.GetRotationsAttr(), rotations, time, value_writer);
set_attribute(skel_anim.GetScalesAttr(), scales, time, value_writer);
}
else {
CLOG_WARN(&LOG, "Could not decompose skeleton transforms for frame time %f", time.GetValue());
}
}
USDArmatureWriter::USDArmatureWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
void USDArmatureWriter::do_write(HierarchyContext &context)
{
if (!(context.object && context.object->type == OB_ARMATURE && context.object->data)) {
BLI_assert_unreachable();
return;
}
/* Create the skeleton. */
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdSkelSkeleton skel = pxr::UsdSkelSkeleton::Define(stage, usd_export_context_.usd_path);
if (!skel) {
CLOG_WARN(&LOG,
"Couldn't define UsdSkelSkeleton %s",
usd_export_context_.usd_path.GetString().c_str());
return;
}
pxr::UsdSkelAnimation skel_anim;
const bool allow_unicode = usd_export_context_.export_params.allow_unicode;
if (usd_export_context_.export_params.export_animation) {
/* Use the action name as the animation name. */
const animrig::Action *action = animrig::get_action(context.object->id);
const pxr::TfToken anim_name(action ? make_safe_name(action->id.name + 2, allow_unicode) :
"Action");
/* Create the skeleton animation primitive as a child of the skeleton. */
pxr::SdfPath anim_path = usd_export_context_.usd_path.AppendChild(anim_name);
skel_anim = pxr::UsdSkelAnimation::Define(stage, anim_path);
if (!skel_anim) {
CLOG_WARN(&LOG, "Couldn't define UsdSkelAnimation %s", anim_path.GetString().c_str());
return;
}
}
Map<StringRef, const Bone *> *deform_map = usd_export_context_.export_params.only_deform_bones ?
&deform_map_ :
nullptr;
if (!this->frame_has_been_written_) {
init_deform_bones_map(context.object, deform_map);
initialize(context.object, skel, skel_anim, deform_map, allow_unicode);
}
if (usd_export_context_.export_params.export_animation) {
add_anim_sample(
skel_anim, context.object, get_export_time_code(), deform_map, usd_value_writer_);
}
}
bool USDArmatureWriter::check_is_animated(const HierarchyContext &context) const
{
const Object *obj = context.object;
if (!(obj && obj->type == OB_ARMATURE)) {
return false;
}
return obj->adt != nullptr;
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include "BLI_map.hh"
namespace blender {
struct Bone;
namespace io::usd {
class USDArmatureWriter : public USDAbstractWriter {
public:
USDArmatureWriter(const USDExporterContext &ctx);
protected:
void do_write(HierarchyContext &context) override;
bool check_is_animated(const HierarchyContext &context) const override;
private:
Map<StringRef, const Bone *> deform_map_;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,132 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_camera.hh"
#include "usd_attribute_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/tokens.h>
#include "BKE_camera.h"
#include "BLI_assert.h"
#include "DEG_depsgraph_query.hh"
#include "DNA_camera_types.h"
#include "DNA_scene_types.h"
namespace blender::io::usd {
USDCameraWriter::USDCameraWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
bool USDCameraWriter::is_supported(const HierarchyContext *context) const
{
const Camera *camera = id_cast<const Camera *>(context->object->data);
return camera->type == CAM_PERSP;
}
static void camera_sensor_size_for_render(const Camera *camera,
const RenderData *rd,
float *r_sensor,
float *r_sensor_x,
float *r_sensor_y)
{
/* Compute the final image size in pixels. */
float sizex = rd->xsch * rd->xasp;
float sizey = rd->ysch * rd->yasp;
int sensor_fit = BKE_camera_sensor_fit(camera->sensor_fit, sizex, sizey);
float sensor_size = BKE_camera_sensor_size(
camera->sensor_fit, camera->sensor_x, camera->sensor_y);
*r_sensor = sensor_size;
switch (sensor_fit) {
case CAMERA_SENSOR_FIT_HOR:
*r_sensor_x = sensor_size;
*r_sensor_y = sensor_size * sizey / sizex;
break;
case CAMERA_SENSOR_FIT_VERT:
*r_sensor_x = sensor_size * sizex / sizey;
*r_sensor_y = sensor_size;
break;
case CAMERA_SENSOR_FIT_AUTO:
BLI_assert_msg(0, "Camera fit should be either horizontal or vertical");
break;
}
}
void USDCameraWriter::do_write(HierarchyContext &context)
{
const double meters_per_unit = get_meters_per_unit(usd_export_context_.export_params);
const float unit_scale = float(1.0 / meters_per_unit);
pxr::UsdTimeCode time = get_export_time_code();
pxr::UsdGeomCamera usd_camera = pxr::UsdGeomCamera::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
const Camera *camera = id_cast<const Camera *>(context.object->data);
const Scene *scene = DEG_get_evaluated_scene(usd_export_context_.depsgraph);
usd_camera.CreateProjectionAttr().Set(pxr::UsdGeomTokens->perspective);
/*
* For USD, these camera properties are in tenths of a world unit.
* https://graphics.pixar.com/usd/release/api/class_usd_geom_camera.html#UsdGeom_CameraUnits
*
* tenth_unit_to_meters = stage_meters_per_unit / 10
* tenth_unit_to_millimeters = 1000 * unit_to_tenth_unit
* = 100 * stage_meters_per_unit
*/
const float tenth_unit_to_mm = float(100.0 * meters_per_unit * scene->unit.scale_length);
float sensor_size, aperture_x, aperture_y;
camera_sensor_size_for_render(camera, &scene->r, &sensor_size, &aperture_x, &aperture_y);
set_attribute(usd_camera.CreateFocalLengthAttr(pxr::VtValue(), true),
camera->lens / tenth_unit_to_mm,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateHorizontalApertureAttr(pxr::VtValue(), true),
aperture_x / tenth_unit_to_mm,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateVerticalApertureAttr(pxr::VtValue(), true),
aperture_y / tenth_unit_to_mm,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateHorizontalApertureOffsetAttr(pxr::VtValue(), true),
sensor_size * camera->shiftx / tenth_unit_to_mm,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateVerticalApertureOffsetAttr(pxr::VtValue(), true),
sensor_size * camera->shifty / tenth_unit_to_mm,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateClippingRangeAttr(pxr::VtValue(), true),
pxr::GfVec2f(camera->clip_start * unit_scale, camera->clip_end * unit_scale),
time,
usd_value_writer_);
/* Write DoF-related attributes. */
if (camera->dof.flag & CAM_DOF_ENABLED) {
const float focus_distance = BKE_camera_object_dof_distance(context.object);
set_attribute(usd_camera.CreateFStopAttr(pxr::VtValue(), true),
camera->dof.aperture_fstop,
time,
usd_value_writer_);
set_attribute(usd_camera.CreateFocusDistanceAttr(pxr::VtValue(), true),
focus_distance * unit_scale,
time,
usd_value_writer_);
}
else {
set_attribute(usd_camera.CreateFStopAttr(pxr::VtValue(), true), 0.0f, time, usd_value_writer_);
}
auto prim = usd_camera.GetPrim();
add_to_prim_map(prim.GetPath(), &camera->id);
write_id_properties(prim, camera->id, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
namespace blender::io::usd {
/* Writer for writing camera data to UsdGeomCamera. */
class USDCameraWriter : public USDAbstractWriter {
public:
USDCameraWriter(const USDExporterContext &ctx);
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,737 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <numeric>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/primvar.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include "usd_attribute_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_utils.hh"
#include "usd_writer_curves.hh"
#include "BLI_array_utils.hh"
#include "BLI_generic_virtual_array.hh"
#include "BLI_set.hh"
#include "BLI_span.hh"
#include "BLI_virtual_array.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_attribute.hh"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curves.hh"
#include "BKE_lib_id.hh"
#include "BKE_material.hh"
#include "BKE_report.hh"
#include "BLT_translation.hh"
#include "DNA_curve_types.h"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_enum_types.hh"
namespace blender::io::usd {
pxr::UsdGeomBasisCurves USDCurvesWriter::DefineUsdGeomBasisCurves(pxr::VtValue curve_basis,
const bool is_cyclic,
const bool is_cubic) const
{
pxr::UsdGeomBasisCurves basis_curves = pxr::UsdGeomBasisCurves::Define(
usd_export_context_.stage, usd_export_context_.usd_path);
/* Not required to set the basis attribute for linear curves
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_basis_curves.html#details */
if (is_cubic) {
basis_curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
basis_curves.CreateBasisAttr(curve_basis);
}
else {
basis_curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->linear));
}
if (is_cyclic) {
basis_curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->periodic));
}
else if (curve_basis == pxr::VtValue(pxr::UsdGeomTokens->catmullRom)) {
/* In Blender the first and last points are treated as endpoints. The pinned attribute tells
* the client that to evaluate or render the curve, it must effectively add 'phantom
* points' at the beginning and end of every curve in a batch. These phantom points are
* injected to ensure that the interpolated curve begins at P[0] and ends at P[n-1]. */
basis_curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->pinned));
}
else {
basis_curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->nonperiodic));
}
return basis_curves;
}
static void populate_curve_widths(const bke::CurvesGeometry &curves, pxr::VtArray<float> &widths)
{
const VArray<float> radii = curves.radius();
widths.resize(radii.size());
for (const int i : radii.index_range()) {
widths[i] = radii[i] * 2.0f;
}
}
static pxr::TfToken get_curve_width_interpolation(const pxr::VtArray<float> &widths,
const pxr::VtArray<int> &segments,
const pxr::VtIntArray &control_point_counts,
const bool is_cyclic,
ReportList *reports)
{
if (widths.empty()) {
return pxr::TfToken();
}
const size_t accumulated_control_point_count = std::accumulate(
control_point_counts.begin(), control_point_counts.end(), 0);
/* For Blender curves, radii are always stored per point. For linear curves, this should match
* with USD's vertex interpolation. For cubic curves, this should match with USD's varying
* interpolation. */
if (widths.size() == accumulated_control_point_count) {
return pxr::UsdGeomTokens->vertex;
}
size_t expectedVaryingSize = std::accumulate(segments.begin(), segments.end(), 0);
if (!is_cyclic) {
expectedVaryingSize += control_point_counts.size();
}
if (widths.size() == expectedVaryingSize) {
return pxr::UsdGeomTokens->varying;
}
BKE_report(reports, RPT_WARNING, "Curve width size not supported for USD interpolation");
return pxr::TfToken();
}
static void populate_curve_verts(const bke::CurvesGeometry &curves,
const Span<float3> positions,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<int> &segments,
const bool is_cyclic,
const bool is_cubic)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int i_curve : curves.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
for (const int i_point : points) {
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
}
const int tot_points = points.size();
control_point_counts[i_curve] = tot_points;
/* For periodic linear curve, segment count = curveVertexCount.
* For periodic cubic curve, segment count = curveVertexCount / vstep.
* For nonperiodic linear curve, segment count = curveVertexCount - 1.
* For nonperiodic cubic curve, segment count = ((curveVertexCount - 4) / vstep) + 1.
* This function handles linear and Catmull-Rom curves. For Catmull-Rom, vstep is 1.
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_basis_curves.html */
if (is_cyclic) {
segments[i_curve] = tot_points;
}
else if (is_cubic) {
segments[i_curve] = (tot_points - 4) + 1;
}
else {
segments[i_curve] = tot_points - 1;
}
}
}
static void populate_curve_props(const bke::CurvesGeometry &curves,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::TfToken &interpolation,
const bool is_cyclic,
const bool is_cubic,
ReportList *reports)
{
const int num_curves = curves.curve_num;
const Span<float3> positions = curves.positions();
pxr::VtArray<int> segments(num_curves);
populate_curve_verts(
curves, positions, verts, control_point_counts, segments, is_cyclic, is_cubic);
populate_curve_widths(curves, widths);
interpolation = get_curve_width_interpolation(
widths, segments, control_point_counts, is_cyclic, reports);
}
static void populate_curve_verts_for_bezier(const bke::CurvesGeometry &curves,
const Span<float3> positions,
const Span<float3> handles_l,
const Span<float3> handles_r,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<int> &segments,
const bool is_cyclic)
{
const int bezier_vstep = 3;
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int i_curve : curves.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
const int start_point_index = points[0];
const int last_point_index = points[points.size() - 1];
const int start_verts_count = verts.size();
for (int i_point = start_point_index; i_point < last_point_index; i_point++) {
/* The order verts in the USD bezier curve representation is [control point 0, right handle
* 0, left handle 1, control point 1, right handle 1, left handle 2, control point 2, ...].
* The last vert in the array doesn't need a right handle because the curve stops at that
* point. */
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
const float3 right_handle = handles_r[i_point];
verts.push_back(pxr::GfVec3f(right_handle[0], right_handle[1], right_handle[2]));
const float3 left_handle = handles_l[i_point + 1];
verts.push_back(pxr::GfVec3f(left_handle[0], left_handle[1], left_handle[2]));
}
verts.push_back(pxr::GfVec3f(positions[last_point_index][0],
positions[last_point_index][1],
positions[last_point_index][2]));
/* For USD periodic bezier curves, since the curve is closed, we need to include
* the right handle of the last point and the left handle of the first point.
*/
if (is_cyclic) {
const float3 right_handle = handles_r[last_point_index];
verts.push_back(pxr::GfVec3f(right_handle[0], right_handle[1], right_handle[2]));
const float3 left_handle = handles_l[start_point_index];
verts.push_back(pxr::GfVec3f(left_handle[0], left_handle[1], left_handle[2]));
}
const int tot_points = verts.size() - start_verts_count;
control_point_counts[i_curve] = tot_points;
if (is_cyclic) {
segments[i_curve] = tot_points / bezier_vstep;
}
else {
segments[i_curve] = ((tot_points - 4) / bezier_vstep) + 1;
}
}
}
static void populate_curve_props_for_bezier(const bke::CurvesGeometry &curves,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::TfToken &interpolation,
const bool is_cyclic,
ReportList *reports)
{
const int num_curves = curves.curve_num;
const Span<float3> positions = curves.positions();
const std::optional<Span<float3>> handles_l = curves.handle_positions_left();
const std::optional<Span<float3>> handles_r = curves.handle_positions_right();
pxr::VtArray<int> segments(num_curves);
populate_curve_verts_for_bezier(curves,
positions,
handles_l.value_or(Span<float3>{}),
handles_r.value_or(Span<float3>{}),
verts,
control_point_counts,
segments,
is_cyclic);
populate_curve_widths(curves, widths);
interpolation = get_curve_width_interpolation(
widths, segments, control_point_counts, is_cyclic, reports);
}
static void populate_curve_props_for_nurbs(const bke::CurvesGeometry &curves,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::VtArray<double> &knots,
pxr::VtArray<double> &weights,
pxr::VtArray<int> &orders,
pxr::TfToken &interpolation,
const bool is_cyclic)
{
/* Order and range, when representing a batched NurbsCurve should be authored one value per
* curve. */
const int num_curves = curves.curve_num;
orders.resize(num_curves);
const Span<float3> positions = curves.positions();
const Span<float> custom_knots = curves.nurbs_custom_knots();
const std::optional<Span<float>> nurbs_weights = curves.nurbs_weights();
VArray<int8_t> geom_orders = curves.nurbs_orders();
VArray<int8_t> knots_modes = curves.nurbs_knots_modes();
const VArray<float> radii = curves.radius();
const OffsetIndices points_by_curve = curves.points_by_curve();
const OffsetIndices custom_knots_by_curve = curves.nurbs_custom_knots_by_curve();
for (const int i_curve : curves.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
const size_t curr_vert_num = verts.size();
for (const int i_point : points) {
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
widths.push_back(radii[i_point] * 2.0f);
}
if (nurbs_weights) {
for (const int i_point : points) {
weights.push_back((*nurbs_weights)[i_point]);
}
}
/* Repeat the first degree(order - 1) number of points and weights if curve is cyclic. */
if (is_cyclic) {
for (const int i_point : points.take_front(geom_orders[i_curve] - 1)) {
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
widths.push_back(radii[i_point] * 2.0f);
if (nurbs_weights) {
weights.push_back((*nurbs_weights)[i_point]);
}
}
}
const int tot_blender_points = int(points.size());
const int tot_usd_points = int(verts.size() - curr_vert_num);
control_point_counts[i_curve] = tot_usd_points;
const int8_t order = geom_orders[i_curve];
orders[i_curve] = int(geom_orders[i_curve]);
const KnotsMode mode = KnotsMode(knots_modes[i_curve]);
const int knots_num = bke::curves::nurbs::knots_num(tot_blender_points, order, is_cyclic);
Array<float> temp_knots(knots_num);
bke::curves::nurbs::load_curve_knots(mode,
tot_blender_points,
order,
is_cyclic,
custom_knots_by_curve[i_curve],
custom_knots,
temp_knots);
/* Knots should be the concatenation of all batched curves.
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_nurbs_curves.html#details */
for (int i_knot = 0; i_knot < knots_num; i_knot++) {
knots.push_back(double(temp_knots[i_knot]));
}
/* For USD it is required to set specific end knots for periodic/non-periodic curves
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_nurbs_curves.html#details */
int zeroth_knot_index = knots.size() - knots_num;
if (is_cyclic) {
knots[zeroth_knot_index] = knots[zeroth_knot_index + 1] -
(knots[knots.size() - 2] - knots[knots.size() - 3]);
knots[knots.size() - 1] = knots[knots.size() - 2] +
(knots[zeroth_knot_index + 2] - knots[zeroth_knot_index + 1]);
}
else {
knots[zeroth_knot_index] = knots[zeroth_knot_index + 1];
knots[knots.size() - 1] = knots[knots.size() - 2];
}
}
interpolation = pxr::UsdGeomTokens->vertex;
}
void USDCurvesWriter::set_writer_attributes_for_nurbs(
const pxr::UsdGeomNurbsCurves &usd_nurbs_curves,
pxr::VtArray<double> &knots,
pxr::VtArray<double> &weights,
pxr::VtArray<int> &orders,
const pxr::UsdTimeCode time)
{
pxr::UsdAttribute attr_knots = usd_nurbs_curves.CreateKnotsAttr(pxr::VtValue(), true);
set_attribute(attr_knots, knots, time, usd_value_writer_);
pxr::UsdAttribute attr_weights = usd_nurbs_curves.CreatePointWeightsAttr(pxr::VtValue(), true);
set_attribute(attr_weights, weights, time, usd_value_writer_);
pxr::UsdAttribute attr_order = usd_nurbs_curves.CreateOrderAttr(pxr::VtValue(), true);
set_attribute(attr_order, orders, time, usd_value_writer_);
}
void USDCurvesWriter::set_writer_attributes(pxr::UsdGeomCurves &usd_curves,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
const pxr::UsdTimeCode time,
const pxr::TfToken interpolation)
{
pxr::UsdAttribute attr_points = usd_curves.CreatePointsAttr(pxr::VtValue(), true);
set_attribute(attr_points, verts, time, usd_value_writer_);
pxr::UsdAttribute attr_counts = usd_curves.CreateCurveVertexCountsAttr(pxr::VtValue(), true);
set_attribute(attr_counts, control_point_counts, time, usd_value_writer_);
pxr::UsdAttribute attr_widths = usd_curves.CreateWidthsAttr(pxr::VtValue(), true);
set_attribute(attr_widths, widths, time, usd_value_writer_);
if (!interpolation.IsEmpty()) {
usd_curves.SetWidthsInterpolation(interpolation);
}
}
static std::optional<pxr::TfToken> convert_blender_domain_to_usd(
const bke::AttrDomain blender_domain, bool is_bezier)
{
switch (blender_domain) {
case bke::AttrDomain::Point:
return is_bezier ? pxr::UsdGeomTokens->varying : pxr::UsdGeomTokens->vertex;
case bke::AttrDomain::Curve:
return pxr::UsdGeomTokens->uniform;
default:
return std::nullopt;
}
}
/* Excluded attributes are those which are handled through native USD concepts
* and should not be exported as generic attributes. */
static bool is_excluded_attr(StringRefNull name)
{
static const Set<StringRefNull> excluded_attrs = []() {
Set<StringRefNull> set;
set.add_new("position");
set.add_new("radius");
set.add_new("resolution");
set.add_new("id");
set.add_new("cyclic");
set.add_new("curve_type");
set.add_new("normal_mode");
set.add_new("handle_left");
set.add_new("handle_right");
set.add_new("handle_type_left");
set.add_new("handle_type_right");
set.add_new("knots_mode");
set.add_new("nurbs_order");
set.add_new("nurbs_weight");
set.add_new("velocity");
return set;
}();
return excluded_attrs.contains(name);
}
void USDCurvesWriter::write_generic_data(const bke::CurvesGeometry &curves,
const bke::AttributeIter &attr,
const pxr::UsdGeomCurves &usd_curves)
{
const CurveType curve_type = CurveType(curves.curve_types().first());
const bool is_bezier = curve_type == CURVE_TYPE_BEZIER;
const std::optional<pxr::TfToken> pv_interp = convert_blender_domain_to_usd(attr.domain,
is_bezier);
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(attr.data_type);
if (!pv_interp || !pv_type) {
BKE_reportf(this->reports(),
RPT_WARNING,
"Attribute '%s' (Blender domain %d, type %d) cannot be converted to USD",
attr.name.c_str(),
int8_t(attr.domain),
int(attr.data_type));
return;
}
const GVArray attribute = *attr.get();
if (attribute.is_empty()) {
return;
}
const pxr::UsdTimeCode time = get_export_time_code();
const pxr::TfToken pv_name(
make_safe_primvar_name(attr.name, usd_export_context_.export_params.allow_unicode));
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_curves);
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type, *pv_interp);
copy_blender_attribute_to_primvar(attribute, attr.data_type, time, pv_attr, usd_value_writer_);
}
void USDCurvesWriter::write_uv_data(const bke::AttributeIter &attr,
const pxr::UsdGeomCurves &usd_curves)
{
const VArray<float2> buffer = *attr.get<float2>(bke::AttrDomain::Curve);
if (buffer.is_empty()) {
return;
}
const pxr::UsdTimeCode time = get_export_time_code();
const pxr::TfToken pv_name(
make_safe_primvar_name(attr.name, usd_export_context_.export_params.allow_unicode));
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_curves);
pxr::UsdGeomPrimvar pv_uv = pv_api.CreatePrimvar(
pv_name, pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->uniform);
copy_blender_buffer_to_primvar<float2, pxr::GfVec2f>(buffer, time, pv_uv, usd_value_writer_);
}
void USDCurvesWriter::write_velocities(const bke::CurvesGeometry &curves,
const pxr::UsdGeomCurves &usd_curves)
{
const VArraySpan velocity = *curves.attributes().lookup<float3>("velocity",
bke::AttrDomain::Point);
if (velocity.is_empty()) {
return;
}
/* Export per-vertex velocity vectors. */
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
pxr::VtVec3fArray usd_velocities;
usd_velocities.assign(data.begin(), data.end());
pxr::UsdTimeCode time = get_export_time_code();
pxr::UsdAttribute attr_vel = usd_curves.CreateVelocitiesAttr(pxr::VtValue(), true);
set_attribute(attr_vel, usd_velocities, time, usd_value_writer_);
}
void USDCurvesWriter::write_custom_data(const bke::CurvesGeometry &curves,
const pxr::UsdGeomCurves &usd_curves)
{
const bke::AttributeAccessor attributes = curves.attributes();
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
/* Skip "internal" Blender properties and attributes dealt with elsewhere. */
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
is_excluded_attr(iter.name))
{
return;
}
/* Spline UV data */
if (iter.domain == bke::AttrDomain::Curve && iter.data_type == bke::AttrType::Float2) {
if (usd_export_context_.export_params.export_uvmaps) {
this->write_uv_data(iter, usd_curves);
}
}
/* Everything else. */
else {
this->write_generic_data(curves, iter, usd_curves);
}
});
}
void USDCurvesWriter::do_write(HierarchyContext &context)
{
Curves *curves_id;
std::unique_ptr<Curves, std::function<void(Curves *)>> converted_curves;
int8_t curve_type_default = CURVE_TYPE_CATMULL_ROM;
switch (context.object->type) {
case OB_CURVES_LEGACY: {
const Curve *legacy_curve = id_cast<Curve *>(context.object->data);
converted_curves = std::unique_ptr<Curves, std::function<void(Curves *)>>(
bke::curve_legacy_to_curves(*legacy_curve), [](Curves *c) { BKE_id_free(nullptr, c); });
curves_id = converted_curves.get();
curve_type_default = CURVE_TYPE_BEZIER;
break;
}
case OB_CURVES:
curves_id = id_cast<Curves *>(context.object->data);
break;
default:
BLI_assert_unreachable();
return;
}
const bke::CurvesGeometry empty;
const bke::CurvesGeometry &curves = curves_id ? curves_id->geometry.wrap() : empty;
const std::array<int, CURVE_TYPES_NUM> &curve_type_counts = curves.curve_type_counts();
const int number_of_curve_types = std::count_if(curve_type_counts.begin(),
curve_type_counts.end(),
[](const int count) { return count > 0; });
if (number_of_curve_types > 1) {
BKE_report(
reports(), RPT_WARNING, "Cannot export mixed curve types in the same Curves object");
return;
}
if (array_utils::booleans_mix_calc(curves.cyclic()) == array_utils::BooleanMix::Mixed) {
BKE_report(reports(),
RPT_WARNING,
"Cannot export mixed cyclic and non-cyclic curves in the same Curves object");
return;
}
const pxr::UsdTimeCode time = get_export_time_code();
const int8_t curve_type_fallback = first_frame_curve_type == -1 ? curve_type_default :
first_frame_curve_type;
const int8_t curve_type = curves.curves_num() > 0 ? curves.curve_types()[0] :
curve_type_fallback;
if (first_frame_curve_type == -1) {
first_frame_curve_type = curve_type;
}
else if (first_frame_curve_type != curve_type) {
const char *first_frame_curve_type_name = nullptr;
RNA_enum_name_from_value(
rna_enum_curves_type_items, int(first_frame_curve_type), &first_frame_curve_type_name);
const char *current_curve_type_name = nullptr;
RNA_enum_name_from_value(
rna_enum_curves_type_items, int(curve_type), &current_curve_type_name);
BKE_reportf(reports(),
RPT_WARNING,
"USD does not support animating curve types. The curve type changes from %s to "
"%s on frame %f",
IFACE_(first_frame_curve_type_name),
IFACE_(current_curve_type_name),
time.GetValue());
return;
}
const bool is_cyclic = curves.curves_num() > 0 ? curves.cyclic().first() : false;
pxr::VtArray<pxr::GfVec3f> verts;
pxr::VtIntArray control_point_counts;
pxr::VtArray<float> widths;
pxr::TfToken interpolation;
pxr::UsdGeomBasisCurves usd_basis_curves;
pxr::UsdGeomNurbsCurves usd_nurbs_curves;
pxr::UsdGeomCurves *usd_curves = nullptr;
control_point_counts.resize(curves.curves_num());
switch (curve_type) {
case CURVE_TYPE_POLY:
usd_basis_curves = DefineUsdGeomBasisCurves(pxr::VtValue(), is_cyclic, false);
usd_curves = &usd_basis_curves;
populate_curve_props(
curves, verts, control_point_counts, widths, interpolation, is_cyclic, false, reports());
break;
case CURVE_TYPE_CATMULL_ROM:
usd_basis_curves = DefineUsdGeomBasisCurves(
pxr::VtValue(pxr::UsdGeomTokens->catmullRom), is_cyclic, true);
usd_curves = &usd_basis_curves;
populate_curve_props(
curves, verts, control_point_counts, widths, interpolation, is_cyclic, true, reports());
break;
case CURVE_TYPE_BEZIER:
usd_basis_curves = DefineUsdGeomBasisCurves(
pxr::VtValue(pxr::UsdGeomTokens->bezier), is_cyclic, true);
usd_curves = &usd_basis_curves;
populate_curve_props_for_bezier(
curves, verts, control_point_counts, widths, interpolation, is_cyclic, reports());
break;
case CURVE_TYPE_NURBS: {
pxr::VtArray<double> knots;
pxr::VtArray<double> weights;
pxr::VtArray<int> orders;
usd_nurbs_curves = pxr::UsdGeomNurbsCurves::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
usd_curves = &usd_nurbs_curves;
populate_curve_props_for_nurbs(curves,
verts,
control_point_counts,
widths,
knots,
weights,
orders,
interpolation,
is_cyclic);
set_writer_attributes_for_nurbs(usd_nurbs_curves, knots, weights, orders, time);
break;
}
default:
BLI_assert_unreachable();
}
this->set_writer_attributes(
*usd_curves, verts, control_point_counts, widths, time, interpolation);
this->assign_materials(context, *usd_curves);
/* TODO: We cannot write custom primvars for cyclic NURBS curves at the moment. */
if (!is_cyclic || (is_cyclic && curve_type != CURVE_TYPE_NURBS)) {
this->write_velocities(curves, *usd_curves);
this->write_custom_data(curves, *usd_curves);
}
if (curves_id) {
const pxr::UsdPrim prim = usd_curves->GetPrim();
add_to_prim_map(prim.GetPath(), &curves_id->id);
write_id_properties(prim, curves_id->id, time);
}
this->author_extent(*usd_curves, curves.bounds_min_max(), time);
}
void USDCurvesWriter::assign_materials(const HierarchyContext &context,
const pxr::UsdGeomCurves &usd_curves)
{
if (context.object->totcol == 0) {
return;
}
bool curve_material_bound = false;
for (int mat_num = 0; mat_num < context.object->totcol; mat_num++) {
Material *material = BKE_object_material_get(context.object, mat_num + 1);
if (material == nullptr) {
continue;
}
pxr::UsdPrim curve_prim = usd_curves.GetPrim();
pxr::UsdShadeMaterialBindingAPI api = pxr::UsdShadeMaterialBindingAPI(curve_prim);
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
api.Bind(usd_material);
pxr::UsdShadeMaterialBindingAPI::Apply(curve_prim);
/* USD seems to support neither per-material nor per-face-group double-sidedness, so we just
* use the flag from the first non-empty material slot. */
usd_curves.CreateDoubleSidedAttr(
pxr::VtValue((material->blend_flag & MA_BL_CULL_BACKFACE) == 0));
curve_material_bound = true;
break;
}
if (!curve_material_bound) {
/* Blender defaults to double-sided, but USD to single-sided. */
usd_curves.CreateDoubleSidedAttr(pxr::VtValue(true));
}
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,62 @@
/* SPDX-FileCopyrightText: 2022 Blender Authors. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
namespace blender {
namespace bke {
class AttributeIter;
class CurvesGeometry;
} // namespace bke
namespace io::usd {
/* Writer for writing Curves data as USD curves. */
class USDCurvesWriter final : public USDAbstractWriter {
public:
USDCurvesWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
~USDCurvesWriter() final = default;
protected:
void do_write(HierarchyContext &context) override;
void assign_materials(const HierarchyContext &context, const pxr::UsdGeomCurves &usd_curves);
private:
int8_t first_frame_curve_type = -1;
pxr::UsdGeomBasisCurves DefineUsdGeomBasisCurves(pxr::VtValue curve_basis,
bool cyclic,
bool cubic) const;
void set_writer_attributes(pxr::UsdGeomCurves &usd_curves,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
const pxr::UsdTimeCode time,
const pxr::TfToken interpolation);
void set_writer_attributes_for_nurbs(const pxr::UsdGeomNurbsCurves &usd_nurbs_curves,
pxr::VtArray<double> &knots,
pxr::VtArray<double> &weights,
pxr::VtArray<int> &orders,
const pxr::UsdTimeCode time);
void write_generic_data(const bke::CurvesGeometry &curves,
const bke::AttributeIter &attr,
const pxr::UsdGeomCurves &usd_curves);
void write_uv_data(const bke::AttributeIter &attr, const pxr::UsdGeomCurves &usd_curves);
void write_velocities(const bke::CurvesGeometry &curves, const pxr::UsdGeomCurves &usd_curves);
void write_custom_data(const bke::CurvesGeometry &curves, const pxr::UsdGeomCurves &usd_curves);
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,123 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_hair.hh"
#include "usd_hierarchy_iterator.hh"
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include "BKE_material.hh"
#include "BKE_particle.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "DNA_particle_types.h"
namespace blender::io::usd {
USDHairWriter::USDHairWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
void USDHairWriter::do_write(HierarchyContext &context)
{
ParticleSystem *psys = context.particle_system;
ParticleCacheKey **cache = psys->pathcache;
if (cache == nullptr) {
return;
}
pxr::UsdTimeCode time = get_export_time_code();
pxr::UsdGeomBasisCurves curves = pxr::UsdGeomBasisCurves::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
if (psys->part->flag & PART_HAIR_BSPLINE) {
curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->bspline));
curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
}
else {
curves.CreateBasisAttr(pxr::VtValue(pxr::UsdGeomTokens->catmullRom));
curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->pinned));
}
pxr::VtArray<pxr::GfVec3f> points;
pxr::VtIntArray curve_point_counts;
curve_point_counts.reserve(psys->totpart);
/* Reverse current transform since the Hair curves will be placed under the object's Xform and we
* don't want a double-transform to happen. */
const float4x4 inv = math::invert(context.object->object_to_world());
ParticleCacheKey *strand;
for (int strand_index = 0; strand_index < psys->totpart; ++strand_index) {
strand = cache[strand_index];
int point_count = strand->segments + 1;
curve_point_counts.push_back(point_count);
for (int point_index = 0; point_index < point_count; ++point_index, ++strand) {
const float3 vert = math::transform_point(inv, float3(strand->co));
points.push_back(pxr::GfVec3f(vert.x, vert.y, vert.z));
}
}
pxr::UsdAttribute attr_points = curves.CreatePointsAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_vertex_counts = curves.CreateCurveVertexCountsAttr(pxr::VtValue(), true);
if (!attr_points.HasValue()) {
attr_points.Set(points, pxr::UsdTimeCode::Default());
attr_vertex_counts.Set(curve_point_counts, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_points, pxr::VtValue(points), time);
usd_value_writer_.SetAttribute(attr_vertex_counts, pxr::VtValue(curve_point_counts), time);
if (psys->totpart > 0) {
pxr::VtArray<pxr::GfVec3f> colors;
colors.push_back(pxr::GfVec3f(cache[0]->col));
curves.CreateDisplayColorAttr(pxr::VtValue(colors));
}
if (psys->part) {
auto prim = curves.GetPrim();
add_to_prim_map(prim.GetPath(), &psys->part->id);
write_id_properties(prim, psys->part->id, time);
}
assign_material(context, curves, psys->part->omat);
this->author_extent(curves, time);
}
void USDHairWriter::assign_material(const HierarchyContext &context,
const pxr::UsdGeomBasisCurves &curves,
const int material_slot)
{
if (!usd_export_context_.export_params.export_materials) {
return;
}
Material *material = BKE_object_material_get_eval(context.object, material_slot);
if (material == nullptr) {
return;
}
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
if (!usd_material) {
return;
}
auto curves_prim = curves.GetPrim();
pxr::UsdShadeMaterialBindingAPI binding(curves_prim);
binding.Bind(usd_material);
pxr::UsdShadeMaterialBindingAPI::Apply(curves_prim);
}
bool USDHairWriter::check_is_animated(const HierarchyContext & /*context*/) const
{
return true;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
namespace blender::io::usd {
/* Writer for writing hair particle data as USD curves. */
class USDHairWriter : public USDAbstractWriter {
public:
USDHairWriter(const USDExporterContext &ctx);
protected:
void do_write(HierarchyContext &context) override;
void assign_material(const HierarchyContext &context,
const pxr::UsdGeomBasisCurves &curves,
const int material_slot);
bool check_is_animated(const HierarchyContext &context) const override;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,187 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_light.hh"
#include "usd_attribute_utils.hh"
#include "usd_colorspace_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include <pxr/usd/usdLux/diskLight.h>
#include <pxr/usd/usdLux/distantLight.h>
#include <pxr/usd/usdLux/rectLight.h>
#include <pxr/usd/usdLux/shapingAPI.h>
#include <pxr/usd/usdLux/sphereLight.h>
#include "BLI_assert.h"
#include "BLI_math_constants.h"
#include "DNA_light_types.h"
#include "DNA_object_types.h"
namespace blender::io::usd {
USDLightWriter::USDLightWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
bool USDLightWriter::is_supported(const HierarchyContext * /*context*/) const
{
return true;
}
void USDLightWriter::do_write(HierarchyContext &context)
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
const pxr::SdfPath &usd_path = usd_export_context_.usd_path;
pxr::UsdTimeCode time = get_export_time_code();
const Light *light = id_cast<const Light *>(context.object->data);
pxr::UsdLuxLightAPI usd_light_api;
switch (light->type) {
case LA_AREA: {
switch (light->area_shape) {
case LA_AREA_RECT: {
pxr::UsdLuxRectLight rect_light = pxr::UsdLuxRectLight::Define(stage, usd_path);
set_attribute(rect_light.CreateWidthAttr(pxr::VtValue(), true),
light->area_size,
time,
usd_value_writer_);
set_attribute(rect_light.CreateHeightAttr(pxr::VtValue(), true),
light->area_sizey,
time,
usd_value_writer_);
usd_light_api = rect_light.LightAPI();
break;
}
case LA_AREA_SQUARE: {
pxr::UsdLuxRectLight rect_light = pxr::UsdLuxRectLight::Define(stage, usd_path);
set_attribute(rect_light.CreateWidthAttr(pxr::VtValue(), true),
light->area_size,
time,
usd_value_writer_);
set_attribute(rect_light.CreateHeightAttr(pxr::VtValue(), true),
light->area_size,
time,
usd_value_writer_);
usd_light_api = rect_light.LightAPI();
break;
}
case LA_AREA_DISK: {
pxr::UsdLuxDiskLight disk_light = pxr::UsdLuxDiskLight::Define(stage, usd_path);
set_attribute(disk_light.CreateRadiusAttr(pxr::VtValue(), true),
light->area_size / 2.0f,
time,
usd_value_writer_);
usd_light_api = disk_light.LightAPI();
break;
}
case LA_AREA_ELLIPSE: {
/* An ellipse light deteriorates into a disk light. */
pxr::UsdLuxDiskLight disk_light = pxr::UsdLuxDiskLight::Define(stage, usd_path);
set_attribute(disk_light.CreateRadiusAttr(pxr::VtValue(), true),
(light->area_size + light->area_sizey) / 4.0f,
time,
usd_value_writer_);
usd_light_api = disk_light.LightAPI();
break;
}
}
break;
}
case LA_LOCAL:
case LA_SPOT: {
pxr::UsdLuxSphereLight sphere_light = pxr::UsdLuxSphereLight::Define(stage, usd_path);
set_attribute(sphere_light.CreateRadiusAttr(pxr::VtValue(), true),
light->radius,
time,
usd_value_writer_);
set_attribute(sphere_light.CreateTreatAsPointAttr(pxr::VtValue(), true),
light->radius == 0.0f,
time,
usd_value_writer_);
if (light->type == LA_SPOT) {
pxr::UsdLuxShapingAPI shaping_api = pxr::UsdLuxShapingAPI::Apply(sphere_light.GetPrim());
if (shaping_api) {
set_attribute(shaping_api.CreateShapingConeAngleAttr(pxr::VtValue(), true),
RAD2DEGF(light->spotsize) / 2.0f,
time,
usd_value_writer_);
set_attribute(shaping_api.CreateShapingConeSoftnessAttr(pxr::VtValue(), true),
light->spotblend,
time,
usd_value_writer_);
}
}
usd_light_api = sphere_light.LightAPI();
break;
}
case LA_SUN: {
pxr::UsdLuxDistantLight distant_light = pxr::UsdLuxDistantLight::Define(stage, usd_path);
set_attribute(distant_light.CreateAngleAttr(pxr::VtValue(), true),
RAD2DEGF(light->sun_angle / 2.0f),
time,
usd_value_writer_);
usd_light_api = distant_light.LightAPI();
break;
}
default:
BLI_assert_unreachable();
break;
}
float intensity;
if (light->type == LA_SUN) {
/* Unclear why, but approximately matches Karma. */
intensity = light->energy / 4.0f;
}
else {
/* Convert from radiant flux to intensity. */
intensity = light->energy / M_PI;
}
set_attribute(
usd_light_api.CreateIntensityAttr(pxr::VtValue(), true), intensity, time, usd_value_writer_);
set_attribute(usd_light_api.CreateExposureAttr(pxr::VtValue(), true),
light->exposure,
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateColorAttr(pxr::VtValue(), true),
pxr::GfVec3f(light->r, light->g, light->b),
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateEnableColorTemperatureAttr(pxr::VtValue(), true),
(light->mode & LA_USE_TEMPERATURE) != 0,
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateColorTemperatureAttr(pxr::VtValue(), true),
light->temperature,
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateDiffuseAttr(pxr::VtValue(), true),
light->diff_fac,
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateSpecularAttr(pxr::VtValue(), true),
light->spec_fac,
time,
usd_value_writer_);
set_attribute(usd_light_api.CreateNormalizeAttr(pxr::VtValue(), true),
(light->mode & LA_UNNORMALIZED) == 0,
time,
usd_value_writer_);
pxr::UsdPrim prim = usd_light_api.GetPrim();
add_to_prim_map(prim.GetPath(), &light->id);
write_id_properties(prim, light->id, time);
colorspace_apply_to_prim(prim);
/* Only a subset of light types are "boundable". */
if (auto boundable = pxr::UsdGeomBoundable(prim)) {
this->author_extent(boundable, time);
}
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,19 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
namespace blender::io::usd {
class USDLightWriter : public USDAbstractWriter {
public:
USDLightWriter(const USDExporterContext &ctx);
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
};
} // namespace blender::io::usd

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include <pxr/usd/usdShade/material.h>
#include <string>
namespace blender {
struct bNode;
struct Image;
struct Material;
struct ReportList;
namespace io::usd {
struct USDExporterContext;
struct USDExportParams;
/**
* Create USDMaterial from Blender material.
*
* \param active_uvmap_name: used as the default UV set name sampled by the `primvar`
* reader shaders generated for image texture nodes that don't have an attached UVMap node.
*/
pxr::UsdShadeMaterial create_usd_material(const USDExporterContext &usd_export_context,
pxr::SdfPath usd_path,
Material *material,
const std::string &active_uvmap_name,
ReportList *reports);
/**
* Create a viewport UsdPreviewSurface material from a Blender material.
*/
void create_usd_viewport_material(const USDExporterContext &usd_export_context,
const Material *material,
const pxr::UsdShadeMaterial &usd_material);
/**
* Returns a USDPreviewSurface token name for a given Blender shader Socket name,
* or an empty TfToken if the input name is not found in the map.
*/
pxr::TfToken token_for_input(const StringRef input_name);
void export_texture(bNode *node,
const pxr::UsdStageRefPtr stage,
const bool allow_overwrite = false,
ReportList *reports = nullptr);
void export_texture(Image *ima,
const pxr::UsdStageRefPtr stage,
const bool allow_overwrite = false,
ReportList *reports = nullptr);
/**
* Gets an asset path for the given texture image / node. The resulting path
* may be absolute, relative to the USD file, or in a 'textures' directory
* in the same directory as the USD file, depending on the export parameters.
* The filename is typically the image filepath but might also be automatically
* generated based on the image name for in-memory textures when exporting textures.
* This function may return an empty string if the image does not have a filepath
* assigned and no asset path could be determined.
*/
std::string get_tex_image_asset_filepath(bNode *node,
const pxr::UsdStageRefPtr stage,
const USDExportParams &export_params);
std::string get_tex_image_asset_filepath(Image *ima,
const pxr::UsdStageRefPtr stage,
const USDExportParams &export_params);
/**
* Return a USD asset path referencing the given texture file.
* The resulting path may be absolute, relative to the USD file,
* or in a 'textures' directory in the same directory as the USD file,
* depending on the export parameters.
*/
std::string get_tex_image_asset_filepath(const std::string &asset_path,
const std::string &stage_path,
const USDExportParams &export_params);
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,960 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_mesh.hh"
#include "usd_armature_utils.hh"
#include "usd_attribute_utils.hh"
#include "usd_blend_shape_utils.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_skel_convert.hh"
#include "usd_utils.hh"
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdSkel/bindingAPI.h>
#include "BLI_array_utils.hh"
#include "BLI_assert.h"
#include "BLI_math_vector_types.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_attribute.hh"
#include "BKE_lib_id.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_mapping.hh"
#include "BKE_mesh_wrapper.hh"
#include "BKE_object.hh"
#include "BKE_report.hh"
#include "BKE_subdiv.hh"
#include "bmesh.hh"
#include "bmesh_tools.hh"
#include "DEG_depsgraph.hh"
#include "DNA_key_types.h"
#include "DNA_material_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
USDGenericMeshWriter::USDGenericMeshWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx)
{
}
bool USDGenericMeshWriter::is_supported(const HierarchyContext *context) const
{
return context->is_object_visible(usd_export_context_.export_params.evaluation_mode);
}
/* Get the last subdiv modifier, regardless of enable/disable status */
static const SubsurfModifierData *get_last_subdiv_modifier(eEvaluationMode eval_mode, Object *obj)
{
BLI_assert(obj);
/* Return the subdiv modifier if it is the last modifier and has
* the required mode enabled. */
ModifierData *md = static_cast<ModifierData *>(obj->modifiers.last);
if (!md) {
return nullptr;
}
/* Determine if the modifier is enabled for the current evaluation mode. */
ModifierMode mod_mode = (eval_mode == DAG_EVAL_RENDER) ? eModifierMode_Render :
eModifierMode_Realtime;
if ((md->mode & mod_mode) != mod_mode) {
return nullptr;
}
if (md->type == eModifierType_Subsurf) {
return reinterpret_cast<SubsurfModifierData *>(md);
}
return nullptr;
}
void USDGenericMeshWriter::do_write(HierarchyContext &context)
{
Object *object_eval = context.object;
bool needsfree = false;
Mesh *mesh = get_export_mesh(object_eval, needsfree);
if (mesh == nullptr) {
return;
}
if (usd_export_context_.export_params.triangulate_meshes) {
const bool tag_only = false;
const int quad_method = usd_export_context_.export_params.quad_method;
const int ngon_method = usd_export_context_.export_params.ngon_method;
BMeshCreateParams bmesh_create_params{};
BMeshFromMeshParams bmesh_from_mesh_params{};
bmesh_from_mesh_params.calc_face_normal = true;
bmesh_from_mesh_params.calc_vert_normal = true;
BMesh *bm = BKE_mesh_to_bmesh_ex(mesh, &bmesh_create_params, &bmesh_from_mesh_params);
BM_mesh_triangulate(bm, quad_method, ngon_method, 4, tag_only, nullptr, nullptr, nullptr);
Mesh *triangulated_mesh = BKE_mesh_from_bmesh_for_eval_nomain(bm, nullptr, mesh);
BM_mesh_free(bm);
if (needsfree) {
free_export_mesh(mesh);
}
mesh = triangulated_mesh;
needsfree = true;
}
try {
/* Fetch the subdiv modifier, if one exists and it is the last modifier. */
const SubsurfModifierData *subsurfData = get_last_subdiv_modifier(
usd_export_context_.export_params.evaluation_mode, object_eval);
write_mesh(context, mesh, subsurfData);
auto prim = usd_export_context_.stage->GetPrimAtPath(usd_export_context_.usd_path);
if (prim.IsValid() && object_eval) {
prim.SetActive((object_eval->duplicator_visibility_flag & OB_DUPLI_FLAG_RENDER) != 0);
add_to_prim_map(prim.GetPath(), &mesh->id);
write_id_properties(prim, mesh->id, get_export_time_code());
}
if (needsfree) {
free_export_mesh(mesh);
}
}
catch (...) {
if (needsfree) {
free_export_mesh(mesh);
}
throw;
}
}
void USDGenericMeshWriter::write_custom_data(const Object *obj,
const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh)
{
const bke::AttributeAccessor attributes = mesh->attributes();
const StringRef active_uvmap_name = mesh->default_uv_map_name();
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
/* Skip "internal" Blender properties and attributes processed elsewhere.
* Skip edge domain because USD doesn't have a good conversion for them. */
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
iter.domain == bke::AttrDomain::Edge ||
ELEM(iter.name,
"position",
"material_index",
"velocity",
"crease_vert",
"custom_normal",
"sharp_face"))
{
return;
}
if ((usd_export_context_.export_params.export_armatures ||
usd_export_context_.export_params.export_shapekeys) &&
iter.name.rfind("skel:") == 0)
{
/* If we're exporting armatures or shape keys to UsdSkel, we skip any
* attributes that have names with the "skel:" namespace, to avoid possible
* conflicts. Such attribute might have been previously imported into Blender
* from USD, but can no longer be considered valid. */
return;
}
if (usd_export_context_.export_params.export_armatures &&
is_armature_modifier_bone_name(*obj, iter.name, usd_export_context_.depsgraph))
{
/* This attribute is likely a vertex group for the armature modifier,
* and it may conflict with skinning data that will be written to
* the USD mesh, so we skip it. Such vertex groups will instead be
* handled in #export_deform_verts(). */
return;
}
/* UV Data. */
if (iter.domain == bke::AttrDomain::Corner && iter.data_type == bke::AttrType::Float2) {
if (usd_export_context_.export_params.export_uvmaps) {
this->write_uv_data(mesh, usd_mesh, iter, active_uvmap_name);
}
}
else {
this->write_generic_data(mesh, usd_mesh, iter);
}
});
}
static std::optional<pxr::TfToken> convert_blender_domain_to_usd(
const bke::AttrDomain blender_domain)
{
switch (blender_domain) {
case bke::AttrDomain::Corner:
return pxr::UsdGeomTokens->faceVarying;
case bke::AttrDomain::Point:
return pxr::UsdGeomTokens->vertex;
case bke::AttrDomain::Face:
return pxr::UsdGeomTokens->uniform;
/* Notice: Edge types are not supported in USD! */
default:
return std::nullopt;
}
}
void USDGenericMeshWriter::write_generic_data(const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh,
const bke::AttributeIter &attr)
{
const pxr::TfToken pv_name(
make_safe_primvar_name(attr.name, usd_export_context_.export_params.allow_unicode));
const bool use_color3f_type = pv_name == usdtokens::displayColor;
const std::optional<pxr::TfToken> pv_interp = convert_blender_domain_to_usd(attr.domain);
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(
attr.data_type, use_color3f_type);
if (!pv_interp || !pv_type) {
BKE_reportf(reports(),
RPT_WARNING,
"Mesh '%s', Attribute '%s' (domain %d, type %d) cannot be converted to USD",
BKE_id_name(mesh->id),
attr.name.c_str(),
int8_t(attr.domain),
int(attr.data_type));
return;
}
const GVArray attribute = *attr.get();
if (attribute.is_empty()) {
return;
}
const pxr::UsdTimeCode time = get_export_time_code();
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_mesh);
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type, *pv_interp);
copy_blender_attribute_to_primvar(attribute, attr.data_type, time, pv_attr, usd_value_writer_);
}
void USDGenericMeshWriter::write_uv_data(const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh,
const bke::AttributeIter &attr,
const StringRef active_uvmap_name)
{
const VArray<float2> buffer = *attr.get<float2>(bke::AttrDomain::Corner);
if (buffer.is_empty()) {
return;
}
/* Optionally rename active UV map to "st", to follow USD conventions
* and better work with MaterialX shader nodes. */
const StringRef name = usd_export_context_.export_params.rename_uvmaps &&
active_uvmap_name == attr.name ?
"st" :
attr.name;
/* Construct the UvVertMap containing the connectivity data for the UVs. */
const OffsetIndices<int> faces = mesh->faces();
const Span<int> corner_verts = mesh->corner_verts();
const VArraySpan<float2> uv_data(buffer);
UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create(
faces, corner_verts, uv_data, mesh->verts_num, float2(STD_UV_CONNECT_LIMIT), false);
/* This will only be a nullptr if the `faces` are empty OR allocating space for
* the VertMap fails. */
if (!uv_vert_map) {
CLOG_WARN(&LOG,
"Couldn't resolve UV connectivity for mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
/* From the connectivity data, extract the unique uvs and a mapping of corner index
* to the index of the corresponding unique uv for that corner. */
pxr::VtArray<pxr::GfVec2f> unique_uvs;
unique_uvs.reserve(mesh->verts_num);
Array<int> corner_to_uv_index(corner_verts.size(), -1);
for (int vertex_index = 0; vertex_index < mesh->verts_num; vertex_index++) {
const UvMapVert *uv_vert = BKE_mesh_uv_vert_map_get_vert(uv_vert_map, vertex_index);
/* Loop over all of the face vertices connected to this mesh vertex and accumulate the
* unique UVs. */
for (; uv_vert; uv_vert = uv_vert->next) {
const int corner_index = faces[uv_vert->face_index].start() + uv_vert->loop_of_face_index;
const float2 uv = uv_data[corner_index];
if (uv_vert->separate) {
unique_uvs.push_back(pxr::GfVec2f(uv.x, uv.y));
}
corner_to_uv_index[corner_index] = unique_uvs.size() - 1;
}
}
BKE_mesh_uv_vert_map_free(uv_vert_map);
/* Finally, build the USD indices array. */
pxr::VtIntArray indices;
indices.reserve(corner_verts.size());
for (const int corner_idx : corner_verts.index_range()) {
indices.push_back(corner_to_uv_index[corner_idx]);
}
const pxr::UsdTimeCode time = get_export_time_code();
const pxr::TfToken pv_name(
make_safe_primvar_name(name, usd_export_context_.export_params.allow_unicode));
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_mesh);
pxr::UsdGeomPrimvar pv_uv = pv_api.CreatePrimvar(
pv_name, pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->faceVarying);
set_attribute(pv_uv, unique_uvs, time, usd_value_writer_);
pxr::UsdAttribute attr_indices = pv_uv.CreateIndicesAttr();
if (!attr_indices.HasValue()) {
attr_indices.Set(indices, time);
}
usd_value_writer_.SetAttribute(attr_indices, pxr::VtValue(indices), time);
}
void USDGenericMeshWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
struct USDMeshData {
pxr::VtArray<pxr::GfVec3f> points;
pxr::VtIntArray face_vertex_counts;
pxr::VtIntArray face_indices;
MaterialFaceGroups face_groups;
/* The length of this array specifies the number of creases on the surface. Each element gives
* the number of (must be adjacent) vertices in each crease, whose indices are linearly laid out
* in the 'creaseIndices' attribute. Since each crease must be at least one edge long, each
* element of this array should be greater than one. */
pxr::VtIntArray crease_lengths;
/* The indices of all vertices forming creased edges. The size of this array must be equal to the
* sum of all elements of the 'creaseLengths' attribute. */
pxr::VtIntArray crease_vertex_indices;
/* The per-crease or per-edge sharpness for all creases (Usd.Mesh.SHARPNESS_INFINITE for a
* perfectly sharp crease). Since 'creaseLengths' encodes the number of vertices in each crease,
* the number of elements in this array will be either `len(creaseLengths)` or the sum over all X
* of `(creaseLengths[X] - 1)`. Note that while the RI spec allows each crease to have either a
* single sharpness or a value per-edge, USD will encode either a single sharpness per crease on
* a mesh, or sharpness's for all edges making up the creases on a mesh. */
pxr::VtFloatArray crease_sharpnesses;
/* The lengths of this array specifies the number of sharp corners (or vertex crease) on the
* surface. Each value is the index of a vertex in the mesh's vertex list. */
pxr::VtIntArray corner_indices;
/* The per-vertex sharpnesses. The lengths of this array must match that of `corner_indices`. */
pxr::VtFloatArray corner_sharpnesses;
};
void USDGenericMeshWriter::write_mesh(HierarchyContext &context,
Mesh *mesh,
const SubsurfModifierData *subsurfData)
{
pxr::UsdTimeCode time = get_export_time_code();
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
const pxr::SdfPath &usd_path = usd_export_context_.usd_path;
pxr::UsdGeomMesh usd_mesh = pxr::UsdGeomMesh::Define(stage, usd_path);
write_visibility(context, time, usd_mesh);
USDMeshData usd_mesh_data;
/* Ensure data exists if currently in edit mode. */
BKE_mesh_wrapper_ensure_mdata(mesh);
get_geometry_data(mesh, usd_mesh_data);
pxr::UsdAttribute attr_points = usd_mesh.CreatePointsAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_face_vertex_counts = usd_mesh.CreateFaceVertexCountsAttr(pxr::VtValue(),
true);
pxr::UsdAttribute attr_face_vertex_indices = usd_mesh.CreateFaceVertexIndicesAttr(pxr::VtValue(),
true);
if (!attr_points.HasValue()) {
/* Provide the initial value as default. This makes USD write the value as constant if they
* don't change over time. */
attr_points.Set(usd_mesh_data.points, pxr::UsdTimeCode::Default());
attr_face_vertex_counts.Set(usd_mesh_data.face_vertex_counts, pxr::UsdTimeCode::Default());
attr_face_vertex_indices.Set(usd_mesh_data.face_indices, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_points, pxr::VtValue(usd_mesh_data.points), time);
usd_value_writer_.SetAttribute(
attr_face_vertex_counts, pxr::VtValue(usd_mesh_data.face_vertex_counts), time);
usd_value_writer_.SetAttribute(
attr_face_vertex_indices, pxr::VtValue(usd_mesh_data.face_indices), time);
if (!usd_mesh_data.crease_lengths.empty()) {
pxr::UsdAttribute attr_crease_lengths = usd_mesh.CreateCreaseLengthsAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_crease_indices = usd_mesh.CreateCreaseIndicesAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_crease_sharpness = usd_mesh.CreateCreaseSharpnessesAttr(pxr::VtValue(),
true);
if (!attr_crease_lengths.HasValue()) {
attr_crease_lengths.Set(usd_mesh_data.crease_lengths, pxr::UsdTimeCode::Default());
attr_crease_indices.Set(usd_mesh_data.crease_vertex_indices, pxr::UsdTimeCode::Default());
attr_crease_sharpness.Set(usd_mesh_data.crease_sharpnesses, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(
attr_crease_lengths, pxr::VtValue(usd_mesh_data.crease_lengths), time);
usd_value_writer_.SetAttribute(
attr_crease_indices, pxr::VtValue(usd_mesh_data.crease_vertex_indices), time);
usd_value_writer_.SetAttribute(
attr_crease_sharpness, pxr::VtValue(usd_mesh_data.crease_sharpnesses), time);
}
if (!usd_mesh_data.corner_indices.empty() &&
usd_mesh_data.corner_indices.size() == usd_mesh_data.corner_sharpnesses.size())
{
pxr::UsdAttribute attr_corner_indices = usd_mesh.CreateCornerIndicesAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_corner_sharpnesses = usd_mesh.CreateCornerSharpnessesAttr(
pxr::VtValue(), true);
if (!attr_corner_indices.HasValue()) {
attr_corner_indices.Set(usd_mesh_data.corner_indices, pxr::UsdTimeCode::Default());
attr_corner_sharpnesses.Set(usd_mesh_data.corner_sharpnesses, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(
attr_corner_indices, pxr::VtValue(usd_mesh_data.corner_indices), time);
usd_value_writer_.SetAttribute(
attr_corner_sharpnesses, pxr::VtValue(usd_mesh_data.corner_sharpnesses), time);
}
write_custom_data(context.object, mesh, usd_mesh);
write_surface_velocity(mesh, usd_mesh);
const pxr::TfToken subdiv_scheme = get_subdiv_scheme(subsurfData);
/* Normals can be animated, so ensure these are written for each frame,
* unless a subdiv modifier is used, in which case normals are computed,
* not stored with the mesh. */
if (usd_export_context_.export_params.export_normals &&
subdiv_scheme == pxr::UsdGeomTokens->none)
{
write_normals(mesh, usd_mesh);
}
this->author_extent(usd_mesh, mesh->bounds_min_max(), time);
/* TODO(Sybren): figure out what happens when the face groups change. */
if (frame_has_been_written_) {
return;
}
/* The subdivision scheme is a uniform according to spec,
* so this value cannot be animated. */
write_subdiv(subdiv_scheme, usd_mesh, subsurfData);
if (usd_export_context_.export_params.export_materials) {
assign_materials(context, usd_mesh, usd_mesh_data.face_groups);
}
}
pxr::TfToken USDGenericMeshWriter::get_subdiv_scheme(const SubsurfModifierData *subsurfData)
{
/* Default to setting the subdivision scheme to None. */
pxr::TfToken subdiv_scheme = pxr::UsdGeomTokens->none;
if (subsurfData) {
if (subsurfData->subdivType == SUBSURF_TYPE_CATMULL_CLARK) {
if (usd_export_context_.export_params.export_subdiv == SubdivExportMode::Match) {
/* If a subdivision modifier exists, and it uses Catmull-Clark, then apply Catmull-Clark
* SubD scheme. */
subdiv_scheme = pxr::UsdGeomTokens->catmullClark;
}
}
else {
/* "Simple" is currently the only other subdivision type provided by Blender, */
/* and we do not yet provide a corresponding representation for USD export. */
BKE_reportf(reports(),
RPT_WARNING,
"USD export: Simple subdivision not supported, exporting subdivided mesh");
}
}
return subdiv_scheme;
}
void USDGenericMeshWriter::write_subdiv(const pxr::TfToken &subdiv_scheme,
const pxr::UsdGeomMesh &usd_mesh,
const SubsurfModifierData *subsurfData)
{
usd_mesh.CreateSubdivisionSchemeAttr().Set(subdiv_scheme);
if (subdiv_scheme == pxr::UsdGeomTokens->catmullClark) {
/* For Catmull-Clark, also consider the various interpolation modes. */
/* For reference, see
* https://graphics.pixar.com/opensubdiv/docs/subdivision_surfaces.html#face-varying-interpolation-rules
*/
switch (subsurfData->uv_smooth) {
case SUBSURF_UV_SMOOTH_NONE:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->all);
break;
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersOnly);
break;
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_AND_JUNCTIONS:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersPlus1);
break;
case SUBSURF_UV_SMOOTH_PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->cornersPlus2);
break;
case SUBSURF_UV_SMOOTH_PRESERVE_BOUNDARIES:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->boundaries);
break;
case SUBSURF_UV_SMOOTH_ALL:
usd_mesh.CreateFaceVaryingLinearInterpolationAttr().Set(pxr::UsdGeomTokens->none);
break;
default:
BLI_assert_msg(0, "Unsupported UV smoothing mode.");
}
/* For reference, see
* https://graphics.pixar.com/opensubdiv/docs/subdivision_surfaces.html#boundary-interpolation-rules
*/
switch (subsurfData->boundary_smooth) {
case SUBSURF_BOUNDARY_SMOOTH_ALL:
usd_mesh.CreateInterpolateBoundaryAttr().Set(pxr::UsdGeomTokens->edgeOnly);
break;
case SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS:
usd_mesh.CreateInterpolateBoundaryAttr().Set(pxr::UsdGeomTokens->edgeAndCorner);
break;
default:
BLI_assert_msg(0, "Unsupported boundary smoothing mode.");
}
}
}
static void get_positions(const Mesh *mesh, USDMeshData &usd_mesh_data)
{
const Span<pxr::GfVec3f> positions = mesh->vert_positions().cast<pxr::GfVec3f>();
usd_mesh_data.points = pxr::VtArray<pxr::GfVec3f>(positions.begin(), positions.end());
}
static void get_loops_polys(const Mesh *mesh, USDMeshData &usd_mesh_data)
{
/* Only construct face groups (a.k.a. geometry subsets) when we need them for material
* assignments. */
const bke::AttributeAccessor attributes = mesh->attributes();
const VArray<int> material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Face, 0);
if (!material_indices.is_single() && mesh->totcol > 1) {
const VArraySpan<int> indices_span(material_indices);
for (const int i : indices_span.index_range()) {
usd_mesh_data.face_groups.lookup_or_add_default(indices_span[i]).push_back(i);
}
}
usd_mesh_data.face_vertex_counts.resize(mesh->faces_num);
const OffsetIndices faces = mesh->faces();
offset_indices::copy_group_sizes(
faces,
faces.index_range(),
MutableSpan(usd_mesh_data.face_vertex_counts.data(), mesh->faces_num));
const Span<int> corner_verts = mesh->corner_verts();
usd_mesh_data.face_indices = pxr::VtIntArray(corner_verts.begin(), corner_verts.end());
}
static void get_edge_creases(const Mesh *mesh, USDMeshData &usd_mesh_data)
{
const bke::AttributeAccessor attributes = mesh->attributes();
const bke::AttributeReader attribute = attributes.lookup<float>("crease_edge",
bke::AttrDomain::Edge);
if (!attribute) {
return;
}
const VArraySpan creases(*attribute);
const Span<int2> edges = mesh->edges();
for (const int i : edges.index_range()) {
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
if (crease != 0.0f) {
usd_mesh_data.crease_vertex_indices.push_back(edges[i][0]);
usd_mesh_data.crease_vertex_indices.push_back(edges[i][1]);
usd_mesh_data.crease_lengths.push_back(2);
usd_mesh_data.crease_sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
}
}
}
static void get_vert_creases(const Mesh *mesh, USDMeshData &usd_mesh_data)
{
const bke::AttributeAccessor attributes = mesh->attributes();
const bke::AttributeReader attribute = attributes.lookup<float>("crease_vert",
bke::AttrDomain::Point);
if (!attribute) {
return;
}
const VArraySpan creases(*attribute);
for (const int i : creases.index_range()) {
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
if (crease != 0.0f) {
usd_mesh_data.corner_indices.push_back(i);
usd_mesh_data.corner_sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
}
}
}
void USDGenericMeshWriter::get_geometry_data(const Mesh *mesh, USDMeshData &usd_mesh_data)
{
get_positions(mesh, usd_mesh_data);
get_loops_polys(mesh, usd_mesh_data);
get_edge_creases(mesh, usd_mesh_data);
get_vert_creases(mesh, usd_mesh_data);
}
void USDGenericMeshWriter::assign_materials(const HierarchyContext &context,
const pxr::UsdGeomMesh &usd_mesh,
const MaterialFaceGroups &usd_face_groups)
{
const int totcol = BKE_object_material_count_eval(context.object);
if (totcol == 0) {
return;
}
/* Binding a material to a geometry subset isn't supported by the Hydra GL viewport yet,
* which is why we always bind the first material to the entire mesh. See
* https://github.com/PixarAnimationStudios/USD/issues/542 for more info. */
bool mesh_material_bound = false;
auto mesh_prim = usd_mesh.GetPrim();
pxr::UsdShadeMaterialBindingAPI material_binding_api(mesh_prim);
for (int mat_num = 0; mat_num < totcol; mat_num++) {
Material *material = BKE_object_material_get_eval(context.object, mat_num + 1);
if (material == nullptr) {
continue;
}
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
material_binding_api.Bind(usd_material);
/* USD seems to support neither per-material nor per-face-group double-sidedness, so we just
* use the flag from the first non-empty material slot. */
usd_mesh.CreateDoubleSidedAttr(
pxr::VtValue((material->blend_flag & MA_BL_CULL_BACKFACE) == 0));
mesh_material_bound = true;
break;
}
if (mesh_material_bound) {
/* USD will require that prims with material bindings have the #MaterialBindingAPI applied
* schema. While Bind() above will create the binding attribute, Apply() needs to be called as
* well to add the #MaterialBindingAPI schema to the prim itself. */
pxr::UsdShadeMaterialBindingAPI::Apply(mesh_prim);
}
else {
/* Blender defaults to double-sided, but USD to single-sided. */
usd_mesh.CreateDoubleSidedAttr(pxr::VtValue(true));
}
if (!mesh_material_bound || usd_face_groups.size() < 2) {
/* Either all material slots were empty or there is only one material in use. As geometry
* subsets are only written when actually used to assign a material, and the mesh already has
* the material assigned, there is no need to continue. */
return;
}
/* Define a geometry subset per material. */
for (const MaterialFaceGroups::Item &face_group : usd_face_groups.items()) {
short material_number = face_group.key;
const pxr::VtIntArray &face_indices = face_group.value;
Material *material = BKE_object_material_get_eval(context.object, material_number + 1);
if (material == nullptr) {
continue;
}
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
pxr::TfToken material_name = usd_material.GetPath().GetNameToken();
pxr::UsdGeomSubset usd_face_subset = material_binding_api.CreateMaterialBindSubset(
material_name, face_indices);
auto subset_prim = usd_face_subset.GetPrim();
auto subset_material_api = pxr::UsdShadeMaterialBindingAPI(subset_prim);
subset_material_api.Bind(usd_material);
/* Apply the #MaterialBindingAPI applied schema, as required by USD. */
pxr::UsdShadeMaterialBindingAPI::Apply(subset_prim);
}
}
void USDGenericMeshWriter::write_normals(const Mesh *mesh, pxr::UsdGeomMesh &usd_mesh)
{
pxr::UsdTimeCode time = get_export_time_code();
pxr::VtVec3fArray loop_normals;
loop_normals.resize(mesh->corners_num);
MutableSpan dst_normals(reinterpret_cast<float3 *>(loop_normals.data()), loop_normals.size());
switch (mesh->normals_domain()) {
case bke::MeshNormalDomain::Point: {
array_utils::gather(mesh->vert_normals(), mesh->corner_verts(), dst_normals);
break;
}
case bke::MeshNormalDomain::Face: {
const OffsetIndices faces = mesh->faces();
const Span<float3> face_normals = mesh->face_normals();
for (const int i : faces.index_range()) {
dst_normals.slice(faces[i]).fill(face_normals[i]);
}
break;
}
case bke::MeshNormalDomain::Corner: {
array_utils::copy(mesh->corner_normals(), dst_normals);
break;
}
}
pxr::UsdAttribute attr_normals = usd_mesh.CreateNormalsAttr(pxr::VtValue(), true);
if (!attr_normals.HasValue()) {
attr_normals.Set(loop_normals, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_normals, pxr::VtValue(loop_normals), time);
usd_mesh.SetNormalsInterpolation(pxr::UsdGeomTokens->faceVarying);
}
void USDGenericMeshWriter::write_surface_velocity(const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh)
{
/* Export velocity attribute output by fluid sim, sequence cache modifier
* and geometry nodes. */
const VArraySpan velocity = *mesh->attributes().lookup<float3>("velocity",
bke::AttrDomain::Point);
if (velocity.is_empty()) {
return;
}
/* Export per-vertex velocity vectors. */
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
pxr::VtVec3fArray usd_velocities;
usd_velocities.assign(data.begin(), data.end());
pxr::UsdTimeCode time = get_export_time_code();
pxr::UsdAttribute attr_vel = usd_mesh.CreateVelocitiesAttr(pxr::VtValue(), true);
if (!attr_vel.HasValue()) {
attr_vel.Set(usd_velocities, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_vel, usd_velocities, time);
}
USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx)
: USDGenericMeshWriter(ctx), write_skinned_mesh_(false), write_blend_shapes_(false)
{
}
void USDMeshWriter::set_skel_export_flags(const HierarchyContext &context)
{
write_skinned_mesh_ = false;
write_blend_shapes_ = false;
const USDExportParams &params = usd_export_context_.export_params;
/* We can write a skinned mesh if exporting armatures is enabled and the object has an armature
* modifier. */
write_skinned_mesh_ = params.export_armatures &&
can_export_skinned_mesh(*context.object, usd_export_context_.depsgraph);
/* We can write blend shapes if exporting shape keys is enabled and the object has shape keys. */
write_blend_shapes_ = params.export_shapekeys && is_mesh_with_shape_keys(context.object);
}
void USDMeshWriter::init_skinned_mesh(const HierarchyContext &context)
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"%s: couldn't get valid mesh prim for mesh %s",
__func__,
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
pxr::UsdSkelBindingAPI skel_api = pxr::UsdSkelBindingAPI::Apply(mesh_prim);
if (!skel_api) {
CLOG_WARN(&LOG,
"Couldn't apply UsdSkelBindingAPI to mesh prim %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
const Object *arm_obj = get_armature_modifier_obj(*context.object,
usd_export_context_.depsgraph);
if (!arm_obj) {
CLOG_WARN(&LOG,
"Couldn't get armature modifier object for skinned mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
Vector<StringRef> bone_names;
get_armature_bone_names(
arm_obj, usd_export_context_.export_params.only_deform_bones, bone_names);
if (bone_names.is_empty()) {
CLOG_WARN(&LOG,
"No armature bones for skinned mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
bool needsfree = false;
Mesh *mesh = get_export_mesh(context.object, needsfree);
if (mesh == nullptr) {
return;
}
try {
export_deform_verts(mesh, skel_api, bone_names);
if (needsfree) {
free_export_mesh(mesh);
}
}
catch (...) {
if (needsfree) {
free_export_mesh(mesh);
}
throw;
}
}
void USDMeshWriter::init_blend_shapes(const HierarchyContext &context)
{
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"Couldn't get valid mesh prim for mesh %s",
mesh_prim.GetPath().GetAsString().c_str());
return;
}
create_blend_shapes(this->usd_export_context_.stage,
context.object,
mesh_prim,
usd_export_context_.export_params.allow_unicode);
}
void USDMeshWriter::do_write(HierarchyContext &context)
{
set_skel_export_flags(context);
if (frame_has_been_written_ && (write_skinned_mesh_ || write_blend_shapes_)) {
/* When writing skinned meshes or blend shapes, we only write the rest mesh once,
* so we return early after the first frame has been written. However, we still
* update blend shape weights if needed. */
if (write_blend_shapes_) {
add_shape_key_weights_sample(context.object);
}
return;
}
USDGenericMeshWriter::do_write(context);
if (write_skinned_mesh_) {
init_skinned_mesh(context);
}
if (write_blend_shapes_) {
init_blend_shapes(context);
add_shape_key_weights_sample(context.object);
}
}
Mesh *USDMeshWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
if (write_blend_shapes_) {
r_needsfree = true;
/* We return the pre-modified mesh with the verts in the shape key
* basis positions. */
return get_shape_key_basis_mesh(object_eval);
}
if (write_skinned_mesh_) {
r_needsfree = false;
/* We must export the skinned mesh in its rest pose. We therefore
* return the pre-modified mesh, so that the armature modifier isn't
* applied. */
/* TODO: Store the "needs free" mesh in a separate variable. */
return const_cast<Mesh *>(BKE_object_get_pre_modified_mesh(object_eval));
}
/* Return the fully evaluated mesh. */
r_needsfree = false;
return BKE_object_get_evaluated_mesh(object_eval);
}
void USDMeshWriter::add_shape_key_weights_sample(const Object *obj)
{
if (!obj) {
return;
}
const Key *key = get_mesh_shape_key(obj);
if (!key) {
return;
}
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdPrim mesh_prim = stage->GetPrimAtPath(usd_export_context_.usd_path);
if (!mesh_prim.IsValid()) {
CLOG_WARN(&LOG,
"Couldn't get valid mesh prim for mesh %s",
usd_export_context_.usd_path.GetAsString().c_str());
return;
}
pxr::VtFloatArray weights = get_blendshape_weights(key);
pxr::UsdTimeCode time = get_export_time_code();
/* Save the weights samples to a temporary privar which will be copied to
* a skeleton animation later. */
pxr::UsdAttribute temp_weights_attr = pxr::UsdGeomPrimvarsAPI(mesh_prim).CreatePrimvar(
TempBlendShapeWeightsPrimvarName, pxr::SdfValueTypeNames->FloatArray);
if (!temp_weights_attr) {
CLOG_WARN(&LOG,
"Couldn't create primvar %s on prim %s",
TempBlendShapeWeightsPrimvarName.GetText(),
mesh_prim.GetPath().GetAsString().c_str());
return;
}
temp_weights_attr.Set(weights, time);
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include "BLI_map.hh"
#include <pxr/usd/usdGeom/mesh.h>
namespace blender {
struct SubsurfModifierData;
namespace bke {
class AttributeIter;
} // namespace bke
namespace io::usd {
struct USDMeshData;
/* Mapping from material slot number to array of face indices with that material. */
using MaterialFaceGroups = Map<short, pxr::VtArray<int>>;
/* Writer for USD geometry. Does not assume the object is a mesh object. */
class USDGenericMeshWriter : public USDAbstractWriter {
public:
USDGenericMeshWriter(const USDExporterContext &ctx);
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
virtual Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) = 0;
virtual void free_export_mesh(Mesh *mesh);
private:
void write_mesh(HierarchyContext &context, Mesh *mesh, const SubsurfModifierData *subsurfData);
pxr::TfToken get_subdiv_scheme(const SubsurfModifierData *subsurfData);
void write_subdiv(const pxr::TfToken &subdiv_scheme,
const pxr::UsdGeomMesh &usd_mesh,
const SubsurfModifierData *subsurfData);
void get_geometry_data(const Mesh *mesh, struct USDMeshData &usd_mesh_data);
void assign_materials(const HierarchyContext &context,
const pxr::UsdGeomMesh &usd_mesh,
const MaterialFaceGroups &usd_face_groups);
void write_normals(const Mesh *mesh, pxr::UsdGeomMesh &usd_mesh);
void write_surface_velocity(const Mesh *mesh, const pxr::UsdGeomMesh &usd_mesh);
void write_custom_data(const Object *obj, const Mesh *mesh, const pxr::UsdGeomMesh &usd_mesh);
void write_generic_data(const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh,
const bke::AttributeIter &attr);
void write_uv_data(const Mesh *mesh,
const pxr::UsdGeomMesh &usd_mesh,
const bke::AttributeIter &attr,
StringRef active_uvmap_name);
};
class USDMeshWriter : public USDGenericMeshWriter {
bool write_skinned_mesh_;
bool write_blend_shapes_;
public:
USDMeshWriter(const USDExporterContext &ctx);
protected:
void do_write(HierarchyContext &context) override;
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
/**
* Determine whether we should write skinned mesh or blend shape data
* based on the export parameters and the modifiers enabled on the object.
*/
void set_skel_export_flags(const HierarchyContext &context);
void init_skinned_mesh(const HierarchyContext &context);
void init_blend_shapes(const HierarchyContext &context);
void add_shape_key_weights_sample(const Object *obj);
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_metaball.hh"
#include "usd_exporter_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_mball.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_mesh_types.h"
namespace blender::io::usd {
USDMetaballWriter::USDMetaballWriter(const USDExporterContext &ctx) : USDGenericMeshWriter(ctx) {}
bool USDMetaballWriter::is_supported(const HierarchyContext *context) const
{
Scene *scene = DEG_get_input_scene(usd_export_context_.depsgraph);
return is_basis_ball(scene, context->object) && USDGenericMeshWriter::is_supported(context);
}
bool USDMetaballWriter::check_is_animated(const HierarchyContext & /*context*/) const
{
/* We assume that meta-balls are always animated, as the current object may
* not be animated but another ball in the same group may be. */
return true;
}
Mesh *USDMetaballWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
Mesh *mesh_eval = BKE_object_get_evaluated_mesh(object_eval);
if (mesh_eval != nullptr) {
/* Mesh_eval only exists when generative modifiers are in use. */
r_needsfree = false;
return mesh_eval;
}
r_needsfree = true;
return BKE_mesh_new_from_object(usd_export_context_.depsgraph, object_eval, false, false, true);
}
void USDMetaballWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
bool USDMetaballWriter::is_basis_ball(Scene *scene, Object *ob) const
{
const Object *basis_ob = BKE_mball_basis_find(*usd_export_context_.bmain, scene, ob);
return ob == basis_ob;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,24 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_mesh.hh"
namespace blender::io::usd {
class USDMetaballWriter : public USDGenericMeshWriter {
public:
USDMetaballWriter(const USDExporterContext &ctx);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
void free_export_mesh(Mesh *mesh) override;
bool is_supported(const HierarchyContext *context) const override;
bool check_is_animated(const HierarchyContext &context) const override;
private:
bool is_basis_ball(Scene *scene, Object *ob) const;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,605 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_pointinstancer.hh"
#include "usd_attribute_utils.hh"
#include "usd_utils.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_collection.hh"
#include "BKE_geometry_set.hh"
#include "BKE_geometry_set_instances.hh"
#include "BKE_instances.hh"
#include "BKE_lib_id.hh"
#include "BKE_report.hh"
#include "BLI_math_euler.hh"
#include "BLI_math_matrix.hh"
#include "DNA_collection_types.h"
#include "DNA_layer_types.h"
#include "DNA_object_types.h"
#include <pxr/base/gf/quatf.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/vt/array.h>
#include <pxr/usd/usdGeom/pointInstancer.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include "IO_abstract_hierarchy_iterator.h"
namespace blender::io::usd {
USDPointInstancerWriter::USDPointInstancerWriter(
const USDExporterContext &ctx,
const Set<std::pair<pxr::SdfPath, Object *>> &prototype_paths,
std::unique_ptr<USDAbstractWriter> base_writer)
: USDAbstractWriter(ctx),
base_writer_(std::move(base_writer)),
prototype_paths_(prototype_paths)
{
}
void USDPointInstancerWriter::do_write(HierarchyContext &context)
{
/* Write the base data first (e.g., mesh, curves, points) */
if (base_writer_) {
base_writer_->write(context);
if (usd_export_context_.add_skel_mapping_fn &&
(usd_export_context_.export_params.export_armatures ||
usd_export_context_.export_params.export_shapekeys))
{
usd_export_context_.add_skel_mapping_fn(context.object, base_writer_->usd_path());
}
}
const pxr::UsdStageRefPtr stage = usd_export_context_.stage;
const Object *object_eval = context.object;
bke::GeometrySet instance_geometry_set = bke::object_get_evaluated_geometry_set(*object_eval);
const bke::GeometryComponent *component = instance_geometry_set.get_component(
bke::GeometryComponent::Type::Instance);
const bke::Instances *instances = static_cast<const bke::InstancesComponent &>(*component).get();
int instance_num = instances->instances_num();
const pxr::SdfPath &usd_path = usd_export_context_.usd_path;
const pxr::UsdGeomPointInstancer usd_instancer = pxr::UsdGeomPointInstancer::Define(stage,
usd_path);
const pxr::UsdTimeCode time = get_export_time_code();
Span<float4x4> transforms = instances->transforms();
BLI_assert(transforms.size() >= instance_num);
if (transforms.size() != instance_num) {
BKE_reportf(this->reports(),
RPT_ERROR,
"Instances number '%d' does not match transforms size '%d'",
instance_num,
int(transforms.size()));
return;
}
/* evaluated positions */
pxr::UsdAttribute position_attr = usd_instancer.CreatePositionsAttr();
pxr::VtArray<pxr::GfVec3f> positions(instance_num);
for (int i = 0; i < instance_num; i++) {
const float3 &pos = transforms[i].location();
positions[i] = pxr::GfVec3f(pos.x, pos.y, pos.z);
}
io::usd::set_attribute(position_attr, positions, time, usd_value_writer_);
/* orientations */
pxr::UsdAttribute orientations_attr = usd_instancer.CreateOrientationsAttr();
pxr::VtArray<pxr::GfQuath> orientation(instance_num);
for (int i = 0; i < instance_num; i++) {
const float3 euler = float3(math::to_euler(math::normalize(transforms[i])));
const math::Quaternion quat = math::to_quaternion(math::EulerXYZ(euler));
orientation[i] = pxr::GfQuath(quat.w, pxr::GfVec3h(quat.x, quat.y, quat.z));
}
io::usd::set_attribute(orientations_attr, orientation, time, usd_value_writer_);
/* scales */
pxr::UsdAttribute scales_attr = usd_instancer.CreateScalesAttr();
pxr::VtArray<pxr::GfVec3f> scales(instance_num);
for (int i = 0; i < instance_num; i++) {
const MatBase<float, 4, 4> &mat = transforms[i];
float3 scale_vec = math::to_scale<true>(mat);
scales[i] = pxr::GfVec3f(scale_vec.x, scale_vec.y, scale_vec.z);
}
io::usd::set_attribute(scales_attr, scales, time, usd_value_writer_);
/* IDs */
const Span<int> ids = instances->unique_ids();
pxr::VtInt64Array usd_ids(ids.begin(), ids.end());
pxr::UsdAttribute attr_ids = usd_instancer.CreateIdsAttr();
io::usd::set_attribute(attr_ids, usd_ids, time, usd_value_writer_);
/* other attr */
bke::AttributeAccessor attributes_eval = *component->attributes();
attributes_eval.foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
ELEM(iter.name, "instance_transform", "scale", "orientation", "mask", "proto_index", "id"))
{
return;
}
this->write_attribute_data(iter, usd_instancer, time);
});
/* prototypes relations */
const pxr::SdfPath protoParentPath = usd_path.AppendChild(pxr::TfToken("Prototypes"));
pxr::UsdPrim prototypesOver = stage->DefinePrim(protoParentPath);
pxr::SdfPathVector proto_wrapper_paths;
Map<std::string, int> proto_index_map;
Map<std::string, pxr::SdfPath> proto_path_map;
if (!prototype_paths_.is_empty() && usd_instancer) {
int iter = 0;
for (const std::pair<pxr::SdfPath, Object *> &entry : prototype_paths_) {
const pxr::SdfPath &source_path = entry.first;
Object *obj = entry.second;
if (source_path.IsEmpty()) {
continue;
}
const pxr::SdfPath proto_path = protoParentPath.AppendChild(
pxr::TfToken("Prototype_" + std::to_string(iter)));
pxr::UsdPrim prim = stage->DefinePrim(proto_path);
/* To avoid USD error of Unresolved reference prim path, make sure the referenced path
* exists. */
stage->DefinePrim(source_path);
prim.GetReferences().AddReference(pxr::SdfReference("", source_path));
proto_wrapper_paths.push_back(proto_path);
std::string ob_name = BKE_id_name(obj->id);
proto_index_map.add_new(ob_name, iter);
proto_path_map.add_new(ob_name, proto_path);
++iter;
}
usd_instancer.GetPrototypesRel().SetTargets(proto_wrapper_paths);
}
/* proto indices */
/* must be the last to populate */
pxr::UsdAttribute proto_indices_attr = usd_instancer.CreateProtoIndicesAttr();
pxr::VtArray<int> proto_indices;
Vector<std::pair<int, int>> collection_instance_object_count_map;
Span<int> reference_handles = instances->reference_handles();
Span<bke::InstanceReference> references = instances->references();
Map<std::string, int> final_proto_index_map;
for (int i = 0; i < instance_num; i++) {
bke::InstanceReference reference = references[reference_handles[i]];
process_instance_reference(reference,
i,
proto_index_map,
final_proto_index_map,
proto_path_map,
stage,
proto_indices,
collection_instance_object_count_map);
}
io::usd::set_attribute(proto_indices_attr, proto_indices, time, usd_value_writer_);
/* Handle Collection Prototypes */
if (!collection_instance_object_count_map.is_empty()) {
handle_collection_prototypes(
usd_instancer, time, instance_num, collection_instance_object_count_map);
}
/* Clean unused prototype. When finding prototype paths under the context of a point instancer,
* all the prototypes are collected, even those used by lower-level nested child PointInstancers.
* It can happen that different levels in nested PointInstancers share the same prototypes, but
* if not, we need to clean the extra prototypes from the prototype relationship for a cleaner
* USD export. */
compact_prototypes(usd_instancer, time, proto_wrapper_paths);
}
void USDPointInstancerWriter::process_instance_reference(
const bke::InstanceReference &reference,
int instance_index,
Map<std::string, int> &proto_index_map,
Map<std::string, int> &final_proto_index_map,
Map<std::string, pxr::SdfPath> &proto_path_map,
pxr::UsdStageRefPtr stage,
pxr::VtArray<int> &proto_indices,
Vector<std::pair<int, int>> &collection_instance_object_count_map)
{
/* TODO: Verify logic around the `add_overwrite` calls below. Using `add_new` will trigger
* asserts because multiple items are being added to the map with the same key. Original code
* was using std::map and repeatedly reassigning with `final_proto_index_map[ob_name] = ...` */
switch (reference.type()) {
case bke::InstanceReference::Type::Object: {
Object &object = reference.object();
std::string ob_name = BKE_id_name(object.id);
if (proto_index_map.contains(ob_name)) {
proto_indices.push_back(proto_index_map.lookup(ob_name));
final_proto_index_map.add_overwrite(ob_name, proto_index_map.lookup(ob_name));
/* If the reference is Object, clear prototype's local transform to identity to avoid
* double transforms. The PointInstancer will fully control instance placement. */
override_transform(stage, proto_path_map.lookup(ob_name), float4x4::identity());
}
break;
}
case bke::InstanceReference::Type::Collection: {
Collection &collection = reference.collection();
int object_num = 0;
FOREACH_COLLECTION_OBJECT_RECURSIVE_BEGIN (&collection, object) {
std::string ob_name = BKE_id_name(object->id);
if (proto_index_map.contains(ob_name)) {
object_num += 1;
proto_indices.push_back(proto_index_map.lookup(ob_name));
final_proto_index_map.add_overwrite(ob_name, proto_index_map.lookup(ob_name));
}
}
FOREACH_COLLECTION_OBJECT_RECURSIVE_END;
collection_instance_object_count_map.append(std::make_pair(instance_index, object_num));
break;
}
case bke::InstanceReference::Type::GeometrySet: {
bke::GeometrySet geometry_set = reference.geometry_set();
const StringRef set_name = geometry_set.name();
if (proto_index_map.contains_as(set_name)) {
proto_indices.push_back(proto_index_map.lookup_as(set_name));
final_proto_index_map.add_overwrite(set_name, proto_index_map.lookup_as(set_name));
}
Vector<const bke::GeometryComponent *> components = geometry_set.get_components();
for (const bke::GeometryComponent *comp : components) {
if (const bke::Instances *instances =
static_cast<const bke::InstancesComponent &>(*comp).get())
{
Span<int> ref_handles = instances->reference_handles();
Span<bke::InstanceReference> refs = instances->references();
/* If the top-level GeometrySet is not in proto_index_map, recursively traverse child
* InstanceReferences to resolve prototype indices. If the name matches proto_index_map,
* skip traversal to avoid duplicates, since GeometrySet names may overlap with object
* names. */
if (!proto_index_map.contains(set_name)) {
for (int index = 0; index < ref_handles.size(); ++index) {
const bke::InstanceReference &child_ref = refs[ref_handles[index]];
/* Recursively traverse nested GeometrySets to resolve prototype indices for all
* instances. */
process_instance_reference(child_ref,
instance_index,
proto_index_map,
final_proto_index_map,
proto_path_map,
stage,
proto_indices,
collection_instance_object_count_map);
}
}
/* If the reference is GeometrySet, then override the transform with the transform of the
* Instance inside this GeometrySet. */
Span<float4x4> transforms = instances->transforms();
if (transforms.size() == 1) {
if (proto_path_map.contains(set_name)) {
override_transform(stage, proto_path_map.lookup(set_name), transforms[0]);
}
}
}
}
break;
}
case bke::InstanceReference::Type::None:
default:
break;
}
}
void USDPointInstancerWriter::compact_prototypes(const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time,
const pxr::SdfPathVector &proto_paths) const
{
pxr::UsdAttribute proto_indices_attr = usd_instancer.GetProtoIndicesAttr();
pxr::VtArray<int> proto_indices;
if (!proto_indices_attr.Get(&proto_indices, time)) {
return;
}
/* Find actually used prototype indices. */
Set<int> used_proto_indices;
used_proto_indices.add_multiple(Span(proto_indices.cbegin(), proto_indices.size()));
Map<int, int> remap;
int new_index = 0;
for (int i = 0; i < proto_paths.size(); ++i) {
if (used_proto_indices.contains(i)) {
remap.add(i, new_index++);
}
}
/* Remap protoIndices. */
for (int &idx : proto_indices) {
idx = remap.lookup(idx);
}
proto_indices_attr.Set(proto_indices, time);
pxr::SdfPathVector compact_proto_paths;
for (int i = 0; i < proto_paths.size(); ++i) {
if (used_proto_indices.contains(i)) {
compact_proto_paths.push_back(proto_paths[i]);
}
}
usd_instancer.GetPrototypesRel().SetTargets(compact_proto_paths);
}
void USDPointInstancerWriter::override_transform(const pxr::UsdStageRefPtr stage,
const pxr::SdfPath &proto_path,
const float4x4 &transform) const
{
pxr::UsdPrim prim = stage->GetPrimAtPath(proto_path);
if (!prim) {
return;
}
/* Extract translation. */
const float3 &pos = transform.location();
pxr::GfVec3d override_position(pos.x, pos.y, pos.z);
/* Extract rotation. */
const float3 euler = float3(math::to_euler(math::normalize(transform)));
pxr::GfVec3f override_rotation(euler.x, euler.y, euler.z);
/* Extract scale. */
const float3 scale_vec = math::to_scale<true>(transform);
pxr::GfVec3f override_scale(scale_vec.x, scale_vec.y, scale_vec.z);
pxr::UsdGeomXformable xformable(prim);
xformable.ClearXformOpOrder();
xformable.AddTranslateOp().Set(override_position);
xformable.AddRotateXYZOp().Set(override_rotation);
xformable.AddScaleOp().Set(override_scale);
}
template<typename T>
static pxr::VtArray<T> DuplicateArray(const pxr::VtArray<T> &original, size_t copies)
{
pxr::VtArray<T> newArray;
size_t originalSize = original.size();
newArray.resize(originalSize * copies);
for (size_t i = 0; i < copies; ++i) {
std::copy(original.begin(), original.end(), newArray.begin() + i * originalSize);
}
return newArray;
}
template<typename T, typename GetterFunc, typename CreatorFunc>
static void DuplicatePerInstanceAttribute(const GetterFunc &getter,
const CreatorFunc &creator,
size_t copies,
const pxr::UsdTimeCode &time)
{
pxr::VtArray<T> values;
if (getter().Get(&values, time) && !values.empty()) {
auto newValues = DuplicateArray(values, copies);
creator().Set(newValues, time);
}
}
template<typename T, typename GetterFunc, typename CreatorFunc>
static void ExpandAttributePerInstance(const GetterFunc &getter,
const CreatorFunc &creator,
const Span<std::pair<int, int>> instance_object_map,
const pxr::UsdTimeCode &time)
{
/* MARK: Handle Collection Prototypes
* ----------------------------------
* In Blender, a Collection is not an actual Object type. When exporting, the iterator
* flattens the Collection hierarchy, treating each object inside the Collection as an
* individual prototype. However, all these prototypes share the same instance attributes
* (e.g., positions, orientations, scales).
*
* To ensure correct arrangement, reading, and drawing in OpenUSD, we need to explicitly
* duplicate the instance attributes across all prototypes derived from the Collection. */
pxr::VtArray<T> original_values;
if (!getter().Get(&original_values, time) || original_values.empty()) {
return;
}
pxr::VtArray<T> expanded_values;
for (const auto &[instance_index, object_count] : instance_object_map) {
if (instance_index < int(original_values.size())) {
for (int i = 0; i < object_count; ++i) {
expanded_values.push_back(original_values[instance_index]);
}
}
}
creator().Set(expanded_values, time);
}
void USDPointInstancerWriter::handle_collection_prototypes(
const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time,
const int instance_num,
const Span<std::pair<int, int>> collection_instance_object_count_map) const
{
/* Duplicate attributes. */
if (usd_instancer.GetPositionsAttr().HasAuthoredValue()) {
ExpandAttributePerInstance<pxr::GfVec3f>([&]() { return usd_instancer.GetPositionsAttr(); },
[&]() { return usd_instancer.CreatePositionsAttr(); },
collection_instance_object_count_map,
time);
}
if (usd_instancer.GetOrientationsAttr().HasAuthoredValue()) {
ExpandAttributePerInstance<pxr::GfQuath>(
[&]() { return usd_instancer.GetOrientationsAttr(); },
[&]() { return usd_instancer.CreateOrientationsAttr(); },
collection_instance_object_count_map,
time);
}
if (usd_instancer.GetScalesAttr().HasAuthoredValue()) {
ExpandAttributePerInstance<pxr::GfVec3f>([&]() { return usd_instancer.GetScalesAttr(); },
[&]() { return usd_instancer.CreateScalesAttr(); },
collection_instance_object_count_map,
time);
}
if (usd_instancer.GetVelocitiesAttr().HasAuthoredValue()) {
ExpandAttributePerInstance<pxr::GfVec3f>(
[&]() { return usd_instancer.GetVelocitiesAttr(); },
[&]() { return usd_instancer.CreateVelocitiesAttr(); },
collection_instance_object_count_map,
time);
}
if (usd_instancer.GetAngularVelocitiesAttr().HasAuthoredValue()) {
ExpandAttributePerInstance<pxr::GfVec3f>(
[&]() { return usd_instancer.GetAngularVelocitiesAttr(); },
[&]() { return usd_instancer.CreateAngularVelocitiesAttr(); },
collection_instance_object_count_map,
time);
}
/* Duplicate Primvars. */
const pxr::UsdGeomPrimvarsAPI primvars_api(usd_instancer);
for (const pxr::UsdGeomPrimvar &primvar : primvars_api.GetPrimvars()) {
if (!primvar.HasAuthoredValue()) {
continue;
}
const pxr::TfToken pv_name = primvar.GetPrimvarName();
const pxr::SdfValueTypeName pv_type = primvar.GetTypeName();
const pxr::TfToken pv_interp = primvar.GetInterpolation();
auto create = [&]() { return primvars_api.CreatePrimvar(pv_name, pv_type, pv_interp); };
if (pv_type == pxr::SdfValueTypeNames->FloatArray) {
ExpandAttributePerInstance<float>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->IntArray) {
ExpandAttributePerInstance<int>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->UCharArray) {
ExpandAttributePerInstance<uchar>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->Float2Array) {
ExpandAttributePerInstance<pxr::GfVec2f>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (ELEM(pv_type,
pxr::SdfValueTypeNames->Float3Array,
pxr::SdfValueTypeNames->Color3fArray,
pxr::SdfValueTypeNames->Color4fArray))
{
ExpandAttributePerInstance<pxr::GfVec3f>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->QuatfArray) {
ExpandAttributePerInstance<pxr::GfQuatf>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->BoolArray) {
ExpandAttributePerInstance<bool>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
else if (pv_type == pxr::SdfValueTypeNames->StringArray) {
ExpandAttributePerInstance<std::string>(
[&]() { return primvar; }, create, collection_instance_object_count_map, time);
}
}
/* MARK: Ensure Instance Indices Exist
* -----------------------------------
* If the PointInstancer has no authored instance indices, manually generate a default
* sequence of indices to ensure the PointInstancer functions correctly in OpenUSD.
* This guarantees that each instance can correctly reference its prototype. */
pxr::UsdAttribute proto_indices_attr = usd_instancer.GetProtoIndicesAttr();
if (!proto_indices_attr.HasAuthoredValue()) {
Vector<int> index;
for (int i = 0; i < prototype_paths_.size(); i++) {
index.append_n_times(i, instance_num);
}
proto_indices_attr.Set(pxr::VtArray<int>(index.begin(), index.end()));
}
}
void USDPointInstancerWriter::write_attribute_data(const bke::AttributeIter &attr,
const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time)
{
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(attr.data_type);
if (!pv_type) {
BKE_reportf(this->reports(),
RPT_WARNING,
"Attribute '%s' (Blender domain %d, type %d) cannot be converted to USD",
attr.name.c_str(),
int(attr.domain),
int(attr.data_type));
return;
}
const GVArray attribute = *attr.get();
if (attribute.is_empty()) {
return;
}
if (attr.name == "mask") {
pxr::UsdAttribute idsAttr = usd_instancer.GetIdsAttr();
if (!idsAttr) {
idsAttr = usd_instancer.CreateIdsAttr();
}
pxr::UsdAttribute invisibleIdsAttr = usd_instancer.GetInvisibleIdsAttr();
if (!invisibleIdsAttr) {
invisibleIdsAttr = usd_instancer.CreateInvisibleIdsAttr();
}
Vector<bool> mask_values(attribute.size());
attribute.materialize(IndexMask(attribute.size()), mask_values.data());
pxr::VtArray<int64_t> ids;
pxr::VtArray<int64_t> invisibleIds;
ids.reserve(mask_values.size());
for (int64_t i = 0; i < mask_values.size(); i++) {
ids.push_back(i);
if (!mask_values[i]) {
invisibleIds.push_back(i);
}
}
io::usd::set_attribute(idsAttr, ids, time, usd_value_writer_);
io::usd::set_attribute(invisibleIdsAttr, invisibleIds, time, usd_value_writer_);
}
const pxr::TfToken pv_name(
make_safe_primvar_name(attr.name, usd_export_context_.export_params.allow_unicode));
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_instancer);
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type);
copy_blender_attribute_to_primvar(attribute, attr.data_type, time, pv_attr, usd_value_writer_);
}
} /* namespace blender::io::usd */

View File

@@ -0,0 +1,63 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include "BLI_map.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
#include <pxr/usd/usdGeom/pointInstancer.h>
struct USDExporterContext;
namespace blender::io::usd {
class USDPointInstancerWriter final : public USDAbstractWriter {
private:
std::unique_ptr<USDAbstractWriter> base_writer_;
Set<std::pair<pxr::SdfPath, Object *>> prototype_paths_;
public:
USDPointInstancerWriter(const USDExporterContext &ctx,
const Set<std::pair<pxr::SdfPath, Object *>> &prototype_paths,
std::unique_ptr<USDAbstractWriter> base_writer);
~USDPointInstancerWriter() override = default;
protected:
void do_write(HierarchyContext &context) override;
private:
void write_attribute_data(const bke::AttributeIter &attr,
const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time);
void process_instance_reference(
const bke::InstanceReference &reference,
int instance_index,
Map<std::string, int> &proto_index_map,
Map<std::string, int> &final_proto_index_map,
Map<std::string, pxr::SdfPath> &proto_path_map,
pxr::UsdStageRefPtr stage,
pxr::VtArray<int> &proto_indices,
Vector<std::pair<int, int>> &collection_instance_object_count_map);
void compact_prototypes(const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time,
const pxr::SdfPathVector &proto_paths) const;
void override_transform(pxr::UsdStageRefPtr stage,
const pxr::SdfPath &proto_path,
const float4x4 &transform) const;
void handle_collection_prototypes(
const pxr::UsdGeomPointInstancer &usd_instancer,
const pxr::UsdTimeCode time,
int instance_num,
const Span<std::pair<int, int>> collection_instance_object_count_map) const;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_points.hh"
#include "usd_attribute_utils.hh"
#include "usd_utils.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_attribute.hh"
#include "BKE_report.hh"
#include "DNA_object_types.h"
#include "DNA_pointcloud_types.h"
#include <pxr/base/vt/array.h>
#include <pxr/usd/usdGeom/points.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
namespace blender::io::usd {
void USDPointsWriter::do_write(HierarchyContext &context)
{
const pxr::UsdStageRefPtr stage = usd_export_context_.stage;
const pxr::SdfPath &usd_path = usd_export_context_.usd_path;
const pxr::UsdTimeCode time = get_export_time_code();
const PointCloud *points = id_cast<const PointCloud *>(context.object->data);
Span<pxr::GfVec3f> positions = points->positions().cast<pxr::GfVec3f>();
VArray<float> radii = points->radius();
const pxr::UsdGeomPoints usd_points = pxr::UsdGeomPoints::Define(stage, usd_path);
pxr::VtArray<pxr::GfVec3f> usd_positions;
usd_positions.assign(positions.begin(), positions.end());
pxr::UsdAttribute attr_positions = usd_points.CreatePointsAttr(pxr::VtValue(), true);
if (!attr_positions.HasValue()) {
attr_positions.Set(usd_positions, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_positions, usd_positions, time);
if (!radii.is_empty()) {
pxr::VtArray<float> usd_widths;
usd_widths.resize(radii.size());
for (const int i : radii.index_range()) {
usd_widths[i] = radii[i] * 2.0f;
}
pxr::UsdAttribute attr_widths = usd_points.CreateWidthsAttr(pxr::VtValue(), true);
if (!attr_widths.HasValue()) {
attr_widths.Set(usd_widths, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_widths, usd_widths, time);
}
this->write_ids(points, usd_points, time);
this->write_velocities(points, usd_points, time);
this->write_custom_data(points, usd_points, time);
this->author_extent(usd_points, points->bounds_min_max(), time);
}
static std::optional<pxr::TfToken> convert_blender_domain_to_usd(
const bke::AttrDomain blender_domain)
{
switch (blender_domain) {
case bke::AttrDomain::Point:
return pxr::UsdGeomTokens->varying;
default:
return std::nullopt;
}
}
void USDPointsWriter::write_generic_data(const bke::AttributeIter &attr,
const pxr::UsdGeomPoints &usd_points,
const pxr::UsdTimeCode time)
{
const std::optional<pxr::TfToken> pv_interp = convert_blender_domain_to_usd(attr.domain);
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(attr.data_type);
if (!pv_interp || !pv_type) {
BKE_reportf(this->reports(),
RPT_WARNING,
"Attribute '%s' (Blender domain %d, type %d) cannot be converted to USD",
attr.name.c_str(),
int(attr.domain),
int(attr.data_type));
return;
}
const GVArray attribute = *attr.get();
if (attribute.is_empty()) {
return;
}
const pxr::TfToken pv_name(
make_safe_primvar_name(attr.name, usd_export_context_.export_params.allow_unicode));
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_points);
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type, *pv_interp);
copy_blender_attribute_to_primvar(attribute, attr.data_type, time, pv_attr, usd_value_writer_);
}
void USDPointsWriter::write_custom_data(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
const pxr::UsdTimeCode time)
{
const bke::AttributeAccessor attributes = points->attributes();
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
/* Skip "internal" Blender properties and attributes dealt with elsewhere. */
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
ELEM(iter.name, "position", "radius", "id", "velocity"))
{
return;
}
this->write_generic_data(iter, usd_points, time);
});
}
void USDPointsWriter::write_ids(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
const pxr::UsdTimeCode time)
{
const VArraySpan ids = *points->attributes().lookup<int>("id", bke::AttrDomain::Point);
if (ids.is_empty()) {
return;
}
pxr::VtInt64Array usd_ids(ids.begin(), ids.end());
pxr::UsdAttribute attr_ids = usd_points.CreateIdsAttr(pxr::VtValue(), true);
set_attribute(attr_ids, usd_ids, time, usd_value_writer_);
}
void USDPointsWriter::write_velocities(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
const pxr::UsdTimeCode time)
{
const VArraySpan velocity = *points->attributes().lookup<float3>("velocity",
bke::AttrDomain::Point);
if (velocity.is_empty()) {
return;
}
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
pxr::VtArray<pxr::GfVec3f> usd_velocities;
usd_velocities.assign(data.begin(), data.end());
pxr::UsdAttribute attr_vel = usd_points.CreateVelocitiesAttr(pxr::VtValue(), true);
if (!attr_vel.HasValue()) {
attr_vel.Set(usd_velocities, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_vel, usd_velocities, time);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include <pxr/usd/usdGeom/points.h>
namespace blender {
struct Main;
struct PointCloud;
namespace bke {
class AttributeIter;
} // namespace bke
namespace io::usd {
/* Writer for USD points. */
class USDPointsWriter final : public USDAbstractWriter {
public:
USDPointsWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
~USDPointsWriter() final = default;
protected:
void do_write(HierarchyContext &context) override;
private:
void write_generic_data(const bke::AttributeIter &attr,
const pxr::UsdGeomPoints &usd_points,
pxr::UsdTimeCode time);
void write_custom_data(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
pxr::UsdTimeCode time);
void write_ids(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
pxr::UsdTimeCode time);
void write_velocities(const PointCloud *points,
const pxr::UsdGeomPoints &usd_points,
pxr::UsdTimeCode time);
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_text.hh"
#include "usd_exporter_context.hh"
#include "BKE_lib_id.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "DNA_mesh_types.h"
namespace blender::io::usd {
USDTextWriter::USDTextWriter(const USDExporterContext &ctx) : USDGenericMeshWriter(ctx) {}
Mesh *USDTextWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
Mesh *mesh_eval = BKE_object_get_evaluated_mesh(object_eval);
if (mesh_eval != nullptr) {
/* Mesh_eval only exists when generative modifiers are in use. */
r_needsfree = false;
return mesh_eval;
}
r_needsfree = true;
return BKE_mesh_new_from_object(usd_export_context_.depsgraph, object_eval, false, false, true);
}
void USDTextWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_mesh.hh"
namespace blender {
struct Mesh;
struct Object;
namespace io::usd {
class USDTextWriter : public USDGenericMeshWriter {
public:
USDTextWriter(const USDExporterContext &ctx);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
void free_export_mesh(Mesh *mesh) override;
};
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,216 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_transform.hh"
#include "usd_hierarchy_iterator.hh"
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/usd/usdGeom/xform.h>
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_quaternion.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_rotation.h"
#include "BLI_vector.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.usd"};
namespace io::usd {
USDTransformWriter::USDTransformWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
pxr::UsdGeomXformable USDTransformWriter::create_xformable() const
{
pxr::UsdGeomXform xform;
/* If prim exists, cast to #UsdGeomXform
* (Solves merge transform and shape issue for animated exports). */
pxr::UsdPrim existing_prim = usd_export_context_.stage->GetPrimAtPath(
usd_export_context_.usd_path);
if (existing_prim.IsValid() && existing_prim.IsA<pxr::UsdGeomXform>()) {
xform = pxr::UsdGeomXform(existing_prim);
}
else {
xform = pxr::UsdGeomXform::Define(usd_export_context_.stage, usd_export_context_.usd_path);
}
return pxr::UsdGeomXformable(xform.GetPrim());
}
bool USDTransformWriter::should_apply_root_xform(const HierarchyContext &context) const
{
if (!(usd_export_context_.export_params.convert_orientation ||
usd_export_context_.export_params.convert_scene_units != SceneUnits::Meters))
{
return false;
}
if (!usd_export_context_.export_params.root_prim_path.empty()) {
return false;
}
if (context.export_parent != nullptr) {
return false;
}
return true;
}
void USDTransformWriter::do_write(HierarchyContext &context)
{
if (context.is_point_proto || context.is_point_instance) {
return;
}
pxr::UsdGeomXformable xform = create_xformable();
if (!xform) {
CLOG_ERROR(&LOG, "USDTransformWriter: couldn't create xformable");
return;
}
float4x4 parent_relative_matrix; /* The object matrix relative to the parent. */
if (should_apply_root_xform(context)) {
float4x4 matrix_world = context.matrix_world;
if (usd_export_context_.export_params.convert_orientation) {
float3x3 mrot;
float4x4 mat = float4x4::identity();
mat3_from_axis_conversion(IO_AXIS_Y,
IO_AXIS_Z,
usd_export_context_.export_params.forward_axis,
usd_export_context_.export_params.up_axis,
mrot.ptr());
mat.view<3, 3>() = math::transpose(mrot);
matrix_world = mat * context.matrix_world;
}
if (usd_export_context_.export_params.convert_scene_units != SceneUnits::Meters) {
const float3 scale = float3(1.0 / get_meters_per_unit(usd_export_context_.export_params));
const float4x4 mat_scale = math::from_scale<float4x4>(scale);
matrix_world = mat_scale * matrix_world;
}
parent_relative_matrix = context.parent_matrix_inv_world * matrix_world;
}
else {
parent_relative_matrix = context.parent_matrix_inv_world * context.matrix_world;
}
/* USD Xforms are by default the identity transform; only write if necessary when static. */
if (is_animated_ || !math::is_equal(parent_relative_matrix, float4x4::identity(), 0.000000001f))
{
set_xform_ops(parent_relative_matrix, xform);
}
if (usd_export_context_.export_params.use_instancing && context.is_instance()) {
mark_as_instance(context, xform.GetPrim());
}
if (context.object) {
auto prim = xform.GetPrim();
add_to_prim_map(prim.GetPath(), &context.object->id);
write_id_properties(prim, context.object->id, get_export_time_code());
}
}
bool USDTransformWriter::check_is_animated(const HierarchyContext &context) const
{
if (context.duplicator != nullptr) {
/* This object is being duplicated, so could be emitted by a particle system and thus
* influenced by forces. TODO(Sybren): Make this more strict. Probably better to get from the
* depsgraph whether this object instance has a time source. */
return true;
}
if (check_has_physics(context)) {
return true;
}
return BKE_object_moves_in_time(context.object, context.animation_check_include_parent);
}
void USDTransformWriter::set_xform_ops(const float4x4 &parent_relative_matrix,
const pxr::UsdGeomXformable &xf)
{
if (!xf) {
return;
}
XformOpMode xfOpMode = usd_export_context_.export_params.xform_op_mode;
if (xformOps_.is_empty()) {
switch (xfOpMode) {
case XformOpMode::TRS:
xformOps_.append(xf.AddTranslateOp());
xformOps_.append(xf.AddRotateXYZOp());
xformOps_.append(xf.AddScaleOp());
break;
case XformOpMode::TOS:
xformOps_.append(xf.AddTranslateOp());
xformOps_.append(xf.AddOrientOp());
xformOps_.append(xf.AddScaleOp());
break;
case XformOpMode::MAT:
xformOps_.append(xf.AddTransformOp());
break;
default:
CLOG_WARN(&LOG, "Unknown XformOp type");
xformOps_.append(xf.AddTransformOp());
break;
}
}
if (xformOps_.is_empty()) {
/* Shouldn't happen. */
return;
}
pxr::UsdTimeCode time_code = get_export_time_code();
if (xformOps_.size() == 1) {
pxr::GfMatrix4d mat_val(parent_relative_matrix.ptr());
usd_value_writer_.SetAttribute(xformOps_[0].GetAttr(), mat_val, time_code);
}
else if (xformOps_.size() == 3) {
float3 loc;
math::Quaternion rot;
float3 scale;
math::to_loc_rot_scale<true>(parent_relative_matrix, loc, rot, scale);
if (xfOpMode == XformOpMode::TRS) {
pxr::GfVec3d loc_val(loc.x, loc.y, loc.z);
usd_value_writer_.SetAttribute(xformOps_[0].GetAttr(), loc_val, time_code);
const math::EulerXYZ eul = math::to_euler(rot);
pxr::GfVec3f rot_val(eul.x().degree(), eul.y().degree(), eul.z().degree());
usd_value_writer_.SetAttribute(xformOps_[1].GetAttr(), rot_val, time_code);
pxr::GfVec3f scale_val(scale.x, scale.y, scale.z);
usd_value_writer_.SetAttribute(xformOps_[2].GetAttr(), scale_val, time_code);
}
else if (xfOpMode == XformOpMode::TOS) {
pxr::GfVec3d loc_val(loc.x, loc.y, loc.z);
usd_value_writer_.SetAttribute(xformOps_[0].GetAttr(), loc_val, time_code);
pxr::GfQuatf quat_val(rot.w, rot.x, rot.y, rot.z);
usd_value_writer_.SetAttribute(xformOps_[1].GetAttr(), quat_val, time_code);
pxr::GfVec3f scale_val(scale.x, scale.y, scale.z);
usd_value_writer_.SetAttribute(xformOps_[2].GetAttr(), scale_val, time_code);
}
}
}
} // namespace io::usd
} // namespace blender

View File

@@ -0,0 +1,32 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_writer_abstract.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_vector.hh"
#include <pxr/usd/usdGeom/xformable.h>
namespace blender::io::usd {
class USDTransformWriter : public USDAbstractWriter {
private:
Vector<pxr::UsdGeomXformOp> xformOps_;
public:
USDTransformWriter(const USDExporterContext &ctx);
protected:
void do_write(HierarchyContext &context) override;
bool check_is_animated(const HierarchyContext &context) const override;
bool should_apply_root_xform(const HierarchyContext &context) const;
void set_xform_ops(const float4x4 &parent_relative_matrix, const pxr::UsdGeomXformable &xf);
/* Subclasses may override this to create prims other than UsdGeomXform. */
virtual pxr::UsdGeomXformable create_xformable() const;
};
} // namespace blender::io::usd

View File

@@ -0,0 +1,226 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_writer_volume.hh"
#include "usd_hierarchy_iterator.hh"
#include "usd_utils.hh"
#include <pxr/base/tf/pathUtils.h>
#include <pxr/base/vt/value.h>
#include <pxr/usd/usdVol/openVDBAsset.h>
#include <pxr/usd/usdVol/volume.h>
#include "DNA_scene_types.h"
#include "DNA_volume_types.h"
#include "BKE_report.hh"
#include "BKE_volume.hh"
#include "BLI_fileops.h"
#include "BLI_index_range.hh"
#include "BLI_math_base.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "DEG_depsgraph_query.hh"
namespace blender::io::usd {
static bool has_varying_modifiers(const Object *ob)
{
/* These modifiers may vary the Volume either over time or by deformation/transformation. */
ModifierData *md = static_cast<ModifierData *>(ob->modifiers.first);
while (md) {
if (ELEM(md->type,
eModifierType_Nodes,
eModifierType_VolumeDisplace,
eModifierType_MeshToVolume))
{
return true;
}
md = md->next;
}
return false;
}
USDVolumeWriter::USDVolumeWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
bool USDVolumeWriter::check_is_animated(const HierarchyContext &context) const
{
const Volume *volume = id_cast<Volume *>(context.object->data);
return volume->is_sequence || has_varying_modifiers(context.object);
}
void USDVolumeWriter::do_write(HierarchyContext &context)
{
Volume *volume = id_cast<Volume *>(context.object->data);
if (!BKE_volume_load(volume, usd_export_context_.bmain)) {
return;
}
const int num_grids = BKE_volume_num_grids(volume);
if (!num_grids) {
return;
}
const bool has_modifiers = has_varying_modifiers(context.object);
auto vdb_file_path = resolve_vdb_file(volume, has_modifiers);
if (!vdb_file_path.has_value()) {
BKE_reportf(reports(),
RPT_WARNING,
"USD Export: failed to resolve .vdb file for object: %s",
volume->id.name + 2);
return;
}
if (usd_export_context_.export_params.relative_paths) {
if (auto relative_vdb_file_path = construct_vdb_relative_file_path(*vdb_file_path)) {
vdb_file_path = relative_vdb_file_path;
}
else {
BKE_reportf(reports(),
RPT_WARNING,
"USD Export: couldn't construct relative file path for .vdb file, absolute path "
"will be used instead");
}
}
const pxr::UsdTimeCode time = get_export_time_code();
const pxr::SdfPath &volume_path = usd_export_context_.usd_path;
pxr::UsdStageRefPtr stage = usd_export_context_.stage;
pxr::UsdVolVolume usd_volume = pxr::UsdVolVolume::Define(stage, volume_path);
for (const int i : IndexRange(num_grids)) {
const bke::VolumeGridData *grid = BKE_volume_grid_get(volume, i);
const std::string grid_name = bke::volume_grid::get_name(*grid);
const std::string grid_id = make_safe_name(grid_name,
usd_export_context_.export_params.allow_unicode);
const pxr::SdfPath grid_path = volume_path.AppendPath(pxr::SdfPath(grid_id));
pxr::UsdVolOpenVDBAsset usd_grid = pxr::UsdVolOpenVDBAsset::Define(stage, grid_path);
pxr::TfToken grid_name_token = pxr::TfToken(grid_name);
pxr::SdfAssetPath asset_path = pxr::SdfAssetPath(*vdb_file_path);
pxr::UsdAttribute attr_field = usd_grid.CreateFieldNameAttr(pxr::VtValue(), true);
pxr::UsdAttribute attr_file = usd_grid.CreateFilePathAttr(pxr::VtValue(), true);
if (!attr_field.HasValue()) {
attr_field.Set(grid_name_token, pxr::UsdTimeCode::Default());
}
if (!attr_file.HasValue()) {
attr_file.Set(asset_path, pxr::UsdTimeCode::Default());
}
usd_value_writer_.SetAttribute(attr_field, grid_name_token, time);
usd_value_writer_.SetAttribute(attr_file, asset_path, time);
usd_volume.CreateFieldRelationship(pxr::TfToken(grid_id), grid_path);
}
this->author_extent(usd_volume, BKE_volume_min_max(volume), time);
BKE_volume_unload(volume);
}
std::optional<std::string> USDVolumeWriter::resolve_vdb_file(const Volume *volume,
bool has_modifiers) const
{
std::optional<std::string> vdb_file_path;
const bool needs_vdb_save = volume->filepath[0] == '\0' || has_modifiers;
if (needs_vdb_save) {
/* Entering this section means that the Volume object contains OpenVDB data that is not
* obtained solely from external `.vdb` files but is generated or modified inside of Blender.
* Write this data as a new `.vdb` files. */
vdb_file_path = construct_vdb_file_path(volume);
if (!BKE_volume_save(
volume, usd_export_context_.bmain, nullptr, vdb_file_path.value_or("").c_str()))
{
return std::nullopt;
}
}
if (!vdb_file_path.has_value()) {
vdb_file_path = BKE_volume_grids_frame_filepath(volume);
if (vdb_file_path->empty()) {
return std::nullopt;
}
}
return vdb_file_path;
}
std::optional<std::string> USDVolumeWriter::construct_vdb_file_path(const Volume *volume) const
{
const std::string usd_file_path = get_export_file_path();
if (usd_file_path.empty()) {
return std::nullopt;
}
char usd_directory_path[FILE_MAX];
char usd_file_name[FILE_MAXFILE];
BLI_path_split_dir_file(usd_file_path.c_str(),
usd_directory_path,
sizeof(usd_directory_path),
usd_file_name,
sizeof(usd_file_name));
if (usd_directory_path[0] == '\0' || usd_file_name[0] == '\0') {
return std::nullopt;
}
const char *vdb_directory_name = "volumes";
char vdb_directory_path[FILE_MAX];
STRNCPY(vdb_directory_path, usd_directory_path);
BLI_strncat(vdb_directory_path, vdb_directory_name, sizeof(vdb_directory_path));
BLI_dir_create_recursive(vdb_directory_path);
const Scene *scene = DEG_get_input_scene(usd_export_context_.depsgraph);
const int max_frame_digits = std::max(2, integer_digits_i(abs(scene->r.efra)));
char vdb_file_name[FILE_MAXFILE];
STRNCPY(vdb_file_name, volume->id.name + 2);
const pxr::UsdTimeCode time = get_export_time_code();
if (!time.IsDefault()) {
const int frame = int(time.GetValue());
BLI_path_frame(vdb_file_name, sizeof(vdb_file_name), frame, max_frame_digits);
}
BLI_strncat(vdb_file_name, ".vdb", sizeof(vdb_file_name));
char vdb_file_path[FILE_MAX];
BLI_path_join(vdb_file_path, sizeof(vdb_file_path), vdb_directory_path, vdb_file_name);
return vdb_file_path;
}
std::optional<std::string> USDVolumeWriter::construct_vdb_relative_file_path(
const std::string &vdb_file_path) const
{
const std::string usd_file_path = get_export_file_path();
if (usd_file_path.empty()) {
return std::nullopt;
}
char relative_path[FILE_MAX];
STRNCPY(relative_path, vdb_file_path.c_str());
BLI_path_rel(relative_path, usd_file_path.c_str());
if (!BLI_path_is_rel(relative_path)) {
return std::nullopt;
}
/* Following code was written with an assumption that Blender's relative paths start with
* `//` characters as well as have OS dependent slashes. Inside of USD files those relative
* paths should start with either `./` or `../` characters and have always forward slashes (`/`)
* separating directories. This is the convention used in USD documentation (and it seems
* to be used in other DCC packages as well). */
std::string relative_path_processed = pxr::TfNormPath(relative_path + 2);
if (relative_path_processed[0] != '.') {
relative_path_processed.insert(0, "./");
}
return relative_path_processed;
}
} // namespace blender::io::usd

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "usd_writer_abstract.hh"
namespace blender {
struct Volume;
namespace io::usd {
/* Writer for writing OpenVDB assets to UsdVolVolume. Volume data is stored in separate `.vdb`
* files which are referenced in USD file. */
class USDVolumeWriter : public USDAbstractWriter {
public:
USDVolumeWriter(const USDExporterContext &ctx);
protected:
bool check_is_animated(const HierarchyContext &context) const override;
void do_write(HierarchyContext &context) override;
private:
/* Try to ensure that external `.vdb` file is available for USD to be referenced. Blender can
* either reference external OpenVDB data or generate such data internally. Latter option will
* mean that `resolve_vdb_file` method will try to export volume data to a new `.vdb` file.
* If successful, this method returns absolute file path to the resolved `.vdb` file, if not,
* returns `std::nullopt`. */
std::optional<std::string> resolve_vdb_file(const Volume *volume, bool has_modifiers) const;
std::optional<std::string> construct_vdb_file_path(const Volume *volume) const;
std::optional<std::string> construct_vdb_relative_file_path(
const std::string &vdb_file_path) const;
};
} // namespace io::usd
} // namespace blender