diff --git a/source/blender/io/usd/intern/usd_capi_export.cc b/source/blender/io/usd/intern/usd_capi_export.cc index 9ec35e0accb..fae8b1ff828 100644 --- a/source/blender/io/usd/intern/usd_capi_export.cc +++ b/source/blender/io/usd/intern/usd_capi_export.cc @@ -388,10 +388,18 @@ std::string cache_image_color(const float color[4]) return file_path; } -static void mark_point_instancer_prototypes_as_over(const pxr::UsdStageRefPtr &stage, - const pxr::SdfPath &wrapper_path, - std::set &visited) +static void collect_point_instancer_prototypes_and_set_extent( + pxr::UsdGeomPointInstancer instancer, + const pxr::UsdStageRefPtr &stage, + const pxr::SdfPath &wrapper_path, + std::vector &proto_list) { + /* Compute extent of the current point instancer.*/ + pxr::VtArray extent; + instancer.ComputeExtentAtTime( + &extent, pxr::UsdTimeCode().Default(), pxr::UsdTimeCode().Default()); + instancer.CreateExtentAttr().Set(extent); + pxr::UsdPrim wrapper_prim = stage->GetPrimAtPath(wrapper_path); if (!wrapper_prim || !wrapper_prim.IsValid()) { return; @@ -423,17 +431,13 @@ static void mark_point_instancer_prototypes_as_over(const pxr::UsdStageRefPtr &s const pxr::SdfPath real_path(real_path_str); pxr::UsdPrim proto_prim = stage->GetPrimAtPath(real_path); - if (visited.count(real_path)) { - return; - } - visited.insert(real_path); - if (!proto_prim || !proto_prim.IsValid()) { CLOG_WARN(&LOG, "Referenced prototype not found at: %s", real_path.GetText()); return; } - proto_prim.SetSpecifier(pxr::SdfSpecifierOver); + proto_list.push_back(proto_prim); + proto_list.push_back(wrapper_prim.GetParent()); std::string doc_message = fmt::format( "This prim is used as a prototype by the PointInstancer \"{}\" so we override the def " @@ -442,12 +446,28 @@ static void mark_point_instancer_prototypes_as_over(const pxr::UsdStageRefPtr &s wrapper_prim.GetName().GetString()); proto_prim.SetDocumentation(doc_message); - if (wrapper_prim.IsA()) { - pxr::UsdGeomPointInstancer nested_instancer(wrapper_prim); + /* Check if the proto prim itself is a PointInstancer. */ + if (proto_prim.IsA()) { + pxr::UsdGeomPointInstancer nested_instancer(proto_prim); pxr::SdfPathVector nested_targets; if (nested_instancer.GetPrototypesRel().GetTargets(&nested_targets)) { for (const pxr::SdfPath &nested_wrapper_path : nested_targets) { - mark_point_instancer_prototypes_as_over(stage, nested_wrapper_path, visited); + collect_point_instancer_prototypes_and_set_extent( + nested_instancer, stage, nested_wrapper_path, proto_list); + } + } + } + + /* Also check all children of the proto prim for nested PointInstancers. */ + for (const pxr::UsdPrim &child : proto_prim.GetAllChildren()) { + if (child.IsA()) { + pxr::UsdGeomPointInstancer nested_instancer(child); + pxr::SdfPathVector nested_targets; + if (nested_instancer.GetPrototypesRel().GetTargets(&nested_targets)) { + for (const pxr::SdfPath &nested_wrapper_path : nested_targets) { + collect_point_instancer_prototypes_and_set_extent( + nested_instancer, stage, nested_wrapper_path, proto_list); + } } } } @@ -635,7 +655,7 @@ static void export_startjob(void *customdata, wmJobWorkerStatus *worker_status) /* Traverse the point instancer to make sure the prototype referenced by nested point instancers * are also marked as over. */ - std::set visited; + std::vector proto_list; for (const pxr::UsdPrim &prim : usd_stage->Traverse()) { if (!prim.IsA()) { continue; @@ -644,11 +664,19 @@ static void export_startjob(void *customdata, wmJobWorkerStatus *worker_status) pxr::SdfPathVector targets; if (instancer.GetPrototypesRel().GetTargets(&targets)) { for (const pxr::SdfPath &wrapper_path : targets) { - mark_point_instancer_prototypes_as_over(usd_stage, wrapper_path, visited); + collect_point_instancer_prototypes_and_set_extent( + instancer, usd_stage, wrapper_path, proto_list); } } } + /* The standard way is to mark the point instancer's prototypes as over. Reference in OpenUSD: + * https://openusd.org/docs/api/class_usd_geom_point_instancer.html#:~:text=place%20them%20under%20a%20prim%20that%20is%20just%20an%20%22over%22 + */ + for (pxr::UsdPrim &proto : proto_list) { + proto.SetSpecifier(pxr::SdfSpecifierOver); + } + usd_stage->GetRootLayer()->Save(); data->export_ok = true; diff --git a/source/blender/io/usd/intern/usd_writer_pointinstancer.cc b/source/blender/io/usd/intern/usd_writer_pointinstancer.cc index dcff561f3ae..810c74834c6 100644 --- a/source/blender/io/usd/intern/usd_writer_pointinstancer.cc +++ b/source/blender/io/usd/intern/usd_writer_pointinstancer.cc @@ -182,7 +182,6 @@ void USDPointInstancerWriter::do_write(HierarchyContext &context) ++iter; } usd_instancer.GetPrototypesRel().SetTargets(proto_wrapper_paths); - prototypesOver.GetPrim().SetSpecifier(pxr::SdfSpecifierOver); stage->GetRootLayer()->Save(); } diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 4e376fd80de..c7bd2a590dc 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -1736,28 +1736,42 @@ class USDExportTest(AbstractUSDTest): 'mesh_count': 3, 'instancer_count': 1, 'total_instances': 16, - 'total_prototypes': 1}, + 'total_prototypes': 1, + 'extent': { + "/root/Plane/Mesh": [Gf.Vec3f(-1.0999999, -1.0999999, -0.1), + Gf.Vec3f(1.1, 1.1, 0.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}, + 'total_prototypes': 2, + 'extent': { + "/root/Plane/Mesh": [Gf.Vec3f(-1.1758227, -1.1, -0.1), + Gf.Vec3f(1.1, 1.1526861, 0.14081651)]}}, # 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}, + 'total_prototypes': 4, + 'extent': { + "/root/Triangle/Triangle": [Gf.Vec3f(-0.976631, -1.2236981, -0.7395363), + Gf.Vec3f(1.8081428, 3.371673, 1.2604637)], + "/root/Plane/Plane": [Gf.Vec3f(-1.164238, -3.5953712, -0.2883494), + Gf.Vec3f(-0.68365526, -3.1147888, -0.18980181)]}}, # 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} + 'total_prototypes': 2, + 'extent': { + "/root/Rotated_and_Scaled_Instances/Cube_003": [Gf.Vec3f(-8.488519, -6.1219244, -6.964829), + Gf.Vec3f(3.2331002, 5.4789553, 7.095813)]}} ] for scenario in point_instance_test_scenarios: @@ -1777,6 +1791,20 @@ class USDExportTest(AbstractUSDTest): 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") + if 'extent' in scenario: + for prim_path, (expected_min, expected_max) in scenario['extent'].items(): + prim = stage.GetPrimAtPath(prim_path) + self.assertTrue(prim.IsValid(), f"Prim {prim_path} not found on stage") + + boundable = UsdGeom.Boundable(prim) + extent_attr = boundable.GetExtentAttr() + self.assertTrue(extent_attr.HasAuthoredValue(), f"Prim {prim_path} has no authored extent") + + extent = extent_attr.Get() + self.assertIsNotNone(extent, f"Extent on {prim_path} could not be retrieved") + + self.compareVec3d(Gf.Vec3d(extent[0]), expected_min) + self.compareVec3d(Gf.Vec3d(extent[1]), expected_max) class USDHookBase: