mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Tests: Add sculpt automask test for 'Face Set' and 'Topology' modes
Adds a new file for testing automask modes, specifically testing the 'Face Set' and 'Topology' modes. This is achieved by performing a stroke on a Suzanne mesh with the face sets initialized by loose parts (i.e. the eyes of the mesh). Additionally, minor changes are included to the common paint test helper to make testing easier. Pull Request: https://projects.blender.org/blender/blender/pulls/154342
This commit is contained in:
parent
2a547ab59e
commit
ac17d71d27
5 changed files with 192 additions and 8 deletions
|
|
@ -1404,6 +1404,13 @@ if(TEST_SRC_DIR_EXISTS)
|
|||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_sculpt_automasking
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/automask_test.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_sculpt_brushes
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/sculpt_brushes_test.py
|
||||
|
|
|
|||
173
tests/python/sculpt_paint/automask_test.py
Normal file
173
tests/python/sculpt_paint/automask_test.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import math
|
||||
import unittest
|
||||
import sys
|
||||
import pathlib
|
||||
import numpy as np
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
blender -b --factory-startup --python tests/python/sculpt_paint/sculpt_brushes_test.py -- --testdir tests/files/mesh_paint/
|
||||
"""
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
||||
from modules.test_helpers import AttributeType, BackendType, get_attribute_data, set_view3d_context_override, generate_stroke, generate_monkey
|
||||
|
||||
args = None
|
||||
|
||||
|
||||
def get_verts_without_face_set(mesh, attr_data, face_set):
|
||||
faces = np.where(attr_data != face_set)
|
||||
verts_per_face = [list(mesh.polygons[int(idx)].vertices) for idx in faces[0]]
|
||||
verts = [v for face_verts in verts_per_face for v in face_verts]
|
||||
|
||||
return list(set(verts))
|
||||
|
||||
|
||||
def get_verts_with_face_set(mesh, attr_data, face_set):
|
||||
faces = np.where(attr_data == face_set)
|
||||
verts_per_face = [list(mesh.polygons[int(idx)].vertices) for idx in faces[0]]
|
||||
verts = [v for face_verts in verts_per_face for v in face_verts]
|
||||
|
||||
return list(set(verts))
|
||||
|
||||
|
||||
def get_verts_with_island_id(attr_data, island_id):
|
||||
verts = np.where(attr_data == island_id)
|
||||
|
||||
return verts
|
||||
|
||||
|
||||
def get_verts_without_island_id(attr_data, island_id):
|
||||
verts = np.where(attr_data != island_id)
|
||||
|
||||
return verts
|
||||
|
||||
|
||||
class BrushAutomaskTest(unittest.TestCase):
|
||||
"""
|
||||
Test that automasking prevents certain vertices from being modified
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(
|
||||
filepath=str(
|
||||
args.testdir /
|
||||
"monkey_realized_island_id_and_face_set.blend"),
|
||||
load_ui=False)
|
||||
bpy.ops.object.mode_set(mode="SCULPT")
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
result = bpy.ops.brush.asset_activate(
|
||||
asset_library_type='ESSENTIALS',
|
||||
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Draw')
|
||||
self.assertEqual({'FINISHED'}, result)
|
||||
|
||||
def test_face_set_automasking_ignores_any_non_starting_face_set(self):
|
||||
# This test mesh has 3 face sets, 1 and 2 are used for the eyes, the rest of the monkey's head is 3
|
||||
ACTIVE_FACE_SET = 3
|
||||
bpy.data.scenes[0].tool_settings.sculpt.use_automasking_face_sets = True
|
||||
|
||||
initial_data = get_attribute_data(BackendType.MESH, AttributeType.POSITION)
|
||||
|
||||
context_override = bpy.context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.sculpt.brush_stroke(
|
||||
stroke=generate_stroke(
|
||||
context_override,
|
||||
start_percent=(0.5, 0.5)),
|
||||
override_location=True)
|
||||
|
||||
new_data = get_attribute_data(BackendType.MESH, AttributeType.POSITION)
|
||||
|
||||
mesh = bpy.context.active_object.data
|
||||
num_faces = mesh.attributes.domain_size('FACE')
|
||||
face_set_data = np.zeros(num_faces, dtype=np.int32)
|
||||
face_set_attribute = mesh.attributes.get(".sculpt_face_set")
|
||||
face_set_attribute.data.foreach_get('value', np.ravel(face_set_data))
|
||||
|
||||
verts_with_face_set = get_verts_with_face_set(mesh, face_set_data, ACTIVE_FACE_SET)
|
||||
|
||||
filtered_initial_data = initial_data[verts_with_face_set]
|
||||
filtered_new_data = new_data[verts_with_face_set]
|
||||
|
||||
any_different = any([orig != new for (orig, new) in zip(filtered_initial_data, filtered_new_data)])
|
||||
self.assertTrue(any_different, "At least one position should be different from its original value")
|
||||
|
||||
verts_without_face_set = get_verts_without_face_set(mesh, face_set_data, ACTIVE_FACE_SET)
|
||||
|
||||
filtered_initial_data = initial_data[verts_without_face_set]
|
||||
filtered_new_data = new_data[verts_without_face_set]
|
||||
|
||||
all_same = all([orig == new for (orig, new) in zip(filtered_initial_data, filtered_new_data)])
|
||||
self.assertTrue(all_same, "Vertices that are not included in the original face sets should be unchanged")
|
||||
|
||||
def test_topology_automasking_ignores_any_non_starting_island(self):
|
||||
# This test mesh has 3 island ids, 0 and 1 are used for the eyes, the rest of the monkey's head is 2
|
||||
ACTIVE_ISLAND_ID = 2
|
||||
bpy.data.scenes[0].tool_settings.sculpt.use_automasking_face_sets = True
|
||||
|
||||
initial_data = get_attribute_data(BackendType.MESH, AttributeType.POSITION)
|
||||
|
||||
context_override = bpy.context.copy()
|
||||
set_view3d_context_override(context_override)
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.sculpt.brush_stroke(
|
||||
stroke=generate_stroke(
|
||||
context_override,
|
||||
start_percent=(0.5, 0.5)),
|
||||
override_location=True)
|
||||
|
||||
new_data = get_attribute_data(BackendType.MESH, AttributeType.POSITION)
|
||||
|
||||
mesh = bpy.context.active_object.data
|
||||
num_faces = mesh.attributes.domain_size('POINT')
|
||||
island_id_data = np.zeros(num_faces, dtype=np.int32)
|
||||
island_id_attribute = mesh.attributes.get("island_id")
|
||||
island_id_attribute.data.foreach_get('value', np.ravel(island_id_data))
|
||||
|
||||
verts_with_island_id = get_verts_with_island_id(island_id_data, ACTIVE_ISLAND_ID)
|
||||
|
||||
filtered_initial_data = initial_data[verts_with_island_id]
|
||||
filtered_new_data = new_data[verts_with_island_id]
|
||||
|
||||
any_different = any([orig != new for (orig, new) in zip(filtered_initial_data, filtered_new_data)])
|
||||
self.assertTrue(any_different, "At least one position should be different from its original value")
|
||||
|
||||
verts_without_island_id = get_verts_without_island_id(island_id_data, ACTIVE_ISLAND_ID)
|
||||
|
||||
filtered_initial_data = initial_data[verts_without_island_id]
|
||||
filtered_new_data = new_data[verts_without_island_id]
|
||||
|
||||
all_same = all([orig == new for (orig, new) in zip(filtered_initial_data, filtered_new_data)])
|
||||
self.assertTrue(all_same, "Vertices that are not part of the initial mesh island should be unchanged")
|
||||
|
||||
|
||||
def main():
|
||||
global args
|
||||
import argparse
|
||||
|
||||
argv = [sys.argv[0]]
|
||||
if '--' in sys.argv:
|
||||
argv += sys.argv[sys.argv.index('--') + 1:]
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--testdir', required=True, type=pathlib.Path)
|
||||
|
||||
args, remaining = parser.parse_known_args(argv)
|
||||
|
||||
unittest.main(argv=remaining)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -170,12 +170,11 @@ def generate_monkey(backend):
|
|||
bpy.ops.object.subdivision_set(level=2, relative=False, ensure_modifier=True)
|
||||
|
||||
|
||||
def generate_stroke(context, start_over_mesh=False):
|
||||
def generate_stroke(context, start_percent=(0.0, 0.0), end_percent=(1.0, 1.0)):
|
||||
"""
|
||||
Generate stroke for any of the paint mode operators (e.g. bpy.ops.sculpt.brush_stroke_
|
||||
|
||||
The generated stroke coves the full plane diagonal.
|
||||
:param start_over_mesh start the stroke in the center of the screen (over the object)
|
||||
"""
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
|
@ -194,12 +193,9 @@ def generate_stroke(context, start_over_mesh=False):
|
|||
}
|
||||
|
||||
num_steps = 50
|
||||
if start_over_mesh:
|
||||
start = Vector((context['area'].width // 2, context['area'].height // 2))
|
||||
else:
|
||||
start = Vector((context['area'].width, context['area'].height))
|
||||
|
||||
end = Vector((0, 0))
|
||||
start = Vector((0 * start_percent[0], 0 * start_percent[1]))
|
||||
end = Vector((context['area'].width * end_percent[0], context['area'].height * end_percent[1]))
|
||||
delta = (end - start) / (num_steps - 1)
|
||||
|
||||
stroke = []
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ class MeshBrushTests(unittest.TestCase):
|
|||
"""
|
||||
Compare the prior and post states of a brush stroke
|
||||
"""
|
||||
if start_over_mesh:
|
||||
start_percent = (0.5, 0.5)
|
||||
else:
|
||||
start_percent = (0.0, 0.0)
|
||||
|
||||
initial_data = get_attribute_data(backend, attribute)
|
||||
|
||||
context_override = bpy.context.copy()
|
||||
|
|
@ -59,7 +64,7 @@ class MeshBrushTests(unittest.TestCase):
|
|||
bpy.ops.sculpt.brush_stroke(
|
||||
stroke=generate_stroke(
|
||||
context_override,
|
||||
start_over_mesh),
|
||||
start_percent=start_percent),
|
||||
override_location=True, **opts)
|
||||
|
||||
new_data = get_attribute_data(backend, attribute)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue