Optimize Cutout Geometry Nodes

- Refactor Depth Cutout to reuse the projected outline field, reducing redundant boundary calculations and improving performance.
- Derive the center field from shared low-frequency fields, eliminating an unnecessary blur operation.
- Implement triangle-only strip cleanup for Depth Cutout, as the input is confirmed to be triangulated.
- Short-circuit zero-thickness branches in both Cutout and Depth Cutout to avoid unnecessary geometry generation.
- Standardize internal attribute naming with a new `_o_` prefix and implement wildcard cleanup for temporary attributes.
- Update relevant tests to ensure functionality remains consistent and validate changes against existing behavior.
This commit is contained in:
2026-09-16 08:42:13 +08:00
parent cf512dc013
commit 7f878a2f3f
22 changed files with 584 additions and 135 deletions

View File

@@ -2,7 +2,7 @@
import bpy
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group
from ..common.cutout import _extrude_layers, _offset_geometry_y, _remove_zero_thickness, _set_layer_y, _shade_output
from ..common.cutout import _extrude_layers, _offset_geometry_y, _set_layer_y, _shade_output
NODE_GROUP_NAME = "O Image Cutout"
@@ -108,6 +108,13 @@ def _set_segmented_layer_y(
def _build_shell(group, geometry, thickness):
nodes = group.nodes
links = group.links
zero = math_node(nodes, "LESS_THAN")
zero.inputs[1].default_value = 1e-6
links.new(thickness, zero.inputs[0])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(zero.outputs[0], choose.inputs["Switch"])
links.new(geometry, choose.inputs["True"])
front = math_node(nodes, "MULTIPLY")
front.inputs[1].default_value = 0.0
links.new(thickness, front.inputs[0])
@@ -121,12 +128,20 @@ def _build_shell(group, geometry, thickness):
back.outputs[0],
thickness,
)
return _remove_zero_thickness(group, geometry, shaped, thickness)
links.new(shaped, choose.inputs["False"])
return choose.outputs[0]
def _build_balloon(group, geometry, thickness):
nodes = group.nodes
links = group.links
zero = math_node(nodes, "LESS_THAN")
zero.inputs[1].default_value = 1e-6
links.new(thickness, zero.inputs[0])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(zero.outputs[0], choose.inputs["Switch"])
links.new(geometry, choose.inputs["True"])
profile = read_float_attribute(group, 'o_balloon')
front_scale = math_node(nodes, "MULTIPLY")
links.new(profile, front_scale.inputs[0])
@@ -142,7 +157,8 @@ def _build_balloon(group, geometry, thickness):
negative_front.outputs[0],
front_scale.outputs[0],
)
return _remove_zero_thickness(group, geometry, shaped, thickness)
links.new(shaped, choose.inputs["False"])
return choose.outputs[0]
def build_image_cutout_group():

View File

