1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# <pep8 compliant> 20 21""" 22./blender.bin --background -noaudio --factory-startup --python tests/python/bl_alembic_io_test.py -- --testdir /path/to/lib/tests/alembic 23""" 24 25import math 26import pathlib 27import sys 28import tempfile 29import unittest 30 31import bpy 32from mathutils import Euler, Matrix, Vector 33 34args = None 35 36 37class AbstractAlembicTest(unittest.TestCase): 38 @classmethod 39 def setUpClass(cls): 40 cls.testdir = args.testdir 41 42 def setUp(self): 43 self.assertTrue(self.testdir.exists(), 44 'Test dir %s should exist' % self.testdir) 45 46 # Make sure we always start with a known-empty file. 47 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend")) 48 49 def assertAlmostEqualFloatArray(self, actual, expect, places=6, delta=None): 50 """Asserts that the arrays of floats are almost equal.""" 51 52 self.assertEqual(len(actual), len(expect), 53 'Actual array has %d items, expected %d' % (len(actual), len(expect))) 54 55 for idx, (act, exp) in enumerate(zip(actual, expect)): 56 self.assertAlmostEqual(act, exp, places=places, delta=delta, 57 msg='%f != %f at index %d' % (act, exp, idx)) 58 59 60class SimpleImportTest(AbstractAlembicTest): 61 def test_import_cube_hierarchy(self): 62 res = bpy.ops.wm.alembic_import( 63 filepath=str(self.testdir / "cubes-hierarchy.abc"), 64 as_background_job=False) 65 self.assertEqual({'FINISHED'}, res) 66 67 # The objects should be linked to scene.collection in Blender 2.8, 68 # and to scene in Blender 2.7x. 69 objects = bpy.context.scene.collection.objects 70 self.assertEqual(13, len(objects)) 71 72 # Test the hierarchy. 73 self.assertIsNone(objects['Cube'].parent) 74 self.assertEqual(objects['Cube'], objects['Cube_001'].parent) 75 self.assertEqual(objects['Cube'], objects['Cube_002'].parent) 76 self.assertEqual(objects['Cube'], objects['Cube_003'].parent) 77 self.assertEqual(objects['Cube_003'], objects['Cube_004'].parent) 78 self.assertEqual(objects['Cube_003'], objects['Cube_005'].parent) 79 self.assertEqual(objects['Cube_003'], objects['Cube_006'].parent) 80 81 def test_inherit_or_not(self): 82 res = bpy.ops.wm.alembic_import( 83 filepath=str(self.testdir / "T52022-inheritance.abc"), 84 as_background_job=False) 85 self.assertEqual({'FINISHED'}, res) 86 87 # The objects should be linked to scene.collection in Blender 2.8, 88 # and to scene in Blender 2.7x. 89 objects = bpy.context.scene.collection.objects 90 91 # ABC parent is top-level object, which translates to nothing in Blender 92 self.assertIsNone(objects['locator1'].parent) 93 94 # ABC parent is locator1, but locator2 has "inherits Xforms" = false, which 95 # translates to "no parent" in Blender. 96 self.assertIsNone(objects['locator2'].parent) 97 98 depsgraph = bpy.context.evaluated_depsgraph_get() 99 100 # Shouldn't have inherited the ABC parent's transform. 101 loc2 = depsgraph.id_eval_get(objects['locator2']) 102 x, y, z = objects['locator2'].matrix_world.to_translation() 103 self.assertAlmostEqual(0, x) 104 self.assertAlmostEqual(0, y) 105 self.assertAlmostEqual(2, z) 106 107 # ABC parent is inherited and translates to normal parent in Blender. 108 self.assertEqual(objects['locator2'], objects['locatorShape2'].parent) 109 110 # Should have inherited its ABC parent's transform. 111 locshp2 = depsgraph.id_eval_get(objects['locatorShape2']) 112 x, y, z = locshp2.matrix_world.to_translation() 113 self.assertAlmostEqual(0, x) 114 self.assertAlmostEqual(0, y) 115 self.assertAlmostEqual(2, z) 116 117 def test_select_after_import(self): 118 # Add a sphere, so that there is something in the scene, selected, and active, 119 # before we do the Alembic import. 120 bpy.ops.mesh.primitive_uv_sphere_add() 121 sphere = bpy.context.active_object 122 self.assertEqual('Sphere', sphere.name) 123 self.assertEqual([sphere], bpy.context.selected_objects) 124 125 bpy.ops.wm.alembic_import( 126 filepath=str(self.testdir / "cubes-hierarchy.abc"), 127 as_background_job=False) 128 129 # The active object is probably the first one that was imported, but this 130 # behaviour is not defined. At least it should be one of the cubes, and 131 # not the sphere. 132 self.assertNotEqual(sphere, bpy.context.active_object) 133 self.assertTrue('Cube' in bpy.context.active_object.name) 134 135 # All cubes should be selected, but the sphere shouldn't be. 136 for ob in bpy.data.objects: 137 self.assertEqual('Cube' in ob.name, ob.select_get()) 138 139 def test_change_path_constraint(self): 140 fname = 'cube-rotating1.abc' 141 abc = self.testdir / fname 142 relpath = bpy.path.relpath(str(abc)) 143 144 res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False) 145 self.assertEqual({'FINISHED'}, res) 146 cube = bpy.context.active_object 147 148 depsgraph = bpy.context.evaluated_depsgraph_get() 149 150 # Check that the file loaded ok. 151 bpy.context.scene.frame_set(10) 152 cube = depsgraph.id_eval_get(cube) 153 x, y, z = cube.matrix_world.to_euler('XYZ') 154 self.assertAlmostEqual(x, 0) 155 self.assertAlmostEqual(y, 0) 156 self.assertAlmostEqual(z, math.pi / 2, places=5) 157 158 # Change path from absolute to relative. This should not break the animation. 159 bpy.context.scene.frame_set(1) 160 bpy.data.cache_files[fname].filepath = relpath 161 bpy.context.scene.frame_set(10) 162 163 cube = depsgraph.id_eval_get(cube) 164 x, y, z = cube.matrix_world.to_euler('XYZ') 165 self.assertAlmostEqual(x, 0) 166 self.assertAlmostEqual(y, 0) 167 self.assertAlmostEqual(z, math.pi / 2, places=5) 168 169 # Replace the Alembic file; this should apply new animation. 170 bpy.data.cache_files[fname].filepath = relpath.replace('1.abc', '2.abc') 171 depsgraph.update() 172 173 cube = depsgraph.id_eval_get(cube) 174 x, y, z = cube.matrix_world.to_euler('XYZ') 175 self.assertAlmostEqual(x, math.pi / 2, places=5) 176 self.assertAlmostEqual(y, 0) 177 self.assertAlmostEqual(z, 0) 178 179 def test_change_path_modifier(self): 180 fname = 'animated-mesh.abc' 181 abc = self.testdir / fname 182 relpath = bpy.path.relpath(str(abc)) 183 184 res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False) 185 self.assertEqual({'FINISHED'}, res) 186 plane = bpy.context.active_object 187 188 depsgraph = bpy.context.evaluated_depsgraph_get() 189 190 # Check that the file loaded ok. 191 bpy.context.scene.frame_set(6) 192 scene = bpy.context.scene 193 plane_eval = plane.evaluated_get(depsgraph) 194 mesh = plane_eval.to_mesh() 195 self.assertAlmostEqual(-1, mesh.vertices[0].co.x) 196 self.assertAlmostEqual(-1, mesh.vertices[0].co.y) 197 self.assertAlmostEqual(0.5905638933181763, mesh.vertices[0].co.z) 198 plane_eval.to_mesh_clear() 199 200 # Change path from absolute to relative. This should not break the animation. 201 scene.frame_set(1) 202 bpy.data.cache_files[fname].filepath = relpath 203 scene.frame_set(6) 204 205 plane_eval = plane.evaluated_get(depsgraph) 206 mesh = plane_eval.to_mesh() 207 self.assertAlmostEqual(1, mesh.vertices[3].co.x) 208 self.assertAlmostEqual(1, mesh.vertices[3].co.y) 209 self.assertAlmostEqual(0.5905638933181763, mesh.vertices[3].co.z) 210 plane_eval.to_mesh_clear() 211 212 def test_import_long_names(self): 213 # This file contains very long names. The longest name is 4047 chars. 214 bpy.ops.wm.alembic_import( 215 filepath=str(self.testdir / "long-names.abc"), 216 as_background_job=False) 217 218 self.assertIn('Cube', bpy.data.objects) 219 self.assertEqual('CubeShape', bpy.data.objects['Cube'].data.name) 220 221 222class VertexColourImportTest(AbstractAlembicTest): 223 def test_import_from_houdini(self): 224 # Houdini saved "face-varying", and as RGB. 225 res = bpy.ops.wm.alembic_import( 226 filepath=str(self.testdir / "vertex-colours-houdini.abc"), 227 as_background_job=False) 228 self.assertEqual({'FINISHED'}, res) 229 230 ob = bpy.context.active_object 231 layer = ob.data.vertex_colors['Cf'] # MeshLoopColorLayer 232 233 # Test some known-good values. 234 self.assertAlmostEqualFloatArray(layer.data[0].color, (0, 0, 0, 1.0)) 235 self.assertAlmostEqualFloatArray(layer.data[98].color, (0.9019607, 0.4745098, 0.2666666, 1.0)) 236 self.assertAlmostEqualFloatArray(layer.data[99].color, (0.8941176, 0.4705882, 0.2627451, 1.0)) 237 238 def test_import_from_blender(self): 239 # Blender saved per-vertex, and as RGBA. 240 res = bpy.ops.wm.alembic_import( 241 filepath=str(self.testdir / "vertex-colours-blender.abc"), 242 as_background_job=False) 243 self.assertEqual({'FINISHED'}, res) 244 245 ob = bpy.context.active_object 246 layer = ob.data.vertex_colors['Cf'] # MeshLoopColorLayer 247 248 # Test some known-good values. 249 self.assertAlmostEqualFloatArray(layer.data[0].color, (1.0, 0.0156862, 0.3607843, 1.0)) 250 self.assertAlmostEqualFloatArray(layer.data[98].color, (0.0941176, 0.1215686, 0.9137254, 1.0)) 251 self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0)) 252 253 254class CameraExportImportTest(unittest.TestCase): 255 names = [ 256 'CAM_Unit_Transform', 257 'CAM_Look_+Y', 258 'CAM_Static_Child_Left', 259 'CAM_Static_Child_Right', 260 'Static_Child', 261 'CAM_Animated', 262 'CAM_Animated_Child_Left', 263 'CAM_Animated_Child_Right', 264 'Animated_Child', 265 ] 266 267 def setUp(self): 268 self._tempdir = tempfile.TemporaryDirectory() 269 self.tempdir = pathlib.Path(self._tempdir.name) 270 271 def tearDown(self): 272 # Unload the current blend file to release the imported Alembic file. 273 # This is necessary on Windows in order to be able to delete the 274 # temporary ABC file. 275 bpy.ops.wm.read_homefile() 276 self._tempdir.cleanup() 277 278 def test_export_hierarchy(self): 279 self.do_export_import_test(flatten=False) 280 281 # Double-check that the export was hierarchical. 282 objects = bpy.context.scene.collection.objects 283 for name in self.names: 284 if 'Child' in name: 285 self.assertIsNotNone(objects[name].parent) 286 else: 287 self.assertIsNone(objects[name].parent) 288 289 def test_export_flattened(self): 290 self.do_export_import_test(flatten=True) 291 292 # Double-check that the export was flat. 293 objects = bpy.context.scene.collection.objects 294 for name in self.names: 295 self.assertIsNone(objects[name].parent) 296 297 def do_export_import_test(self, *, flatten: bool): 298 bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "camera_transforms.blend")) 299 300 abc_path = self.tempdir / "camera_transforms.abc" 301 self.assertIn('FINISHED', bpy.ops.wm.alembic_export( 302 filepath=str(abc_path), 303 renderable_only=False, 304 flatten=flatten, 305 )) 306 307 # Re-import what we just exported into an empty file. 308 bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend")) 309 self.assertIn('FINISHED', bpy.ops.wm.alembic_import(filepath=str(abc_path))) 310 311 # Test that the import was ok. 312 bpy.context.scene.frame_set(1) 313 self.loc_rot_scale('CAM_Unit_Transform', (0, 0, 0), (0, 0, 0)) 314 315 self.loc_rot_scale('CAM_Look_+Y', (2, 0, 0), (90, 0, 0)) 316 self.loc_rot_scale('CAM_Static_Child_Left', (2 - 0.15, 0, 0), (90, 0, 0)) 317 self.loc_rot_scale('CAM_Static_Child_Right', (2 + 0.15, 0, 0), (90, 0, 0)) 318 self.loc_rot_scale('Static_Child', (2, 0, 1), (90, 0, 0)) 319 320 self.loc_rot_scale('CAM_Animated', (4, 0, 0), (90, 0, 0)) 321 self.loc_rot_scale('CAM_Animated_Child_Left', (4 - 0.15, 0, 0), (90, 0, 0)) 322 self.loc_rot_scale('CAM_Animated_Child_Right', (4 + 0.15, 0, 0), (90, 0, 0)) 323 self.loc_rot_scale('Animated_Child', (4, 0, 1), (90, 0, 0)) 324 325 bpy.context.scene.frame_set(10) 326 327 self.loc_rot_scale('CAM_Animated', (4, 1, 2), (90, 0, 25)) 328 self.loc_rot_scale('CAM_Animated_Child_Left', (3.864053, 0.936607, 2), (90, 0, 25)) 329 self.loc_rot_scale('CAM_Animated_Child_Right', (4.135946, 1.063392, 2), (90, 0, 25)) 330 self.loc_rot_scale('Animated_Child', (4, 1, 3), (90, -45, 25)) 331 332 def loc_rot_scale(self, name: str, expect_loc, expect_rot_deg): 333 """Assert world loc/rot/scale is OK.""" 334 335 objects = bpy.context.scene.collection.objects 336 depsgraph = bpy.context.evaluated_depsgraph_get() 337 ob_eval = objects[name].evaluated_get(depsgraph) 338 339 actual_loc = ob_eval.matrix_world.to_translation() 340 actual_rot = ob_eval.matrix_world.to_euler('XYZ') 341 actual_scale = ob_eval.matrix_world.to_scale() 342 343 # Precision of the 'almost equal' comparisons. 344 delta_loc = delta_scale = 1e-6 345 delta_rot = math.degrees(1e-6) 346 347 self.assertAlmostEqual(expect_loc[0], actual_loc.x, delta=delta_loc) 348 self.assertAlmostEqual(expect_loc[1], actual_loc.y, delta=delta_loc) 349 self.assertAlmostEqual(expect_loc[2], actual_loc.z, delta=delta_loc) 350 351 self.assertAlmostEqual(expect_rot_deg[0], math.degrees(actual_rot.x), delta=delta_rot) 352 self.assertAlmostEqual(expect_rot_deg[1], math.degrees(actual_rot.y), delta=delta_rot) 353 self.assertAlmostEqual(expect_rot_deg[2], math.degrees(actual_rot.z), delta=delta_rot) 354 355 # This test doesn't use scale. 356 self.assertAlmostEqual(1, actual_scale.x, delta=delta_scale) 357 self.assertAlmostEqual(1, actual_scale.y, delta=delta_scale) 358 self.assertAlmostEqual(1, actual_scale.z, delta=delta_scale) 359 360 361def main(): 362 global args 363 import argparse 364 365 if '--' in sys.argv: 366 argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] 367 else: 368 argv = sys.argv 369 370 parser = argparse.ArgumentParser() 371 parser.add_argument('--testdir', required=True, type=pathlib.Path) 372 args, remaining = parser.parse_known_args(argv) 373 374 unittest.main(argv=remaining) 375 376 377if __name__ == "__main__": 378 main() 379