Files
blender/tests/python/sculpt_brush_render_tests.py
Sean Kim 9d930c3b0f Tests: Add initial sculpt mesh render test
* Adds new entry into python CMakeLists.txt for running these tests
* Adds `sculpt_brush_render_tests.py` as the report runner and
  test data initializer for the sculpt tests.

This commit adds the ability to do render tests with the Draw brush in
Sculpt mode on a specified mesh by specifying and performing a stroke in
area-space coordinates.

Like other render tests, these tests map a single .blend file to a
single reference image. Currently we test the following cases:

* A "base" mesh.
* A mesh with a Subdiv modifier added but not applied to it.
* A mesh with a basis & relative shape key added.
* A mesh with the multiresolution modifier added.

The associated reference image used is 200x200 pixels, in testing
with the draw brush, even this small resolution was able to detect
differences in detail caused by major regressions.

In total, the new files in the associated assets repository sum up
to roughly 4.5 MB.

Other than the mesh and modifiers, each of the files also contains a
scene with a camera, with workbench settings set to use a matcap for
the final rendering to better highlight curvature differences.

Part of #130772

Pull Request: https://projects.blender.org/blender/blender/pulls/133602
2025-02-14 07:36:00 +01:00

159 lines
4.1 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import argparse
import os
import sys
def set_view3d_context_override(context_override):
"""
Set context override to become the first viewport in the active workspace
The ``context_override`` is expected to be a copy of an actual current context
obtained by `context.copy()`
"""
for area in context_override["screen"].areas:
if area.type != 'VIEW_3D':
continue
for space in area.spaces:
if space.type != 'VIEW_3D':
continue
for region in area.regions:
if region.type != 'WINDOW':
continue
context_override["area"] = area
context_override["region"] = region
def generate_stroke(context):
"""
Generate stroke for the bpy.ops.sculpt.brush_stroke operator
The generated stroke covers the full plane diagonal.
"""
from mathutils import Vector
template = {
"name": "stroke",
"mouse": (0.0, 0.0),
"mouse_event": (0, 0),
"location": (0.0, 0.0, 0.0),
"is_start": True,
"pressure": 1.0,
"time": 1.0,
"size": 1.0,
"x_tilt": 0,
"y_tilt": 0
}
num_steps = 250
start = Vector((context['area'].width, context['area'].height))
end = Vector((0, 0))
delta = (end - start) / (num_steps - 1)
stroke = []
for i in range(num_steps):
step = template.copy()
step["mouse"] = start + delta * i
step["mouse_event"] = start + delta * i
stroke.append(step)
return stroke
def setup():
"""
Prepare the scene for rendering - generates objects then performs a stroke
"""
import bpy
context = bpy.context
# Create an undo stack explicitly. This isn't created by default in background mode.
bpy.ops.ed.undo_push()
# Forcibly flip the object out of and back into sculpt mode to avoid poll errors due to non-initialized
# tool runtime data.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='SCULPT')
context_override = context.copy()
set_view3d_context_override(context_override)
with context.temp_override(**context_override):
bpy.ops.sculpt.brush_stroke(stroke=generate_stroke(context_override), override_location=True)
# Multires workaround - we need to leave sculpt mode currently to flush MDISP data so that the
# render actually works.
bpy.ops.object.mode_set(mode='OBJECT')
try:
import bpy
inside_blender = True
except ImportError:
inside_blender = False
if inside_blender:
try:
setup()
except Exception as e:
print(e)
sys.exit(1)
def get_arguments(filepath, output_filepath):
dirname = os.path.dirname(filepath)
args = [
"--background",
"--factory-startup",
"--enable-autoexec",
"--debug-memory",
"--debug-exit-on-error",
filepath,
"-E", "BLENDER_WORKBENCH",
"-P", os.path.realpath(__file__),
"-o", output_filepath,
"-f", "1",
"-F", "PNG"]
return args
def create_argparse():
parser = argparse.ArgumentParser(
description="Run test script for each blend file in TESTDIR, comparing the render result with known output."
)
parser.add_argument("--blender", required=True)
parser.add_argument("--testdir", required=True)
parser.add_argument("--outdir", required=True)
parser.add_argument("--oiiotool", required=True)
parser.add_argument("--batch", default=False, action="store_true")
return parser
def main():
parser = create_argparse()
args = parser.parse_args()
from modules import render_report
report = render_report.Report("Sculpt", args.outdir, args.oiiotool)
report.set_pixelated(True)
report.set_fail_threshold(2.0 / 255.0)
report.set_fail_percent(1.5)
report.set_reference_dir("reference")
ok = report.run(args.testdir, args.blender, get_arguments, batch=args.batch)
sys.exit(not ok)
if not inside_blender and __name__ == "__main__":
main()