blender/tests/python/bl_mesh_modifiers.py
Campbell Barton e955c94ed3 License Headers: Set copyright to "Blender Authors", add AUTHORS
Listing the "Blender Foundation" as copyright holder implied the Blender
Foundation holds copyright to files which may include work from many
developers.

While keeping copyright on headers makes sense for isolated libraries,
Blender's own code may be refactored or moved between files in a way
that makes the per file copyright holders less meaningful.

Copyright references to the "Blender Foundation" have been replaced with
"Blender Authors", with the exception of `./extern/` since these this
contains libraries which are more isolated, any changed to license
headers there can be handled on a case-by-case basis.

Some directories in `./intern/` have also been excluded:

- `./intern/cycles/` it's own `AUTHORS` file is planned.
- `./intern/opensubdiv/`.

An "AUTHORS" file has been added, using the chromium projects authors
file as a template.

Design task: #110784

Ref !110783.
2023-08-16 00:20:26 +10:00

835 lines
19 KiB
Python

# SPDX-FileCopyrightText: 2012-2022 Blender Authors
#
# 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)