blender_test/tests/python/bl_mesh_modifiers.py
Campbell Barton 39c341bf4a Cleanup: remove redundant braces from assert & raise
autopep8 v1.7 added a space after assert & raise,
remove the braces as they aren't needed.
2022-09-14 16:18:59 +10:00

833 lines
19 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
# Currently this script only generates images from different modifier
# combinations and does not validate they work correctly,
# this is because we don't get 1:1 match with bmesh.
#
# Later, we may have a way to check the results are valid.
# ./blender.bin --factory-startup --python tests/python/bl_mesh_modifiers.py
#
import math
USE_QUICK_RENDER = False
# -----------------------------------------------------------------------------
# utility functions
def render_gl(context, filepath, shade):
def ctx_shading_type(context, shade):
for area in context.window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
# rv3d = space.region_3d
space.shading.type = shade
import bpy
scene = context.scene
render = scene.render
render.filepath = filepath
render.image_settings.file_format = 'PNG'
render.image_settings.color_mode = 'RGB'
render.use_file_extension = True
render.use_antialiasing = False
# render size
render.resolution_percentage = 100
render.resolution_x = 512
render.resolution_y = 512
ctx_shading_type(context, shade)
# stop to inspect!
# if filepath == "test_cube_shell_solidify_subsurf_wp_wire":
# assert(0)
# else:
# return
bpy.ops.render.opengl(write_still=True,
view_context=True)
def render_gl_all_modes(context, obj, filepath=""):
assert obj is not None
assert filepath != ""
scene = context.scene
# avoid drawing outline/center dot
bpy.ops.object.select_all(action='DESELECT')
scene.objects.active = None
# editmode
scene.tool_settings.mesh_select_mode = False, True, False
# render
render_gl(context, filepath + "_ob_solid", shade='SOLID')
if USE_QUICK_RENDER:
return
render_gl(context, filepath + "_ob_wire", shade='WIREFRAME')
render_gl(context, filepath + "_ob_textured", shade='TEXTURED')
# -------------------------------------------------------------------------
# not just draw modes, but object modes!
scene.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
bpy.ops.mesh.select_all(action='DESELECT')
render_gl(context, filepath + "_edit_wire", shade='WIREFRAME')
render_gl(context, filepath + "_edit_solid", shade='SOLID')
render_gl(context, filepath + "_edit_textured", shade='TEXTURED')
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
bpy.ops.object.mode_set(mode='WEIGHT_PAINT', toggle=False)
render_gl(context, filepath + "_wp_wire", shade='WIREFRAME')
assert 1
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
scene.objects.active = None
def ctx_clear_scene(): # copied from batch_import.py
import bpy
unique_obs = set()
for scene in bpy.data.scenes:
for obj in scene.objects[:]:
scene.objects.unlink(obj)
unique_obs.add(obj)
# remove obdata, for now only worry about the startup scene
for bpy_data_iter in (bpy.data.objects,
bpy.data.meshes,
bpy.data.lights,
bpy.data.cameras,
):
for id_data in bpy_data_iter:
bpy_data_iter.remove(id_data)
def ctx_viewport_camera(context):
# because gl render without view_context has no shading option.
for area in context.window.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces.active
space.region_3d.view_perspective = 'CAMERA'
def ctx_camera_setup(context,
location=(0.0, 0.0, 0.0),
lookat=(0.0, 0.0, 0.0),
# most likely the following vars can be left as defaults
up=(0.0, 0.0, 1.0),
lookat_axis='-Z',
up_axis='Y',
):
camera = bpy.data.cameras.new(whoami())
obj = bpy.data.objects.new(whoami(), camera)
scene = context.scene
scene.objects.link(obj)
scene.camera = obj
from mathutils import Vector, Matrix
# setup transform
view_vec = Vector(lookat) - Vector(location)
rot_mat = view_vec.to_track_quat(lookat_axis, up_axis).to_matrix().to_4x4()
tra_mat = Matrix.Translation(location)
obj.matrix_world = tra_mat * rot_mat
ctx_viewport_camera(context)
return obj
# -----------------------------------------------------------------------------
# inspect functions
import inspect
# functions
def whoami():
return inspect.stack()[1][3]
def whosdaddy():
return inspect.stack()[2][3]
# -----------------------------------------------------------------------------
# models (defaults)
def defaults_object(obj):
obj.show_wire = True
if obj.type == 'MESH':
obj.show_all_edges = True
mesh = obj.data
mesh.show_normal_vertex = True
for poly in mesh.polygons:
poly.use_smooth = True
def defaults_modifier(mod):
mod.show_in_editmode = True
mod.show_on_cage = True
# -----------------------------------------------------------------------------
# models (utils)
def mesh_bmesh_poly_elems(poly, elems):
vert_start = poly.loop_start
vert_total = poly.loop_total
return elems[vert_start:vert_start + vert_total]
def mesh_bmesh_poly_vertices(poly):
return [loop.vertex_index
for loop in mesh_bmesh_poly_elems(poly, poly.id_data.loops)]
def mesh_bounds(mesh):
xmin = ymin = zmin = +100000000.0
xmax = ymax = zmax = -100000000.0
for v in mesh.vertices:
x, y, z = v.co
xmax = max(x, xmax)
ymax = max(y, ymax)
zmax = max(z, zmax)
xmin = min(x, xmin)
ymin = min(y, ymin)
zmin = min(z, zmin)
return (xmin, ymin, zmin), (xmax, ymax, zmax)
def mesh_uv_add(obj):
uvs = ((0.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
(1.0, 0.0))
uv_lay = obj.data.uv_layers.new()
# XXX, odd that we need to do this. until UV's and texface
# are separated we will need to keep it
uv_loops = obj.data.uv_layers[-1]
uv_list = uv_loops.data[:]
for poly in obj.data.polygons:
poly_uvs = mesh_bmesh_poly_elems(poly, uv_list)
for i, c in enumerate(poly_uvs):
c.uv = uvs[i % 4]
return uv_lay
def mesh_vcol_add(obj, mode=0):
colors = ((0.0, 0.0, 0.0), # black
(1.0, 0.0, 0.0), # red
(0.0, 1.0, 0.0), # green
(0.0, 0.0, 1.0), # blue
(1.0, 1.0, 0.0), # yellow
(0.0, 1.0, 1.0), # cyan
(1.0, 0.0, 1.0), # magenta
(1.0, 1.0, 1.0), # white
)
def colors_get(i):
return colors[i % len(colors)]
vcol_lay = obj.data.vertex_colors.new()
mesh = obj.data
col_list = vcol_lay.data[:]
for poly in mesh.polygons:
face_verts = mesh_bmesh_poly_vertices(poly)
poly_cols = mesh_bmesh_poly_elems(poly, col_list)
for i, c in enumerate(poly_cols):
c.color = colors_get(face_verts[i])
return vcol_lay
def mesh_vgroup_add(obj, name="Group", axis=0, invert=False, mode=0):
mesh = obj.data
vgroup = obj.vertex_groups.new(name=name)
vgroup.add(list(range(len(mesh.vertices))), 1.0, 'REPLACE')
group_index = len(obj.vertex_groups) - 1
min_bb, max_bb = mesh_bounds(mesh)
range_axis = max_bb[axis] - min_bb[axis]
# gradient
for v in mesh.vertices:
for vg in v.groups:
if vg.group == group_index:
f = (v.co[axis] - min_bb[axis]) / range_axis
vg.weight = 1.0 - f if invert else f
return vgroup
def mesh_shape_add(obj, mode=0):
pass
def mesh_armature_add(obj, mode=0):
pass
# -----------------------------------------------------------------------------
# modifiers
def modifier_subsurf_add(scene, obj, levels=2):
mod = obj.modifiers.new(name=whoami(), type='SUBSURF')
defaults_modifier(mod)
mod.levels = levels
mod.render_levels = levels
return mod
def modifier_armature_add(scene, obj):
mod = obj.modifiers.new(name=whoami(), type='ARMATURE')
defaults_modifier(mod)
arm_data = bpy.data.armatures.new(whoami())
obj_arm = bpy.data.objects.new(whoami(), arm_data)
scene.objects.link(obj_arm)
obj_arm.select = True
scene.objects.active = obj_arm
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
# XXX, annoying, remove bone.
while arm_data.edit_bones:
obj_arm.edit_bones.remove(arm_data.edit_bones[-1])
bone_a = arm_data.edit_bones.new("Bone.A")
bone_b = arm_data.edit_bones.new("Bone.B")
bone_b.parent = bone_a
bone_a.head = -1, 0, 0
bone_a.tail = 0, 0, 0
bone_b.head = 0, 0, 0
bone_b.tail = 1, 0, 0
# Get armature animation data
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
# 45d armature
obj_arm.pose.bones["Bone.B"].rotation_quaternion = 1, -0.5, 0, 0
# set back to the original
scene.objects.active = obj
# display options
obj_arm.show_in_front = True
arm_data.draw_type = 'STICK'
# apply to modifier
mod.object = obj_arm
mesh_vgroup_add(obj, name="Bone.A", axis=0, invert=True)
mesh_vgroup_add(obj, name="Bone.B", axis=0, invert=False)
return mod
def modifier_mirror_add(scene, obj):
mod = obj.modifiers.new(name=whoami(), type='MIRROR')
defaults_modifier(mod)
return mod
def modifier_solidify_add(scene, obj, thickness=0.25):
mod = obj.modifiers.new(name=whoami(), type='SOLIDIFY')
defaults_modifier(mod)
mod.thickness = thickness
return mod
def modifier_hook_add(scene, obj, use_vgroup=True):
scene.objects.active = obj
# no nice way to add hooks from py api yet
# assume object mode, hook first face!
mesh = obj.data
if use_vgroup:
for v in mesh.vertices:
v.select = True
else:
for v in mesh.vertices:
v.select = False
for i in mesh.faces[0].vertices:
mesh.vertices[i].select = True
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
bpy.ops.object.hook_add_newob()
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
# mod = obj.modifiers.new(name=whoami(), type='HOOK')
mod = obj.modifiers[-1]
defaults_modifier(mod)
obj_hook = mod.object
obj_hook.rotation_euler = 0, math.radians(45), 0
obj_hook.show_in_front = True
if use_vgroup:
mod.vertex_group = obj.vertex_groups[0].name
return mod
def modifier_decimate_add(scene, obj):
mod = obj.modifiers.new(name=whoami(), type='DECIMATE')
defaults_modifier(mod)
mod.ratio = 1 / 3
return mod
def modifier_build_add(scene, obj):
mod = obj.modifiers.new(name=whoami(), type='BUILD')
defaults_modifier(mod)
# ensure we display some faces
totface = len(obj.data.polygons)
mod.frame_start = totface // 2
mod.frame_duration = totface
return mod
def modifier_mask_add(scene, obj):
mod = obj.modifiers.new(name=whoami(), type='MASK')
defaults_modifier(mod)
mod.vertex_group = obj.vertex_groups[0].name
return mod
# -----------------------------------------------------------------------------
# models
# useful since its solid boxy shape but simple enough to debug errors
cube_like_vertices = (
(1, 1, -1),
(1, -1, -1),
(-1, -1, -1),
(-1, 1, -1),
(1, 1, 1),
(1, -1, 1),
(-1, -1, 1),
(-1, 1, 1),
(0, -1, -1),
(1, 0, -1),
(0, 1, -1),
(-1, 0, -1),
(1, 0, 1),
(0, -1, 1),
(-1, 0, 1),
(0, 1, 1),
(1, -1, 0),
(1, 1, 0),
(-1, -1, 0),
(-1, 1, 0),
(0, 0, -1),
(0, 0, 1),
(1, 0, 0),
(0, -1, 0),
(-1, 0, 0),
(2, 0, 0),
(2, 0, -1),
(2, 1, 0),
(2, 1, -1),
(0, 1, 2),
(0, 0, 2),
(-1, 0, 2),
(-1, 1, 2),
(-1, 0, 3),
(-1, 1, 3),
(0, 1, 3),
(0, 0, 3),
)
cube_like_faces = (
(0, 9, 20, 10),
(0, 10, 17),
(0, 17, 27, 28),
(1, 16, 23, 8),
(2, 18, 24, 11),
(3, 19, 10),
(4, 15, 21, 12),
(4, 17, 15),
(7, 14, 31, 32),
(7, 15, 19),
(8, 23, 18, 2),
(9, 0, 28, 26),
(9, 1, 8, 20),
(9, 22, 16, 1),
(10, 20, 11, 3),
(11, 24, 19, 3),
(12, 21, 13, 5),
(13, 6, 18),
(14, 21, 30, 31),
(15, 7, 32, 29),
(15, 17, 10, 19),
(16, 5, 13, 23),
(17, 4, 12, 22),
(17, 22, 25, 27),
(18, 6, 14, 24),
(20, 8, 2, 11),
(21, 14, 6, 13),
(21, 15, 29, 30),
(22, 9, 26, 25),
(22, 12, 5, 16),
(23, 13, 18),
(24, 14, 7, 19),
(28, 27, 25, 26),
(29, 32, 34, 35),
(30, 29, 35, 36),
(31, 30, 36, 33),
(32, 31, 33, 34),
(35, 34, 33, 36),
)
# useful since its a shell for solidify and it can be mirrored
cube_shell_vertices = (
(0, 0, 1),
(0, 1, 1),
(-1, 1, 1),
(-1, 0, 1),
(0, 0, 0),
(0, 1, 0),
(-1, 1, 0),
(-1, 0, 0),
(-1, -1, 0),
(0, -1, 0),
(0, 0, -1),
(0, 1, -1),
)
cube_shell_face = (
(0, 1, 2, 3),
(0, 3, 8, 9),
(1, 5, 6, 2),
(2, 6, 7, 3),
(3, 7, 8),
(4, 7, 10),
(6, 5, 11),
(7, 4, 9, 8),
(10, 7, 6, 11),
)
def make_cube(scene):
bpy.ops.mesh.primitive_cube_add(align='WORLD',
enter_editmode=False,
location=(0, 0, 0),
rotation=(0, 0, 0),
)
obj = scene.objects.active
defaults_object(obj)
return obj
def make_cube_extra(scene):
obj = make_cube(scene)
# extra data layers
mesh_uv_add(obj)
mesh_vcol_add(obj)
mesh_vgroup_add(obj)
return obj
def make_cube_like(scene):
mesh = bpy.data.meshes.new(whoami())
mesh.from_pydata(cube_like_vertices, (), cube_like_faces)
mesh.update() # add edges
obj = bpy.data.objects.new(whoami(), mesh)
scene.objects.link(obj)
defaults_object(obj)
return obj
def make_cube_like_extra(scene):
obj = make_cube_like(scene)
# extra data layers
mesh_uv_add(obj)
mesh_vcol_add(obj)
mesh_vgroup_add(obj)
return obj
def make_cube_shell(scene):
mesh = bpy.data.meshes.new(whoami())
mesh.from_pydata(cube_shell_vertices, (), cube_shell_face)
mesh.update() # add edges
obj = bpy.data.objects.new(whoami(), mesh)
scene.objects.link(obj)
defaults_object(obj)
return obj
def make_cube_shell_extra(scene):
obj = make_cube_shell(scene)
# extra data layers
mesh_uv_add(obj)
mesh_vcol_add(obj)
mesh_vgroup_add(obj)
return obj
def make_monkey(scene):
bpy.ops.mesh.primitive_monkey_add(align='WORLD',
enter_editmode=False,
location=(0, 0, 0),
rotation=(0, 0, 0),
)
obj = scene.objects.active
defaults_object(obj)
return obj
def make_monkey_extra(scene):
obj = make_monkey(scene)
# extra data layers
mesh_uv_add(obj)
mesh_vcol_add(obj)
mesh_vgroup_add(obj)
return obj
# -----------------------------------------------------------------------------
# tests (utils)
global_tests = []
global_tests.append(
("none",
(),
)
)
# single
global_tests.append(
("subsurf_single",
((modifier_subsurf_add, dict(levels=2)), ),
)
)
global_tests.append(
("armature_single",
((modifier_armature_add, dict()), ),
)
)
global_tests.append(
("mirror_single",
((modifier_mirror_add, dict()), ),
)
)
global_tests.append(
("hook_single",
((modifier_hook_add, dict()), ),
)
)
global_tests.append(
("decimate_single",
((modifier_decimate_add, dict()), ),
)
)
global_tests.append(
("build_single",
((modifier_build_add, dict()), ),
)
)
global_tests.append(
("mask_single",
((modifier_mask_add, dict()), ),
)
)
# combinations
global_tests.append(
("mirror_subsurf",
((modifier_mirror_add, dict()),
(modifier_subsurf_add, dict(levels=2))),
)
)
global_tests.append(
("solidify_subsurf",
((modifier_solidify_add, dict()),
(modifier_subsurf_add, dict(levels=2))),
)
)
def apply_test(
test, scene, obj,
render_func=None,
render_args=None,
render_kwargs=None,
):
test_name, test_funcs = test
for cb, kwargs in test_funcs:
cb(scene, obj, **kwargs)
render_kwargs_copy = render_kwargs.copy()
# add test name in filepath
render_kwargs_copy["filepath"] += "_%s" % test_name
render_func(*render_args, **render_kwargs_copy)
# -----------------------------------------------------------------------------
# tests themselves!
# having the 'test_' prefix automatically means these functions are called
# for testing
def test_cube(context, test):
scene = context.scene
obj = make_cube_extra(scene)
ctx_camera_setup(context, location=(3, 3, 3))
apply_test(
test, scene, obj,
render_func=render_gl_all_modes,
render_args=(context, obj),
render_kwargs=dict(filepath=whoami())
)
def test_cube_like(context, test):
scene = context.scene
obj = make_cube_like_extra(scene)
ctx_camera_setup(context, location=(5, 5, 5))
apply_test(
test, scene, obj,
render_func=render_gl_all_modes,
render_args=(context, obj),
render_kwargs=dict(filepath=whoami())
)
def test_cube_shell(context, test):
scene = context.scene
obj = make_cube_shell_extra(scene)
ctx_camera_setup(context, location=(4, 4, 4))
apply_test(
test, scene, obj,
render_func=render_gl_all_modes,
render_args=(context, obj),
render_kwargs=dict(filepath=whoami())
)
# -----------------------------------------------------------------------------
# call all tests
def main():
print("Calling main!")
# render_gl(bpy.context, "/testme")
# ctx_clear_scene()
context = bpy.context
ctx_clear_scene()
# run all tests
for key, val in sorted(globals().items()):
if key.startswith("test_") and hasattr(val, "__call__"):
print("calling:", key)
for t in global_tests:
val(context, test=t)
ctx_clear_scene()
# -----------------------------------------------------------------------------
# annoying workaround for theme initialization
if __name__ == "__main__":
import bpy
from bpy.app.handlers import persistent
@persistent
def load_handler(dummy):
print("Load Handler:", bpy.data.filepath)
if load_handler.first is False:
bpy.app.handlers.scene_update_post.remove(load_handler)
main()
else:
load_handler.first = False
load_handler.first = True
bpy.app.handlers.scene_update_post.append(load_handler)