From 000416c933de383d2a7dc8f5b56f76e2e4a33e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles=20Fl=C3=A8che?= Date: Thu, 14 Nov 2024 18:28:23 +0100 Subject: [PATCH] USD: Add option to merge shape with parent xform during import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new import option will allow users to chose if we merge USD prims with their Xform parent. Given a USD like: ``` def Xform "MyObject" { def Mesh "MyObject_LOD0" { } } ``` When the option is set to True (existing default), only the mesh will be imported and its parent Xform transformation will be baked into the Mesh. ``` # In blender after import - MyObject_LOD0 (Mesh) ``` When the option is set to False, the parent Xform will always be imported as an Empty object: ``` # In blender after import - MyObject (Empty) ├─ MyObject_LOD0 (Mesh) ``` Co-authored-by: Odréanne Breton Co-authored-by: Sttevan Carnali Joga --- source/blender/editors/io/io_usd.cc | 11 +++++++++ .../blender/io/usd/intern/usd_reader_stage.cc | 10 ++++---- .../blender/io/usd/intern/usd_reader_stage.hh | 7 ++++++ source/blender/io/usd/usd.hh | 1 + tests/python/bl_usd_import_test.py | 24 +++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index 71edd3aa091..0c53528043a 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -935,6 +935,8 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op) const bool create_world_material = RNA_boolean_get(op->ptr, "create_world_material"); + const bool merge_parent_xform = RNA_boolean_get(op->ptr, "merge_parent_xform"); + /* TODO(makowalski): Add support for sequences. */ const bool is_sequence = false; int offset = 0; @@ -982,6 +984,7 @@ static int wm_usd_import_exec(bContext *C, wmOperator *op) params.import_blendshapes = import_blendshapes; params.validate_meshes = validate_meshes; + params.merge_parent_xform = merge_parent_xform; params.import_guide = import_guide; params.import_proxy = import_proxy; @@ -1073,6 +1076,7 @@ static void wm_usd_import_draw(bContext *C, wmOperator *op) col = uiLayoutColumn(panel, false); uiItemR(col, ptr, "validate_meshes", UI_ITEM_NONE, nullptr, ICON_NONE); + uiItemR(col, ptr, "merge_parent_xform", UI_ITEM_NONE, nullptr, ICON_NONE); } if (uiLayout *panel = uiLayoutPanel(C, layout, "USD_import_rigging", true, IFACE_("Rigging"))) { @@ -1322,6 +1326,13 @@ void WM_OT_usd_import(wmOperatorType *ot) "Defined Primitives Only", "Import only defined USD primitives. When disabled this allows importing USD " "primitives which are not defined, such as those with an override specifier"); + + RNA_def_boolean(ot->srna, + "merge_parent_xform", + true, + "Merge parent Xform", + "Allow USD primitives to merge with their Xform parent " + "if they are the only child in the hierarchy"); } namespace blender::ed::io { diff --git a/source/blender/io/usd/intern/usd_reader_stage.cc b/source/blender/io/usd/intern/usd_reader_stage.cc index 06830fb5b0f..e6be67aaf15 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.cc +++ b/source/blender/io/usd/intern/usd_reader_stage.cc @@ -281,11 +281,13 @@ bool USDStageReader::include_by_purpose(const pxr::UsdGeomImageable &imageable) return true; } -/* Determine if the given reader can use the parent of the encapsulated USD prim - * to compute the Blender object's transform. If so, the reader is appropriately - * flagged and the function returns true. Otherwise, the function returns false. */ -static bool merge_with_parent(USDPrimReader *reader) +bool USDStageReader::merge_with_parent(USDPrimReader *reader) const { + /* Don't merge if the param is set to false */ + if (!params_.merge_parent_xform) { + return false; + } + USDXformReader *xform_reader = dynamic_cast(reader); if (!xform_reader) { diff --git a/source/blender/io/usd/intern/usd_reader_stage.hh b/source/blender/io/usd/intern/usd_reader_stage.hh index 2df9bb106a1..81953c2ad54 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.hh +++ b/source/blender/io/usd/intern/usd_reader_stage.hh @@ -170,6 +170,13 @@ class USDStageReader { */ bool include_by_purpose(const pxr::UsdGeomImageable &imageable) const; + /** + * Returns true if the given reader can use the parent of the encapsulated USD prim + * to compute the Blender object's transform. If so, the reader is appropriately + * flagged and the function returns true. Otherwise, the function returns false. + */ + bool merge_with_parent(USDPrimReader *reader) const; + /** * Returns true if the specified UsdPrim is a UsdGeom primitive, * procedural shape, such as UsdGeomCube. diff --git a/source/blender/io/usd/usd.hh b/source/blender/io/usd/usd.hh index 9dd8c1e806d..38201f1f5a3 100644 --- a/source/blender/io/usd/usd.hh +++ b/source/blender/io/usd/usd.hh @@ -212,6 +212,7 @@ struct USDImportParams { bool set_material_blend; bool validate_meshes; + bool merge_parent_xform; eUSDMtlPurpose mtl_purpose; eUSDMtlNameCollisionMode mtl_name_collision_mode; diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 491b54c5c95..4d2dc88067b 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -72,6 +72,30 @@ class USDImportTest(AbstractUSDTest): self.assertEqual(objects['World'], objects['Empty'].parent, "Empty should be a child of /World") self.assertEqual(objects['Empty'], objects['Plane_002'].parent, "Plane_002 should be a child of /World") + def test_import_xform_and_mesh_merged_false(self): + """Test importing a simple object hierarchy (xform and mesh) from a USDA file.""" + + infile = str(self.testdir / "usd_mesh_polygon_types.usda") + + res = bpy.ops.wm.usd_import(filepath=infile, merge_parent_xform=False) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}") + + objects = bpy.context.scene.collection.objects + self.assertEqual(10, len(objects), f"Test scene {infile} should have ten objects; found {len(objects)}") + + # Test the hierarchy. + self.assertEqual( + objects['degenerate'], + objects['m_degenerate'].parent, + "m_degenerate should be child of /degenerate") + self.assertEqual( + objects['triangles'], + objects['m_triangles'].parent, + "m_triangles should be a child of /triangles") + self.assertEqual(objects['quad'], objects['m_quad'].parent, "m_quad should be a child of /quad") + self.assertEqual(objects['ngon_concave'], objects['m_ngon_concave'].parent, + "m_ngon_concave should be a child of /ngon_concave") + def test_import_mesh_topology(self): """Test importing meshes with different polygon types."""