Merge branch 'blender2.7'
[blender.git] / tests / python / bl_mesh_modifiers.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 # Currently this script only generates images from different modifier
22 # combinations and does not validate they work correctly,
23 # this is because we don't get 1:1 match with bmesh.
24 #
25 # Later, we may have a way to check the results are valid.
26
27
28 # ./blender.bin --factory-startup --python tests/python/bl_mesh_modifiers.py
29 #
30
31 import math
32
33 USE_QUICK_RENDER = False
34
35 # -----------------------------------------------------------------------------
36 # utility functions
37
38
39 def render_gl(context, filepath, shade):
40
41     def ctx_shading_type(context, shade):
42         for area in context.window.screen.areas:
43             if area.type == 'VIEW_3D':
44                 space = area.spaces.active
45                 # rv3d = space.region_3d
46                 space.shading.type = shade
47
48     import bpy
49     scene = context.scene
50     render = scene.render
51     render.filepath = filepath
52     render.image_settings.file_format = 'PNG'
53     render.image_settings.color_mode = 'RGB'
54     render.use_file_extension = True
55     render.use_antialiasing = False
56
57     # render size
58     render.resolution_percentage = 100
59     render.resolution_x = 512
60     render.resolution_y = 512
61
62     ctx_shading_type(context, shade)
63
64     # stop to inspect!
65     # if filepath == "test_cube_shell_solidify_subsurf_wp_wire":
66     #     assert(0)
67     # else:
68     #     return
69
70     bpy.ops.render.opengl(write_still=True,
71                           view_context=True)
72
73
74 def render_gl_all_modes(context, obj, filepath=""):
75
76     assert(obj is not None)
77     assert(filepath != "")
78
79     scene = context.scene
80
81     # avoid drawing outline/center dot
82     bpy.ops.object.select_all(action='DESELECT')
83     scene.objects.active = None
84
85     # editmode
86     scene.tool_settings.mesh_select_mode = False, True, False
87
88     # render
89     render_gl(context, filepath + "_ob_solid", shade='SOLID')
90
91     if USE_QUICK_RENDER:
92         return
93
94     render_gl(context, filepath + "_ob_wire", shade='WIREFRAME')
95     render_gl(context, filepath + "_ob_textured", shade='TEXTURED')
96
97     # -------------------------------------------------------------------------
98     # not just draw modes, but object modes!
99     scene.objects.active = obj
100
101     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
102     bpy.ops.mesh.select_all(action='DESELECT')
103     render_gl(context, filepath + "_edit_wire", shade='WIREFRAME')
104     render_gl(context, filepath + "_edit_solid", shade='SOLID')
105     render_gl(context, filepath + "_edit_textured", shade='TEXTURED')
106     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
107
108     bpy.ops.object.mode_set(mode='WEIGHT_PAINT', toggle=False)
109
110     render_gl(context, filepath + "_wp_wire", shade='WIREFRAME')
111
112     assert(1)
113
114     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
115
116     scene.objects.active = None
117
118
119 def ctx_clear_scene():  # copied from batch_import.py
120     import bpy
121     unique_obs = set()
122     for scene in bpy.data.scenes:
123         for obj in scene.objects[:]:
124             scene.objects.unlink(obj)
125             unique_obs.add(obj)
126
127     # remove obdata, for now only worry about the startup scene
128     for bpy_data_iter in (bpy.data.objects,
129                           bpy.data.meshes,
130                           bpy.data.lights,
131                           bpy.data.cameras,
132                           ):
133
134         for id_data in bpy_data_iter:
135             bpy_data_iter.remove(id_data)
136
137
138 def ctx_viewport_camera(context):
139     # because gl render without view_context has no shading option.
140     for area in context.window.screen.areas:
141         if area.type == 'VIEW_3D':
142             space = area.spaces.active
143             space.region_3d.view_perspective = 'CAMERA'
144
145
146 def ctx_camera_setup(context,
147                      location=(0.0, 0.0, 0.0),
148                      lookat=(0.0, 0.0, 0.0),
149                      # most likely the following vars can be left as defaults
150                      up=(0.0, 0.0, 1.0),
151                      lookat_axis='-Z',
152                      up_axis='Y',
153                      ):
154
155     camera = bpy.data.cameras.new(whoami())
156     obj = bpy.data.objects.new(whoami(), camera)
157
158     scene = context.scene
159     scene.objects.link(obj)
160     scene.camera = obj
161
162     from mathutils import Vector, Matrix
163
164     # setup transform
165     view_vec = Vector(lookat) - Vector(location)
166     rot_mat = view_vec.to_track_quat(lookat_axis, up_axis).to_matrix().to_4x4()
167     tra_mat = Matrix.Translation(location)
168
169     obj.matrix_world = tra_mat * rot_mat
170
171     ctx_viewport_camera(context)
172
173     return obj
174
175
176 # -----------------------------------------------------------------------------
177 # inspect functions
178
179 import inspect
180
181
182 # functions
183
184 def whoami():
185     return inspect.stack()[1][3]
186
187
188 def whosdaddy():
189     return inspect.stack()[2][3]
190
191
192 # -----------------------------------------------------------------------------
193 # models (defaults)
194
195 def defaults_object(obj):
196     obj.show_wire = True
197
198     if obj.type == 'MESH':
199         obj.show_all_edges = True
200
201         mesh = obj.data
202
203         mesh.show_normal_vertex = True
204
205         for poly in mesh.polygons:
206             poly.use_smooth = True
207
208
209 def defaults_modifier(mod):
210     mod.show_in_editmode = True
211     mod.show_on_cage = True
212
213
214 # -----------------------------------------------------------------------------
215 # models (utils)
216
217 def mesh_bmesh_poly_elems(poly, elems):
218     vert_start = poly.loop_start
219     vert_total = poly.loop_total
220     return elems[vert_start:vert_start + vert_total]
221
222
223 def mesh_bmesh_poly_vertices(poly):
224     return [loop.vertex_index
225             for loop in mesh_bmesh_poly_elems(poly, poly.id_data.loops)]
226
227
228 def mesh_bounds(mesh):
229     xmin = ymin = zmin = +100000000.0
230     xmax = ymax = zmax = -100000000.0
231
232     for v in mesh.vertices:
233         x, y, z = v.co
234         xmax = max(x, xmax)
235         ymax = max(y, ymax)
236         zmax = max(z, zmax)
237
238         xmin = min(x, xmin)
239         ymin = min(y, ymin)
240         zmin = min(z, zmin)
241
242     return (xmin, ymin, zmin), (xmax, ymax, zmax)
243
244
245 def mesh_uv_add(obj):
246
247     uvs = ((0.0, 0.0),
248            (0.0, 1.0),
249            (1.0, 1.0),
250            (1.0, 0.0))
251
252     uv_lay = obj.data.uv_layers.new()
253
254     # XXX, odd that we need to do this. until UV's and texface
255     # are separated we will need to keep it
256     uv_loops = obj.data.uv_layers[-1]
257     uv_list = uv_loops.data[:]
258     for poly in obj.data.polygons:
259         poly_uvs = mesh_bmesh_poly_elems(poly, uv_list)
260         for i, c in enumerate(poly_uvs):
261             c.uv = uvs[i % 4]
262
263     return uv_lay
264
265
266 def mesh_vcol_add(obj, mode=0):
267
268     colors = ((0.0, 0.0, 0.0),  # black
269               (1.0, 0.0, 0.0),  # red
270               (0.0, 1.0, 0.0),  # green
271               (0.0, 0.0, 1.0),  # blue
272               (1.0, 1.0, 0.0),  # yellow
273               (0.0, 1.0, 1.0),  # cyan
274               (1.0, 0.0, 1.0),  # magenta
275               (1.0, 1.0, 1.0),  # white
276               )
277
278     def colors_get(i):
279         return colors[i % len(colors)]
280
281     vcol_lay = obj.data.vertex_colors.new()
282
283     mesh = obj.data
284
285     col_list = vcol_lay.data[:]
286     for poly in mesh.polygons:
287         face_verts = mesh_bmesh_poly_vertices(poly)
288         poly_cols = mesh_bmesh_poly_elems(poly, col_list)
289         for i, c in enumerate(poly_cols):
290             c.color = colors_get(face_verts[i])
291
292     return vcol_lay
293
294
295 def mesh_vgroup_add(obj, name="Group", axis=0, invert=False, mode=0):
296     mesh = obj.data
297     vgroup = obj.vertex_groups.new(name=name)
298     vgroup.add(list(range(len(mesh.vertices))), 1.0, 'REPLACE')
299     group_index = len(obj.vertex_groups) - 1
300
301     min_bb, max_bb = mesh_bounds(mesh)
302
303     range_axis = max_bb[axis] - min_bb[axis]
304
305     # gradient
306     for v in mesh.vertices:
307         for vg in v.groups:
308             if vg.group == group_index:
309                 f = (v.co[axis] - min_bb[axis]) / range_axis
310                 vg.weight = 1.0 - f if invert else f
311
312     return vgroup
313
314
315 def mesh_shape_add(obj, mode=0):
316     pass
317
318
319 def mesh_armature_add(obj, mode=0):
320     pass
321
322
323 # -----------------------------------------------------------------------------
324 # modifiers
325
326 def modifier_subsurf_add(scene, obj, levels=2):
327     mod = obj.modifiers.new(name=whoami(), type='SUBSURF')
328     defaults_modifier(mod)
329
330     mod.levels = levels
331     mod.render_levels = levels
332     return mod
333
334
335 def modifier_armature_add(scene, obj):
336     mod = obj.modifiers.new(name=whoami(), type='ARMATURE')
337     defaults_modifier(mod)
338
339     arm_data = bpy.data.armatures.new(whoami())
340     obj_arm = bpy.data.objects.new(whoami(), arm_data)
341
342     scene.objects.link(obj_arm)
343
344     obj_arm.select = True
345     scene.objects.active = obj_arm
346
347     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
348     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
349
350     # XXX, annoying, remove bone.
351     while arm_data.edit_bones:
352         obj_arm.edit_bones.remove(arm_data.edit_bones[-1])
353
354     bone_a = arm_data.edit_bones.new("Bone.A")
355     bone_b = arm_data.edit_bones.new("Bone.B")
356     bone_b.parent = bone_a
357
358     bone_a.head = -1, 0, 0
359     bone_a.tail = 0, 0, 0
360     bone_b.head = 0, 0, 0
361     bone_b.tail = 1, 0, 0
362
363     # Get armature animation data
364     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
365
366     # 45d armature
367     obj_arm.pose.bones["Bone.B"].rotation_quaternion = 1, -0.5, 0, 0
368
369     # set back to the original
370     scene.objects.active = obj
371
372     # display options
373     obj_arm.show_in_front = True
374     arm_data.draw_type = 'STICK'
375
376     # apply to modifier
377     mod.object = obj_arm
378
379     mesh_vgroup_add(obj, name="Bone.A", axis=0, invert=True)
380     mesh_vgroup_add(obj, name="Bone.B", axis=0, invert=False)
381
382     return mod
383
384
385 def modifier_mirror_add(scene, obj):
386     mod = obj.modifiers.new(name=whoami(), type='MIRROR')
387     defaults_modifier(mod)
388
389     return mod
390
391
392 def modifier_solidify_add(scene, obj, thickness=0.25):
393     mod = obj.modifiers.new(name=whoami(), type='SOLIDIFY')
394     defaults_modifier(mod)
395
396     mod.thickness = thickness
397
398     return mod
399
400
401 def modifier_hook_add(scene, obj, use_vgroup=True):
402     scene.objects.active = obj
403
404     # no nice way to add hooks from py api yet
405     # assume object mode, hook first face!
406     mesh = obj.data
407
408     if use_vgroup:
409         for v in mesh.vertices:
410             v.select = True
411     else:
412         for v in mesh.vertices:
413             v.select = False
414
415         for i in mesh.faces[0].vertices:
416             mesh.vertices[i].select = True
417
418     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
419     bpy.ops.object.hook_add_newob()
420     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
421
422     # mod = obj.modifiers.new(name=whoami(), type='HOOK')
423     mod = obj.modifiers[-1]
424     defaults_modifier(mod)
425
426     obj_hook = mod.object
427     obj_hook.rotation_euler = 0, math.radians(45), 0
428     obj_hook.show_in_front = True
429
430     if use_vgroup:
431         mod.vertex_group = obj.vertex_groups[0].name
432
433     return mod
434
435
436 def modifier_decimate_add(scene, obj):
437     mod = obj.modifiers.new(name=whoami(), type='DECIMATE')
438     defaults_modifier(mod)
439
440     mod.ratio = 1 / 3
441
442     return mod
443
444
445 def modifier_build_add(scene, obj):
446     mod = obj.modifiers.new(name=whoami(), type='BUILD')
447     defaults_modifier(mod)
448
449     # ensure we display some faces
450     totface = len(obj.data.polygons)
451
452     mod.frame_start = totface // 2
453     mod.frame_duration = totface
454
455     return mod
456
457
458 def modifier_mask_add(scene, obj):
459     mod = obj.modifiers.new(name=whoami(), type='MASK')
460     defaults_modifier(mod)
461
462     mod.vertex_group = obj.vertex_groups[0].name
463
464     return mod
465
466
467 # -----------------------------------------------------------------------------
468 # models
469
470 # useful since its solid boxy shape but simple enough to debug errors
471 cube_like_vertices = (
472     (1, 1, -1),
473     (1, -1, -1),
474     (-1, -1, -1),
475     (-1, 1, -1),
476     (1, 1, 1),
477     (1, -1, 1),
478     (-1, -1, 1),
479     (-1, 1, 1),
480     (0, -1, -1),
481     (1, 0, -1),
482     (0, 1, -1),
483     (-1, 0, -1),
484     (1, 0, 1),
485     (0, -1, 1),
486     (-1, 0, 1),
487     (0, 1, 1),
488     (1, -1, 0),
489     (1, 1, 0),
490     (-1, -1, 0),
491     (-1, 1, 0),
492     (0, 0, -1),
493     (0, 0, 1),
494     (1, 0, 0),
495     (0, -1, 0),
496     (-1, 0, 0),
497     (2, 0, 0),
498     (2, 0, -1),
499     (2, 1, 0),
500     (2, 1, -1),
501     (0, 1, 2),
502     (0, 0, 2),
503     (-1, 0, 2),
504     (-1, 1, 2),
505     (-1, 0, 3),
506     (-1, 1, 3),
507     (0, 1, 3),
508     (0, 0, 3),
509 )
510
511
512 cube_like_faces = (
513     (0, 9, 20, 10),
514     (0, 10, 17),
515     (0, 17, 27, 28),
516     (1, 16, 23, 8),
517     (2, 18, 24, 11),
518     (3, 19, 10),
519     (4, 15, 21, 12),
520     (4, 17, 15),
521     (7, 14, 31, 32),
522     (7, 15, 19),
523     (8, 23, 18, 2),
524     (9, 0, 28, 26),
525     (9, 1, 8, 20),
526     (9, 22, 16, 1),
527     (10, 20, 11, 3),
528     (11, 24, 19, 3),
529     (12, 21, 13, 5),
530     (13, 6, 18),
531     (14, 21, 30, 31),
532     (15, 7, 32, 29),
533     (15, 17, 10, 19),
534     (16, 5, 13, 23),
535     (17, 4, 12, 22),
536     (17, 22, 25, 27),
537     (18, 6, 14, 24),
538     (20, 8, 2, 11),
539     (21, 14, 6, 13),
540     (21, 15, 29, 30),
541     (22, 9, 26, 25),
542     (22, 12, 5, 16),
543     (23, 13, 18),
544     (24, 14, 7, 19),
545     (28, 27, 25, 26),
546     (29, 32, 34, 35),
547     (30, 29, 35, 36),
548     (31, 30, 36, 33),
549     (32, 31, 33, 34),
550     (35, 34, 33, 36),
551 )
552
553
554 # useful since its a shell for solidify and it can be mirrored
555 cube_shell_vertices = (
556     (0, 0, 1),
557     (0, 1, 1),
558     (-1, 1, 1),
559     (-1, 0, 1),
560     (0, 0, 0),
561     (0, 1, 0),
562     (-1, 1, 0),
563     (-1, 0, 0),
564     (-1, -1, 0),
565     (0, -1, 0),
566     (0, 0, -1),
567     (0, 1, -1),
568 )
569
570
571 cube_shell_face = (
572     (0, 1, 2, 3),
573     (0, 3, 8, 9),
574     (1, 5, 6, 2),
575     (2, 6, 7, 3),
576     (3, 7, 8),
577     (4, 7, 10),
578     (6, 5, 11),
579     (7, 4, 9, 8),
580     (10, 7, 6, 11),
581 )
582
583
584 def make_cube(scene):
585     bpy.ops.mesh.primitive_cube_add(view_align=False,
586                                     enter_editmode=False,
587                                     location=(0, 0, 0),
588                                     rotation=(0, 0, 0),
589                                     )
590
591     obj = scene.objects.active
592
593     defaults_object(obj)
594     return obj
595
596
597 def make_cube_extra(scene):
598     obj = make_cube(scene)
599
600     # extra data layers
601     mesh_uv_add(obj)
602     mesh_vcol_add(obj)
603     mesh_vgroup_add(obj)
604
605     return obj
606
607
608 def make_cube_like(scene):
609     mesh = bpy.data.meshes.new(whoami())
610
611     mesh.from_pydata(cube_like_vertices, (), cube_like_faces)
612     mesh.update()  # add edges
613     obj = bpy.data.objects.new(whoami(), mesh)
614     scene.objects.link(obj)
615
616     defaults_object(obj)
617     return obj
618
619
620 def make_cube_like_extra(scene):
621     obj = make_cube_like(scene)
622
623     # extra data layers
624     mesh_uv_add(obj)
625     mesh_vcol_add(obj)
626     mesh_vgroup_add(obj)
627
628     return obj
629
630
631 def make_cube_shell(scene):
632     mesh = bpy.data.meshes.new(whoami())
633
634     mesh.from_pydata(cube_shell_vertices, (), cube_shell_face)
635     mesh.update()  # add edges
636     obj = bpy.data.objects.new(whoami(), mesh)
637     scene.objects.link(obj)
638
639     defaults_object(obj)
640     return obj
641
642
643 def make_cube_shell_extra(scene):
644     obj = make_cube_shell(scene)
645
646     # extra data layers
647     mesh_uv_add(obj)
648     mesh_vcol_add(obj)
649     mesh_vgroup_add(obj)
650
651     return obj
652
653
654 def make_monkey(scene):
655     bpy.ops.mesh.primitive_monkey_add(view_align=False,
656                                       enter_editmode=False,
657                                       location=(0, 0, 0),
658                                       rotation=(0, 0, 0),
659                                       )
660     obj = scene.objects.active
661
662     defaults_object(obj)
663     return obj
664
665
666 def make_monkey_extra(scene):
667     obj = make_monkey(scene)
668
669     # extra data layers
670     mesh_uv_add(obj)
671     mesh_vcol_add(obj)
672     mesh_vgroup_add(obj)
673
674     return obj
675
676
677 # -----------------------------------------------------------------------------
678 # tests (utils)
679
680 global_tests = []
681
682 global_tests.append(
683     ("none",
684      (),
685      )
686 )
687 # single
688 global_tests.append(
689     ("subsurf_single",
690      ((modifier_subsurf_add, dict(levels=2)), ),
691      )
692 )
693
694 global_tests.append(
695     ("armature_single",
696      ((modifier_armature_add, dict()), ),
697      )
698 )
699
700 global_tests.append(
701     ("mirror_single",
702      ((modifier_mirror_add, dict()), ),
703      )
704 )
705
706 global_tests.append(
707     ("hook_single",
708      ((modifier_hook_add, dict()), ),
709      )
710 )
711
712 global_tests.append(
713     ("decimate_single",
714      ((modifier_decimate_add, dict()), ),
715      )
716 )
717
718 global_tests.append(
719     ("build_single",
720      ((modifier_build_add, dict()), ),
721      )
722 )
723
724 global_tests.append(
725     ("mask_single",
726      ((modifier_mask_add, dict()), ),
727      )
728 )
729
730
731 # combinations
732 global_tests.append(
733     ("mirror_subsurf",
734      ((modifier_mirror_add, dict()),
735       (modifier_subsurf_add, dict(levels=2))),
736      )
737 )
738
739 global_tests.append(
740     ("solidify_subsurf",
741      ((modifier_solidify_add, dict()),
742       (modifier_subsurf_add, dict(levels=2))),
743      )
744 )
745
746
747 def apply_test(
748         test, scene, obj,
749         render_func=None,
750         render_args=None,
751         render_kwargs=None,
752 ):
753
754     test_name, test_funcs = test
755
756     for cb, kwargs in test_funcs:
757         cb(scene, obj, **kwargs)
758
759     render_kwargs_copy = render_kwargs.copy()
760
761     # add test name in filepath
762     render_kwargs_copy["filepath"] += "_%s" % test_name
763
764     render_func(*render_args, **render_kwargs_copy)
765
766
767 # -----------------------------------------------------------------------------
768 # tests themselves!
769 # having the 'test_' prefix automatically means these functions are called
770 # for testing
771
772
773 def test_cube(context, test):
774     scene = context.scene
775     obj = make_cube_extra(scene)
776     ctx_camera_setup(context, location=(3, 3, 3))
777
778     apply_test(
779         test, scene, obj,
780         render_func=render_gl_all_modes,
781         render_args=(context, obj),
782         render_kwargs=dict(filepath=whoami())
783     )
784
785
786 def test_cube_like(context, test):
787     scene = context.scene
788     obj = make_cube_like_extra(scene)
789     ctx_camera_setup(context, location=(5, 5, 5))
790
791     apply_test(
792         test, scene, obj,
793         render_func=render_gl_all_modes,
794         render_args=(context, obj),
795         render_kwargs=dict(filepath=whoami())
796     )
797
798
799 def test_cube_shell(context, test):
800     scene = context.scene
801     obj = make_cube_shell_extra(scene)
802     ctx_camera_setup(context, location=(4, 4, 4))
803
804     apply_test(
805         test, scene, obj,
806         render_func=render_gl_all_modes,
807         render_args=(context, obj),
808         render_kwargs=dict(filepath=whoami())
809     )
810
811
812 # -----------------------------------------------------------------------------
813 # call all tests
814
815 def main():
816     print("Calling main!")
817     # render_gl(bpy.context, "/testme")
818     # ctx_clear_scene()
819
820     context = bpy.context
821
822     ctx_clear_scene()
823
824     # run all tests
825     for key, val in sorted(globals().items()):
826         if key.startswith("test_") and hasattr(val, "__call__"):
827             print("calling:", key)
828             for t in global_tests:
829                 val(context, test=t)
830                 ctx_clear_scene()
831
832
833 # -----------------------------------------------------------------------------
834 # annoying workaround for theme initialization
835
836 if __name__ == "__main__":
837     import bpy
838     from bpy.app.handlers import persistent
839
840     @persistent
841     def load_handler(dummy):
842         print("Load Handler:", bpy.data.filepath)
843         if load_handler.first is False:
844             bpy.app.handlers.scene_update_post.remove(load_handler)
845             try:
846                 main()
847                 import sys
848                 sys.exit(0)
849             except:
850                 import traceback
851                 traceback.print_exc()
852
853                 # import sys
854                 # sys.exit(1)  # comment to debug
855
856         else:
857             load_handler.first = False
858
859     load_handler.first = True
860     bpy.app.handlers.scene_update_post.append(load_handler)