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

@@ -427,6 +427,81 @@ def _position_field(group, depth_image, location):
return position_texture.outputs["Color"]
def _capture_edge_length(group, geometry, name, location):
nodes = group.nodes
links = group.links
x, y = location
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
edge_vertices.location = (x, y + 250.0)
edge_length = nodes.new("ShaderNodeVectorMath")
edge_length.operation = "DISTANCE"
edge_length.location = (x + 220.0, y + 250.0)
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
capture = nodes.new("GeometryNodeCaptureAttribute")
capture.domain = "POINT"
capture.capture_items.clear()
capture.capture_items.new("FLOAT", name)
capture.location = (x + 440.0, y)
links.new(geometry, capture.inputs["Geometry"])
links.new(edge_length.outputs["Value"], capture.inputs[name])
return capture.outputs["Geometry"], capture.outputs[name], edge_length.outputs["Value"]
def _remove_stretched_points(
group,
geometry,
original_edge_length,
current_edge_length,
stretch_limit,
location,
):
nodes = group.nodes
links = group.links
x, y = location
remaining_ratio = _math(nodes, "DIVIDE", (x + 220.0, y + 300.0))
links.new(original_edge_length, remaining_ratio.inputs[0])
links.new(current_edge_length, remaining_ratio.inputs[1])
stretched = _math(nodes, "LESS_THAN", (x + 450.0, y + 200.0))
links.new(remaining_ratio.outputs[0], stretched.inputs[0])
links.new(stretch_limit, stretched.inputs[1])
delete_stretched = nodes.new("GeometryNodeDeleteGeometry")
delete_stretched.domain = "POINT"
delete_stretched.mode = "ALL"
delete_stretched.location = (x + 680.0, y)
links.new(geometry, delete_stretched.inputs["Geometry"])
links.new(stretched.outputs[0], delete_stretched.inputs["Selection"])
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
edge_neighbors.location = (x + 930.0, y + 500.0)
loose_edge = _math(nodes, "LESS_THAN", (x + 1150.0, y + 500.0))
loose_edge.inputs[1].default_value = 1.0
links.new(edge_neighbors.outputs["Face Count"], loose_edge.inputs[0])
delete_loose_edges = nodes.new("GeometryNodeDeleteGeometry")
delete_loose_edges.domain = "EDGE"
delete_loose_edges.mode = "ALL"
delete_loose_edges.location = (x + 1370.0, y)
links.new(delete_stretched.outputs["Geometry"], delete_loose_edges.inputs["Geometry"])
links.new(loose_edge.outputs[0], delete_loose_edges.inputs["Selection"])
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
vertex_neighbors.location = (x + 1620.0, y + 500.0)
loose_point = _math(nodes, "LESS_THAN", (x + 1840.0, y + 500.0))
loose_point.inputs[1].default_value = 1.0
links.new(vertex_neighbors.outputs["Face Count"], loose_point.inputs[0])
delete_loose_points = nodes.new("GeometryNodeDeleteGeometry")
delete_loose_points.domain = "POINT"
delete_loose_points.mode = "ALL"
delete_loose_points.location = (x + 2060.0, y)
links.new(
delete_loose_edges.outputs["Geometry"],
delete_loose_points.inputs["Geometry"],
)
links.new(loose_point.outputs[0], delete_loose_points.inputs["Selection"])
return delete_loose_points.outputs["Geometry"]
def _plane_depth(group, direction, reference_depth, location):
nodes = group.nodes
links = group.links
@@ -610,12 +685,19 @@ def _build_depth_surface(
geometry,
thickness,
depth_scale,
stretch_limit,
uniform_scale,
reference_depth,
depth_image,
):
nodes = group.nodes
links = group.links
geometry, original_edge_length, current_edge_length = _capture_edge_length(
group,
geometry,
"Original Edge Length",
(400.0, 850.0),
)
camera_position = _position_field(group, depth_image, (-200.0, -200.0))
scaled = nodes.new("ShaderNodeVectorMath")
scaled.operation = "SCALE"
@@ -651,30 +733,56 @@ def _build_depth_surface(
links.new(depth_scale, blend.inputs[0])
links.new(current_position.outputs["Position"], blend.inputs[4])
links.new(projected.outputs["Vector"], blend.inputs[5])
blended_axes = nodes.new("ShaderNodeSeparateXYZ")
blended_axes.location = (4100.0, -500.0)
links.new(blend.outputs["Result"], blended_axes.inputs["Vector"])
surface_position = nodes.new("ShaderNodeCombineXYZ")
surface_position.location = (4100.0, -200.0)
links.new(front.outputs[0], surface_position.inputs["X"])
links.new(blended_axes.outputs["Y"], surface_position.inputs["Y"])
links.new(blended_axes.outputs["Z"], surface_position.inputs["Z"])
project_geometry = nodes.new("GeometryNodeSetPosition")
project_geometry.location = (4100.0, 400.0)
project_geometry.location = (4400.0, 400.0)
links.new(geometry, project_geometry.inputs["Geometry"])
links.new(blend.outputs["Result"], project_geometry.inputs["Position"])
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])
geometry = _remove_stretched_points(
group,
project_geometry.outputs["Geometry"],
original_edge_length,
current_edge_length,
stretch_limit,
(4700.0, 400.0),
)
zero_x = nodes.new("ShaderNodeValue")
zero_x.location = (7100.0, 100.0)
zero_x.outputs["Value"].default_value = 0.0
geometry = _set_geometry_x(
group,
geometry,
zero_x.outputs["Value"],
(7200.0, 400.0),
)
back = _math(nodes, "ADD", (3900.0, -100.0))
links.new(front.outputs[0], back.inputs[0])
links.new(thickness, back.inputs[1])
shaped = _set_segmented_layer_x(
group,
project_geometry.outputs["Geometry"],
geometry,
front.outputs[0],
back.outputs[0],
thickness,
(4400.0, 400.0),
(8300.0, 400.0),
)
front_only = _set_geometry_x(
group,
project_geometry.outputs["Geometry"],
geometry,
front.outputs[0],
(4600.0, -200.0),
(8500.0, -200.0),
)
geometry = _remove_zero_thickness(
group, front_only, shaped, thickness, (7900.0, 400.0)
group, front_only, shaped, thickness, (11800.0, 400.0)
)
return geometry
@@ -698,6 +806,7 @@ def build_node_group(shape):
thickness.description = "Final thickness in scene distance"
depth_image = None
stretch_limit = None
if shape == "DEPTH_BALLOON":
depth_scale = _float_input(group, "Depth Scale", 1.0)
depth_scale.description = "Scale the fitted-plane-corrected metric depth"
@@ -734,6 +843,19 @@ def build_node_group(shape):
)
elif shape == "DEPTH_SURFACE":
options = group.interface.new_panel(name="Options", default_closed=True)
stretch_limit = _float_input(
group,
"Stretch Limit",
0.0,
0.0,
1.0,
subtype="FACTOR",
parent=options,
)
stretch_limit.description = (
"Delete faces when original edge length divided by projected edge "
"length falls below this threshold; zero disables culling"
)
reference_depth = _float_input(
group,
"Reference Depth",
@@ -773,6 +895,7 @@ def build_node_group(shape):
"Uniform Scale",
"Reference Depth",
"Depth Direction",
"Stretch Limit",
},
)
geometry = controls.outputs["Geometry"]
@@ -805,11 +928,12 @@ def build_node_group(shape):
geometry,
controls.outputs["Thickness"],
controls.outputs["Depth Scale"],
controls.outputs["Stretch Limit"],
controls.outputs["Uniform Scale"],
controls.outputs["Reference Depth"],
controls.outputs["Depth Image"],
)
output_location = (8200.0, 400.0)
output_location = (12100.0, 400.0)
_localize_group_inputs(group, controls)
_shade_output(group, geometry, output_location)
group.color_tag = "GEOMETRY"
@@ -874,6 +998,19 @@ def build_cutout_modifier_group(shape_groups):
parent=options,
)
smooth.description = "Blend each smoothing pass into the selected shape"
stretch_limit = _float_input(
group,
"Stretch Limit",
0.0,
0.0,
1.0,
subtype="FACTOR",
parent=options,
)
stretch_limit.description = (
"Delete Depth Surface faces when original edge length divided by projected "
"edge length falls below this threshold; zero disables culling"
)
reference_depth = _float_input(
group,