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:
@@ -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
|
||||
|
||||
@@ -113,6 +113,7 @@ def test_symmetry_interface_and_cutout_defaults(symmetry):
|
||||
"Scale",
|
||||
"Fill Sides",
|
||||
"Fill Smooth",
|
||||
"Seam Smooth",
|
||||
]
|
||||
assert tuple(mirror_inputs[1].default_value) == (0, 0, 1)
|
||||
assert mirror_inputs[3].default_value == 1
|
||||
@@ -219,7 +220,7 @@ def test_disabled_fill_keeps_the_welded_seam(symmetry):
|
||||
assert not any(plane[edge[0]] and plane[edge[1]] for edge in open_edges)
|
||||
|
||||
|
||||
def test_fill_smooth_only_relaxes_the_seam(symmetry):
|
||||
def test_fill_smooth_relaxes_the_junction_band(symmetry):
|
||||
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
|
||||
mirror_setter("Fill Sides", True)
|
||||
mirror_setter("Fill Smooth", 0)
|
||||
@@ -232,17 +233,67 @@ def test_fill_smooth_only_relaxes_the_seam(symmetry):
|
||||
plane = np.abs(points[:, 0]) <= tolerance
|
||||
assert plane.any()
|
||||
assert not any(all(plane[index] for index in face) for face in faces)
|
||||
plain_interior = _front_vertices(
|
||||
plain_points, plain_faces, plain_marker, lambda point: point[0] > tolerance
|
||||
assert smooth_faces == plain_faces
|
||||
axis = np.abs(plain_points[:, 0]) <= tolerance
|
||||
moved = np.linalg.norm(smooth_points - plain_points, axis=1)
|
||||
# 衔接带同时覆盖对称轴上的点与外侧轮廓。
|
||||
assert moved[axis].max() > 1e-4
|
||||
plain_rim = _positions(_front_rim(plain_points, plain_faces, plain_marker, tolerance))
|
||||
smooth_rim = _positions(_front_rim(smooth_points, smooth_faces, smooth_marker, tolerance))
|
||||
assert plain_rim != smooth_rim
|
||||
|
||||
|
||||
def test_fill_smooth_band_reaches_beyond_two_rings():
|
||||
"""Fill Smooth 平滑的是衔接带,覆盖范围比原先的两圈更宽。"""
|
||||
from tools.nodes.common.depth_surface import _math
|
||||
from tools.nodes.groups.image_cutout_symmetry import FILL_SMOOTH_RINGS, _falloff
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
size, rows = FILL_SMOOTH_RINGS + 4, 3
|
||||
points = [(x, 0.0, y) for y in range(rows) 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(rows - 1)
|
||||
for x in range(size - 1)
|
||||
]
|
||||
mesh = bpy.data.meshes.new("Fill band")
|
||||
mesh.from_pydata(points, [], faces)
|
||||
obj = bpy.data.objects.new("Fill band", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Fill band", "GeometryNodeTree")
|
||||
for direction in ("INPUT", "OUTPUT"):
|
||||
group.interface.new_socket(
|
||||
name="Geometry", in_out=direction, socket_type="NodeSocketGeometry"
|
||||
)
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
position = group.nodes.new("GeometryNodeInputPosition")
|
||||
axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
group.links.new(position.outputs[0], axes.inputs[0])
|
||||
outline = _math(group, "COMPARE", axes.outputs["X"], 0)
|
||||
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store.data_type, store.domain = "FLOAT", "POINT"
|
||||
store.inputs["Name"].default_value = "fill_band"
|
||||
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
||||
group.links.new(_falloff(group, outline, FILL_SMOOTH_RINGS), store.inputs["Value"])
|
||||
group.links.new(
|
||||
store.outputs["Geometry"], group.nodes.new("NodeGroupOutput").inputs["Geometry"]
|
||||
)
|
||||
smooth_interior = _front_vertices(
|
||||
smooth_points, smooth_faces, smooth_marker, lambda point: point[0] > tolerance
|
||||
)
|
||||
assert _positions(plain_interior) == _positions(smooth_interior)
|
||||
plain_rim = _front_rim(plain_points, plain_faces, plain_marker, tolerance)
|
||||
smooth_rim = _front_rim(smooth_points, smooth_faces, smooth_marker, tolerance)
|
||||
assert plain_rim.shape == smooth_rim.shape
|
||||
assert not np.allclose(np.sort(plain_rim[:, 1:], axis=0), np.sort(smooth_rim[:, 1:], axis=0))
|
||||
obj.modifiers.new("Fill band", "NODES").node_group = group
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
data = result.to_mesh()
|
||||
try:
|
||||
band = np.array(
|
||||
[item.value for item in data.attributes["fill_band"].data]
|
||||
).reshape(rows, size)
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
assert (band[:, 3] > 0).all()
|
||||
assert (band[:, FILL_SMOOTH_RINGS + 1] == 0).all()
|
||||
|
||||
|
||||
def test_object_normal_attributes_follow_final_orientation(symmetry):
|
||||
|
||||
@@ -215,7 +215,8 @@ def test_depth_cutout_reuses_boundary_and_slow_field_nodes():
|
||||
group = build_image_depth_cutout_group()
|
||||
counts = Counter(node.bl_idname for node in group.nodes)
|
||||
assert counts["GeometryNodeInputMeshEdgeNeighbors"] <= 15
|
||||
assert counts["GeometryNodeBlurAttribute"] <= 12
|
||||
# 内部平滑、边界条带与 pin sharp 的法线相干度各一个 Blur。
|
||||
assert counts["GeometryNodeBlurAttribute"] <= 13
|
||||
|
||||
|
||||
def test_center_derives_from_shared_slow_fields():
|
||||
|
||||
@@ -13,8 +13,8 @@ def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
||||
}
|
||||
assert list(inputs) == [
|
||||
"Color", "Alpha", "Normal", "Bump Scale", "Alpha Fix",
|
||||
"Normal Scale", "Object Space",
|
||||
"Color", "Alpha", "Normal", "Normal Scale", "Bump Scale",
|
||||
"Alpha Fix", "Object Space",
|
||||
]
|
||||
strength = inputs["Alpha Fix"]
|
||||
output = next(node for node in image_layer.nodes if node.type == "GROUP_OUTPUT")
|
||||
|
||||
Reference in New Issue
Block a user