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
@@ -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