feat: Implement Depth Surface Stretch Limit for improved edge culling

- Added a new "Stretch Limit" parameter to the Depth Surface node, allowing for dynamic edge culling based on the ratio of original to projected edge lengths.
- Updated the geometry processing to capture original edge lengths before projection and calculate current lengths after projection.
- Implemented logic to delete edges and faces that exceed the stretch limit, ensuring cleaner topology.
- Enhanced tests to validate the new functionality, including scenarios for edge retention and deletion based on stretch limits.
- Updated documentation and node assets to reflect the new parameter and its implications on geometry processing.
This commit is contained in:
2026-09-03 12:31:30 +08:00
parent bdbde1a10d
commit bbaa4bff46
9 changed files with 677 additions and 13 deletions

View File

@@ -140,6 +140,7 @@ def main():
"Smooth Iterations",
"Cleanup",
"Smooth",
"Stretch Limit",
"Reference Depth",
"Depth Direction",
"Uniform Scale",
@@ -162,6 +163,7 @@ def main():
"Geometry",
"Thickness",
"Depth Scale",
"Stretch Limit",
"Reference Depth",
"Uniform Scale",
"Depth Image",
@@ -183,6 +185,7 @@ def main():
assert dependencies == {
"Thickness": set(shapes.CUTOUT_SHAPES),
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Stretch Limit": {"DEPTH_SURFACE"},
"Double Sided": {"DEPTH_BALLOON"},
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Depth Direction": {"DEPTH_BALLOON"},
@@ -232,6 +235,77 @@ def main():
)
assert cleanup.default_value == 0.0
assert cleanup.subtype == "NONE"
stretch_limit = next(
item
for item in wrapper.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
)
child_stretch_limit = next(
item
for item in groups["DEPTH_SURFACE"].interface.items_tree
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
)
assert stretch_limit.default_value == 0.0
assert stretch_limit.min_value == 0.0
assert stretch_limit.max_value == 1.0
assert stretch_limit.subtype == "FACTOR"
assert getattr(stretch_limit.parent, "name", "") == "Options"
assert child_stretch_limit.default_value == 0.0
assert child_stretch_limit.min_value == 0.0
assert child_stretch_limit.max_value == 1.0
assert child_stretch_limit.subtype == "FACTOR"
assert getattr(child_stretch_limit.parent, "name", "") == "Options"
depth_surface_nodes = groups["DEPTH_SURFACE"].nodes
captures = [
node
for node in depth_surface_nodes
if node.bl_idname == "GeometryNodeCaptureAttribute"
]
assert len(captures) == 1
assert captures[0].domain == "POINT"
assert [item.name for item in captures[0].capture_items] == [
"Original Edge Length"
]
captured_distance = next(
link.from_node
for link in groups["DEPTH_SURFACE"].links
if link.to_node == captures[0]
and link.to_socket.name == "Original Edge Length"
)
assert captured_distance.bl_idname == "ShaderNodeVectorMath"
assert captured_distance.operation == "DISTANCE"
ratio = next(
link.to_node
for link in groups["DEPTH_SURFACE"].links
if link.from_node == captured_distance
and link.to_node.bl_idname == "ShaderNodeMath"
and link.to_node.operation == "DIVIDE"
)
assert any(
link.from_node == captures[0]
and link.to_node == ratio
and link.to_socket == ratio.inputs[0]
for link in groups["DEPTH_SURFACE"].links
)
assert any(
link.from_node == captured_distance
and link.to_node == ratio
and link.to_socket == ratio.inputs[1]
for link in groups["DEPTH_SURFACE"].links
)
samples = [
node
for node in depth_surface_nodes
if node.bl_idname == "GeometryNodeSampleIndex"
]
assert not samples
delete_domains = [
node.domain
for node in depth_surface_nodes
if node.bl_idname == "GeometryNodeDeleteGeometry"
]
assert delete_domains.count("EDGE") == 1
assert delete_domains.count("POINT") == 2
cleanup_scale = next(
link.to_node
for link in wrapper.links
@@ -262,7 +336,7 @@ def main():
assert not (
abs(node.location.x - other.location.x) < 150.0
and abs(node.location.y - other.location.y) < 100.0
)
), (group.name, node.name, other.name)
for group_input in (
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
):
@@ -392,7 +466,8 @@ def main():
pixels = []
for z in range(3):
for y in range(3):
pixels.extend((-y * 0.5, -z * 0.5, 2.0 + y * 0.25, 0.0))
depth = 7.0 if (y, z) == (0, 0) else 2.0 + y * 0.25
pixels.extend((-y * 0.5, -z * 0.5, depth, 0.0))
point_image.pixels.foreach_set(pixels)
for name, value in (
("Depth Image", point_image),
@@ -417,12 +492,62 @@ def main():
)
for point, target in zip(
coordinates,
((y * 0.25, y * 0.5, z * 0.5) for z in range(3) for y in range(3)),
(
(
5.0 if (y, z) == (0, 0) else y * 0.25,
y * 0.5,
z * 0.5,
)
for z in range(3)
for y in range(3)
),
)
)
finally:
evaluated.to_mesh_clear()
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, "Stretch Limit"),
0.6,
)
depth_surface.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(depth_surface)
try:
assert 0 < len(result.polygons) < 8
face_edges = {
tuple(sorted((start, end)))
for polygon in result.polygons
for start, end in zip(
polygon.vertices,
(*polygon.vertices[1:], polygon.vertices[0]),
)
}
assert all(tuple(sorted(edge.vertices)) in face_edges for edge in result.edges)
face_vertices = {
vertex
for polygon in result.polygons
for vertex in polygon.vertices
}
assert face_vertices == set(range(len(result.vertices)))
finally:
evaluated.to_mesh_clear()
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, "Depth Scale"),
0.0,
)
depth_surface.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(depth_surface)
try:
assert len(result.vertices) == 9
assert len(result.polygons) == 8
finally:
evaluated.to_mesh_clear()
print(
f"Validated Blender {bpy.app.version_string}: "
f"{wrapper.name} wrapping "