From 37f8616bd572508fc4d53ca1b897c165920b6de9 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Thu, 12 Jun 2025 19:32:43 +0200 Subject: [PATCH] Fix #140225: Always ensure mesh topology is up to date during USD import It is possible for a mesh to change topology across frames but still be detected as not needing a topology update. Until we can make a finer-grained check against the before and after topology, unconditionally ensure it's updated for now. Adds a new test that checks a few frames of changing topology that is similar, but not the same. Pull Request: https://projects.blender.org/blender/blender/pulls/140253 --- .../blender/io/usd/intern/usd_reader_mesh.cc | 6 ++ tests/files/usd/usd_mesh_topology_change.usda | 64 +++++++++++++++++++ tests/python/bl_usd_import_test.py | 26 ++++++++ 3 files changed, 96 insertions(+) create mode 100644 tests/files/usd/usd_mesh_topology_change.usda diff --git a/source/blender/io/usd/intern/usd_reader_mesh.cc b/source/blender/io/usd/intern/usd_reader_mesh.cc index 3984457fc3b..56ed33b8f62 100644 --- a/source/blender/io/usd/intern/usd_reader_mesh.cc +++ b/source/blender/io/usd/intern/usd_reader_mesh.cc @@ -324,6 +324,12 @@ bool USDMeshReader::read_faces(Mesh *mesh) const bke::mesh_calc_edges(*mesh, false, false); + /* It's possible that the number of faces, indices, and verts remain the same but the topology + * itself is different. Until finer-grained topology detection can be implemented, always tag the + * mesh as needing updated topology mappings. Without this, a time varying mesh could trigger + * undefined behavior. */ + mesh->tag_topology_changed(); + return all_faces_ok; } diff --git a/tests/files/usd/usd_mesh_topology_change.usda b/tests/files/usd/usd_mesh_topology_change.usda new file mode 100644 index 00000000000..cd30ade1445 --- /dev/null +++ b/tests/files/usd/usd_mesh_topology_change.usda @@ -0,0 +1,64 @@ +#usda 1.0 +( + defaultPrim = "root" + doc = "Blender v4.5.0 Beta" + endTimeCode = 4 + metersPerUnit = 1 + startTimeCode = 1 + timeCodesPerSecond = 24 + upAxis = "Z" +) + +def Xform "root" ( + customData = { + dictionary Blender = { + bool generated = 1 + } + } +) +{ + def Xform "TopoTest" + { + def Mesh "TopoTest" ( + active = true + ) + { + float3[] extent = [(-0.5, -0.5, 0), (3.5, 0.5, 0)] + int[] faceVertexCounts = [4, 4, 4] + int[] faceVertexCounts.timeSamples = { + 1: [4, 4, 4], + 2: [3, 4, 5], + 3: [3, 3, 6], + 4: [4, 4, 4], + } + int[] faceVertexIndices = [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 11, 10] + int[] faceVertexIndices.timeSamples = { + 1: [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 11, 10], + 2: [0, 1, 2, 3, 4, 11, 5, 7, 8, 10, 6, 9], + 3: [0, 1, 2, 4, 5, 6, 8, 3, 9, 11, 7, 10], + 4: [0, 1, 3, 2, 4, 5, 7, 6, 8, 9, 11, 10], + } + normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] ( + interpolation = "faceVarying" + ) + point3f[] points = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0), (1, -0.5, 0), (2, -0.5, 0), (1, 0.5, 0), (2, 0.5, 0), (2.5, -0.5, 0), (3.5, -0.5, 0), (2.5, 0.5, 0), (3.5, 0.5, 0)] + point3f[] points.timeSamples = { + 1: [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0), (1, -0.5, 0), (2, -0.5, 0), (1, 0.5, 0), (2, 0.5, 0), (2.5, -0.5, 0), (3.5, -0.5, 0), (2.5, 0.5, 0), (3.5, 0.5, 0)], + 2: [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (1, -0.5, 0), (2, -0.5, 0), (1, 0.5, 0), (3, 0.5, 0), (2.5, -0.5, 0), (3.5, -0.5, 0), (2.5, 0.5, 0), (3.5, 0.5, 0), (2, 0.5, 0)], + 3: [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (3, -0.5, 0), (1, -0.5, 0), (2, -0.5, 0), (1, 0.5, 0), (3, 0.5, 0), (2.5, -0.5, 0), (3.5, -0.5, 0), (2.5, 0.5, 0), (3.5, 0.5, 0)], + 4: [(-0.5, -0.5, 0), (0.5, -0.5, 0), (-0.5, 0.5, 0), (0.5, 0.5, 0), (1, -0.5, 0), (2, -0.5, 0), (1, 0.5, 0), (2, 0.5, 0), (2.5, -0.5, 0), (3.5, -0.5, 0), (2.5, 0.5, 0), (3.5, 0.5, 0)], + } + texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)] ( + interpolation = "faceVarying" + ) + texCoord2f[] primvars:st.timeSamples = { + 1: [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)], + 2: [(0, 0), (1, 0), (0, 1), (0, 0), (1, 0), (0.5, 0.5), (0, 1), (0, 0), (1, 0), (1, 1), (0.5, 1), (0, 1)], + 3: [(0, 0), (1, 0), (0, 1), (0, 0), (1, 0), (0, 1), (0, 0), (0.5, 0), (1, 0), (1, 1), (0.5, 1), (0, 1)], + 4: [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)], + } + uniform token subdivisionScheme = "none" + } + } +} + diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 3969f54e984..ad35fa69fb2 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -160,6 +160,32 @@ class USDImportTest(AbstractUSDTest): self.assertEqual(len(mesh.vertices), 5) self.assertEqual(len(mesh.polygons[0].vertices), 5) + def test_import_mesh_topology_change(self): + """Test importing meshes with changing topology over time.""" + + infile = str(self.testdir / "usd_mesh_topology_change.usda") + res = bpy.ops.wm.usd_import(filepath=infile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}") + + # Check topology for all frames against expected vertex and face counts + expected_face_verts = [ + (4, 4, 4), + (3, 4, 5), + (3, 3, 6), + (4, 4, 4), + ] + for frame in range(1, 5): + bpy.context.scene.frame_set(frame) + depsgraph = bpy.context.evaluated_depsgraph_get() + + mesh = depsgraph.objects["TopoTest"].data + + expected = expected_face_verts[frame - 1] + self.assertEqual(len(mesh.polygons), len(expected), f"Unexpected data for {frame=}") + for face in range(0, 3): + verts = mesh.polygons[face].vertices + self.assertEqual(len(verts), expected[face], f"Unexpected data for {frame=} {face=}") + def test_import_mesh_uv_maps(self): """Test importing meshes with udim UVs and multiple UV sets."""