diff --git a/tests/data b/tests/data index efc4f5aa793..ef396e1c8ba 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit efc4f5aa793e17f10998e42cb43a36d6891ff245 +Subproject commit ef396e1c8bac8bf42d6dad9b6476f3abe7753086 diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index a38e6765bd2..727367e4bc7 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -1124,6 +1124,28 @@ else() endforeach() endif() +# ------------------------------------------------------------------------------ +# SCULPTING RENDER TESTS + +if(WITH_GPU_RENDER_TESTS) + if(NOT OPENIMAGEIO_TOOL) + message(STATUS "Disabling sculpting render tests because OIIO oiiotool does not exist") + else() + set(render_tests + brushes + ) + + foreach(render_test ${render_tests}) + add_render_test( + sculpt_render_${render_test} + ${CMAKE_CURRENT_LIST_DIR}/sculpt_brush_render_tests.py + --testdir "${TEST_SRC_DIR}/sculpting/${render_test}" + --outdir "${TEST_OUT_DIR}/sculpting" + ) + endforeach() + endif() +endif() + # ------------------------------------------------------------------------------ # Headless GUI Tests diff --git a/tests/python/sculpt_brush_render_tests.py b/tests/python/sculpt_brush_render_tests.py new file mode 100644 index 00000000000..61977236041 --- /dev/null +++ b/tests/python/sculpt_brush_render_tests.py @@ -0,0 +1,158 @@ +#!/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()