blender/tests/python/bl_alembic_io_test.py
Kévin Dietrich cc44ebbd24 Alembic: remember default and active color attributes
Prior to version 3.5, when importing an Alembic file the default and
active color attributes were set to the first color layer. Since then,
as the default and active color attributes are stored as strings on the
Mesh object, importing an Alembic file leaves those uninitialized.

Note that this only worked prior to 3.5 as the indices for the active
and default layers were initialized by default to zero.

Remembering those default and active attributes is necessary after
an import for e.g. being able to visualize the colors using viewport
shading overlays.

As Alembic does not have a concept of default or active color
attribute, this writes the name of those attributes in the mesh user
properties during export.

During import, we lookup those properties and use them to set the
default and active color layers. If the properties do not exist, we
default to the first color layer.

This adds a test for the new export and import of the default and
active names, and updates old tests to check that the fallback to the
first layer works.

Pull Request: https://projects.blender.org/blender/blender/pulls/164075
2026-09-21 21:11:36 +02:00

796 lines
31 KiB
Python

# SPDX-FileCopyrightText: 2017-2022 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
./blender.bin --background --factory-startup --python tests/python/bl_alembic_io_test.py -- --testdir /path/to/tests/files/alembic
"""
import math
import pathlib
import sys
import tempfile
import unittest
import bpy
sys.path.append(str(pathlib.Path(__file__).parent.absolute()))
args = None
class AbstractAlembicTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.testdir = args.testdir
def setUp(self):
self.assertTrue(self.testdir.exists(),
'Test dir %s should exist' % self.testdir)
# Make sure we always start with a known-empty file.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
def assertAlmostEqualFloatArray(self, actual, expect, places=6, delta=None):
"""Asserts that the arrays of floats are almost equal."""
self.assertEqual(len(actual), len(expect),
'Actual array has %d items, expected %d' % (len(actual), len(expect)))
for idx, (act, exp) in enumerate(zip(actual, expect)):
self.assertAlmostEqual(act, exp, places=places, delta=delta,
msg='%f != %f at index %d' % (act, exp, idx))
class SimpleImportTest(AbstractAlembicTest):
def test_import_cube_hierarchy(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "cubes-hierarchy.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
# The objects should be linked to scene.collection in Blender 2.8,
# and to scene in Blender 2.7x.
objects = bpy.context.scene.collection.objects
self.assertEqual(13, len(objects))
# Test the hierarchy.
self.assertIsNone(objects['Cube'].parent)
self.assertEqual(objects['Cube'], objects['Cube_001'].parent)
self.assertEqual(objects['Cube'], objects['Cube_002'].parent)
self.assertEqual(objects['Cube'], objects['Cube_003'].parent)
self.assertEqual(objects['Cube_003'], objects['Cube_004'].parent)
self.assertEqual(objects['Cube_003'], objects['Cube_005'].parent)
self.assertEqual(objects['Cube_003'], objects['Cube_006'].parent)
def test_inherit_or_not(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "T52022-inheritance.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
# The objects should be linked to scene.collection in Blender 2.8,
# and to scene in Blender 2.7x.
objects = bpy.context.scene.collection.objects
# ABC parent is top-level object, which translates to nothing in Blender
self.assertIsNone(objects['locator1'].parent)
# ABC parent is locator1, but locator2 has "inherits Xforms" = false, which
# translates to "no parent" in Blender.
self.assertIsNone(objects['locator2'].parent)
depsgraph = bpy.context.evaluated_depsgraph_get()
# Shouldn't have inherited the ABC parent's transform.
loc2 = depsgraph.id_eval_get(objects['locator2'])
x, y, z = objects['locator2'].matrix_world.to_translation()
self.assertAlmostEqual(0, x)
self.assertAlmostEqual(0, y)
self.assertAlmostEqual(2, z)
# ABC parent is inherited and translates to normal parent in Blender.
self.assertEqual(objects['locator2'], objects['locatorShape2'].parent)
# Should have inherited its ABC parent's transform.
locshp2 = depsgraph.id_eval_get(objects['locatorShape2'])
x, y, z = locshp2.matrix_world.to_translation()
self.assertAlmostEqual(0, x)
self.assertAlmostEqual(0, y)
self.assertAlmostEqual(2, z)
def test_select_after_import(self):
# Add a sphere, so that there is something in the scene, selected, and active,
# before we do the Alembic import.
bpy.ops.mesh.primitive_uv_sphere_add()
sphere = bpy.context.active_object
self.assertEqual('Sphere', sphere.name)
self.assertEqual([sphere], bpy.context.selected_objects)
bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "cubes-hierarchy.abc"),
as_background_job=False)
# The active object is probably the first one that was imported, but this
# behavior is not defined. At least it should be one of the cubes, and
# not the sphere.
self.assertNotEqual(sphere, bpy.context.active_object)
self.assertTrue('Cube' in bpy.context.active_object.name)
# All cubes should be selected, but the sphere shouldn't be.
for ob in bpy.data.objects:
self.assertEqual('Cube' in ob.name, ob.select_get())
def test_change_path_constraint(self):
fname = 'cube-rotating1.abc'
abc = self.testdir / fname
relpath = bpy.path.relpath(str(abc))
res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
self.assertEqual({'FINISHED'}, res)
cube = bpy.context.active_object
depsgraph = bpy.context.evaluated_depsgraph_get()
# Check that the file loaded ok.
bpy.context.scene.frame_set(10)
cube = depsgraph.id_eval_get(cube)
x, y, z = cube.matrix_world.to_euler('XYZ')
self.assertAlmostEqual(x, 0)
self.assertAlmostEqual(y, 0)
self.assertAlmostEqual(z, math.pi / 2, places=5)
# Change path from absolute to relative. This should not break the animation.
bpy.context.scene.frame_set(1)
bpy.data.cache_files[fname].filepath = relpath
bpy.context.scene.frame_set(10)
cube = depsgraph.id_eval_get(cube)
x, y, z = cube.matrix_world.to_euler('XYZ')
self.assertAlmostEqual(x, 0)
self.assertAlmostEqual(y, 0)
self.assertAlmostEqual(z, math.pi / 2, places=5)
# Replace the Alembic file; this should apply new animation.
bpy.data.cache_files[fname].filepath = relpath.replace('1.abc', '2.abc')
depsgraph.update()
cube = depsgraph.id_eval_get(cube)
x, y, z = cube.matrix_world.to_euler('XYZ')
self.assertAlmostEqual(x, math.pi / 2, places=5)
self.assertAlmostEqual(y, 0)
self.assertAlmostEqual(z, 0)
def test_change_path_modifier(self):
fname = 'animated-mesh.abc'
abc = self.testdir / fname
relpath = bpy.path.relpath(str(abc))
res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
self.assertEqual({'FINISHED'}, res)
plane = bpy.context.active_object
depsgraph = bpy.context.evaluated_depsgraph_get()
# Check that the file loaded ok.
bpy.context.scene.frame_set(6)
scene = bpy.context.scene
plane_eval = plane.evaluated_get(depsgraph)
mesh = plane_eval.to_mesh()
self.assertAlmostEqual(-1, mesh.vertices[0].co.x)
self.assertAlmostEqual(-1, mesh.vertices[0].co.y)
self.assertAlmostEqual(0.5905638933181763, mesh.vertices[0].co.z)
plane_eval.to_mesh_clear()
# Change path from absolute to relative. This should not break the animation.
scene.frame_set(1)
bpy.data.cache_files[fname].filepath = relpath
scene.frame_set(6)
plane_eval = plane.evaluated_get(depsgraph)
mesh = plane_eval.to_mesh()
self.assertAlmostEqual(1, mesh.vertices[3].co.x)
self.assertAlmostEqual(1, mesh.vertices[3].co.y)
self.assertAlmostEqual(0.5905638933181763, mesh.vertices[3].co.z)
plane_eval.to_mesh_clear()
def test_import_long_names(self):
# This file contains very long names. The longest name is 4047 chars.
bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "long-names.abc"),
as_background_job=False)
self.assertIn('Cube', bpy.data.objects)
self.assertEqual('CubeShape', bpy.data.objects['Cube'].data.name)
class VertexColourImportTest(AbstractAlembicTest):
def test_import_from_houdini(self):
# Houdini saved "face-varying", and as RGB.
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "vertex-colours-houdini.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
ob = bpy.context.active_object
layer = ob.data.vertex_colors['Cf'] # MeshLoopColorLayer
# Test some known-good values.
self.assertAlmostEqualFloatArray(layer.data[0].color, (0, 0, 0, 1.0))
self.assertAlmostEqualFloatArray(layer.data[98].color, (0.9019607, 0.4745098, 0.2666666, 1.0))
self.assertAlmostEqualFloatArray(layer.data[99].color, (0.8941176, 0.4705882, 0.2627451, 1.0))
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Cf')
self.assertEqual(mesh.color_attributes.default_color_name, 'Cf')
def test_import_from_blender(self):
# Blender saved per-vertex, and as RGBA.
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "vertex-colours-blender.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
ob = bpy.context.active_object
layer = ob.data.vertex_colors['Cf'] # MeshLoopColorLayer
# Test some known-good values.
self.assertAlmostEqualFloatArray(layer.data[0].color, (1.0, 0.0156862, 0.3607843, 1.0))
self.assertAlmostEqualFloatArray(layer.data[98].color, (0.0941176, 0.1215686, 0.9137254, 1.0))
self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0))
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Cf')
self.assertEqual(mesh.color_attributes.default_color_name, 'Cf')
class VertexColourImportExportTest(AbstractAlembicTest):
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
self.tempdir = pathlib.Path(self._tempdir.name)
def test_export_import_default_active(self):
"""Test export and import of the default and active color attribute names"""
abc_path = str(self.tempdir / "vertex-colours-default-active.abc")
# Export
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "vertex-colours-default-active.blend"))
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
filepath=abc_path,
vcolors=True
))
# Import back
res = bpy.ops.wm.alembic_import(filepath=abc_path, as_background_job=False)
self.assertEqual({'FINISHED'}, res)
ob = bpy.context.active_object
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Active')
self.assertEqual(mesh.color_attributes.default_color_name, 'Default')
class CameraExportImportTest(unittest.TestCase):
names = [
'CAM_Unit_Transform',
'CAM_Look_+Y',
'CAM_Static_Child_Left',
'CAM_Static_Child_Right',
'Static_Child',
'CAM_Animated',
'CAM_Animated_Child_Left',
'CAM_Animated_Child_Right',
'Animated_Child',
]
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
self.tempdir = pathlib.Path(self._tempdir.name)
def tearDown(self):
# Unload the current blend file to release the imported Alembic file.
# This is necessary on Windows in order to be able to delete the
# temporary ABC file.
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
self._tempdir.cleanup()
def test_export_hierarchy(self):
self.do_export_import_test(flatten=False)
# Double-check that the export was hierarchical.
objects = bpy.context.scene.collection.objects
for name in self.names:
if 'Child' in name:
self.assertIsNotNone(objects[name].parent)
else:
self.assertIsNone(objects[name].parent)
def test_export_flattened(self):
self.do_export_import_test(flatten=True)
# Double-check that the export was flat.
objects = bpy.context.scene.collection.objects
for name in self.names:
self.assertIsNone(objects[name].parent)
def test_mesh_subd_varying(self):
"""Test meshes with subdivision crease values varying over time."""
abc_path = str(self.tempdir / "mesh_subd_varying.abc")
# Export
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "mesh_subd_varying.blend"))
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
filepath=abc_path,
subdiv_schema=True
))
# Re-import what we just exported into an empty file.
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend"))
bpy.ops.wm.alembic_import(
filepath=abc_path,
as_background_job=False)
#
# Validate Mesh data
#
blender_mesh1 = bpy.data.objects["mesh_edge_crease"]
blender_mesh2 = bpy.data.objects["mesh_vert_crease"]
# A MeshSequenceCache modifier should be present on every imported object
for blender_mesh in [blender_mesh1, blender_mesh2]:
self.assertTrue(len(blender_mesh.modifiers) >= 1 and blender_mesh.modifiers[0].type ==
'MESH_SEQUENCE_CACHE', f"{blender_mesh.name} has incorrect modifiers")
# Conversion from USD to Blender convention
def sharpness_to_crease(sharpness):
return math.sqrt(sharpness * 0.1)
# Compare Blender data 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()
blender_mesh1_eval = bpy.data.objects["mesh_edge_crease"].evaluated_get(depsgraph)
blender_mesh2_eval = bpy.data.objects["mesh_vert_crease"].evaluated_get(depsgraph)
# The file was written using a simple formula for each frame's crease value
expected_edge_creases = [round(frame / 24.0, 3)] * 48
# Creases are not propagated to edges that do not come from a coarse edge
expected_edge_creases.extend([0.0] * 144)
expected_vert_creases = [round(frame / 24.0, 3)] * 25
# Check crease values
blender_crease_data = [round(d.value, 3) for d in blender_mesh1_eval.data.attributes["crease_edge"].data]
self.assertEqual(
blender_crease_data,
expected_edge_creases,
f"Frame {frame}: {blender_mesh1_eval.name} crease values do not match")
blender_crease_data = [round(d.value, 3) for d in blender_mesh2_eval.data.attributes["crease_vert"].data]
self.assertEqual(
blender_crease_data,
expected_vert_creases,
f"Frame {frame}: {blender_mesh2_eval.name} crease values do not match")
def test_mesh_subd_params(self):
"""Test export of subdivision parameters."""
abc_path = str(self.tempdir / "mesh_subd_params.abc")
# Export
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "mesh_subd_params.blend"))
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
filepath=abc_path,
subdiv_schema=True
))
# Re-import what we just exported into an empty file.
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend"))
bpy.ops.wm.alembic_import(
filepath=abc_path,
as_background_job=False)
uv_smooth_options = [
'NONE',
'PRESERVE_CORNERS',
# NOTE: in Blender this should be PRESERVE_CORNERS_AND_JUNCTIONS, however
# Alembic does not have this concept.
'PRESERVE_CORNERS',
'PRESERVE_CORNERS_JUNCTIONS_AND_CONCAVE',
'PRESERVE_BOUNDARIES',
'SMOOTH_ALL'
]
boundary_smooth_options = [
'PRESERVE_CORNERS',
'ALL'
]
for uv_smooth_index in range(len(uv_smooth_options)):
uv_smooth = uv_smooth_options[uv_smooth_index]
for boundary_smooth_index in range(len(boundary_smooth_options)):
boundary_smooth = boundary_smooth_options[boundary_smooth_index]
obj_name = 'obj{}{}'.format(uv_smooth_index, boundary_smooth_index)
obj = bpy.data.objects[obj_name]
modifier = obj.modifiers['Subdivision']
self.assertEqual(modifier.uv_smooth, uv_smooth)
self.assertEqual(modifier.boundary_smooth, boundary_smooth)
self.assertEqual(modifier.levels, 1 + uv_smooth_index)
self.assertEqual(modifier.render_levels, 2 + uv_smooth_index)
def do_export_import_test(self, *, flatten: bool):
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "camera_transforms.blend"))
abc_path = self.tempdir / "camera_transforms.abc"
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
filepath=str(abc_path),
flatten=flatten,
))
# Re-import what we just exported into an empty file.
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend"))
self.assertIn('FINISHED', bpy.ops.wm.alembic_import(filepath=str(abc_path)))
# Test that the import was ok.
bpy.context.scene.frame_set(1)
self.loc_rot_scale('CAM_Unit_Transform', (0, 0, 0), (0, 0, 0))
self.loc_rot_scale('CAM_Look_+Y', (2, 0, 0), (90, 0, 0))
self.loc_rot_scale('CAM_Static_Child_Left', (2 - 0.15, 0, 0), (90, 0, 0))
self.loc_rot_scale('CAM_Static_Child_Right', (2 + 0.15, 0, 0), (90, 0, 0))
self.loc_rot_scale('Static_Child', (2, 0, 1), (90, 0, 0))
self.loc_rot_scale('CAM_Animated', (4, 0, 0), (90, 0, 0))
self.loc_rot_scale('CAM_Animated_Child_Left', (4 - 0.15, 0, 0), (90, 0, 0))
self.loc_rot_scale('CAM_Animated_Child_Right', (4 + 0.15, 0, 0), (90, 0, 0))
self.loc_rot_scale('Animated_Child', (4, 0, 1), (90, 0, 0))
bpy.context.scene.frame_set(10)
self.loc_rot_scale('CAM_Animated', (4, 1, 2), (90, 0, 25))
self.loc_rot_scale('CAM_Animated_Child_Left', (3.864053, 0.936607, 2), (90, 0, 25))
self.loc_rot_scale('CAM_Animated_Child_Right', (4.135946, 1.063392, 2), (90, 0, 25))
self.loc_rot_scale('Animated_Child', (4, 1, 3), (90, -45, 25))
def loc_rot_scale(self, name: str, expect_loc, expect_rot_deg):
"""Assert world loc/rot/scale is OK."""
objects = bpy.context.scene.collection.objects
depsgraph = bpy.context.evaluated_depsgraph_get()
ob_eval = objects[name].evaluated_get(depsgraph)
actual_loc = ob_eval.matrix_world.to_translation()
actual_rot = ob_eval.matrix_world.to_euler('XYZ')
actual_scale = ob_eval.matrix_world.to_scale()
# Precision of the 'almost equal' comparisons.
delta_loc = delta_scale = 1e-6
delta_rot = math.degrees(1e-6)
self.assertAlmostEqual(expect_loc[0], actual_loc.x, delta=delta_loc)
self.assertAlmostEqual(expect_loc[1], actual_loc.y, delta=delta_loc)
self.assertAlmostEqual(expect_loc[2], actual_loc.z, delta=delta_loc)
self.assertAlmostEqual(expect_rot_deg[0], math.degrees(actual_rot.x), delta=delta_rot)
self.assertAlmostEqual(expect_rot_deg[1], math.degrees(actual_rot.y), delta=delta_rot)
self.assertAlmostEqual(expect_rot_deg[2], math.degrees(actual_rot.z), delta=delta_rot)
# This test doesn't use scale.
self.assertAlmostEqual(1, actual_scale.x, delta=delta_scale)
self.assertAlmostEqual(1, actual_scale.y, delta=delta_scale)
self.assertAlmostEqual(1, actual_scale.z, delta=delta_scale)
class OverrideLayersTest(AbstractAlembicTest):
def test_import_layer(self):
fname = 'cube-base-file.abc'
fname_layer = 'cube-hi-res.abc'
abc = self.testdir / fname
abc_layer = self.testdir / fname_layer
# We need a cache reader to ensure that the data will be updated after adding a layer.
res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False, always_add_cache_reader=True)
self.assertEqual({'FINISHED'}, res)
# Check that the file loaded ok.
cube = bpy.context.active_object
depsgraph = bpy.context.evaluated_depsgraph_get()
scene = bpy.context.scene
cube_eval = cube.evaluated_get(depsgraph)
mesh = cube_eval.to_mesh()
# The base file should be a default cube.
self.assertEqual(len(mesh.vertices), 8)
self.assertEqual(len(mesh.edges), 12)
self.assertEqual(len(mesh.polygons), 6)
# Add a layer.
cache_file = bpy.data.cache_files[fname]
self.assertEqual(len(cache_file.layers), 0)
layer = cache_file.layers.new(filepath=str(abc_layer))
self.assertEqual(len(cache_file.layers), 1)
self.assertIsNotNone(layer)
# The layer added a higher res version of the mesh.
depsgraph = bpy.context.evaluated_depsgraph_get()
cube_eval = cube.evaluated_get(depsgraph)
mesh = cube_eval.to_mesh()
self.assertEqual(len(mesh.vertices), 26)
self.assertEqual(len(mesh.edges), 48)
self.assertEqual(len(mesh.polygons), 24)
# Remove the layer.
cache_file.layers.remove(layer)
self.assertEqual(len(cache_file.layers), 0)
# We should have reverted to the default cube.
depsgraph = bpy.context.evaluated_depsgraph_get()
cube_eval = cube.evaluated_get(depsgraph)
mesh = cube_eval.to_mesh()
self.assertEqual(len(mesh.vertices), 8)
self.assertEqual(len(mesh.edges), 12)
self.assertEqual(len(mesh.polygons), 6)
class AlembicVisibilityImportTests(AbstractAlembicTest):
def assertObjectVisibility(self, ob_name, state):
view_layer = bpy.context.view_layer
ob = bpy.data.objects[ob_name]
self.assertEqual(ob.hide_get(view_layer=view_layer), state)
self.assertEqual(ob.hide_render, state)
def assertObjectHidden(self, ob_name):
self.assertObjectVisibility(ob_name, True)
def assertObjectVisible(self, ob_name):
self.assertObjectVisibility(ob_name, False)
def test_import_visibility_hidden(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "visibility-hidden.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
self.assertObjectHidden('HIDDEN')
self.assertObjectVisible('VISIBLE')
def test_import_visibility_deferred_parent_hidden(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "visibility-parent-hidden-child-deferred.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
self.assertObjectHidden('HIDDEN')
def test_import_visibility_deferred_parent_visible(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "visibility-parent-visible-child-deferred.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
self.assertObjectVisible('VISIBLE')
def test_import_visibility_visible_parent_hidden(self):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / "visibility-parent-hidden-child-visible.abc"),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
# Objects inherit the name of their parent Xform which is called 'HIDDEN' here
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
sensor_width = 35.0 + frame
sensor_height = 23.0 + frame
shift_x = (frame - 1) * (1.0 / 24.0)
shift_y = -(frame - 1) * (1.0 / 24.0)
clip_start = frame
clip_end = frame * 100.0
focus_distance = frame * 10.0
aperture_fstop = frame * 3.0
convergence_distance = frame * 2.0
interocular_distance = frame * 0.05
self.assertEqual(
lens,
camera.lens,
f"Frame {frame}: {camera_object.name} lens values do not match")
self.assertEqual(
sensor_width,
camera.sensor_width,
f"Frame {frame}: {camera_object.name} sensor_width values do not match")
self.assertEqual(
sensor_height,
camera.sensor_height,
f"Frame {frame}: {camera_object.name} sensor_height values do not match")
self.assertAlmostEqual(
shift_x,
camera.shift_x,
places=6,
msg=f"Frame {frame}: {camera_object.name} shift_x values do not match")
self.assertAlmostEqual(
shift_y,
camera.shift_y,
places=6,
msg=f"Frame {frame}: {camera_object.name} shift_y values do not match")
self.assertEqual(
clip_start,
camera.clip_start,
f"Frame {frame}: {camera_object.name} clip_start values do not match")
self.assertEqual(
clip_end,
camera.clip_end,
f"Frame {frame}: {camera_object.name} clip_end values do not match")
self.assertEqual(
focus_distance,
camera.dof.focus_distance,
f"Frame {frame}: {camera_object.name} focus_distance values do not match")
self.assertEqual(
aperture_fstop,
camera.dof.aperture_fstop,
f"Frame {frame}: {camera_object.name} aperture_fstop values do not match")
self.assertEqual(
convergence_distance,
camera.stereo.convergence_distance,
f"Frame {frame}: {camera_object.name} convergence_distance values do not match")
self.assertAlmostEqual(
interocular_distance,
camera.stereo.interocular_distance,
delta=1e-6,
msg=f"Frame {frame}: {camera_object.name} interocular_distance values do not match")
class AlembicAnimatedSchemaImportTests(AbstractAlembicTest):
def do_import_test(self, abc_file_name, first_frame, last_frame, object_name):
res = bpy.ops.wm.alembic_import(
filepath=str(self.testdir / abc_file_name),
as_background_job=False)
self.assertEqual({'FINISHED'}, res)
# The data stored in the attribute is random, so we check that the object
# has a modifier (which should only be added if the data is animated) and
# the frame range for the scene is detected properly.
scene = bpy.context.scene
self.assertEqual(scene.frame_start, first_frame)
self.assertEqual(scene.frame_end, last_frame)
ob = bpy.data.objects[object_name]
self.assertEqual(len(ob.modifiers), 1)
modifier = ob.modifiers[0]
self.assertEqual(modifier.type, 'MESH_SEQUENCE_CACHE')
def test_animated_point_width(self):
self.do_import_test("point-width-animated.abc", 12, 144, "PointCloud")
def test_animated_point_velocity(self):
self.do_import_test("point-velocity-animated.abc", 13, 169, "PointCloud")
def test_animated_curve_width(self):
self.do_import_test("curve-width-animated.abc", 12, 144, "Curves")
def test_animated_curve_velocity(self):
self.do_import_test("curve-velocity-animated.abc", 13, 169, "Curves")
class AlembicImportComparisonTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.testdir = args.testdir
cls.output_dir = args.outdir
def test_import_alembic(self):
comparisondir = self.testdir.joinpath("compare")
input_files = sorted(pathlib.Path(comparisondir).glob("*.abc"))
self.passed_tests = []
self.failed_tests = []
self.updated_tests = []
from modules import io_report
report = io_report.Report("Alembic Import", self.output_dir, comparisondir, comparisondir.joinpath("reference"))
io_report.Report.context_lines = 8
for input_file in input_files:
input_file_path = pathlib.Path(input_file)
io_report.Report.side_to_print_single_line = 5
io_report.Report.side_to_print_multi_line = 3
with self.subTest(input_file_path.stem):
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
ok = report.import_and_check(
input_file, lambda filepath, params: bpy.ops.wm.alembic_import(
filepath=str(input_file), **params))
if not ok:
self.fail(f"{input_file.stem} import result does not match expectations")
report.finish("io_alembic_import")
def main():
global args
import argparse
if '--' in sys.argv:
argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
else:
argv = sys.argv
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
parser.add_argument('--outdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == "__main__":
main()