@@ -3,7 +3,7 @@ import bpy
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import interface_socket, group_input, float_input, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
from ..common.boundary_smoothing import smooth_cut_boundary
from ..common.cutout import _position_field, _remove_zero_thickness, _shade_output
from ..common.cutout import _position_field, _shade_output
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera, thicken_depth_surface
from ..common.smoothing import edge_boundary_field
from ..common.cutout_boundary import original_boundary_field, remove_boundary_triangles, taper_split_profile
@@ -43,22 +43,28 @@ def blend_normal(group, original, smoothed, weight):
return normalize.outputs[0]
def build_volume_fields(group):
def build_volume_fields(group, rim=None):
"""Evaluate the center and thickness on the original front surface."""
links = group.links
rim = evaluate_field(group, edge_boundary_field(group.nodes, links), "FLOAT", "POINT")
if rim is None:
rim = evaluate_field(group, edge_boundary_field(group.nodes, links), "FLOAT", "POINT")
profile = read_float_attribute(group, "o_balloon")
position = group.nodes.new("GeometryNodeInputPosition")
axes = group.nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs[0], axes.inputs[0])
seed = _math(group, "SUBTRACT", axes.outputs["Y"], profile)
interior = _math(group, "SUBTRACT", 1, rim)
blur = group.nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT"
blur.inputs["Iterations"].default_value = 256
links.new(seed, blur.inputs["Value"])
links.new(interior, blur.inputs["Weight"])
center = blur.outputs[0]
depth = axes.outputs["Y"]
depth_slow = group.nodes.new("GeometryNodeBlurAttribute")
depth_slow.data_type = "FLOAT"
depth_slow.inputs["Iterations"].default_value = 256
links.new(depth, depth_slow.inputs["Value"])
links.new(interior, depth_slow.inputs["Weight"])
profile_slow = group.nodes.new("GeometryNodeBlurAttribute")
profile_slow.data_type = "FLOAT"
profile_slow.inputs["Iterations"].default_value = 256
links.new(profile, profile_slow.inputs["Value"])
links.new(interior, profile_slow.inputs["Weight"])
center = _math(group, "SUBTRACT", depth_slow.outputs[0], profile_slow.outputs[0])
residual = _math(group, "SUBTRACT", axes.outputs["Y"], center)
full = _math(group, "ADD", residual, profile)
minimum = _math(group, "MULTIPLY", profile, 0.05)
@@ -66,7 +72,8 @@ def build_volume_fields(group):
control = group_input(group.nodes, {"Thickness"})
thickness = _math(group, "MULTIPLY", positive, control.outputs["Thickness"])
return {
"profile": profile, "depth": axes.outputs["Y"], "interior": interior,
"profile": profile, "depth": depth, "interior": interior,
"depth_slow": depth_slow.outputs[0], "profile_slow": profile_slow.outputs[0],
"center": center, "thickness": thickness,
}
@@ -113,9 +120,8 @@ def build_front_fields(group, volume, front_normal, normal_weight=1):
links.new(weight, result.inputs["Weight"])
return result.outputs[0]
profile, depth, interior = volume["profile"], volume["depth"], volume["interior"]
depth_slow = blur(depth, interior)
profile_slow = blur(profile, interior)
profile, depth = volume["profile"], volume["depth"]
depth_slow, profile_slow = volume["depth_slow"], volume["profile_slow"]
depth_bump = _math(group, "SUBTRACT", depth, depth_slow)
profile_bump = _math(group, "SUBTRACT", profile, profile_slow)
numerator = _math(group, "MULTIPLY", depth_bump, profile_bump)
@@ -269,14 +275,16 @@ def _build_depth_surface(group, geometry, controls):
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"], triangles=True,
)
geometry = remove_boundary_triangles(group, geometry)
outline = evaluate_field(group, edge_boundary_field(group.nodes, group.links), "BOOLEAN", "POINT")
boundary = edge_boundary_field(group.nodes, group.links)
outline = evaluate_field(group, boundary, "BOOLEAN", "POINT")
rim = evaluate_field(group, boundary, "FLOAT", "POINT")
camera = build_surface_camera(
group, _position_field(group, image), corner_camera, cut_vertex, split, always_vertex=outline,
)
projection = project_depth_surface(
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split,
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"],
)
projection = smooth_cut_boundary(group, projection, cut_vertex)
projection = smooth_cut_boundary(group, projection, cut_vertex, boundary=rim)
# Clamped depth pixels can collapse adjacent outline samples after projection.
edge = group.nodes.new("GeometryNodeInputMeshEdgeVertices")
length = group.nodes.new("ShaderNodeVectorMath")
@@ -292,7 +300,7 @@ def _build_depth_surface(group, geometry, controls):
links.new(projection, merge.inputs["Geometry"])
links.new(_math(group, "MULTIPLY", typical.outputs["Mean"], 1e-6), merge.inputs["Distance"])
projection = merge.outputs["Geometry"]
projection = taper_split_profile(group, projection, original_boundary_field(group, original))
projection = taper_split_profile(group, projection, original_boundary_field(group, original, rim))
# The mode field is shared by the geometry and thickness choices.
mode = group.nodes.new("GeometryNodeMenuSwitch")
mode.data_type = "BOOLEAN"
@@ -302,7 +310,17 @@ def _build_depth_surface(group, geometry, controls):
mode.inputs["Balloon"].default_value = False
mode.inputs["Shell"].default_value = True
is_shell = mode.outputs[0]
volume = build_volume_fields(group)
selected_thickness = group.nodes.new("GeometryNodeSwitch")
selected_thickness.input_type = "FLOAT"
links.new(is_shell, selected_thickness.inputs["Switch"])
links.new(controls.outputs["Thickness"], selected_thickness.inputs["False"])
links.new(controls.outputs["Shell Thickness"], selected_thickness.inputs["True"])
has_thickness = _math(group, "GREATER_THAN", selected_thickness.outputs[0], 1e-6)
zero_shell = group.nodes.new("GeometryNodeSwitch")
zero_shell.input_type = "GEOMETRY"
links.new(has_thickness, zero_shell.inputs["Switch"])
links.new(projection, zero_shell.inputs["False"])
volume = build_volume_fields(group, rim)
normal_weight = build_normal_weight(group, is_shell)
normal = build_front_normal(group, normal_weight)
front = build_front_fields(group, volume, normal, normal_weight)
@@ -324,12 +342,8 @@ def _build_depth_surface(group, geometry, controls):
})
oriented = build_shell_position(group, shell["position"], shell["front"], is_shell, fields)
links.new(oriented, shell["position_input"])
amount = group.nodes.new("GeometryNodeSwitch")
amount.input_type = "FLOAT"
links.new(is_shell, amount.inputs["Switch"])
links.new(controls.outputs["Thickness"], amount.inputs["False"])
links.new(controls.outputs["Shell Thickness"], amount.inputs["True"])
return _remove_zero_thickness(group, projection, shell["geometry"], amount.outputs[0])
links.new(shell["geometry"], zero_shell.inputs["True"])
return zero_shell.outputs[0]
def build_image_depth_cutout_group():
@@ -361,7 +375,7 @@ def build_image_depth_cutout_group():
interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
controls = group_input(group.nodes, set())
geometry = store_float_attribute(group, controls.outputs['Geometry'], NORMAL_REDUCTION_ATTRIBUTE_NAME, 0.0, domain='FACE')
geometry = store_float_attribute(group, controls.outputs['Geometry'], NORMAL_REDUCTION_ATTRIBUTE_NAME, 0.0)
geometry = _build_depth_surface(group, geometry, controls)
_shade_output(group, geometry)
prepare_node_group(group)

