USD: Add support for Point Instancing during Export

Adds a Point Instancing exporter based on the existing
USDPointInstancerReader. Covers both round-trip and Blender-native
workflows. Exports 'Instance on Points' setups as USDGeomPointInstancer,
supporting objects, collections, and nested prototypes.

A warning is shown during export if invalid prototype references are
detected. These would occur if an instancer attempts to instance itself.

This feature is currently gated behind an off-by-default export option
(`use_instancing`) as there are still a few cases which can yield
incorrect results.

Further details in the PR.
Ref: #139758

Authored by Apple: Zili (Liz) Zhou

Pull Request: https://projects.blender.org/blender/blender/pulls/139760
This commit is contained in:
Michael B Johnson
2025-06-14 01:10:55 +02:00
committed by Jesse Yurkovich
parent fc0b659066
commit 07342407d3
15 changed files with 1097 additions and 10 deletions

View File

@@ -1689,6 +1689,95 @@ class USDExportTest(AbstractUSDTest):
self.assertTupleEqual(expected, actual)
def test_point_instancing_export(self):
"""Test exporting scenes that use point instancing."""
def confirm_point_instancing_stats(stage, num_meshes, num_instancers, num_instances, num_prototypes):
mesh_count = 0
instancer_count = 0
instance_count = 0
prototype_count = 0
for prim in stage.TraverseAll():
prim_path = prim.GetPath()
prim_type_name = prim.GetTypeName()
if prim_type_name == "PointInstancer":
point_instancer = UsdGeom.PointInstancer(prim)
if point_instancer:
# get instance count
positions_attr = point_instancer.GetPositionsAttr()
if positions_attr:
positions = positions_attr.Get()
if positions:
instance_count += len(positions)
# get prototype count
prototypes_rel = point_instancer.GetPrototypesRel()
if prototypes_rel:
target_prims = prototypes_rel.GetTargets()
prototype_count += len(target_prims)
# show all prims and types
# output_string = f" Path: {prim_path}, Type: {prim_type_name}"
# print(output_string)
stats = UsdUtils.ComputeUsdStageStats(stage)
mesh_count = stats['primary']['primCountsByType']['Mesh']
instancer_count = stats['primary']['primCountsByType']['PointInstancer']
return mesh_count, instancer_count, instance_count, prototype_count
point_instance_test_scenarios = [
# object reference treated as geometry set
{'input_file': str(self.testdir / "usd_point_instancer_object_ref.blend"),
'output_file': self.tempdir / "usd_export_point_instancer_object_ref.usda",
'mesh_count': 3,
'instancer_count': 1,
'total_instances': 16,
'total_prototypes': 1},
# collection reference from single point instancer
{'input_file': str(self.testdir / "usd_point_instancer_collection_ref.blend"),
'output_file': self.tempdir / "usd_export_point_instancer_collection_ref.usda",
'mesh_count': 5,
'instancer_count': 1,
'total_instances': 32,
'total_prototypes': 2},
# collection references in nested point instancer
{'input_file': str(self.testdir / "usd_point_instancer_nested.blend"),
'output_file': self.tempdir / "usd_export_point_instancer_nested.usda",
'mesh_count': 9,
'instancer_count': 3,
'total_instances': 14,
'total_prototypes': 4},
# object reference coming from a collection with separate children
{'input_file': str(self.testdir / "../render/shader/texture_coordinate_camera.blend"),
'output_file': self.tempdir / "usd_export_point_instancer_separate_children.usda",
'mesh_count': 9,
'instancer_count': 1,
'total_instances': 4,
'total_prototypes': 2}
]
for scenario in point_instance_test_scenarios:
bpy.ops.wm.open_mainfile(filepath=scenario['input_file'])
export_path = scenario['output_file']
self.export_and_validate(
filepath=str(export_path),
use_instancing=True
)
stage = Usd.Stage.Open(str(export_path))
mesh_count, instancer_count, instance_count, proto_count = confirm_point_instancing_stats(
stage, scenario['mesh_count'], scenario['instancer_count'], scenario['total_instances'], scenario['total_prototypes'])
self.assertEqual(scenario['mesh_count'], mesh_count, "Unexpected number of primary meshes")
self.assertEqual(scenario['instancer_count'], instancer_count, "Unexpected number of point instancers")
self.assertEqual(scenario['total_instances'], instance_count, "Unexpected number of total instances")
self.assertEqual(scenario['total_prototypes'], proto_count, "Unexpected number of total prototypes")
class USDHookBase:
instructions = {}