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

@@ -62,7 +62,7 @@ def symmetry(request):
cutout, mirror = setters
mirror('Fill Smooth', 0)
for name, value in [('Depth Image', image), ('Reference Depth', 1.45),
('Depth Split', 0), ('Boundary Smooth', 0), ('Smooth', 0), ('Depth Axis', 0)]:
('Depth Split', 0), ('Boundary Smooth', 0), ('Depth Axis', 0)]:
cutout(name, value)
yield obj, cutout, mirror
bpy.ops.wm.read_factory_settings(use_empty=True)
@@ -84,7 +84,7 @@ def test_zero_thickness_matches_original_projected(symmetry, mode):
cutout('Mode', mode)
baseline = json.loads((Path(__file__).parent / 'fixtures/projected_symmetry.json').read_text())
for case in baseline['cases']:
mirror('Symmetry Direction', case['direction'])
mirror('Direction', case['direction'])
mirror('Fill Sides', case['filled'])
points, faces = evaluated(obj)
expected = np.array(case['points'])
@@ -99,7 +99,7 @@ def test_completed_geometry_is_mirrored_and_closed(symmetry, mode, smooth):
obj, cutout, mirror = symmetry
cutout('Mode', mode)
cutout('Thickness', .2 if mode else .6, 'DISTANCE' if mode else 'NONE')
mirror('Symmetry Direction', (.3, -.2, 1))
mirror('Direction', (.3, -.2, 1))
mirror('Fill Smooth', smooth)
points, faces = evaluated(obj)
assert len(points) and np.isfinite(points).all()
@@ -150,25 +150,47 @@ def test_fill_smoothing_preserves_surfaces_and_only_moves_wall_interiors(symmetr
def test_direction_offset_and_empty_geometry(symmetry):
obj, _, mirror = symmetry
mirror('Depth Offset', 2.)
mirror('Symmetry Direction', (.3, -.2, 1))
mirror('Offset', 2.)
mirror('Direction', (.3, -.2, 1))
original, original_faces = evaluated(obj)
mirror('Depth Offset', 2.2)
mirror('Offset', 2.2)
points, faces = evaluated(obj)
assert faces == original_faces
np.testing.assert_allclose(points[:, :2], original[:, :2], atol=1e-6)
np.testing.assert_allclose(abs(points[:, 2]) - abs(original[:, 2]), .2, atol=1e-6)
mirror('Depth Offset', -10.)
mirror('Offset', -10.)
points, faces = evaluated(obj)
assert not len(points) and not faces
def test_symmetry_scale_uses_aligned_axis(symmetry):
obj, _, mirror = symmetry
mirror('Direction', (.3, -.2, 1))
mirror('Offset', .5)
mirror('Fill Sides', False)
original, original_faces = evaluated(obj)
mirror('Scale', 2)
points, faces = evaluated(obj)
assert faces == original_faces
np.testing.assert_allclose(points[:, :2], original[:, :2], atol=1e-6)
np.testing.assert_allclose(points[:, 2], original[:, 2] * 2, atol=1e-6)
def test_symmetry_interface_and_cutout_defaults(symmetry):
obj, _, _ = symmetry
inputs = [item for item in obj.modifiers[1].node_group.interface.items_tree
if item.item_type == 'SOCKET' and item.in_out == 'INPUT']
assert [item.name for item in inputs] == ['Geometry', 'Symmetry Direction', 'Depth Offset', 'Fill Sides', 'Fill Smooth']
assert [item.name for item in inputs] == [
'Geometry', 'Direction', 'Offset', 'Scale', 'Fill Sides', 'Fill Smooth',
]
assert inputs[3].default_value == 1
assert (inputs[2].min_value, inputs[2].max_value) == (-10, 10)
assert (inputs[3].min_value, inputs[3].max_value) == (0, 2)
assert inputs[-1].default_value == 2 and inputs[-2].default_value is True
assert (inputs[-1].min_value, inputs[-1].max_value) == (0, 16)
assert inputs[-1].parent == inputs[-2].parent
assert inputs[-1].parent.name == 'Options'
assert inputs[-1].parent.default_closed is True
thickness = [item for item in obj.modifiers[0].node_group.interface.items_tree
if item.item_type == 'SOCKET' and item.name == 'Thickness']
assert len(thickness) == 2 and all(item.default_value == 0 for item in thickness)