Add tests and implement boundary smoothing and cutout functionality

- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity.
- Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending.
- Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised.
- Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry.
- Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity.
- Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles.
- Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
2026-09-11 20:37:20 +08:00
parent e8129fe495
commit 31074bd511
85 changed files with 2501 additions and 1451 deletions

View File

@@ -0,0 +1,79 @@
"""Apply boundary-preserving smoothing within a narrow cut-edge band."""
from .nodes import evaluate_field, group_input
from .depth_surface import _math
from .smoothing import edge_boundary_field, expand_smooth
def boundary_influence(group, protected=False):
"""Fade a boundary field through two rings while excluding protected points."""
nodes, links = group.nodes, group.links
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
unprotected = _math(group, "SUBTRACT", 1, protected)
cut = _math(group, "MULTIPLY", boundary, unprotected)
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT"
blur.inputs["Iterations"].default_value = 2
blur.inputs["Weight"].default_value = 1
links.new(cut, blur.inputs["Value"])
return _math(group, "MULTIPLY", _math(group, "MAXIMUM", cut, blur.outputs["Value"]), unprotected)
def smooth_boundary_depth(group, depth, protected=False):
"""Blur point depth on the current mesh and apply it only in the edge band."""
nodes, links = group.nodes, group.links
depth = evaluate_field(group, depth, "FLOAT", "POINT")
iterations = group_input(nodes, {"Boundary Smooth"}).outputs["Boundary Smooth"]
influence = boundary_influence(group, protected)
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT"
blur.inputs["Weight"].default_value = 0.5
links.new(depth, blur.inputs["Value"])
links.new(iterations, blur.inputs["Iterations"])
mix = nodes.new("ShaderNodeMix")
mix.data_type = "FLOAT"
links.new(influence, mix.inputs[0])
links.new(depth, mix.inputs[2])
links.new(blur.outputs["Value"], mix.inputs[3])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "FLOAT"
links.new(influence, choose.inputs["Switch"])
links.new(depth, choose.inputs["False"])
links.new(mix.outputs[0], choose.inputs["True"])
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "FLOAT"
links.new(iterations, enabled.inputs["Switch"])
links.new(depth, enabled.inputs["False"])
links.new(choose.outputs[0], enabled.inputs["True"])
return enabled.outputs[0]
def smooth_boundary_camera(group, camera, protected=False):
"""Reconstruct blurred camera depth along each unchanged projection ray."""
nodes, links = group.nodes, group.links
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(camera, axes.inputs[0])
depth = axes.outputs["Z"]
smoothed = smooth_boundary_depth(group, depth, protected)
changed = _math(group, "GREATER_THAN", _math(group, "ABSOLUTE", _math(group, "SUBTRACT", smoothed, depth)), 0)
enabled = _math(group, "MULTIPLY", changed, _math(group, "GREATER_THAN", depth, 1e-8))
projected = nodes.new("ShaderNodeVectorMath")
projected.operation = "SCALE"
links.new(camera, projected.inputs[0])
links.new(_math(group, "DIVIDE", smoothed, depth), projected.inputs[3])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "VECTOR"
links.new(enabled, choose.inputs["Switch"])
links.new(camera, choose.inputs["False"])
links.new(projected.outputs[0], choose.inputs["True"])
return choose.outputs[0]
def smooth_cut_boundary(group, geometry, original_boundary=False):
"""Blend O Smooth's pinned-boundary result through a two-ring edge mask."""
influence = boundary_influence(group, original_boundary)
# The support band depends on topology, which stays unchanged in the loop.
return expand_smooth(
group, geometry, iterations_name="Boundary Smooth", weight_name="Smooth Weight",
influence=influence, pin_sharp=False,
)

View File

@@ -0,0 +1,45 @@
"""Clean cutout boundary triangles and taper the split thickness profile."""
from .depth_surface import _math
from .nodes import evaluate_field, group_input, read_float_attribute, sample_field, store_float_attribute
from .smoothing import edge_boundary_field
from .boundary_smoothing import boundary_influence
def remove_boundary_triangles(group, geometry):
"""Remove all-boundary triangles once on the current topology."""
nodes, links = group.nodes, group.links
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
all_boundary = _math(group, "GREATER_THAN", evaluate_field(group, rim, "FLOAT", "FACE"), 0.99999)
count = nodes.new("GeometryNodeInputMeshFaceNeighbors").outputs["Vertex Count"]
selected = _math(group, "MULTIPLY", all_boundary, _math(group, "COMPARE", count, 3))
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(geometry, delete.inputs["Geometry"])
links.new(selected, delete.inputs["Selection"])
return delete.outputs["Geometry"]
def taper_split_profile(group, original, geometry):
"""Taper balloon thickness through the shared two-ring influence builder."""
nodes, links = group.nodes, group.links
uv = nodes.new("GeometryNodeInputNamedAttribute")
uv.data_type = "FLOAT_VECTOR"
uv.inputs["Name"].default_value = "UVMap"
original_uv = nodes.new("GeometryNodeSetPosition")
links.new(original, original_uv.inputs["Geometry"])
links.new(uv.outputs["Attribute"], original_uv.inputs["Position"])
nearest = nodes.new("GeometryNodeSampleNearest")
nearest.domain = "POINT"
links.new(original_uv.outputs["Geometry"], nearest.inputs["Geometry"])
links.new(uv.outputs["Attribute"], nearest.inputs["Sample Position"])
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
protected = sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
influence = boundary_influence(group, protected)
controls = group_input(nodes, {"Edge Round", "Split Threshold"})
strength = _math(group, "ADD", 1, _math(group, "MULTIPLY", controls.outputs["Edge Round"], 4))
band = _math(group, "MINIMUM", _math(group, "MULTIPLY", influence, strength), 1)
band = _math(group, "MULTIPLY", band, _math(group, "GREATER_THAN", controls.outputs["Split Threshold"], 0))
retained = _math(group, "SQRT", _math(group, "MAXIMUM", _math(group, "SUBTRACT", 1, band), 0))
profile = _math(group, "MULTIPLY", read_float_attribute(group, "o_balloon"), retained)
return store_float_attribute(group, geometry, "o_balloon", profile)

View File

