USD: Add support for animated point instancers

The existing point instancer reader is slightly refactored to allow for
animated setups. The primary change is simply to inherit from
`USDGeomReader` rather than `USDXformReader`. This allows access to the
`read_geometry` API used by the cache modifier.

The existing `read_object_data` method is split into two parts with
`read_geometry` loading per-frame USD data and `read_object_data`
coordinating the initial loading process, including creating the GN node
tree just once.

A new test has been added (a variation of the nested point instancer
file) with time samples on various attributes and on both point
instancers. This also fixes #129502 and the file provided in that issue.

----
The already added test file is `tests/data/usd/usd_point_instancer_anim.usda`

Pull Request: https://projects.blender.org/blender/blender/pulls/129881
This commit is contained in:
Jesse Yurkovich
2024-11-20 22:03:32 +01:00
committed by Jesse Yurkovich
parent 8bcb714b9e
commit 2523958e0e
4 changed files with 118 additions and 24 deletions

View File

@@ -36,8 +36,8 @@ class USDImportTest(AbstractUSDTest):
# Utility function to round each component of a vector to a few digits. The "+ 0" is to
# ensure that any negative zeros (-0.0) are converted to positive zeros (0.0).
@staticmethod
def round_vector(vector):
return [round(c, 5) + 0 for c in vector]
def round_vector(vector, digits=5):
return [round(c, digits) + 0 for c in vector]
def test_import_operator(self):
"""Test running the import operator on valid and invalid files."""
@@ -863,6 +863,58 @@ class USDImportTest(AbstractUSDTest):
self.assertEqual(3, vertical_points)
self.assertEqual(2, horizontal_points)
def test_import_point_instancer_animation(self):
"""Test importing an animated point instancer setup."""
infile = str(self.testdir / "usd_point_instancer_anim.usda")
res = bpy.ops.wm.usd_import(filepath=infile)
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
prev_unique_positions = set()
prev_unique_scales = set()
prev_unique_quats = set()
# Check all frames to ensure instances are moving correctly
for frame in range(1, 5):
bpy.context.scene.frame_set(frame)
depsgraph = bpy.context.evaluated_depsgraph_get()
# Gather the instance data in a set so we can detect unique values
unique_positions = set()
unique_scales = set()
unique_quats = set()
mesh_count = 0
for inst in depsgraph.object_instances:
if inst.is_instance and inst.object.type == 'MESH':
mesh_count += 1
unique_positions.add(tuple(self.round_vector(inst.matrix_world.to_translation())))
unique_scales.add(tuple(self.round_vector(inst.matrix_world.to_scale(), 1)))
unique_quats.add(tuple(self.round_vector(inst.matrix_world.to_quaternion())))
# There should be 6 total mesh instances
self.assertEqual(mesh_count, 6)
# Positions: All positions should be unique during each frame.
# Scale and Orientation: One unique value on frame 1. Subsequent frames have different
# combinations of unique values.
self.assertEqual(len(unique_positions), 6, f"Frame {frame}: positions are unexpected")
if frame == 1:
self.assertEqual(len(unique_scales), 1, f"Frame {frame}: scales are unexpected")
self.assertEqual(len(unique_quats), 1, f"Frame {frame}: orientations are unexpected")
else:
self.assertEqual(len(unique_scales), 2, f"Frame {frame}: scales are unexpected")
self.assertEqual(len(unique_quats), 3, f"Frame {frame}: orientations are unexpected")
# Every frame is different. Ensure that the current frame's values do NOT match the
# previous frame's data.
self.assertNotEqual(unique_positions, prev_unique_positions)
self.assertNotEqual(unique_scales, prev_unique_scales)
self.assertNotEqual(unique_quats, prev_unique_quats)
prev_unique_positions = unique_positions
prev_unique_scales = unique_scales
prev_unique_quats = unique_quats
def test_import_light_types(self):
"""Test importing light types and attributes."""