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