@@ -1,6 +1,9 @@
"""Face separation and connected thickness for Depth Surface assets."""
from .nodes import capture_fields, evaluate_field, store_float_attribute
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attributes, sample_field
CUT_ATTRIBUTE = ".o_anyimage_depth_cut"
def _math(group, operation, a, b=None):
@@ -211,10 +214,9 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
cut_vertices.data_type = "FLOAT"
cut_vertices.domain = "POINT"
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
corners, captured = capture_fields(
group, geometry, "CORNER",
Camera=("VECTOR", face_camera),
Cut=("BOOLEAN", _math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0)),
corners, cut = store_boolean_attribute(
group, geometry, CUT_ATTRIBUTE,
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
)
split = nodes.new("GeometryNodeSplitEdges")
links.new(corners, split.inputs["Mesh"])
@@ -225,17 +227,16 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
links.new(enabled, choose.inputs["Switch"])
links.new(geometry, choose.inputs["False"])
links.new(_remove_strip_faces(group, split.outputs["Mesh"], triangles=triangles), choose.inputs["True"])
return choose.outputs["Output"], captured["Camera"], captured["Cut"]
return choose.outputs["Output"], face_camera, cut
def build_surface_camera(group, original, separated, cut_vertex, strength):
"""Change cut depth along the original ray, preserving every other vertex."""
enabled = _math(
group,
"MULTIPLY",
_math(group, "GREATER_THAN", strength, 0.0),
cut_vertex,
)
enabled = group.nodes.new("GeometryNodeSwitch")
enabled.input_type = "BOOLEAN"
group.links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
enabled.inputs["False"].default_value = False
group.links.new(cut_vertex, enabled.inputs["True"])
original_axes = group.nodes.new("ShaderNodeSeparateXYZ")
separated_axes = group.nodes.new("ShaderNodeSeparateXYZ")
group.links.new(original, original_axes.inputs[0])
@@ -249,7 +250,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength):
group.links.new(ratio, projected.inputs[3])
choose = group.nodes.new("GeometryNodeSwitch")
choose.input_type = "VECTOR"
group.links.new(enabled, choose.inputs["Switch"])
group.links.new(enabled.outputs["Output"], choose.inputs["Switch"])
group.links.new(original, choose.inputs["False"])
group.links.new(projected.outputs[0], choose.inputs["True"])
return evaluate_field(group, choose.outputs["Output"], "FLOAT_VECTOR", "POINT")
@@ -261,36 +262,32 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
nodes, links = group.nodes, group.links
position = nodes.new("GeometryNodeInputPosition")
index = nodes.new("GeometryNodeInputIndex")
fields = {"Position": ("VECTOR", position.outputs[0]), "Vertex": ("INT", index.outputs[0])}
if normal_smooth is not None:
normal = nodes.new("GeometryNodeInputNormal")
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT_VECTOR"
links.new(normal_smooth, blur.inputs["Iterations"])
links.new(normal.outputs[0], blur.inputs["Value"])
normalize = nodes.new("ShaderNodeVectorMath")
normalize.operation = "NORMALIZE"
links.new(blur.outputs[0], normalize.inputs[0])
fields["Normal"] = ("VECTOR", normalize.outputs[0])
geometry, captured = capture_fields(group, geometry, "POINT", **fields)
flattened = nodes.new("ShaderNodeVectorMath")
flattened.operation = "MULTIPLY"
flattened.inputs[1].default_value = (1.0, 1.0, 0.0)
links.new(position.outputs[0], flattened.inputs[0])
sheet = nodes.new("GeometryNodeSetPosition")
links.new(geometry, sheet.inputs["Geometry"])
links.new(flattened.outputs[0], sheet.inputs["Position"])
saved = sheet.outputs["Geometry"]
# Weld in an integer identity grid, never in scene coordinates. Nearby cuts
# retain distinct IDs; copies of one rim vertex share the same ID and layer.
# Encode identity in the construction geometry before extrusion. The source
# surface remains a separate sampling input; no position or ID is stored.
identity = nodes.new("ShaderNodeCombineXYZ")
for operation, axis in (("MODULO", "X"), ("DIVIDE", "Y")):
part = nodes.new("FunctionNodeIntegerMath")
part.operation = operation
part.inputs[1].default_value = 4096
links.new(captured["Vertex"], part.inputs[0])
links.new(index.outputs[0], part.inputs[0])
links.new(part.outputs[0], identity.inputs[axis])
sheet = nodes.new("GeometryNodeSetPosition")
links.new(geometry, sheet.inputs["Geometry"])
links.new(identity.outputs[0], sheet.inputs["Position"])
saved = sheet.outputs["Geometry"]
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs[0], layer_axes.inputs[0])
row = nodes.new("FunctionNodeIntegerMath")
row.operation = "MULTIPLY"
row.inputs[1].default_value = 4096
links.new(layer_axes.outputs["Y"], row.inputs[0])
vertex = nodes.new("FunctionNodeIntegerMath")
vertex.operation = "ADD"
links.new(layer_axes.outputs["X"], vertex.inputs[0])
links.new(row.outputs[0], vertex.inputs[1])
source_index = vertex.outputs[0]
source_position = sample_field(group, geometry, position.outputs[0], "FLOAT_VECTOR", index=source_index)
source_thickness = sample_field(group, geometry, thickness, "FLOAT", index=source_index)
extrude = nodes.new("GeometryNodeExtrudeMesh")
extrude.mode = "FACES"
@@ -315,53 +312,62 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
join = nodes.new("GeometryNodeJoinGeometry")
rear = flip_back.outputs["Mesh"]
rim = flip_sides.outputs["Mesh"]
back_parts = nodes.new("GeometryNodeJoinGeometry")
links.new(rear, back_parts.inputs["Geometry"])
links.new(rim, back_parts.inputs["Geometry"])
rear = back_parts.outputs["Geometry"]
if back_mask is not None:
rear = store_float_attribute(group, rear, back_mask, 0.0, domain="FACE")
rim = store_float_attribute(group, rim, back_mask, 0.0, domain="FACE")
for value in (
saved,
rear,
rim,
):
links.new(value, join.inputs["Geometry"])
encoded = nodes.new("GeometryNodeSetPosition")
links.new(join.outputs["Geometry"], encoded.inputs["Geometry"])
links.new(identity.outputs[0], encoded.inputs["Position"])
construction = join.outputs["Geometry"]
if weld_zero_thickness:
positive = _math(group, "GREATER_THAN", source_thickness, 1e-12)
flattened = nodes.new("ShaderNodeCombineXYZ")
for axis in ("X", "Y"):
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Z"]), flattened.inputs["Z"])
collapse = nodes.new("GeometryNodeSetPosition")
links.new(construction, collapse.inputs["Geometry"])
links.new(flattened.outputs[0], collapse.inputs["Position"])
construction = collapse.outputs["Geometry"]
merge = nodes.new("GeometryNodeMergeByDistance")
merge.mode = "ALL"
merge.inputs["Distance"].default_value = 0.1
links.new(encoded.outputs["Geometry"], merge.inputs["Geometry"])
links.new(construction, merge.inputs["Geometry"])
displacement = nodes.new("ShaderNodeCombineXYZ")
layer_position = nodes.new("GeometryNodeInputPosition")
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(layer_position.outputs[0], layer_axes.inputs[0])
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], thickness)
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], source_thickness)
links.new(distance, displacement.inputs["Z"])
restored = nodes.new("ShaderNodeVectorMath")
restored.operation = "ADD"
links.new(captured["Position"], restored.inputs[0])
links.new(source_position, restored.inputs[0])
links.new(displacement.outputs[0], restored.inputs[1])
result = nodes.new("GeometryNodeSetPosition")
links.new(merge.outputs["Geometry"], result.inputs["Geometry"])
links.new(restored.outputs[0], result.inputs["Position"])
local_position = nodes.new("GeometryNodeInputPosition")
local_axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(local_position.outputs[0], local_axes.inputs[0])
layer = local_axes.outputs["Z"]
if weld_zero_thickness:
positive = _math(group, "GREATER_THAN", thickness, 1e-12)
layer = _math(group, "MULTIPLY", positive, layer)
links.new(layer, identity.inputs["Z"])
if normal_smooth is not None:
normal = nodes.new("GeometryNodeInputNormal")
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT_VECTOR"
links.new(normal_smooth, blur.inputs["Iterations"])
links.new(normal.outputs[0], blur.inputs["Value"])
normalize = nodes.new("ShaderNodeVectorMath")
normalize.operation = "NORMALIZE"
links.new(blur.outputs[0], normalize.inputs[0])
source_normal = sample_field(group, geometry, normalize.outputs[0], "FLOAT_VECTOR", index=source_index)
normal_offset = nodes.new("ShaderNodeVectorMath")
normal_offset.operation = "SCALE"
links.new(captured["Normal"], normal_offset.inputs[0])
links.new(source_normal, normal_offset.inputs[0])
links.new(distance, normal_offset.inputs[3])
links.new(normal_offset.outputs[0], restored.inputs[1])
nodes.remove(displacement)
return {
"geometry": result.outputs["Geometry"],
"front": captured["Position"],
"front": source_position,
"source_index": source_index,
"position": restored.outputs[0],
"position_input": result.inputs["Position"],
}
@@ -374,7 +380,7 @@ def _projection_math(nodes, operation):
def project_depth_surface(
group, geometry, camera_position, uniform_scale, reference_depth, depth_scale
group, geometry, camera_position, uniform_scale, reference_depth, depth_scale, split_strength
):
"""Interpolate the rectangular plane into the model camera coordinates."""
nodes, links = group.nodes, group.links
@@ -414,4 +420,7 @@ def project_depth_surface(
links.new(geometry, project_geometry.inputs["Geometry"])
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])
return project_geometry.outputs["Geometry"]
return remove_attributes(
group, project_geometry.outputs["Geometry"], CUT_ATTRIBUTE,
enabled=_math(group, "GREATER_THAN", split_strength, 0.0),
)

