# 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()