Merge branch 'blender2.7'
[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         # Shouldn't have inherited the ABC parent's transform.
96         loc2 = bpy.context.depsgraph.id_eval_get(objects['locator2'])
97         x, y, z = objects['locator2'].matrix_world.to_translation()
98         self.assertAlmostEqual(0, x)
99         self.assertAlmostEqual(0, y)
100         self.assertAlmostEqual(2, z)
101
102         # ABC parent is inherited and translates to normal parent in Blender.
103         self.assertEqual(objects['locator2'], objects['locatorShape2'].parent)
104
105         # Should have inherited its ABC parent's transform.
106         locshp2 = bpy.context.depsgraph.id_eval_get(objects['locatorShape2'])
107         x, y, z = locshp2.matrix_world.to_translation()
108         self.assertAlmostEqual(0, x)
109         self.assertAlmostEqual(0, y)
110         self.assertAlmostEqual(2, z)
111
112     def test_select_after_import(self):
113         # Add a sphere, so that there is something in the scene, selected, and active,
114         # before we do the Alembic import.
115         bpy.ops.mesh.primitive_uv_sphere_add()
116         sphere = bpy.context.active_object
117         self.assertEqual('Sphere', sphere.name)
118         self.assertEqual([sphere], bpy.context.selected_objects)
119
120         bpy.ops.wm.alembic_import(
121             filepath=str(self.testdir / "cubes-hierarchy.abc"),
122             as_background_job=False)
123
124         # The active object is probably the first one that was imported, but this
125         # behaviour is not defined. At least it should be one of the cubes, and
126         # not the sphere.
127         self.assertNotEqual(sphere, bpy.context.active_object)
128         self.assertTrue('Cube' in bpy.context.active_object.name)
129
130         # All cubes should be selected, but the sphere shouldn't be.
131         for ob in bpy.data.objects:
132             self.assertEqual('Cube' in ob.name, ob.select_get())
133
134     def test_change_path_constraint(self):
135         import math
136
137         fname = 'cube-rotating1.abc'
138         abc = self.testdir / fname
139         relpath = bpy.path.relpath(str(abc))
140
141         res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
142         self.assertEqual({'FINISHED'}, res)
143         cube = bpy.context.active_object
144
145         # Check that the file loaded ok.
146         bpy.context.scene.frame_set(10)
147         cube = bpy.context.depsgraph.id_eval_get(cube)
148         x, y, z = cube.matrix_world.to_euler('XYZ')
149         self.assertAlmostEqual(x, 0)
150         self.assertAlmostEqual(y, 0)
151         self.assertAlmostEqual(z, math.pi / 2, places=5)
152
153         # Change path from absolute to relative. This should not break the animation.
154         bpy.context.scene.frame_set(1)
155         bpy.data.cache_files[fname].filepath = relpath
156         bpy.context.scene.frame_set(10)
157
158         cube = bpy.context.depsgraph.id_eval_get(cube)
159         x, y, z = cube.matrix_world.to_euler('XYZ')
160         self.assertAlmostEqual(x, 0)
161         self.assertAlmostEqual(y, 0)
162         self.assertAlmostEqual(z, math.pi / 2, places=5)
163
164         # Replace the Alembic file; this should apply new animation.
165         bpy.data.cache_files[fname].filepath = relpath.replace('1.abc', '2.abc')
166         bpy.context.scene.update()
167
168         if args.with_legacy_depsgraph:
169             bpy.context.scene.frame_set(10)
170
171         cube = bpy.context.depsgraph.id_eval_get(cube)
172         x, y, z = cube.matrix_world.to_euler('XYZ')
173         self.assertAlmostEqual(x, math.pi / 2, places=5)
174         self.assertAlmostEqual(y, 0)
175         self.assertAlmostEqual(z, 0)
176
177     def test_change_path_modifier(self):
178         fname = 'animated-mesh.abc'
179         abc = self.testdir / fname
180         relpath = bpy.path.relpath(str(abc))
181
182         res = bpy.ops.wm.alembic_import(filepath=str(abc), as_background_job=False)
183         self.assertEqual({'FINISHED'}, res)
184         plane = bpy.context.active_object
185
186         # Check that the file loaded ok.
187         bpy.context.scene.frame_set(6)
188         scene = bpy.context.scene
189         mesh = plane.to_mesh(bpy.context.depsgraph, True, True, False)
190         self.assertAlmostEqual(-1, mesh.vertices[0].co.x)
191         self.assertAlmostEqual(-1, mesh.vertices[0].co.y)
192         self.assertAlmostEqual(0.5905638933181763, mesh.vertices[0].co.z)
193
194         # Change path from absolute to relative. This should not break the animation.
195         scene.frame_set(1)
196         bpy.data.cache_files[fname].filepath = relpath
197         scene.frame_set(6)
198
199         mesh = plane.to_mesh(bpy.context.depsgraph, True, True, False)
200         self.assertAlmostEqual(1, mesh.vertices[3].co.x)
201         self.assertAlmostEqual(1, mesh.vertices[3].co.y)
202         self.assertAlmostEqual(0.5905638933181763, mesh.vertices[3].co.z)
203
204     def test_import_long_names(self):
205         # This file contains very long names. The longest name is 4047 chars.
206         bpy.ops.wm.alembic_import(
207             filepath=str(self.testdir / "long-names.abc"),
208             as_background_job=False)
209
210         self.assertIn('Cube', bpy.data.objects)
211         self.assertEqual('CubeShape', bpy.data.objects['Cube'].data.name)
212
213
214 class VertexColourImportTest(AbstractAlembicTest):
215     def test_import_from_houdini(self):
216         # Houdini saved "face-varying", and as RGB.
217         res = bpy.ops.wm.alembic_import(
218             filepath=str(self.testdir / "vertex-colours-houdini.abc"),
219             as_background_job=False)
220         self.assertEqual({'FINISHED'}, res)
221
222         ob = bpy.context.active_object
223         layer = ob.data.vertex_colors['Cf']  # MeshLoopColorLayer
224
225         # Test some known-good values.
226         self.assertAlmostEqualFloatArray(layer.data[0].color, (0, 0, 0, 1.0))
227         self.assertAlmostEqualFloatArray(layer.data[98].color, (0.9019607, 0.4745098, 0.2666666, 1.0))
228         self.assertAlmostEqualFloatArray(layer.data[99].color, (0.8941176, 0.4705882, 0.2627451, 1.0))
229
230     def test_import_from_blender(self):
231         # Blender saved per-vertex, and as RGBA.
232         res = bpy.ops.wm.alembic_import(
233             filepath=str(self.testdir / "vertex-colours-blender.abc"),
234             as_background_job=False)
235         self.assertEqual({'FINISHED'}, res)
236
237         ob = bpy.context.active_object
238         layer = ob.data.vertex_colors['Cf']  # MeshLoopColorLayer
239
240         # Test some known-good values.
241         self.assertAlmostEqualFloatArray(layer.data[0].color, (1.0, 0.0156862, 0.3607843, 1.0))
242         self.assertAlmostEqualFloatArray(layer.data[98].color, (0.0941176, 0.1215686, 0.9137254, 1.0))
243         self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0))
244
245
246 def main():
247     global args
248     import argparse
249
250     if '--' in sys.argv:
251         argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
252     else:
253         argv = sys.argv
254
255     parser = argparse.ArgumentParser()
256     parser.add_argument('--testdir', required=True, type=pathlib.Path)
257     parser.add_argument('--with-legacy-depsgraph', default=False,
258                         type=lambda v: v in {'ON', 'YES', 'TRUE'})
259     args, remaining = parser.parse_known_args(argv)
260
261     unittest.main(argv=remaining)
262
263
264 if __name__ == "__main__":
265     import traceback
266     # So a python error exits Blender itself too
267     try:
268         main()
269     except SystemExit:
270         raise
271     except:
272         traceback.print_exc()
273         sys.exit(1)