View File

@@ -3,20 +3,23 @@
def inherit_material_slots(group, geometry, source):
nodes, links = group.nodes, group.links
capture = nodes.new("GeometryNodeCaptureAttribute")
capture.domain = "POINT"
capture.capture_items.clear()
capture.capture_items.new("BOOLEAN", "Source")
capture.inputs["Source"].default_value = True
links.new(source, capture.inputs["Geometry"])
size = nodes.new("GeometryNodeAttributeDomainSize")
size.component = "MESH"
links.new(source, size.inputs["Geometry"])
index = nodes.new("GeometryNodeInputIndex")
source_vertices = nodes.new("FunctionNodeCompare")
source_vertices.data_type, source_vertices.operation = "INT", "LESS_THAN"
links.new(index.outputs["Index"], source_vertices.inputs[2])
links.new(size.outputs["Point Count"], source_vertices.inputs[3])
join = nodes.new("GeometryNodeJoinGeometry")
# Multi-input links are prepended: source occupies the first point range.
links.new(geometry, join.inputs["Geometry"])
links.new(capture.outputs["Geometry"], join.inputs["Geometry"])
links.new(source, join.inputs["Geometry"])
material = nodes.new("GeometryNodeSetMaterialIndex")
material.inputs["Material Index"].default_value = 0
links.new(join.outputs["Geometry"], material.inputs["Geometry"])
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain = "POINT"
links.new(material.outputs["Geometry"], delete.inputs["Geometry"])
links.new(capture.outputs["Source"], delete.inputs["Selection"])
links.new(source_vertices.outputs["Result"], delete.inputs["Selection"])
return delete.outputs["Geometry"]