View File

@@ -7,9 +7,9 @@ import bpy
from ..common.validity import delete_invalid_points
from ..common.depth_surface import (
_math, split_depth_surface, build_surface_camera, CUT_ATTRIBUTE,
_math, split_depth_surface, build_surface_camera,
)
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field, remove_attributes
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field, remove_attribute_pattern
from ..common.material_slots import inherit_material_slots
from ..common.boundary_smoothing import smooth_boundary_depth, smooth_cut_boundary
@@ -175,10 +175,7 @@ def build_image_depth_panorama_group():
deform = nodes.new("GeometryNodeSetPosition")
links.new(geometry, deform.inputs["Geometry"])
links.new(radial.outputs[0], deform.inputs["Position"])
geometry = remove_attributes(
group, deform.outputs[0], CUT_ATTRIBUTE,
enabled=_math(group, "GREATER_THAN", strength, 0.0),
)
geometry = remove_attribute_pattern(group, deform.outputs[0], "_o_*")
geometry = smooth_cut_boundary(group, geometry, cut_vertex, outline_scale=1.0)
flip = nodes.new("GeometryNodeFlipFaces")
links.new(geometry, flip.inputs["Mesh"])

View File

@@ -130,7 +130,7 @@ def build_image_depth_plane_group(image_plane_group):
surface = project_depth_surface(
group, geometry, camera,
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
depth_input.outputs["Depth Scale"], split_input.outputs["Depth Split"],
depth_input.outputs["Depth Scale"],
)
surface = smooth_cut_boundary(
group, surface, cut_vertex, protected=original_boundary, outline_scale=1.0,