Cleanup: use '_len' instead of '_size' w/ BLI API
[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_viewport_shade(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.viewport_shade = 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_viewport_shade(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.lamps,
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 def mesh_bmesh_poly_vertices(poly):
223     return [loop.vertex_index
224             for loop in mesh_bmesh_poly_elems(poly, poly.id_data.loops)]
225
226
227 def mesh_bounds(mesh):
228     xmin = ymin = zmin = +100000000.0
229     xmax = ymax = zmax = -100000000.0
230
231     for v in mesh.vertices:
232         x, y, z = v.co
233         xmax = max(x, xmax)
234         ymax = max(y, ymax)
235         zmax = max(z, zmax)
236
237         xmin = min(x, xmin)
238         ymin = min(y, ymin)
239         zmin = min(z, zmin)
240
241     return (xmin, ymin, zmin), (xmax, ymax, zmax)
242
243
244 def mesh_uv_add(obj):
245
246     uvs = ((0.0, 0.0),
247            (0.0, 1.0),
248            (1.0, 1.0),
249            (1.0, 0.0))
250
251     uv_lay = obj.data.uv_textures.new()
252
253     # XXX, odd that we need to do this. until UV's and texface
254     # are separated we will need to keep it
255     uv_loops = obj.data.uv_layers[-1]
256     uv_list = uv_loops.data[:]
257     for poly in obj.data.polygons:
258         poly_uvs = mesh_bmesh_poly_elems(poly, uv_list)
259         for i, c in enumerate(poly_uvs):
260             c.uv = uvs[i % 4]
261
262     return uv_lay
263
264
265 def mesh_vcol_add(obj, mode=0):
266
267     colors = ((0.0, 0.0, 0.0),  # black
268               (1.0, 0.0, 0.0),  # red
269               (0.0, 1.0, 0.0),  # green
270               (0.0, 0.0, 1.0),  # blue
271               (1.0, 1.0, 0.0),  # yellow
272               (0.0, 1.0, 1.0),  # cyan
273               (1.0, 0.0, 1.0),  # magenta
274               (1.0, 1.0, 1.0),  # white
275               )
276
277     def colors_get(i):
278         return colors[i % len(colors)]
279
280     vcol_lay = obj.data.vertex_colors.new()
281
282     mesh = obj.data
283
284     col_list = vcol_lay.data[:]
285     for poly in mesh.polygons:
286         face_verts = mesh_bmesh_poly_vertices(poly)
287         poly_cols = mesh_bmesh_poly_elems(poly, col_list)
288         for i, c in enumerate(poly_cols):
289             c.color = colors_get(face_verts[i])
290
291     return vcol_lay
292
293
294 def mesh_vgroup_add(obj, name="Group", axis=0, invert=False, mode=0):
295     mesh = obj.data
296     vgroup = obj.vertex_groups.new(name=name)
297     vgroup.add(list(range(len(mesh.vertices))), 1.0, 'REPLACE')
298     group_index = len(obj.vertex_groups) - 1
299
300     min_bb, max_bb = mesh_bounds(mesh)
301
302     range_axis = max_bb[axis] - min_bb[axis]
303
304     # gradient
305     for v in mesh.vertices:
306         for vg in v.groups:
307             if vg.group == group_index:
308                 f = (v.co[axis] - min_bb[axis]) / range_axis
309                 vg.weight = 1.0 - f if invert else f
310
311     return vgroup
312
313
314 def mesh_shape_add(obj, mode=0):
315     pass
316
317
318 def mesh_armature_add(obj, mode=0):
319     pass
320
321
322 # -----------------------------------------------------------------------------
323 # modifiers
324
325 def modifier_subsurf_add(scene, obj, levels=2):
326     mod = obj.modifiers.new(name=whoami(), type='SUBSURF')
327     defaults_modifier(mod)
328
329     mod.levels = levels
330     mod.render_levels = levels
331     return mod
332
333
334 def modifier_armature_add(scene, obj):
335     mod = obj.modifiers.new(name=whoami(), type='ARMATURE')
336     defaults_modifier(mod)
337
338     arm_data = bpy.data.armatures.new(whoami())
339     obj_arm = bpy.data.objects.new(whoami(), arm_data)
340
341     scene.objects.link(obj_arm)
342
343     obj_arm.select = True
344     scene.objects.active = obj_arm
345
346     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
347     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
348
349     # XXX, annoying, remove bone.
350     while arm_data.edit_bones:
351         obj_arm.edit_bones.remove(arm_data.edit_bones[-1])
352
353     bone_a = arm_data.edit_bones.new("Bone.A")
354     bone_b = arm_data.edit_bones.new("Bone.B")
355     bone_b.parent = bone_a
356
357     bone_a.head = -1, 0, 0
358     bone_a.tail = 0, 0, 0
359     bone_b.head = 0, 0, 0
360     bone_b.tail = 1, 0, 0
361
362     # Get armature animation data
363     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
364
365     # 45d armature
366     obj_arm.pose.bones["Bone.B"].rotation_quaternion = 1, -0.5, 0, 0
367
368     # set back to the original
369     scene.objects.active = obj
370
371     # display options
372     obj_arm.show_x_ray = True
373     arm_data.draw_type = 'STICK'
374
375     # apply to modifier
376     mod.object = obj_arm
377
378     mesh_vgroup_add(obj, name="Bone.A", axis=0, invert=True)
379     mesh_vgroup_add(obj, name="Bone.B", axis=0, invert=False)
380
381     return mod
382
383
384 def modifier_mirror_add(scene, obj):
385     mod = obj.modifiers.new(name=whoami(), type='MIRROR')
386     defaults_modifier(mod)
387
388     return mod
389
390
391 def modifier_solidify_add(scene, obj, thickness=0.25):
392     mod = obj.modifiers.new(name=whoami(), type='SOLIDIFY')
393     defaults_modifier(mod)
394
395     mod.thickness = thickness
396
397     return mod
398
399
400 def modifier_hook_add(scene, obj, use_vgroup=True):
401     scene.objects.active = obj
402
403     # no nice way to add hooks from py api yet
404     # assume object mode, hook first face!
405     mesh = obj.data
406
407     if use_vgroup:
408         for v in mesh.vertices:
409             v.select = True
410     else:
411         for v in mesh.vertices:
412             v.select = False
413
414         for i in mesh.faces[0].vertices:
415             mesh.vertices[i].select = True
416
417     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
418     bpy.ops.object.hook_add_newob()
419     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
420
421     # mod = obj.modifiers.new(name=whoami(), type='HOOK')
422     mod = obj.modifiers[-1]
423     defaults_modifier(mod)
424
425     obj_hook = mod.object
426     obj_hook.rotation_euler = 0, math.radians(45), 0
427     obj_hook.show_x_ray = True
428
429     if use_vgroup:
430         mod.vertex_group = obj.vertex_groups[0].name
431
432     return mod
433
434
435 def modifier_decimate_add(scene, obj):
436     mod = obj.modifiers.new(name=whoami(), type='DECIMATE')
437     defaults_modifier(mod)
438
439     mod.ratio = 1 / 3
440
441     return mod
442
443
444 def modifier_build_add(scene, obj):
445     mod = obj.modifiers.new(name=whoami(), type='BUILD')
446     defaults_modifier(mod)
447
448     # ensure we display some faces
449     totface = len(obj.data.polygons)
450
451     mod.frame_start = totface // 2
452     mod.frame_duration = totface
453
454     return mod
455
456
457 def modifier_mask_add(scene, obj):
458     mod = obj.modifiers.new(name=whoami(), type='MASK')
459     defaults_modifier(mod)
460
461     mod.vertex_group = obj.vertex_groups[0].name
462
463     return mod
464
465
466 # -----------------------------------------------------------------------------
467 # models
468
469 # useful since its solid boxy shape but simple enough to debug errors
470 cube_like_vertices = (
471     (1, 1, -1),
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     (0, -1, -1),
480     (1, 0, -1),
481     (0, 1, -1),
482     (-1, 0, -1),
483     (1, 0, 1),
484     (0, -1, 1),
485     (-1, 0, 1),
486     (0, 1, 1),
487     (1, -1, 0),
488     (1, 1, 0),
489     (-1, -1, 0),
490     (-1, 1, 0),
491     (0, 0, -1),
492     (0, 0, 1),
493     (1, 0, 0),
494     (0, -1, 0),
495     (-1, 0, 0),
496     (2, 0, 0),
497     (2, 0, -1),
498     (2, 1, 0),
499     (2, 1, -1),
500     (0, 1, 2),
501     (0, 0, 2),
502     (-1, 0, 2),
503     (-1, 1, 2),
504     (-1, 0, 3),
505     (-1, 1, 3),
506     (0, 1, 3),
507     (0, 0, 3),
508     )
509
510
511 cube_like_faces = (
512     (0, 9, 20, 10),
513     (0, 10, 17),
514     (0, 17, 27, 28),
515     (1, 16, 23, 8),
516     (2, 18, 24, 11),
517     (3, 19, 10),
518     (4, 15, 21, 12),
519     (4, 17, 15),
520     (7, 14, 31, 32),
521     (7, 15, 19),
522     (8, 23, 18, 2),
523     (9, 0, 28, 26),
524     (9, 1, 8, 20),
525     (9, 22, 16, 1),
526     (10, 20, 11, 3),
527     (11, 24, 19, 3),
528     (12, 21, 13, 5),
529     (13, 6, 18),
530     (14, 21, 30, 31),
531     (15, 7, 32, 29),
532     (15, 17, 10, 19),
533     (16, 5, 13, 23),
534     (17, 4, 12, 22),
535     (17, 22, 25, 27),
536     (18, 6, 14, 24),
537     (20, 8, 2, 11),
538     (21, 14, 6, 13),
539     (21, 15, 29, 30),
540     (22, 9, 26, 25),
541     (22, 12, 5, 16),
542     (23, 13, 18),
543     (24, 14, 7, 19),
544     (28, 27, 25, 26),
545     (29, 32, 34, 35),
546     (30, 29, 35, 36),
547     (31, 30, 36, 33),
548     (32, 31, 33, 34),
549     (35, 34, 33, 36),
550     )
551
552
553 # useful since its a shell for solidify and it can be mirrored
554 cube_shell_vertices = (
555     (0, 0, 1),
556     (0, 1, 1),
557     (-1, 1, 1),
558     (-1, 0, 1),
559     (0, 0, 0),
560     (0, 1, 0),
561     (-1, 1, 0),
562     (-1, 0, 0),
563     (-1, -1, 0),
564     (0, -1, 0),
565     (0, 0, -1),
566     (0, 1, -1),
567     )
568
569
570 cube_shell_face = (
571     (0, 1, 2, 3),
572     (0, 3, 8, 9),
573     (1, 5, 6, 2),
574     (2, 6, 7, 3),
575     (3, 7, 8),
576     (4, 7, 10),
577     (6, 5, 11),
578     (7, 4, 9, 8),
579     (10, 7, 6, 11),
580     )
581
582
583 def make_cube(scene):
584     bpy.ops.mesh.primitive_cube_add(view_align=False,
585                                     enter_editmode=False,
586                                     location=(0, 0, 0),
587                                     rotation=(0, 0, 0),
588                                     )
589
590     obj = scene.objects.active
591
592     defaults_object(obj)
593     return obj
594
595
596 def make_cube_extra(scene):
597     obj = make_cube(scene)
598
599     # extra data layers
600     mesh_uv_add(obj)
601     mesh_vcol_add(obj)
602     mesh_vgroup_add(obj)
603
604     return obj
605
606
607 def make_cube_like(scene):
608     mesh = bpy.data.meshes.new(whoami())
609
610     mesh.from_pydata(cube_like_vertices, (), cube_like_faces)
611     mesh.update()  # add edges
612     obj = bpy.data.objects.new(whoami(), mesh)
613     scene.objects.link(obj)
614
615     defaults_object(obj)
616     return obj
617
618
619 def make_cube_like_extra(scene):
620     obj = make_cube_like(scene)
621
622     # extra data layers
623     mesh_uv_add(obj)
624     mesh_vcol_add(obj)
625     mesh_vgroup_add(obj)
626
627     return obj
628
629
630 def make_cube_shell(scene):
631     mesh = bpy.data.meshes.new(whoami())
632
633     mesh.from_pydata(cube_shell_vertices, (), cube_shell_face)
634     mesh.update()  # add edges
635     obj = bpy.data.objects.new(whoami(), mesh)
636     scene.objects.link(obj)
637
638     defaults_object(obj)
639     return obj
640
641
642 def make_cube_shell_extra(scene):
643     obj = make_cube_shell(scene)
644
645     # extra data layers
646     mesh_uv_add(obj)
647     mesh_vcol_add(obj)
648     mesh_vgroup_add(obj)
649
650     return obj
651
652
653 def make_monkey(scene):
654     bpy.ops.mesh.primitive_monkey_add(view_align=False,
655                                       enter_editmode=False,
656                                       location=(0, 0, 0),
657                                       rotation=(0, 0, 0),
658                                       )
659     obj = scene.objects.active
660
661     defaults_object(obj)
662     return obj
663
664
665 def make_monkey_extra(scene):
666     obj = make_monkey(scene)
667
668     # extra data layers
669     mesh_uv_add(obj)
670     mesh_vcol_add(obj)
671     mesh_vgroup_add(obj)
672
673     return obj
674
675
676 # -----------------------------------------------------------------------------
677 # tests (utils)
678
679 global_tests = []
680
681 global_tests.append(("none",
682     (),
683     ))
684
685 # single
686 global_tests.append(("subsurf_single",
687     ((modifier_subsurf_add, dict(levels=2)), ),
688     ))
689
690
691 global_tests.append(("armature_single",
692     ((modifier_armature_add, dict()), ),
693     ))
694
695
696 global_tests.append(("mirror_single",
697     ((modifier_mirror_add, dict()), ),
698     ))
699
700 global_tests.append(("hook_single",
701     ((modifier_hook_add, dict()), ),
702     ))
703
704 global_tests.append(("decimate_single",
705     ((modifier_decimate_add, dict()), ),
706     ))
707
708 global_tests.append(("build_single",
709     ((modifier_build_add, dict()), ),
710     ))
711
712 global_tests.append(("mask_single",
713     ((modifier_mask_add, dict()), ),
714     ))
715
716
717 # combinations
718 global_tests.append(("mirror_subsurf",
719     ((modifier_mirror_add, dict()),
720      (modifier_subsurf_add, dict(levels=2))),
721     ))
722
723 global_tests.append(("solidify_subsurf",
724     ((modifier_solidify_add, dict()),
725      (modifier_subsurf_add, dict(levels=2))),
726     ))
727
728
729 def apply_test(test, scene, obj,
730                render_func=None,
731                render_args=None,
732                render_kwargs=None,
733                ):
734
735     test_name, test_funcs = test
736
737     for cb, kwargs in test_funcs:
738         cb(scene, obj, **kwargs)
739
740     render_kwargs_copy = render_kwargs.copy()
741
742     # add test name in filepath
743     render_kwargs_copy["filepath"] += "_%s" % test_name
744
745     render_func(*render_args, **render_kwargs_copy)
746
747
748 # -----------------------------------------------------------------------------
749 # tests themselves!
750 # having the 'test_' prefix automatically means these functions are called
751 # for testing
752
753
754 def test_cube(context, test):
755     scene = context.scene
756     obj = make_cube_extra(scene)
757     ctx_camera_setup(context, location=(3, 3, 3))
758
759     apply_test(test, scene, obj,
760                render_func=render_gl_all_modes,
761                render_args=(context, obj),
762                render_kwargs=dict(filepath=whoami()))
763
764
765 def test_cube_like(context, test):
766     scene = context.scene
767     obj = make_cube_like_extra(scene)
768     ctx_camera_setup(context, location=(5, 5, 5))
769
770     apply_test(test, scene, obj,
771                render_func=render_gl_all_modes,
772                render_args=(context, obj),
773                render_kwargs=dict(filepath=whoami()))
774
775
776 def test_cube_shell(context, test):
777     scene = context.scene
778     obj = make_cube_shell_extra(scene)
779     ctx_camera_setup(context, location=(4, 4, 4))
780
781     apply_test(test, scene, obj,
782                render_func=render_gl_all_modes,
783                render_args=(context, obj),
784                render_kwargs=dict(filepath=whoami()))
785
786
787 # -----------------------------------------------------------------------------
788 # call all tests
789
790 def main():
791     print("Calling main!")
792     # render_gl(bpy.context, "/testme")
793     # ctx_clear_scene()
794
795     context = bpy.context
796
797     ctx_clear_scene()
798
799     # run all tests
800     for key, val in sorted(globals().items()):
801         if key.startswith("test_") and hasattr(val, "__call__"):
802             print("calling:", key)
803             for t in global_tests:
804                 val(context, test=t)
805                 ctx_clear_scene()
806
807
808 # -----------------------------------------------------------------------------
809 # annoying workaround for theme initialization
810
811 if __name__ == "__main__":
812     import bpy
813     from bpy.app.handlers import persistent
814
815     @persistent
816     def load_handler(dummy):
817         print("Load Handler:", bpy.data.filepath)
818         if load_handler.first is False:
819             bpy.app.handlers.scene_update_post.remove(load_handler)
820             try:
821                 main()
822                 import sys
823                 sys.exit(0)
824             except:
825                 import traceback
826                 traceback.print_exc()
827
828                 # import sys
829                 # sys.exit(1)  # comment to debug
830
831         else:
832             load_handler.first = False
833
834     load_handler.first = True
835     bpy.app.handlers.scene_update_post.append(load_handler)