View File

@@ -114,13 +114,45 @@ def evaluate_field(group, value, data_type, domain):
return node.outputs["Value"]
def capture_fields(group, geometry, domain, **fields):
"""Keep source fields attached to geometry through later changes."""
node = group.nodes.new("GeometryNodeCaptureAttribute")
node.domain = domain
node.capture_items.clear()
def sample_field(group, geometry, value, data_type, domain="POINT", index=None):
"""Evaluate a field on an explicit source geometry and index domain."""
node = group.nodes.new("GeometryNodeSampleIndex")
node.data_type, node.domain = data_type, domain
node.clamp = False
if index is None:
index = group.nodes.new("GeometryNodeInputIndex").outputs["Index"]
group.links.new(geometry, node.inputs["Geometry"])
for name, (data_type, value) in fields.items():
node.capture_items.new(data_type, name)
group.links.new(value, node.inputs[name])
return node.outputs["Geometry"], {name: node.outputs[name] for name in fields}
group.links.new(value, node.inputs["Value"])
group.links.new(index, node.inputs["Index"])
return node.outputs["Value"]
def store_boolean_attribute(group, geometry, name, value, domain="POINT"):
"""Store a topology marker needed after destructive mesh operations."""
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
node.domain, node.data_type = domain, "BOOLEAN"
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(value, node.inputs["Value"])
read = group.nodes.new("GeometryNodeInputNamedAttribute")
read.data_type = "BOOLEAN"
read.inputs["Name"].default_value = name
return node.outputs["Geometry"], read.outputs["Attribute"]
def remove_attributes(group, geometry, *names, enabled=None):
"""Remove only the explicitly owned temporary attributes."""
original = geometry
for name in names:
node = group.nodes.new("GeometryNodeRemoveAttribute")
node.inputs["Name"].default_value = name
group.links.new(geometry, node.inputs["Geometry"])
geometry = node.outputs["Geometry"]
if enabled is not None:
choose = group.nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
group.links.new(enabled, choose.inputs["Switch"])
group.links.new(original, choose.inputs["False"])
group.links.new(geometry, choose.inputs["True"])
geometry = choose.outputs["Output"]
return geometry

