USD: Import and export custom properties
Adding support for converting between Blender custom properties and USD user-defined custom attributes. Custom attributes on Xforms, many data types, and materials are all supported for round-tripping. Please see the USD attributes documentation for more information on custom attributes. Properties are exported with a userProperties: namespace for simple filtering in external apps. This namespace is stripped on import, but other namespace are allowed to persist. An "Import Attributes" parameter has been added with options "None" (do not import attributes), "User" (import attributes in the 'userProperties' namespace only), "All custom" (import all USD custom attributes, the default). An "Export Custom Properties" export option has been added. The property conversion code handles float, double, string and bool types, as well as tuples of size 2, 3 and 4. Note that USD quaternions and arrays of arbitrary length are not yet supported. There is currently no attempt to set the Blender property subtype based on the USD type "role" (e.g., specifying Color or XYZ vector subtypes). This can be addressed in future work. In addition to exporting custom properties, the original Blender object and data names are now saved as USD custom string attributes "userProperties:blender:object_name" and "userProperties:blender:data_name", respectively, on the corresponding USD prims. This feature is enabled with the "Author Blender Name" export option. If a Blender custom string property is named "displayName", it's handled in a special way on export in that its value is used to set the USD prim's "displayName" metadata. Co-authored-by: kiki <charles@skeletalstudios.com> Co-authored-by: Michael Kowalski <makowalski@nvidia.com> Co-authored-by: Charles Wardlaw <kattkieru@users.noreply.github.com> Pull Request: https://projects.blender.org/blender/blender/pulls/118938
This commit is contained in:
committed by
Jesse Yurkovich
parent
37da07e51a
commit
60bc34b494
@@ -74,6 +74,24 @@ const EnumPropertyItem rna_enum_usd_mtl_name_collision_mode_items[] = {
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
const EnumPropertyItem rna_enum_usd_attr_import_mode_items[] = {
|
||||
{USD_ATTR_IMPORT_NONE, "NONE", 0, "None", "Do not import attributes"},
|
||||
{USD_ATTR_IMPORT_USER,
|
||||
"USER",
|
||||
0,
|
||||
"User",
|
||||
"Import attributes in the 'userProperties' namespace as "
|
||||
"Blender custom properties. The namespace will "
|
||||
"be stripped from the property names"},
|
||||
{USD_ATTR_IMPORT_ALL,
|
||||
"ALL",
|
||||
0,
|
||||
"All Custom",
|
||||
"Import all USD custom attributes as Blender custom properties. "
|
||||
"Namespaces will be retained in the property names"},
|
||||
{0, nullptr, 0, nullptr, nullptr},
|
||||
};
|
||||
|
||||
const EnumPropertyItem rna_enum_usd_tex_import_mode_items[] = {
|
||||
{USD_TEX_IMPORT_NONE, "IMPORT_NONE", 0, "None", "Don't import textures"},
|
||||
{USD_TEX_IMPORT_PACK, "IMPORT_PACK", 0, "Packed", "Import textures as packed data"},
|
||||
@@ -189,6 +207,9 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
|
||||
const bool export_shapekeys = RNA_boolean_get(op->ptr, "export_shapekeys");
|
||||
const bool only_deform_bones = RNA_boolean_get(op->ptr, "only_deform_bones");
|
||||
|
||||
const bool export_custom_properties = RNA_boolean_get(op->ptr, "export_custom_properties");
|
||||
const bool author_blender_name = RNA_boolean_get(op->ptr, "author_blender_name");
|
||||
|
||||
char root_prim_path[FILE_MAX];
|
||||
RNA_string_get(op->ptr, "root_prim_path", root_prim_path);
|
||||
process_prim_path(root_prim_path);
|
||||
@@ -212,6 +233,8 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
|
||||
export_textures,
|
||||
overwrite_textures,
|
||||
relative_paths,
|
||||
export_custom_properties,
|
||||
author_blender_name,
|
||||
};
|
||||
|
||||
STRNCPY(params.root_prim_path, root_prim_path);
|
||||
@@ -238,6 +261,13 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op)
|
||||
uiItemR(col, ptr, "visible_objects_only", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
}
|
||||
|
||||
col = uiLayoutColumn(box, true);
|
||||
uiItemR(col, ptr, "export_custom_properties", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
|
||||
col = uiLayoutColumn(box, true);
|
||||
uiItemR(col, ptr, "author_blender_name", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiLayoutSetActive(col, RNA_boolean_get(op->ptr, "export_custom_properties"));
|
||||
|
||||
col = uiLayoutColumn(box, true);
|
||||
uiItemR(col, ptr, "export_animation", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(col, ptr, "export_hair", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
@@ -280,8 +310,8 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op)
|
||||
uiItemR(col, ptr, "relative_paths", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
|
||||
box = uiLayoutBox(layout);
|
||||
uiItemL(box, IFACE_("Experimental"), ICON_NONE);
|
||||
uiItemR(box, ptr, "use_instancing", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
col = uiLayoutColumnWithHeading(box, true, IFACE_("Experimental"));
|
||||
uiItemR(col, ptr, "use_instancing", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
}
|
||||
|
||||
static void free_operator_customdata(wmOperator *op)
|
||||
@@ -453,6 +483,18 @@ void WM_OT_usd_export(wmOperatorType *ot)
|
||||
"Root Prim",
|
||||
"If set, add a transform primitive with the given path to the stage "
|
||||
"as the parent of all exported data");
|
||||
|
||||
RNA_def_boolean(ot->srna,
|
||||
"export_custom_properties",
|
||||
true,
|
||||
"Export Custom Properties",
|
||||
"When checked, custom properties will be exported as USD User Properties");
|
||||
|
||||
RNA_def_boolean(ot->srna,
|
||||
"author_blender_name",
|
||||
true,
|
||||
"Author Blender Name",
|
||||
"When checked, custom userProperties will be authored to allow a round trip");
|
||||
}
|
||||
|
||||
/* ====== USD Import ====== */
|
||||
@@ -533,6 +575,9 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
|
||||
const eUSDMtlNameCollisionMode mtl_name_collision_mode = eUSDMtlNameCollisionMode(
|
||||
RNA_enum_get(op->ptr, "mtl_name_collision_mode"));
|
||||
|
||||
const eUSDAttrImportMode attr_import_mode = eUSDAttrImportMode(
|
||||
RNA_enum_get(op->ptr, "attr_import_mode"));
|
||||
|
||||
/* TODO(makowalski): Add support for sequences. */
|
||||
const bool is_sequence = false;
|
||||
int offset = 0;
|
||||
@@ -589,6 +634,7 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op)
|
||||
params.import_textures_mode = import_textures_mode;
|
||||
params.tex_name_collision_mode = tex_name_collision_mode;
|
||||
params.import_all_materials = import_all_materials;
|
||||
params.attr_import_mode = attr_import_mode;
|
||||
|
||||
STRNCPY(params.import_textures_dir, import_textures_dir);
|
||||
|
||||
@@ -641,6 +687,7 @@ static void wm_usd_import_draw(bContext * /*C*/, wmOperator *op)
|
||||
uiItemR(col, ptr, "relative_path", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(col, ptr, "create_collection", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(box, ptr, "light_intensity_scale", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(col, ptr, "attr_import_mode", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
|
||||
box = uiLayoutBox(layout);
|
||||
col = uiLayoutColumnWithHeading(box, true, IFACE_("Materials"));
|
||||
@@ -830,6 +877,13 @@ void WM_OT_usd_import(wmOperatorType *ot)
|
||||
USD_TEX_NAME_COLLISION_USE_EXISTING,
|
||||
"File Name Collision",
|
||||
"Behavior when the name of an imported texture file conflicts with an existing file");
|
||||
|
||||
RNA_def_enum(ot->srna,
|
||||
"attr_import_mode",
|
||||
rna_enum_usd_attr_import_mode_items,
|
||||
USD_ATTR_IMPORT_ALL,
|
||||
"Import Custom Properties",
|
||||
"Behavior when importing USD attributes as Blender custom properties");
|
||||
}
|
||||
|
||||
namespace blender::ed::io {
|
||||
|
||||
@@ -120,6 +120,7 @@ set(SRC
|
||||
intern/usd_reader_stage.cc
|
||||
intern/usd_reader_volume.cc
|
||||
intern/usd_reader_xform.cc
|
||||
intern/usd_reader_utils.cc
|
||||
intern/usd_skel_convert.cc
|
||||
intern/usd_skel_root_utils.cc
|
||||
|
||||
@@ -160,6 +161,7 @@ set(SRC
|
||||
intern/usd_reader_stage.hh
|
||||
intern/usd_reader_volume.hh
|
||||
intern/usd_reader_xform.hh
|
||||
intern/usd_reader_utils.hh
|
||||
intern/usd_skel_convert.hh
|
||||
intern/usd_skel_root_utils.hh
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "usd_reader_material.hh"
|
||||
#include "usd_reader_utils.hh"
|
||||
|
||||
#include "usd_asset_utils.hh"
|
||||
|
||||
@@ -493,6 +494,9 @@ Material *USDMaterialReader::add_material(const pxr::UsdShadeMaterial &usd_mater
|
||||
}
|
||||
}
|
||||
|
||||
/* Load custom properties directly from the Material's prim. */
|
||||
set_id_props_from_prim(&mtl->id, usd_material.GetPrim());
|
||||
|
||||
return mtl;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,52 @@
|
||||
* 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"
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
void USDPrimReader::set_props(const bool merge_with_parent,
|
||||
const pxr::UsdTimeCode motionSampleTime)
|
||||
{
|
||||
if (!prim_ || !object_) {
|
||||
return;
|
||||
}
|
||||
|
||||
eUSDAttrImportMode attr_import_mode = this->import_params_.attr_import_mode;
|
||||
|
||||
if (attr_import_mode == USD_ATTR_IMPORT_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, attr_import_mode, motionSampleTime);
|
||||
}
|
||||
}
|
||||
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_, attr_import_mode, motionSampleTime);
|
||||
}
|
||||
|
||||
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(
|
||||
static_cast<ID *>(object_->data), prim_, attr_import_mode, motionSampleTime);
|
||||
}
|
||||
}
|
||||
|
||||
USDPrimReader::USDPrimReader(const pxr::UsdPrim &prim,
|
||||
const USDImportParams &import_params,
|
||||
const ImportSettings &settings)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
struct CacheFile;
|
||||
@@ -161,6 +162,28 @@ class USDPrimReader {
|
||||
}
|
||||
|
||||
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 motionSampleTime: The time code for sampling tha USD attributes
|
||||
*/
|
||||
void set_props(bool merge_with_parent = false,
|
||||
pxr::UsdTimeCode motionSampleTime = pxr::UsdTimeCode::Default());
|
||||
};
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
290
source/blender/io/usd/intern/usd_reader_utils.cc
Normal file
290
source/blender/io/usd/intern/usd_reader_utils.cc
Normal file
@@ -0,0 +1,290 @@
|
||||
/* SPDX-FileCopyrightText: 2024 NVIDIA Corporation. All rights reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "usd_reader_utils.hh"
|
||||
|
||||
#include <pxr/usd/usd/attribute.h>
|
||||
|
||||
#include "CLG_log.h"
|
||||
static CLG_LogRef LOG = {"io.usd"};
|
||||
|
||||
namespace {
|
||||
|
||||
template<typename VECT>
|
||||
void set_array_prop(IDProperty *idgroup,
|
||||
const char *prop_name,
|
||||
const pxr::UsdAttribute &attr,
|
||||
const pxr::UsdTimeCode motionSampleTime)
|
||||
{
|
||||
if (!idgroup || !attr) {
|
||||
return;
|
||||
}
|
||||
|
||||
VECT vec;
|
||||
if (!attr.Get<VECT>(&vec, motionSampleTime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IDPropertyTemplate val = {0};
|
||||
val.array.len = static_cast<int>(vec.dimension);
|
||||
|
||||
if (val.array.len <= 0) {
|
||||
CLOG_WARN(&LOG, "Invalid array length for prop %s", prop_name);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
IDProperty *prop = IDP_New(IDP_ARRAY, &val, prop_name);
|
||||
|
||||
if (!prop) {
|
||||
CLOG_WARN(&LOG, "Couldn't create array prop %s", prop_name);
|
||||
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 blender::io::usd {
|
||||
|
||||
/* TfToken objects are not cheap to construct, so we do it once. */
|
||||
namespace usdtokens {
|
||||
static const pxr::TfToken userProperties("userProperties", pxr::TfToken::Immortal);
|
||||
} // namespace usdtokens
|
||||
|
||||
static void set_string_prop(IDProperty *idgroup, const char *prop_name, const char *str_val)
|
||||
{
|
||||
if (!idgroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
IDPropertyTemplate val = {0};
|
||||
val.string.str = str_val;
|
||||
/* Note length includes null terminator. */
|
||||
val.string.len = strlen(str_val) + 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 char *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 char *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 char *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 char *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);
|
||||
}
|
||||
|
||||
void set_id_props_from_prim(ID *id,
|
||||
const pxr::UsdPrim &prim,
|
||||
const eUSDAttrImportMode attr_import_mode,
|
||||
const pxr::UsdTimeCode time_code)
|
||||
{
|
||||
pxr::UsdAttributeVector attribs = prim.GetAuthoredAttributes();
|
||||
if (attribs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool all_custom_attrs = (attr_import_mode == USD_ATTR_IMPORT_ALL);
|
||||
|
||||
for (const pxr::UsdAttribute &attr : attribs) {
|
||||
if (!attr.IsCustom()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> attr_names = attr.SplitName();
|
||||
|
||||
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 std::string token = "userProperties:";
|
||||
const std::string name = attr.GetName().GetString();
|
||||
attr_name = pxr::TfToken(name.substr(token.size(), name.size() - 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().c_str(), 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().c_str(), 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().c_str(), 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().c_str(), 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().c_str(), sval.c_str());
|
||||
}
|
||||
}
|
||||
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().c_str(), tval.GetString().c_str());
|
||||
}
|
||||
}
|
||||
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().c_str(), aval.GetAssetPath().c_str());
|
||||
}
|
||||
}
|
||||
else if (type_name == pxr::SdfValueTypeNames->Bool) {
|
||||
bool bval = false;
|
||||
if (attr.Get<bool>(&bval, time_code)) {
|
||||
set_bool_prop(idgroup, attr_name.GetString().c_str(), bval);
|
||||
}
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float2)) {
|
||||
set_array_prop<pxr::GfVec2f>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float3)) {
|
||||
set_array_prop<pxr::GfVec3f>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Float4)) {
|
||||
set_array_prop<pxr::GfVec4f>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double2)) {
|
||||
set_array_prop<pxr::GfVec2d>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double3)) {
|
||||
set_array_prop<pxr::GfVec3d>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Double4)) {
|
||||
set_array_prop<pxr::GfVec4d>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int2)) {
|
||||
set_array_prop<pxr::GfVec2i>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int3)) {
|
||||
set_array_prop<pxr::GfVec3i>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Int4)) {
|
||||
set_array_prop<pxr::GfVec4i>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half2)) {
|
||||
set_array_prop<pxr::GfVec2h>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half3)) {
|
||||
set_array_prop<pxr::GfVec3h>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
else if (equivalent(type_name, pxr::SdfValueTypeNames->Half4)) {
|
||||
set_array_prop<pxr::GfVec4h>(idgroup, attr_name.GetString().c_str(), attr, time_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
19
source/blender/io/usd/intern/usd_reader_utils.hh
Normal file
19
source/blender/io/usd/intern/usd_reader_utils.hh
Normal file
@@ -0,0 +1,19 @@
|
||||
/* 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 "BKE_idprop.hh"
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
void set_id_props_from_prim(ID *id,
|
||||
const pxr::UsdPrim &prim,
|
||||
eUSDAttrImportMode attr_import_mode = USD_ATTR_IMPORT_ALL,
|
||||
pxr::UsdTimeCode time_code = pxr::UsdTimeCode::Default());
|
||||
|
||||
} // namespace blender::io::usd
|
||||
@@ -54,6 +54,9 @@ void USDXformReader::read_object_data(Main * /*bmain*/, const double motionSampl
|
||||
}
|
||||
|
||||
BKE_object_apply_mat4(object_, transform_from_usd, true, false);
|
||||
|
||||
/* Make sure to collect custom attributes */
|
||||
set_props(use_parent_xform(), motionSampleTime);
|
||||
}
|
||||
|
||||
void USDXformReader::read_matrix(float r_mat[4][4] /* local matrix */,
|
||||
|
||||
@@ -27,6 +27,7 @@ static const pxr::TfToken preview_shader("previewShader", pxr::TfToken::Immortal
|
||||
static const pxr::TfToken preview_surface("UsdPreviewSurface", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken surface("surface", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken blender_ns("userProperties:blender", pxr::TfToken::Immortal);
|
||||
} // namespace usdtokens
|
||||
|
||||
static std::string get_mesh_active_uvlayer_name(const Object *ob)
|
||||
@@ -42,8 +43,109 @@ static std::string get_mesh_active_uvlayer_name(const Object *ob)
|
||||
return name ? 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 &timecode)
|
||||
{
|
||||
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, timecode);
|
||||
}
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
static void create_vector_attrib(const pxr::UsdPrim &prim,
|
||||
const IDProperty *prop,
|
||||
const pxr::TfToken &prop_token,
|
||||
const pxr::UsdTimeCode &timecode)
|
||||
{
|
||||
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, timecode);
|
||||
}
|
||||
else if (prop->len == 3) {
|
||||
type_name = pxr::SdfValueTypeNames->Float3;
|
||||
success = set_vec_attrib<pxr::GfVec3f>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
else if (prop->len == 4) {
|
||||
type_name = pxr::SdfValueTypeNames->Float4;
|
||||
success = set_vec_attrib<pxr::GfVec4f>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
}
|
||||
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, timecode);
|
||||
}
|
||||
else if (prop->len == 3) {
|
||||
type_name = pxr::SdfValueTypeNames->Double3;
|
||||
success = set_vec_attrib<pxr::GfVec3d>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
else if (prop->len == 4) {
|
||||
type_name = pxr::SdfValueTypeNames->Double4;
|
||||
success = set_vec_attrib<pxr::GfVec4d>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
}
|
||||
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, timecode);
|
||||
}
|
||||
else if (prop->len == 3) {
|
||||
type_name = pxr::SdfValueTypeNames->Int3;
|
||||
success = set_vec_attrib<pxr::GfVec3i>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
else if (prop->len == 4) {
|
||||
type_name = pxr::SdfValueTypeNames->Int4;
|
||||
success = set_vec_attrib<pxr::GfVec4i>(prim, prop, prop_token, type_name, timecode);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -121,7 +223,14 @@ pxr::UsdShadeMaterial USDAbstractWriter::ensure_usd_material(const HierarchyCont
|
||||
}
|
||||
|
||||
std::string active_uv = get_mesh_active_uvlayer_name(context.object);
|
||||
return create_usd_material(usd_export_context_, usd_path, material, active_uv, reports());
|
||||
|
||||
usd_material = create_usd_material(
|
||||
usd_export_context_, usd_path, material, active_uv, reports());
|
||||
|
||||
auto prim = usd_material.GetPrim();
|
||||
write_id_properties(prim, material->id, get_export_time_code());
|
||||
|
||||
return usd_material;
|
||||
}
|
||||
|
||||
void USDAbstractWriter::write_visibility(const HierarchyContext &context,
|
||||
@@ -165,6 +274,111 @@ bool USDAbstractWriter::mark_as_instance(const HierarchyContext &context, const
|
||||
return true;
|
||||
}
|
||||
|
||||
void USDAbstractWriter::write_id_properties(const pxr::UsdPrim &prim,
|
||||
const ID &id,
|
||||
pxr::UsdTimeCode timecode) 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, timecode);
|
||||
}
|
||||
}
|
||||
|
||||
void USDAbstractWriter::write_user_properties(const pxr::UsdPrim &prim,
|
||||
IDProperty *properties,
|
||||
pxr::UsdTimeCode timecode) const
|
||||
{
|
||||
if (properties == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (properties->type != IDP_GROUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
const StringRef displayName_identifier = "displayName";
|
||||
|
||||
for (IDProperty *prop = (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;
|
||||
}
|
||||
|
||||
std::string prop_name = pxr::TfMakeValidIdentifier(prop->name);
|
||||
std::string full_prop_name = "userProperties:" + prop_name;
|
||||
|
||||
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, timecode);
|
||||
}
|
||||
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), timecode);
|
||||
}
|
||||
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), timecode);
|
||||
}
|
||||
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), timecode);
|
||||
}
|
||||
break;
|
||||
case IDP_BOOLEAN:
|
||||
if (pxr::UsdAttribute bool_attr = prim.CreateAttribute(
|
||||
prop_token, pxr::SdfValueTypeNames->Bool, true))
|
||||
{
|
||||
bool_attr.Set<bool>(prop->data.val, timecode);
|
||||
}
|
||||
break;
|
||||
case IDP_ARRAY:
|
||||
create_vector_attrib(prim, prop, prop_token, timecode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void USDAbstractWriter::author_extent(const pxr::UsdTimeCode timecode, pxr::UsdGeomBoundable &prim)
|
||||
{
|
||||
/* Do not use any existing `extentsHint` that may be authored, instead recompute the extent when
|
||||
|
||||
@@ -67,6 +67,13 @@ class USDAbstractWriter : public AbstractHierarchyWriter {
|
||||
pxr::SdfPath get_material_library_path() const;
|
||||
pxr::UsdShadeMaterial ensure_usd_material(const HierarchyContext &context, Material *material);
|
||||
|
||||
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 timecode,
|
||||
pxr::UsdGeomImageable &usd_geometry);
|
||||
|
||||
@@ -97,6 +97,9 @@ void USDCameraWriter::do_write(HierarchyContext &context)
|
||||
float focus_distance = BKE_camera_object_dof_distance(context.object);
|
||||
usd_camera.CreateFocusDistanceAttr().Set(focus_distance, timecode);
|
||||
}
|
||||
|
||||
auto prim = usd_camera.GetPrim();
|
||||
write_id_properties(prim, camera->id, timecode);
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
@@ -505,6 +505,9 @@ void USDCurvesWriter::do_write(HierarchyContext &context)
|
||||
set_writer_attributes(usd_curves, verts, control_point_counts, widths, timecode, interpolation);
|
||||
|
||||
assign_materials(context, usd_curves);
|
||||
|
||||
auto prim = usd_curves.GetPrim();
|
||||
write_id_properties(prim, curves->id, timecode);
|
||||
}
|
||||
|
||||
void USDCurvesWriter::assign_materials(const HierarchyContext &context,
|
||||
|
||||
@@ -62,6 +62,11 @@ void USDHairWriter::do_write(HierarchyContext &context)
|
||||
curves.CreateDisplayColorAttr(pxr::VtValue(colors));
|
||||
}
|
||||
|
||||
if (psys->part) {
|
||||
auto prim = curves.GetPrim();
|
||||
write_id_properties(prim, psys->part->id, timecode);
|
||||
}
|
||||
|
||||
this->author_extent(timecode, curves);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,9 @@ void USDLightWriter::do_write(HierarchyContext &context)
|
||||
usd_light_api.CreateSpecularAttr().Set(light->spec_fac, timecode);
|
||||
usd_light_api.CreateNormalizeAttr().Set(true, timecode);
|
||||
|
||||
auto prim = usd_light_api.GetPrim();
|
||||
write_id_properties(prim, light->id, timecode);
|
||||
|
||||
set_light_extents(usd_light_api.GetPrim(), timecode);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,12 @@ void USDGenericMeshWriter::do_write(HierarchyContext &context)
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
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);
|
||||
write_id_properties(prim, mesh->id, get_export_time_code());
|
||||
}
|
||||
}
|
||||
|
||||
void USDGenericMeshWriter::write_custom_data(const Object *obj,
|
||||
|
||||
@@ -30,6 +30,11 @@ void USDTransformWriter::do_write(HierarchyContext &context)
|
||||
|
||||
pxr::GfMatrix4d mat_val(parent_relative_matrix);
|
||||
usd_value_writer_.SetAttribute(xformOp_.GetAttr(), mat_val, get_export_time_code());
|
||||
|
||||
if (context.object) {
|
||||
auto prim = xform.GetPrim();
|
||||
write_id_properties(prim, context.object->id, get_export_time_code());
|
||||
}
|
||||
}
|
||||
|
||||
bool USDTransformWriter::check_is_animated(const HierarchyContext &context) const
|
||||
|
||||
@@ -34,6 +34,17 @@ enum eUSDMtlNameCollisionMode {
|
||||
USD_MTL_NAME_COLLISION_REFERENCE_EXISTING = 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Behavior for importing of custom
|
||||
* attributes / properties outside
|
||||
* a prim's regular schema.
|
||||
*/
|
||||
typedef enum eUSDAttrImportMode {
|
||||
USD_ATTR_IMPORT_NONE = 0,
|
||||
USD_ATTR_IMPORT_USER = 1,
|
||||
USD_ATTR_IMPORT_ALL = 2,
|
||||
} eUSDAttrImportMode;
|
||||
|
||||
/**
|
||||
* Behavior when importing textures from a package
|
||||
* (e.g., USDZ archive) or from a URI path.
|
||||
@@ -84,6 +95,8 @@ struct USDExportParams {
|
||||
bool export_textures = true;
|
||||
bool overwrite_textures = true;
|
||||
bool relative_paths = true;
|
||||
bool export_custom_properties = true;
|
||||
bool author_blender_name = true;
|
||||
char root_prim_path[1024] = ""; /* FILE_MAX */
|
||||
char collection[MAX_IDPROP_NAME] = "";
|
||||
|
||||
@@ -126,6 +139,7 @@ struct USDImportParams {
|
||||
char import_textures_dir[768]; /* FILE_MAXDIR */
|
||||
eUSDTexNameCollisionMode tex_name_collision_mode;
|
||||
bool import_all_materials;
|
||||
eUSDAttrImportMode attr_import_mode;
|
||||
|
||||
/**
|
||||
* Communication structure between the wmJob management code and the worker code. Currently used
|
||||
|
||||
Reference in New Issue
Block a user