PyAPI: use public API's for module & builtin access
[blender.git] / tests / python / bl_alembic_import_test.py
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_import_test.py -- --testdir /path/to/lib/tests/alembic
23 """
24
25 import pathlib
26 import sys
27 import unittest
28
29 import bpy
30
31 args = None
32
33
34 class AbstractAlembicTest(unittest.TestCase):
35     @classmethod
36     def setUpClass(cls):
37         cls.testdir = args.testdir
38
39     def setUp(self):
40         self.assertTrue(self.testdir.exists(),
41                         'Test dir %s should exist' % self.testdir)
42
43         # Make sure we always start with a known-empty file.
44         bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
45
46     def assertAlmostEqualFloatArray(self, actual, expect, places=6, delta=None):
47         """Asserts that the arrays of floats are almost equal."""
48
49         self.assertEqual(len(actual), len(expect),
50                          'Actual array has %d items, expected %d' % (len(actual), len(expect)))
51
52         for idx, (act, exp) in enumerate(zip(actual, expect)):
53             self.assertAlmostEqual(act, exp, places=places, delta=delta,
54                                    msg='%f != %f at index %d' % (act, exp, idx))
55
56
57 class SimpleImportTest(AbstractAlembicTest):
58     def test_import_cube_hierarchy(self):
59         res = bpy.ops.wm.alembic_import(
60             filepath=str(self.testdir / "cubes-hierarchy.abc"),
61             as_background_job=False)
62         self.assertEqual({'FINISHED'}, res)
63
64         # The objects should be linked to scene.collection in Blender 2.8,
65         # and to scene in Blender 2.7x.
66         objects = bpy.context.scene.collection.objects
67         self.assertEqual(13, len(objects))
68
69         # Test the hierarchy.
70         self.assertIsNone(objects['Cube'].parent)
71         self.assertEqual(objects['Cube'], objects['Cube_001'].parent)
72         self.assertEqual(objects['Cube'], objects['Cube_002'].parent)
73         self.assertEqual(objects['Cube'], objects['Cube_003'].parent)
74         self.assertEqual(objects['Cube_003'], objects['Cube_004'].parent)
75         self.assertEqual(objects['Cube_003'], objects['Cube_005'].parent)
76         self.assertEqual(objects['Cube_003'], objects['Cube_006'].parent)
77
78     def test_inherit_or_not(self):
79         res = bpy.ops.wm.alembic_import(
80             filepath=str(self.testdir / "T52022-inheritance.abc"),
81             as_background_job=False)
82         self.assertEqual({'FINISHED'}, res)
83
84         # The objects should be linked to scene.collection in Blender 2.8,
85         # and to scene in Blender 2.7x.
86         objects = bpy.context.scene.collection.objects
87
88         # ABC parent is top-level object, which translates to nothing in Blender
89         self.assertIsNone(objects['locator1'].parent)
90
91         # ABC parent is locator1, but locator2 has "inherits Xforms" = false, which
92         # translates to "no parent" in Blender.
93         self.assertIsNone(objects['locator2'].parent)
94
95         depsgraph = bpy.context.evaluated_depsgraph_get()
96
97         # Shouldn't have inherited the ABC parent's transform.
98         loc2 = depsgraph.id_eval_get(objects['locator2'])
99         x, y, z = objects['locator2'].matrix_world.to_translation()
100         self.assertAlmostEqual(0, x)
101         self.assertAlmostEqual(0, y)
102         self.assertAlmostEqual(2, z)
103
104         # ABC parent is inherited and translates to normal parent in Blender.
105         self.assertEqual(objects['locator2'], objects['locatorShape2'].parent)
106
107         # Should have inherited its ABC parent's transform.
108         locshp2 = depsgraph.id_eval_get(objects['locatorShape2'])
109         x, y, z = locshp2.matrix_world.to_translation()
110         self.assertAlmostEqual(0, x)
111         self.assertAlmostEqual(0, y)
112         self.assertAlmostEqual(2, z)
113
114     def test_select_after_import(self):
115         # Add a sphere, so that there is something in the scene, selected, and active,
116         # before we do the Alembic import.
117         bpy.ops.mesh.primitive_uv_sphere_add()
118         sphere = bpy.context.active_object
119         self.assertEqual('Sphere', sphere.name)
120         self.assertEqual([sphere], bpy.context.selected_objects)
121
122         bpy.ops.wm.alembic_import(
123             filepath=str(self.testdir / "cubes-hierarchy.abc"),
124             as_background_job=False)
125
126         # The active object is probably the first one that was imported, but this
127         # behaviour is not defined. At least it should be one of the cubes, and
128         # not the sphere.
129         self.assertNotEqual(sphere, bpy.context.active_object)
130         self.assertTrue('Cube' in bpy.context.active_object.name)
131
132         # All cubes should be selected, but the sphere shouldn't be.
133         for ob in bpy.data.objects:
134             self.assertEqual('Cube' in ob.name, ob.select_get())
135
136     def test_change_path_constraint(self):
137         import math
138
139         fname = 'cube-rotating1.abc'
140         abc = self.testdir / fname
141         relpath = bpy.path.relpath(str(abc))
142
143         res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
144         self.assertEqual({'FINISHED'}, res)
145         cube = bpy.context.active_object
146
147         depsgraph = bpy.context.evaluated_depsgraph_get()
148
149         # Check that the file loaded ok.
150         bpy.context.scene.frame_set(10)
151         cube = depsgraph.id_eval_get(cube)
152         x, y, z = cube.matrix_world.to_euler('XYZ')
153         self.assertAlmostEqual(x, 0)
154         self.assertAlmostEqual(y, 0)
155         self.assertAlmostEqual(z, math.pi / 2, places=5)
156
157         # Change path from absolute to relative. This should not break the animation.
158         bpy.context.scene.frame_set(1)
159         bpy.data.cache_files[fname].filepath = relpath
160         bpy.context.scene.frame_set(10)
161
162         cube = depsgraph.id_eval_get(cube)
163         x, y, z = cube.matrix_world.to_euler('XYZ')
164         self.assertAlmostEqual(x, 0)
165         self.assertAlmostEqual(y, 0)
166         self.assertAlmostEqual(z, math.pi / 2, places=5)
167
168         # Replace the Alembic file; this should apply new animation.
169         bpy.data.cache_files[fname].filepath = relpath.replace('1.abc', '2.abc')
170         depsgraph.update()
171
172         cube = depsgraph.id_eval_get(cube)
173         x, y, z = cube.matrix_world.to_euler('XYZ')
174         self.assertAlmostEqual(x, math.pi / 2, places=5)
175         self.assertAlmostEqual(y, 0)
176         self.assertAlmostEqual(z, 0)
177
178     def test_change_path_modifier(self):
179         fname = 'animated-mesh.abc'
180         abc = self.testdir / fname
181         relpath = bpy.path.relpath(str(abc))
182
183         res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
184         self.assertEqual({'FINISHED'}, res)
185         plane = bpy.context.active_object
186
187         depsgraph = bpy.context.evaluated_depsgraph_get()
188
189         # Check that the file loaded ok.
190         bpy.context.scene.frame_set(6)
191         scene = bpy.context.scene
192         plane_eval = plane.evaluated_get(depsgraph)
193         mesh = plane_eval.to_mesh()
194         self.assertAlmostEqual(-1, mesh.vertices[0].co.x)
195         self.assertAlmostEqual(-1, mesh.vertices[0].co.y)
196         self.assertAlmostEqual(0.5905638933181763, mesh.vertices[0].co.z)
197         plane_eval.to_mesh_clear()
198
199         # Change path from absolute to relative. This should not break the animation.
200         scene.frame_set(1)
201         bpy.data.cache_files[fname].filepath = relpath
202         scene.frame_set(6)
203
204         plane_eval = plane.evaluated_get(depsgraph)
205         mesh = plane_eval.to_mesh()
206         self.assertAlmostEqual(1, mesh.vertices[3].co.x)
207         self.assertAlmostEqual(1, mesh.vertices[3].co.y)
208         self.assertAlmostEqual(0.5905638933181763, mesh.vertices[3].co.z)
209         plane_eval.to_mesh_clear()
210
211     def test_import_long_names(self):
212         # This file contains very long names. The longest name is 4047 chars.
213         bpy.ops.wm.alembic_import(
214             filepath=str(self.testdir / "long-names.abc"),
215             as_background_job=False)
216
217         self.assertIn('Cube', bpy.data.objects)
218         self.assertEqual('CubeShape', bpy.data.objects['Cube'].data.name)
219
220
221 class VertexColourImportTest(AbstractAlembicTest):
222     def test_import_from_houdini(self):
223         # Houdini saved "face-varying", and as RGB.
224         res = bpy.ops.wm.alembic_import(
225             filepath=str(self.testdir / "vertex-colours-houdini.abc"),
226             as_background_job=False)
227         self.assertEqual({'FINISHED'}, res)
228
229         ob = bpy.context.active_object
230         layer = ob.data.vertex_colors['Cf']  # MeshLoopColorLayer
231
232         # Test some known-good values.
233         self.assertAlmostEqualFloatArray(layer.data[0].color, (0, 0, 0, 1.0))
234         self.assertAlmostEqualFloatArray(layer.data[98].color, (0.9019607, 0.4745098, 0.2666666, 1.0))
235         self.assertAlmostEqualFloatArray(layer.data[99].color, (0.8941176, 0.4705882, 0.2627451, 1.0))
236
237     def test_import_from_blender(self):
238         # Blender saved per-vertex, and as RGBA.
239         res = bpy.ops.wm.alembic_import(
240             filepath=str(self.testdir / "vertex-colours-blender.abc"),
241             as_background_job=False)
242         self.assertEqual({'FINISHED'}, res)
243
244         ob = bpy.context.active_object
245         layer = ob.data.vertex_colors['Cf']  # MeshLoopColorLayer
246
247         # Test some known-good values.
248         self.assertAlmostEqualFloatArray(layer.data[0].color, (1.0, 0.0156862, 0.3607843, 1.0))
249         self.assertAlmostEqualFloatArray(layer.data[98].color, (0.0941176, 0.1215686, 0.9137254, 1.0))
250         self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0))
251
252
253 def main():
254     global args
255     import argparse
256
257     if '--' in sys.argv:
258         argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
259     else:
260         argv = sys.argv
261
262     parser = argparse.ArgumentParser()
263     parser.add_argument('--testdir', required=True, type=pathlib.Path)
264     args, remaining = parser.parse_known_args(argv)
265
266     unittest.main(argv=remaining)
267
268
269 if __name__ == "__main__":
270     import traceback
271     # So a python error exits Blender itself too
272     try:
273         main()
274     except SystemExit:
275         raise
276     except:
277         traceback.print_exc()
278         sys.exit(1)