View File

@@ -1,5 +1,5 @@
"""Build shared boundary fields and iterative smoothing."""
from .nodes import group_input
from .nodes import group_input, sample_field
def edge_boundary_field(nodes, links):
@@ -41,7 +41,8 @@ def edge_boundary_field(nodes, links):
def expand_smooth(
group, geometry, iterations_name="Iterations", weight_name="Smooth"
group, geometry, iterations_name="Iterations", weight_name="Smooth", *,
influence=None, pin_sharp=True,
):
nodes = group.nodes
links = group.links
@@ -55,7 +56,6 @@ def expand_smooth(
iteration_controls.outputs[iterations_name],
repeat_input.inputs["Iterations"],
)
links.new(geometry, repeat_input.inputs["Geometry"])
boundary_field = edge_boundary_field(nodes, links)
boundary_points = nodes.new("GeometryNodeFieldOnDomain")
@@ -67,6 +67,21 @@ def expand_smooth(
boundary_geometry.domain = "POINT"
links.new(repeat_input.outputs["Geometry"], boundary_geometry.inputs["Geometry"])
links.new(boundary_points.outputs["Value"], boundary_geometry.inputs["Selection"])
links.new(geometry, repeat_input.inputs["Geometry"])
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
nearest_boundary.domain = "POINT"
if influence is not None:
# Both boundary meshes keep the same compact index order: only positions change.
source_boundary = nodes.new("GeometryNodeSeparateGeometry")
source_boundary.domain = "POINT"
links.new(geometry, source_boundary.inputs["Geometry"])
links.new(boundary_points.outputs["Value"], source_boundary.inputs["Selection"])
links.new(source_boundary.outputs["Selection"], nearest_boundary.inputs["Geometry"])
source_position = nodes.new("GeometryNodeInputPosition").outputs["Position"]
fixed_position = sample_field(group, geometry, source_position, "FLOAT_VECTOR")
links.new(fixed_position, nearest_boundary.inputs["Sample Position"])
else:
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
position = nodes.new("GeometryNodeInputPosition")
smooth_all = nodes.new("GeometryNodeBlurAttribute")
@@ -81,9 +96,6 @@ def expand_smooth(
smooth_boundary.inputs["Iterations"].default_value = 1
smooth_boundary.inputs["Weight"].default_value = 0.5
links.new(boundary_position.outputs["Position"], smooth_boundary.inputs["Value"])
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
nearest_boundary.domain = "POINT"
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
sample_boundary = nodes.new("GeometryNodeSampleIndex")
sample_boundary.data_type = "FLOAT_VECTOR"
sample_boundary.domain = "POINT"
@@ -94,8 +106,7 @@ def expand_smooth(
choose_boundary = nodes.new("GeometryNodeSwitch")
choose_boundary.input_type = "VECTOR"
direct_boundary = edge_boundary_field(nodes, links)
links.new(direct_boundary, choose_boundary.inputs["Switch"])
links.new(boundary_points.outputs["Value"], choose_boundary.inputs["Switch"])
links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"])
links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"])
boundary_mix = nodes.new("ShaderNodeMix")
@@ -104,24 +115,33 @@ def expand_smooth(
links.new(smooth_all.outputs["Value"], boundary_mix.inputs[4])
links.new(choose_boundary.outputs["Output"], boundary_mix.inputs[5])
normal = nodes.new("GeometryNodeInputNormal")
smooth_normal = nodes.new("GeometryNodeBlurAttribute")
smooth_normal.data_type = "FLOAT_VECTOR"
smooth_normal.inputs["Iterations"].default_value = 10
smooth_normal.inputs["Weight"].default_value = 1.0
links.new(normal.outputs["Normal"], smooth_normal.inputs["Value"])
normal_coherence = nodes.new("ShaderNodeVectorMath")
normal_coherence.operation = "LENGTH"
links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0])
sharp_switch = nodes.new("GeometryNodeSwitch")
sharp_switch.input_type = "FLOAT"
sharp_switch.inputs["Switch"].default_value = True
sharp_switch.inputs["False"].default_value = 1.0
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
smooth_weight = nodes.new("ShaderNodeMath")
smooth_weight.operation = "MULTIPLY"
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
links.new(sharp_switch.outputs["Output"], smooth_weight.inputs[1])
weight = weight_controls.outputs[weight_name]
if pin_sharp:
normal = nodes.new("GeometryNodeInputNormal")
smooth_normal = nodes.new("GeometryNodeBlurAttribute")
smooth_normal.data_type = "FLOAT_VECTOR"
smooth_normal.inputs["Iterations"].default_value = 10
smooth_normal.inputs["Weight"].default_value = 1.0
links.new(normal.outputs["Normal"], smooth_normal.inputs["Value"])
normal_coherence = nodes.new("ShaderNodeVectorMath")
normal_coherence.operation = "LENGTH"
links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0])
sharp_switch = nodes.new("GeometryNodeSwitch")
sharp_switch.input_type = "FLOAT"
sharp_switch.inputs["Switch"].default_value = pin_sharp
sharp_switch.inputs["False"].default_value = 1.0
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
smooth_weight = nodes.new("ShaderNodeMath")
smooth_weight.operation = "MULTIPLY"
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
links.new(sharp_switch.outputs["Output"], smooth_weight.inputs[1])
weight = smooth_weight.outputs[0]
if influence is not None:
masked_weight = nodes.new("ShaderNodeMath")
masked_weight.operation = "MULTIPLY"
links.new(weight, masked_weight.inputs[0])
links.new(influence, masked_weight.inputs[1])
weight = masked_weight.outputs[0]
target_position = nodes.new("GeometryNodeSwitch")
target_position.input_type = "VECTOR"
@@ -131,11 +151,18 @@ def expand_smooth(
current_position = nodes.new("GeometryNodeInputPosition")
mix_position = nodes.new("ShaderNodeMix")
mix_position.data_type = "VECTOR"
links.new(smooth_weight.outputs[0], mix_position.inputs[0])
links.new(weight, mix_position.inputs[0])
links.new(current_position.outputs["Position"], mix_position.inputs[4])
links.new(target_position.outputs["Output"], mix_position.inputs[5])
set_position = nodes.new("GeometryNodeSetPosition")
links.new(repeat_input.outputs["Geometry"], set_position.inputs["Geometry"])
links.new(mix_position.outputs["Result"], set_position.inputs["Position"])
links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"])
if influence is not None:
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "GEOMETRY"
links.new(iteration_controls.outputs[iterations_name], enabled.inputs["Switch"])
links.new(geometry, enabled.inputs["False"])
links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"])
return enabled.outputs["Output"]
return repeat_output.outputs["Geometry"]

View File

@@ -0,0 +1,21 @@
"""Remove invalid vertices and the wire edges left by point deletion."""
def delete_invalid_points(group, geometry, invalid):
nodes, links = group.nodes, group.links
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "POINT", "ALL"
links.new(geometry, delete.inputs["Geometry"])
links.new(invalid, delete.inputs["Selection"])
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
wire = nodes.new("FunctionNodeCompare")
wire.data_type, wire.operation = "INT", "EQUAL"
a = next(socket for socket in wire.inputs if socket.identifier == "A_INT")
b = next(socket for socket in wire.inputs if socket.identifier == "B_INT")
b.default_value = 0
links.new(neighbors.outputs["Face Count"], a)
cleanup = nodes.new("GeometryNodeDeleteGeometry")
cleanup.domain, cleanup.mode = "EDGE", "ALL"
links.new(delete.outputs["Geometry"], cleanup.inputs["Geometry"])
links.new(wire.outputs["Result"], cleanup.inputs["Selection"])
return cleanup.outputs["Geometry"]