USD: Scene graph instancing export

Refactored USD instancing export to support instanceable references.

With this change, it's now possible to instance object hierarchies and
geometry types other than meshes (e.g., curves, point clouds, etc.).

No longer marking mesh prims as instances in
USDGenericMeshWriter::write_mesh().

USDTransformWriter::do_write() now marks the Xform as instanceable
with a reference to the prototype's Xform when the Blender object is
an instance.

In USDAbstractWriter::mark_as_instance() the target prim is now marked
as instanceable.

Added AbstractHierarchyIterator virtual functions include_data_writers()
and include_child_writers() to allow pruning children of instanceable Xforms
in AbstractHierarchyIterator::make_writers(). These functions return true
in the base class implementation, so that the iterator behavior for Alembic
exports is unaffected.  In the USDHierarchyIterator subclass, these functions
are overridden to return false if instancing is enabled and the objects are
instances.

Added virtual function AbstractHierarchyIterator::should_determine_duplication_references()
which returns true if duplication references should be resolved for children
of a given context.  This function is overridden in USDHierarchyIterator to
skip processing children of instances, which is more efficient for USD export,
since children of instances are pruned during traversal for writing.  For nested
instances where the original prototype is not included in the export, this also
avoids designating a duplicated object parented to an instance as "the original",
which would cause USD errors since defining a prim under an instance
proxy is not allowed.

Extended logic in `AbstractHierarchyIterator::determine_duplication_references()`
to identify prototypes.

Added new function `HierarchyContext::is_prototype()`.

Disallowing merging with parent for instances and prototypes, since
the Xforms cannot be discarded in those cases.

Extended `USDWriterAbstract::ensure_usd_material()` with special logic
to ensure materials for prototype prims are defined in the subtree of the
prototype.  This helps ensure the hierarchical encapsulation requirement
for prototypes and is required by certain renderers (e.g., Houdini's Karma)
for instance materials to render.

Added a new `process_scene_graph_instances()` function to ensure
prototypes are exported as abstract prims.

Added python tests test_export_native_instancing_true and
test_export_native_instancing_false.

Pull Request: https://projects.blender.org/blender/blender/pulls/131707
This commit is contained in:
Michael Kowalski 2025-01-28 15:51:56 +01:00 • committed by Michael Kowalski
parent b3ae8b5396
commit 319c64205d
13 changed files with 513 additions and 22 deletions

View file

@ -1361,6 +1361,72 @@ class USDExportTest(AbstractUSDTest):
mpu = UsdGeom.GetStageMetersPerUnit(stage)
self.assertAlmostEqual(mpu, 0.1)
def test_export_native_instancing_true(self):
"""Test exporting instanced objects to native (scne graph) instances."""
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
export_path = self.tempdir / "usd_export_nested_instancing_true.usda"
self.export_and_validate(
filepath=str(export_path),
use_instancing=True
)
# The USD should contain two instances of a plane which has two
# instances of a point cloud as children.
stage = Usd.Stage.Open(str(export_path))
stats = UsdUtils.ComputeUsdStageStats(stage)
self.assertEqual(stats['totalInstanceCount'], 6, "Unexpected number of instances")
self.assertEqual(stats['prototypeCount'], 2, "Unexpected number of prototypes")
self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 1, "Unexpected number of primary meshes")
self.assertEqual(stats['primary']['primCountsByType']['Points'], 1, "Unexpected number of primary point clouds")
self.assertEqual(stats['prototypes']['primCountsByType']['Mesh'], 1, "Unexpected number of prototype meshes")
self.assertEqual(stats['prototypes']['primCountsByType']['Points'],
1, "Unexpected number of prototype point clouds")
# Get the prototypes root.
protos_root_path = Sdf.Path("/root/prototypes")
prim = stage.GetPrimAtPath(protos_root_path)
assert prim
self.assertTrue(prim.IsAbstract())
# Get the first plane instance.
prim = stage.GetPrimAtPath("/root/plane_001/Plane_0")
assert prim
assert prim.IsInstance()
# Get the second plane instance.
prim = stage.GetPrimAtPath("/root/plane/Plane_0")
assert prim
assert prim.IsInstance()
# Ensure all the prototype paths are under the pototypes root.
for prim in stage.Traverse():
if prim.IsInstance():
arcs = Usd.PrimCompositionQuery.GetDirectReferences(prim).GetCompositionArcs()
for arc in arcs:
target_path = arc.GetTargetPrimPath()
self.assertTrue(target_path.HasPrefix(protos_root_path))
def test_export_native_instancing_false(self):
"""Test exporting instanced objects with instancing disabled."""
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
export_path = self.tempdir / "usd_export_nested_instancing_false.usda"
self.export_and_validate(
filepath=str(export_path),
use_instancing=False
)
# The USD should contain no instances.
stage = Usd.Stage.Open(str(export_path))
stats = UsdUtils.ComputeUsdStageStats(stage)
self.assertEqual(stats['totalInstanceCount'], 0, "Unexpected number of instances")
self.assertEqual(stats['prototypeCount'], 0, "Unexpected number of prototypes")
self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 2, "Unexpected number of primary meshes")
self.assertEqual(stats['primary']['primCountsByType']['Points'], 4, "Unexpected number of primary point clouds")
def test_texture_export_hook(self):
"""Exporting textures from on_material_export USD hook."""