From 56779c7bb05f3f1b1fd6dd9625d78aa4e1193fa3 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Sun, 11 Aug 2024 23:36:40 +0200 Subject: [PATCH] Fix: USD: Ensure mesh velocity data is written sparsely and add tests The mesh velocity data was not using the UsdUtilsSparseValueWriter and was writing out data for all frames even if the velocity didn't change. Adds test coverage for this scenario as well as other situations where a MeshSequenceCache (MSC) would be required: - Ensures that when positions vary a MSC is added - Ensures that when velocities vary a MSC is added (see blender/blender@c862d40e095) - Ensures that when attributes vary a MSC is added (see blender/blender@3c394d39f2c) Pull Request: https://projects.blender.org/blender/blender/pulls/126208 --- .../blender/io/usd/intern/usd_writer_mesh.cc | 24 +++---- tests/python/bl_usd_export_test.py | 35 ++++++++++ tests/python/bl_usd_import_test.py | 69 ++++++++++++++++++- 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/source/blender/io/usd/intern/usd_writer_mesh.cc b/source/blender/io/usd/intern/usd_writer_mesh.cc index 2ba9a91320a..ae344f4bf91 100644 --- a/source/blender/io/usd/intern/usd_writer_mesh.cc +++ b/source/blender/io/usd/intern/usd_writer_mesh.cc @@ -769,26 +769,24 @@ void USDGenericMeshWriter::write_surface_velocity(const Mesh *mesh, { /* Export velocity attribute output by fluid sim, sequence cache modifier * and geometry nodes. */ - AttributeOwner owner = AttributeOwner::from_id(const_cast(&mesh->id)); - CustomDataLayer *velocity_layer = BKE_attribute_find( - owner, "velocity", CD_PROP_FLOAT3, bke::AttrDomain::Point); - - if (velocity_layer == nullptr) { + const VArraySpan velocity = *mesh->attributes().lookup("velocity", + blender::bke::AttrDomain::Point); + if (velocity.is_empty()) { return; } - const float(*velocities)[3] = reinterpret_cast(velocity_layer->data); - /* Export per-vertex velocity vectors. */ + Span data = velocity.cast(); pxr::VtVec3fArray usd_velocities; - usd_velocities.reserve(mesh->verts_num); - - for (int vertex_idx = 0, totvert = mesh->verts_num; vertex_idx < totvert; ++vertex_idx) { - usd_velocities.push_back(pxr::GfVec3f(velocities[vertex_idx])); - } + usd_velocities.assign(data.begin(), data.end()); pxr::UsdTimeCode timecode = get_export_time_code(); - usd_mesh.CreateVelocitiesAttr().Set(usd_velocities, timecode); + pxr::UsdAttribute attr_vel = usd_mesh.CreateVelocitiesAttr(pxr::VtValue(), true); + if (!attr_vel.HasValue()) { + attr_vel.Set(usd_velocities, pxr::UsdTimeCode::Default()); + } + + usd_value_writer_.SetAttribute(attr_vel, usd_velocities, timecode); } USDMeshWriter::USDMeshWriter(const USDExporterContext &ctx) diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 52d38fcccab..4ed34239e07 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -283,6 +283,41 @@ class USDExportTest(AbstractUSDTest): self.check_primvar(prim, "sp_quat", "VtArray", "uniform", 3) self.check_primvar_missing(prim, "sp_mat4x4") + def test_export_attributes_varying(self): + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_varying_test.blend")) + # Ensure the simulation zone data is baked for all relevant frames... + for frame in range(1, 16): + bpy.context.scene.frame_set(frame) + bpy.context.scene.frame_set(1) + + export_path = self.tempdir / "usd_attribute_varying_test.usda" + res = bpy.ops.wm.usd_export(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER") + self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}") + + stage = Usd.Stage.Open(str(export_path)) + + # + # Validate Mesh data + # + mesh1 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh1/mesh1")) + mesh2 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh2/mesh2")) + mesh3 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh3/mesh3")) + + sparse_frames = [4.0, 5.0, 8.0, 9.0, 12.0, 13.0] + + # Positions (should be sparsely written) + self.assertEqual(mesh1.GetPointsAttr().GetTimeSamples(), sparse_frames) + self.assertEqual(mesh2.GetPointsAttr().GetTimeSamples(), []) + self.assertEqual(mesh3.GetPointsAttr().GetTimeSamples(), []) + # Velocity (should be sparsely written) + self.assertEqual(mesh1.GetVelocitiesAttr().GetTimeSamples(), []) + self.assertEqual(mesh2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames) + self.assertEqual(mesh3.GetVelocitiesAttr().GetTimeSamples(), []) + # Regular primvar (should be sparsely written) + self.assertEqual(UsdGeom.PrimvarsAPI(mesh1).GetPrimvar("test").GetTimeSamples(), []) + self.assertEqual(UsdGeom.PrimvarsAPI(mesh2).GetPrimvar("test").GetTimeSamples(), []) + self.assertEqual(UsdGeom.PrimvarsAPI(mesh3).GetPrimvar("test").GetTimeSamples(), sparse_frames) + def test_animation(self): bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend")) export_path = self.tempdir / "usd_anim_test.usda" diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 18ccedcd457..9c05da5ccce 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -727,11 +727,11 @@ class USDImportTest(AbstractUSDTest): self.assertFalse(attribute_name in blender_data.attributes) def test_import_attributes(self): - testfile = str(self.tempdir / "usd_attribute_test.usda") - # Use the existing attributes file to create the USD test file # for import. It is validated as part of the bl_usd_export test. bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_test.blend")) + + testfile = str(self.tempdir / "usd_attribute_test.usda") res = bpy.ops.wm.usd_export(filepath=testfile, evaluation_mode="RENDER") self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}") @@ -832,6 +832,71 @@ class USDImportTest(AbstractUSDTest): self.check_attribute(curves, "sp_quat", 'CURVE', 'QUATERNION', 3) self.check_attribute_missing(curves, "sp_mat4x4") + def test_import_attributes_varying(self): + # Use the existing attributes file to create the USD test file + # for import. It is validated as part of the bl_usd_export test. + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_varying_test.blend")) + for frame in range(1, 16): + bpy.context.scene.frame_set(frame) + bpy.context.scene.frame_set(1) + + testfile = str(self.tempdir / "usd_attribute_varying_test.usda") + res = bpy.ops.wm.usd_export(filepath=testfile, export_animation=True, evaluation_mode="RENDER") + self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}") + + # Reload the empty file and import back in + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend")) + res = bpy.ops.wm.usd_import(filepath=testfile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}") + + stage = Usd.Stage.Open(testfile) + + # + # Validate Mesh data + # + blender_mesh = [bpy.data.objects["mesh1"], bpy.data.objects["mesh2"], bpy.data.objects["mesh3"]] + usd_mesh = [UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh1/mesh1")), + UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh2/mesh2")), + UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh3/mesh3"))] + mesh_num = len(blender_mesh) + + # A MeshSequenceCache modifier should be present on every imported object + for i in range(0, mesh_num): + self.assertTrue(len(blender_mesh[i].modifiers) == 1 and blender_mesh[i].modifiers[0].type == + 'MESH_SEQUENCE_CACHE', f"{blender_mesh[i].name} has incorrect modifiers") + + def round_vector(vector): + return (round(vector[0], 5), round(vector[1], 5), round(vector[2], 5)) + + # Compare Blender and USD data against each other for every frame + for frame in range(1, 16): + bpy.context.scene.frame_set(frame) + depsgraph = bpy.context.evaluated_depsgraph_get() + for i in range(0, mesh_num): + blender_mesh[i] = bpy.data.objects["mesh" + str(i + 1)].evaluated_get(depsgraph) + + # Check positions, velocity, and test data + for i in range(0, mesh_num): + blender_pos_data = [round_vector(d.vector) for d in blender_mesh[i].data.attributes["position"].data] + blender_vel_data = [round_vector(d.vector) for d in blender_mesh[i].data.attributes["velocity"].data] + blender_test_data = [round(d.value, 5) for d in blender_mesh[i].data.attributes["test"].data] + usd_pos_data = [round_vector(d) for d in usd_mesh[i].GetPointsAttr().Get(frame)] + usd_vel_data = [round_vector(d) for d in usd_mesh[i].GetVelocitiesAttr().Get(frame)] + usd_test_data = [round(d, 5) for d in UsdGeom.PrimvarsAPI(usd_mesh[i]).GetPrimvar("test").Get(frame)] + + self.assertEqual( + blender_pos_data, + usd_pos_data, + f"Frame {frame}: {blender_mesh[i].name} positions do not match") + self.assertEqual( + blender_vel_data, + usd_vel_data, + f"Frame {frame}: {blender_mesh[i].name} velocities do not match") + self.assertEqual( + blender_test_data, + usd_test_data, + f"Frame {frame}: {blender_mesh[i].name} test attributes do not match") + def main(): global args