Alembic: support camera and visibility animation via FCurves

This adds support to import animations on the various camera properties
and on object visibilities via FCurves for non-sequence Alembic files.

This adds a single routine `create_keyframes` to create FCurves for all
objects which need some for the entire duration of the animation in the
archive.

The FCurves creation and key frame addition are done via some newly
added `FCurveCreationHelper` objects which are created and gathered
during object creation if need be.

For camera data animation, as Alembic does not separate the various
properties into separate sample arrays, we initially create FCurves for
all properties and remove the ones which were unnecessary at the end.

Ref: #69191
Ref: #54050

See PR for video examples.

Pull Request: https://projects.blender.org/blender/blender/pulls/158321
This commit is contained in:
Kévin Dietrich 2026-05-22 16:08:34 +02:00 • committed by Kévin Dietrich
parent 0972fef660
commit fab51c3e35
11 changed files with 504 additions and 7 deletions

View file

@ -499,6 +499,83 @@ class AlembicVisibilityImportTests(AbstractAlembicTest):
self.assertObjectVisible('HIDDEN')
class AlembicAnimatedVisibilityImportTests(AbstractAlembicTest):
def test_import_visibility_animated(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "visibility-animated.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
view_layer = bpy.context.view_layer
# The object was written to be visible on frames following the Fibonacci sequence
a = 0
b = 1
next_fib = a + b
# Compare visibility against the expected value for every frame
for frame in range(1, 25):
bpy.context.scene.frame_set(frame)
depsgraph = bpy.context.evaluated_depsgraph_get()
point_object = bpy.data.objects["ANIMATED"].evaluated_get(depsgraph)
is_hidden = True
if frame == next_fib:
a = b
b = next_fib
next_fib = a + b
is_hidden = False
# Unlike static visibility, animated visibility by default should not set the visibility on the `Base`
self.assertEqual(point_object.hide_get(view_layer=view_layer), False)
self.assertEqual(point_object.hide_viewport, is_hidden)
self.assertEqual(point_object.hide_render, is_hidden)
class AlembicAnimatedCameraImportTests(AbstractAlembicTest):
def test_import_camera_data_animation(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "camera-data-animated.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
view_layer = bpy.context.view_layer
# Compare Blender data against the expected value for every frame
for frame in range(1, 26):
bpy.context.scene.frame_set(frame)
depsgraph = bpy.context.evaluated_depsgraph_get()
camera_object = bpy.data.objects["Camera"].evaluated_get(depsgraph)
camera = camera_object.data
lens = 50.0 + frame
shift = (frame - 1) * (1.0 / 24.0)
clip = frame
self.assertEqual(
lens,
camera.lens,
f"Frame {frame}: {camera_object.name} lens values do not match")
self.assertAlmostEqual(
shift,
camera.shift_x,
places=6,
msg=f"Frame {frame}: {camera_object.name} shift_x values do not match")
self.assertAlmostEqual(
shift,
camera.shift_y,
places=6,
msg=f"Frame {frame}: {camera_object.name} shift_y values do not match")
self.assertEqual(
clip,
camera.clip_start,
f"Frame {frame}: {camera_object.name} clip_start values do not match")
class AlembicImportComparisonTests(unittest.TestCase):
@classmethod
def setUpClass(cls):