Tests: Simple automated sculpt brush stroke performance test
As an initial step to creating automated regression tests for sculpt brushes, make our existing performance test script into an automated performance test. The test uses the brush active in each file and runs the brush stroke operator on a large generated grid. The time is just for the brush evaluation, it doesn't include building the PBVH, drawing, etc. I'm not sure about the consequences of conditionally disabling `view3d_operator_needs_opengl`, but it was needed to make the test work in background mode. Pull Request: https://projects.blender.org/blender/blender/pulls/123148
This commit is contained in:
parent
057fdf4224
commit
bec350ba6e
@ -46,6 +46,7 @@
|
||||
#include "BKE_colortools.hh"
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_customdata.hh"
|
||||
#include "BKE_global.hh"
|
||||
#include "BKE_image.h"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_layer.hh"
|
||||
@ -5567,7 +5568,9 @@ static void sculpt_brush_stroke_init(bContext *C)
|
||||
SculptSession &ss = *CTX_data_active_object(C)->sculpt;
|
||||
const Brush *brush = BKE_paint_brush_for_read(&sd.paint);
|
||||
|
||||
view3d_operator_needs_opengl(C);
|
||||
if (!G.background) {
|
||||
view3d_operator_needs_opengl(C);
|
||||
}
|
||||
sculpt_brush_init_tex(sd, ss);
|
||||
|
||||
const bool needs_colors = SCULPT_tool_is_paint(brush->sculpt_tool) &&
|
||||
|
155
tests/performance/tests/sculpt.py
Normal file
155
tests/performance/tests/sculpt.py
Normal file
@ -0,0 +1,155 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import api
|
||||
|
||||
|
||||
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 prepare_sculpt_scene(context):
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
"""
|
||||
Prepare a clean state of the scene suitable for benchmarking
|
||||
|
||||
It creates a high-res object and moves it to a sculpt mode.
|
||||
"""
|
||||
|
||||
# Ensure the current mode is object, as it might not be the always the case
|
||||
# if the benchmark script is run from a non-clean state of the .blend file.
|
||||
if context.object:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Delete all current objects from the scene.
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
bpy.ops.outliner.orphans_purge()
|
||||
|
||||
group = bpy.data.node_groups.new("Test", 'GeometryNodeTree')
|
||||
group.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
|
||||
group_output_node = group.nodes.new('NodeGroupOutput')
|
||||
|
||||
size = 1500
|
||||
|
||||
grid_node = group.nodes.new('GeometryNodeMeshGrid')
|
||||
grid_node.inputs["Size X"].default_value = 2.0
|
||||
grid_node.inputs["Size Y"].default_value = 2.0
|
||||
grid_node.inputs["Vertices X"].default_value = size
|
||||
grid_node.inputs["Vertices Y"].default_value = size
|
||||
|
||||
group.links.new(grid_node.outputs["Mesh"], group_output_node.inputs[0])
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=2, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
|
||||
ob = context.object
|
||||
md = ob.modifiers.new("Test", 'NODES')
|
||||
md.node_group = group
|
||||
|
||||
bpy.ops.object.modifier_apply(modifier="Test")
|
||||
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
# Move the plane to the sculpt mode.
|
||||
bpy.ops.object.mode_set(mode='SCULPT')
|
||||
|
||||
|
||||
def generate_stroke(context):
|
||||
"""
|
||||
Generate stroke for the bpy.ops.sculpt.brush_stroke operator
|
||||
|
||||
The generated stroke coves the full plane diagonal.
|
||||
"""
|
||||
from mathutils import Vector
|
||||
|
||||
template = {
|
||||
"name": "stroke",
|
||||
"mouse": (0.0, 0.0),
|
||||
"mouse_event": (0, 0),
|
||||
"pen_flip": False,
|
||||
"is_start": True,
|
||||
"location": (0, 0, 0),
|
||||
"pressure": 1.0,
|
||||
"time": 1.0,
|
||||
"size": 1.0,
|
||||
"x_tilt": 0,
|
||||
"y_tilt": 0
|
||||
}
|
||||
|
||||
num_steps = 100
|
||||
start = Vector((-1, -1, 0))
|
||||
end = Vector((1, 1, 0))
|
||||
delta = (end - start) / (num_steps - 1)
|
||||
|
||||
stroke = []
|
||||
for i in range(num_steps):
|
||||
step = template.copy()
|
||||
step["location"] = start + delta * i
|
||||
# TODO: mouse and mouse_event?
|
||||
stroke.append(step)
|
||||
|
||||
return stroke
|
||||
|
||||
|
||||
def _run(args):
|
||||
import bpy
|
||||
import time
|
||||
context = bpy.context
|
||||
|
||||
# Create an undo stack explicitly. This isn't created by default in background mode.
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
prepare_sculpt_scene(context)
|
||||
|
||||
context_override = context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
|
||||
with context.temp_override(**context_override):
|
||||
start = time.time()
|
||||
bpy.ops.sculpt.brush_stroke(stroke=generate_stroke(context_override))
|
||||
end = time.time()
|
||||
|
||||
result = {'time': end - start}
|
||||
# bpy.ops.wm.save_mainfile(filepath="/home/hans/Documents/test.blend")
|
||||
return result
|
||||
|
||||
|
||||
class SculptBrushTest(api.Test):
|
||||
def __init__(self, filepath):
|
||||
self.filepath = filepath
|
||||
|
||||
def name(self):
|
||||
return self.filepath.stem
|
||||
|
||||
def category(self):
|
||||
return "sculpt"
|
||||
|
||||
def run(self, env, device_id):
|
||||
args = {}
|
||||
|
||||
result, _ = env.run_in_blender(_run, args, [self.filepath])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate(env):
|
||||
filepaths = env.find_blend_files('sculpt/*')
|
||||
return [SculptBrushTest(filepath) for filepath in filepaths]
|
Loading…
Reference in New Issue
Block a user