Refactor depth surface handling and unify depth sampling rules

- Removed the `always_vertex` parameter from `build_surface_camera` to streamline depth sampling for cutouts and ensure consistent behavior across depth plane and panorama.
- Updated depth sampling logic to ensure that all free boundary points, including the original frame of the depth plane, use neighboring face depth instead of their own texture sampling.
- Adjusted the depth mask deletion process to operate on faces rather than points, eliminating the need for additional cleanup of dangling edges.
- Ensured that the cutout rules are consistently applied across all relevant nodes, removing exceptions and simplifying the logic.
- Updated tests to verify the new behavior, ensuring that cutout points follow the depth of adjacent faces and that the original frame maintains its depth sampling.
- Refactored geometry handling in multiple depth-related nodes to improve clarity and maintainability.
This commit is contained in:
2026-09-16 16:30:56 +08:00
parent 7f69630371
commit c62ec2cd48
28 changed files with 880 additions and 173 deletions

View File

@@ -9,6 +9,67 @@ import pytest
from tests.support.depth_surface import surface, evaluated, assert_closed, component_count
from tools.nodes.common.boundary_smoothing import cut_boundary_influence
from tools.nodes.common.smoothing import pinned_smooth
def pinned_group(
iterations=1,
region=1.0,
weight=1.0,
movement=None,
pin_boundary=True,
pin_sharp=True,
):
group = bpy.data.node_groups.new("Pinned smooth", "GeometryNodeTree")
for direction in ("INPUT", "OUTPUT"):
group.interface.new_socket(
name="Geometry", in_out=direction, socket_type="NodeSocketGeometry"
)
smooth = group.interface.new_socket(
name="Smooth", in_out="INPUT", socket_type="NodeSocketInt"
)
smooth.default_value = iterations
source = group.nodes.new("NodeGroupInput")
control = group.nodes.new("NodeGroupInput")
region_value = group.nodes.new("ShaderNodeValue")
region_value.outputs[0].default_value = region
result = pinned_smooth(
group,
source.outputs["Geometry"],
control.outputs["Smooth"],
region_value.outputs[0],
weight=weight,
movement=movement,
pin_boundary=pin_boundary,
pin_sharp=pin_sharp,
)
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
return group
def pinned_result(points, faces, **options):
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("Pinned")
mesh.from_pydata(points, [], faces)
obj = bpy.data.objects.new("Pinned", mesh)
bpy.context.collection.objects.link(obj)
obj.modifiers.new("Pinned", "NODES").node_group = pinned_group(**options)
return evaluated(obj)[0]
def grid(size):
points = [(x, y, 0.0) for y in range(size) for x in range(size)]
faces = [
(
y * size + x,
y * size + x + 1,
(y + 1) * size + x + 1,
(y + 1) * size + x,
)
for y in range(size - 1)
for x in range(size - 1)
]
return points, faces
def diagonal_surface(triangles=False):
@@ -287,3 +348,64 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band():
np.testing.assert_array_equal(result_uv, uv)
np.testing.assert_array_equal(after[outside], before[outside])
assert np.max(np.linalg.norm(after-before, axis=1)) > 1e-4
def test_boundary_points_follow_the_boundary_strip_blur():
"""A boundary spike relaxes along the outline instead of being pulled inward."""
points, faces = grid(4)
points[1] = (1.0, -0.5, 0.0)
result = pinned_result(points, faces)
# 边界条带只连接边界邻居:自身权重 1,两个边界邻居权重 0.5。
np.testing.assert_allclose(result[1], (1.0, -0.25, 0.0), atol=1e-6)
def test_sharp_features_lower_the_smoothing_weight():
"""A crease inside the normal blur range reduces the pinned displacement."""
points, faces = grid(3)
points[4] = (1.0, -0.5, 0.0)
flat = pinned_result(points, faces)
creased = list(points)
creased[0] = (0.0, 0.0, 1.0)
creased = pinned_result(creased, faces)
# 折痕不在尖点的邻居集合内,两次运行的平滑目标相同,只有锐度系数不同。
flat_step = abs(flat[4][1] + 0.5)
creased_step = abs(creased[4][1] + 0.5)
assert flat_step > 1e-3
assert creased_step < flat_step * 0.99
def test_junction_band_uses_the_neighbourhood_average():
"""Without the boundary pin a boundary spike follows every connected neighbour."""
points, faces = grid(4)
points[1] = (1.0, -0.5, 0.0)
result = pinned_result(points, faces, pin_boundary=False)
# 自身权重 1 与三个邻居的平均:(-0.5 + 0 + 0 + 1) / 4。
np.testing.assert_allclose(result[1], (1.0, 0.125, 0.0), atol=1e-6)
def test_junction_band_keeps_the_sharpness_weight():
"""The sharpness weight stays active without the boundary pin."""
points, faces = grid(3)
points[4] = (1.0, -0.5, 0.0)
flat = pinned_result(points, faces, pin_boundary=False)
creased = list(points)
creased[0] = (0.0, 0.0, 1.0)
creased = pinned_result(creased, faces, pin_boundary=False)
flat_step = abs(flat[4][1] + 0.5)
creased_step = abs(creased[4][1] + 0.5)
assert flat_step > 1e-3
assert creased_step < flat_step * 0.99
def test_movement_mask_keeps_the_symmetry_plane():
"""Restricting the movement leaves the masked axis untouched."""
points, faces = grid(4)
# 网格落在 XZ 对称平面上,Y 是只允许被松开的对称轴。
points = [(x, z, y) for x, y, z in points]
points[5] = (2.0, 0.5, 1.0)
original = np.array(points)
masked = pinned_result(points, faces, movement=(1, 0, 1))
np.testing.assert_array_equal(masked[:, 1], original[:, 1])
assert np.max(np.abs(masked[:, 0] - original[:, 0])) > 1e-4
full = pinned_result(points, faces)
assert np.max(np.abs(full[:, 1] - original[:, 1])) > 1e-4