feat(cutout): implement boundary padding for cutout textures

- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px.
- Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth.
- Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data.
- Added tests to validate the new boundary padding functionality and its integration with existing cutout processes.

feat(geometry): remove overall mesh smoothing from geometry nodes

- Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing.
- Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes.
- Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent.
- Updated tests and documentation to reflect the changes in node interfaces and behaviors.

feat(normals): scale tangent normals with depth

- Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`.
- Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute.
- Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
This commit is contained in:
2026-09-15 00:40:24 +08:00
parent ab282e4fdf
commit 591e62085a
93 changed files with 1915 additions and 2851 deletions

View File

@@ -64,23 +64,8 @@ def test_quad_strips_and_corners_are_classified_once(cells, expected, offset, re
assert cleaned_faces(vertices, faces) == expected
@pytest.mark.parametrize("count", [3, 5, 6])
def test_non_quad_faces_are_preserved(count):
angles = np.linspace(0, 2 * np.pi, count, endpoint=False)
vertices = [(np.cos(a), np.sin(a), 0) for a in angles]
assert cleaned_faces(vertices, [tuple(range(count))]) == {0}
def test_nonmanifold_edge_is_not_a_boundary():
vertices = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0),
(2, 0, 0), (1, 2, 0), (0, 2, 1), (-1, 0, 0)]
faces = [(0, 1, 2, 3), (2, 1, 4), (3, 2, 5), (2, 3, 6), (0, 3, 7)]
assert cleaned_faces(vertices, faces) == set(range(5))
@pytest.mark.parametrize("width, height, expected", [(1, 1, 0), (5, 1, 0), (2, 2, 8), (3, 3, 18)])
@pytest.mark.parametrize("reverse", [False, True])
def test_triangle_pairs_remove_strips_and_preserve_surface_corners(width, height, expected, reverse):
@pytest.mark.parametrize("width,height,expected", [(2, 2, 8), (3, 3, 18)])
def test_triangle_pairs_remove_strips_and_preserve_surface_corners(width, height, expected):
vertices = [(x, y, 0) for y in range(height + 1) for x in range(width + 1)]
faces = []
for y in range(height):
@@ -89,36 +74,11 @@ def test_triangle_pairs_remove_strips_and_preserve_surface_corners(width, height
b, c, d = a + 1, a + width + 2, a + width + 1
faces.extend([(a, b, c), (a, c, d)])
# Reverse alternate faces to test matching by vertex identity, not winding.
if reverse:
faces = [face[::-1] if i % 2 else face[1:] + face[:1] for i, face in enumerate(faces)]
faces = [face[::-1] if i % 2 else face[1:] + face[:1] for i, face in enumerate(faces)]
result = cleaned_faces(vertices, faces, triangles=True)
assert len(result) == expected
def test_isolated_triangle_is_removed_only_for_cutout():
vertices = [(0, 0, 0), (1, 0, 0), (0, 1, 0)]
assert cleaned_faces(vertices, [(0, 1, 2)], triangles=True) == set()
assert cleaned_faces(vertices, [(0, 1, 2)]) == {0}
def test_triangle_pair_requires_a_manifold_shared_edge():
vertices = [(0, 0, 0), (1, 0, 0), (.5, 1, 0), (.5, -1, 0), (.5, 0, 1)]
faces = [(0, 1, 2), (1, 0, 3), (0, 1, 4)]
assert cleaned_faces(vertices, faces, triangles=True) == {0, 1, 2}
def test_triangle_pair_requires_four_distinct_vertices():
vertices = [(0, 0, 0), (1, 0, 0), (0, 1, 0)]
assert cleaned_faces(vertices, [(0, 1, 2), (2, 1, 0)], triangles=True) == {0, 1}
def test_triangle_cleanup_marks_both_faces_without_recursive_peeling():
vertices = [(0, 0, 0), (2, 0, 0), (1, 2, 0), (1, -2, 0),
(3, 2, 0), (-1, 2, 0), (-1, -1, 0)]
faces = [(0, 1, 2), (1, 0, 3), (2, 1, 4), (0, 2, 5), (3, 0, 6)]
assert cleaned_faces(vertices, faces, triangles=True) == {0, 2, 3}
@pytest.mark.parametrize("kind", ["plane", "cutout"])
def test_strips_are_removed_before_projection_and_closed_thickness(kind):
from tests.support.depth_surface import surface, evaluated, assert_closed, component_count
@@ -144,15 +104,11 @@ def test_strips_are_removed_before_projection_and_closed_thickness(kind):
assert len(faces) == 4 and len(vertices) == 9
assert component_count(faces) == 1
set_value("Thickness", 0.2)
before_smooth, shell = evaluated(obj)
vertices, shell = evaluated(obj)
assert len(shell) == 16
assert_closed(shell)
set_value("Smooth", 2)
after_smooth, smooth_shell = evaluated(obj)
assert smooth_shell == shell
assert np.isfinite(after_smooth).all()
assert_closed(smooth_shell)
assert component_count(smooth_shell) == 1
assert np.isfinite(vertices).all()
assert component_count(shell) == 1
set_value("Thickness", 0.0)
assert len(evaluated(obj)[1]) == 4