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@c862d40e09)
- Ensures that when attributes vary a MSC is added (see blender/blender@3c394d39f2)

Pull Request: https://projects.blender.org/blender/blender/pulls/126208
This commit is contained in:
Jesse Yurkovich
2024-08-11 23:36:40 +02:00
committed by Jesse Yurkovich
parent b2f65b9bcb
commit 56779c7bb0
3 changed files with 113 additions and 15 deletions

View File

@@ -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<ID *>(&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<float3>("velocity",
blender::bke::AttrDomain::Point);
if (velocity.is_empty()) {
return;
}
const float(*velocities)[3] = reinterpret_cast<float(*)[3]>(velocity_layer->data);
/* Export per-vertex velocity vectors. */
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
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)

View File

@@ -283,6 +283,41 @@ class USDExportTest(AbstractUSDTest):
self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "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"

View File

@@ -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