USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
This commit is contained in:
committed by
Jesse Yurkovich
parent
acf82cb711
commit
428ab699dc
@@ -299,6 +299,8 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
|
||||
|
||||
const int usdz_downscale_custom_size = RNA_int_get(op->ptr, "usdz_downscale_custom_size");
|
||||
|
||||
const bool merge_parent_xform = RNA_boolean_get(op->ptr, "merge_parent_xform");
|
||||
|
||||
# if PXR_VERSION >= 2403
|
||||
const bool allow_unicode = RNA_boolean_get(op->ptr, "allow_unicode");
|
||||
# else
|
||||
@@ -387,6 +389,8 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op)
|
||||
params.usdz_downscale_size = usdz_downscale_size;
|
||||
params.usdz_downscale_custom_size = usdz_downscale_custom_size;
|
||||
|
||||
params.merge_parent_xform = merge_parent_xform;
|
||||
|
||||
STRNCPY(params.root_prim_path, root_prim_path);
|
||||
STRNCPY(params.custom_properties_namespace, custom_properties_namespace);
|
||||
RNA_string_get(op->ptr, "collection", params.collection);
|
||||
@@ -460,6 +464,7 @@ static void wm_usd_export_draw(bContext *C, wmOperator *op)
|
||||
uiItemR(col, ptr, "rename_uvmaps", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(col, ptr, "export_normals", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
|
||||
uiItemR(col, ptr, "merge_parent_xform", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
uiItemR(col, ptr, "triangulate_meshes", UI_ITEM_NONE, nullptr, ICON_NONE);
|
||||
if (RNA_boolean_get(ptr, "triangulate_meshes")) {
|
||||
uiItemR(col, ptr, "quad_method", UI_ITEM_NONE, IFACE_("Method Quads"), ICON_NONE);
|
||||
@@ -844,6 +849,14 @@ void WM_OT_usd_export(wmOperatorType *ot)
|
||||
"Custom size for downscaling exported textures",
|
||||
128,
|
||||
8192);
|
||||
|
||||
RNA_def_boolean(ot->srna,
|
||||
"merge_parent_xform",
|
||||
false,
|
||||
"Merge parent Xform",
|
||||
"Merge USD primitives with their Xform parent if possible: "
|
||||
"USD does not allow nested UsdGeomGprim. Intermediary Xform will "
|
||||
"be defined to keep the USD file valid.");
|
||||
}
|
||||
|
||||
/* ====== USD Import ====== */
|
||||
|
||||
@@ -68,6 +68,18 @@ struct HierarchyContext {
|
||||
* it's animated. This is necessary when a parent object in Blender is not part of the export. */
|
||||
bool animation_check_include_parent;
|
||||
|
||||
/* The flag makes unambiguous the fact that the current context targets object or data. This is
|
||||
* notably used in USDHierarchyIterator::create_usd_export_context: options like
|
||||
* merge_parent_xform option is meaningless for object, it only makes sense for data. */
|
||||
bool is_object_data_context;
|
||||
|
||||
/* This flag tells, within a object data context, if an object is the parent of other objects.
|
||||
* This is useful when exporting UsdGeomGprim: those cannot be nested into each other. For
|
||||
* example, an UsdGeomMesh cannot have other UsdGeomMesh as descendants and other hierarchy
|
||||
* strategies need to be adopted.
|
||||
*/
|
||||
bool is_parent;
|
||||
|
||||
/*********** Determined during writer creation: ***************/
|
||||
float parent_matrix_inv_world[4][4]; /* Inverse of the parent's world matrix. */
|
||||
std::string export_path; /* Hierarchical path, such as "/grandparent/parent/object_name". */
|
||||
|
||||
@@ -406,6 +406,7 @@ void AbstractHierarchyIterator::visit_object(Object *object,
|
||||
{
|
||||
HierarchyContext *context = new HierarchyContext();
|
||||
context->object = object;
|
||||
context->is_object_data_context = false;
|
||||
context->export_name = get_object_name(object);
|
||||
context->export_parent = export_parent;
|
||||
context->duplicator = nullptr;
|
||||
@@ -446,6 +447,7 @@ void AbstractHierarchyIterator::visit_dupli_object(DupliObject *dupli_object,
|
||||
{
|
||||
HierarchyContext *context = new HierarchyContext();
|
||||
context->object = dupli_object->ob;
|
||||
context->is_object_data_context = false;
|
||||
context->duplicator = duplicator;
|
||||
context->persistent_id = PersistentID(dupli_object);
|
||||
context->weak_export = false;
|
||||
@@ -619,6 +621,12 @@ HierarchyContext AbstractHierarchyIterator::context_for_object_data(
|
||||
const HierarchyContext *object_context) const
|
||||
{
|
||||
HierarchyContext data_context = *object_context;
|
||||
data_context.is_object_data_context = true;
|
||||
|
||||
ExportGraph::key_type object_key = ObjectIdentifier::for_real_object(data_context.object);
|
||||
auto iter = export_graph_.find(object_key);
|
||||
data_context.is_parent = iter->second.size() > 0;
|
||||
|
||||
data_context.higher_up_export_path = object_context->export_path;
|
||||
data_context.export_name = get_object_data_name(data_context.object);
|
||||
data_context.export_path = path_concatenate(data_context.higher_up_export_path,
|
||||
|
||||
@@ -110,6 +110,17 @@ USDExporterContext USDHierarchyIterator::create_usd_export_context(const Hierarc
|
||||
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 (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();
|
||||
|
||||
@@ -168,6 +168,8 @@ struct USDExportParams {
|
||||
char collection[MAX_IDPROP_NAME] = "";
|
||||
char custom_properties_namespace[MAX_IDPROP_NAME] = "";
|
||||
|
||||
bool merge_parent_xform = false;
|
||||
|
||||
/** Communication structure between the wmJob management code and the worker code. Currently used
|
||||
* to generate safely reports from the worker thread. */
|
||||
wmJobWorkerStatus *worker_status = nullptr;
|
||||
|
||||
@@ -965,6 +965,120 @@ class USDExportTest(AbstractUSDTest):
|
||||
self.assertEqual(USDHookBase.responses["on_export"], [])
|
||||
self.assertEqual(USDHookBase.responses["on_import"], [])
|
||||
|
||||
def test_merge_parent_xform_false(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
|
||||
|
||||
test_path = self.tempdir / "test_merge_parent_xform_false.usda"
|
||||
|
||||
self.export_and_validate(filepath=str(test_path), merge_parent_xform=False)
|
||||
|
||||
expected = (
|
||||
("/root", "Xform"),
|
||||
("/root/Dupli1", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
|
||||
("/root/_materials", "Scope"),
|
||||
("/root/_materials/Head", "Material"),
|
||||
("/root/_materials/Head/Principled_BSDF", "Shader"),
|
||||
("/root/_materials/Nose", "Material"),
|
||||
("/root/_materials/Nose/Principled_BSDF", "Shader"),
|
||||
("/root/ParentOfDupli2", "Xform"),
|
||||
("/root/ParentOfDupli2/Icosphere", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
|
||||
("/root/Ground_plane", "Xform"),
|
||||
("/root/Ground_plane/Plane", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear", "Mesh"),
|
||||
("/root/Camera", "Xform"),
|
||||
("/root/Camera/Camera", "Camera"),
|
||||
("/root/env_light", "DomeLight")
|
||||
)
|
||||
|
||||
def key(el):
|
||||
return el[0]
|
||||
|
||||
expected = tuple(sorted(expected, key=key))
|
||||
|
||||
stage = Usd.Stage.Open(str(test_path))
|
||||
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
|
||||
actual = tuple(sorted(actual, key=key))
|
||||
|
||||
self.assertTupleEqual(expected, actual)
|
||||
|
||||
def test_merge_parent_xform_true(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
|
||||
|
||||
test_path = self.tempdir / "test_merge_parent_xform_true.usda"
|
||||
|
||||
self.export_and_validate(filepath=str(test_path), merge_parent_xform=True)
|
||||
|
||||
expected = (
|
||||
("/root", "Xform"),
|
||||
("/root/Dupli1", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0", "Xform"),
|
||||
("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
|
||||
("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Mesh"),
|
||||
("/root/_materials", "Scope"),
|
||||
("/root/_materials/Head", "Material"),
|
||||
("/root/_materials/Head/Principled_BSDF", "Shader"),
|
||||
("/root/_materials/Nose", "Material"),
|
||||
("/root/_materials/Nose/Principled_BSDF", "Shader"),
|
||||
("/root/ParentOfDupli2", "Xform"),
|
||||
("/root/ParentOfDupli2/Icosphere", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
|
||||
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Mesh"),
|
||||
("/root/Ground_plane", "Xform"),
|
||||
("/root/Ground_plane/Plane", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Mesh"),
|
||||
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Mesh"),
|
||||
("/root/Camera", "Camera"),
|
||||
("/root/env_light", "DomeLight")
|
||||
)
|
||||
|
||||
def key(el):
|
||||
return el[0]
|
||||
|
||||
expected = tuple(sorted(expected, key=key))
|
||||
|
||||
stage = Usd.Stage.Open(str(test_path))
|
||||
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
|
||||
actual = tuple(sorted(actual, key=key))
|
||||
|
||||
self.assertTupleEqual(expected, actual)
|
||||
|
||||
|
||||
class USDHookBase():
|
||||
instructions = {}
|
||||
|
||||
Reference in New Issue
Block a user