Files
AnyImage/tools/nodes/common/smoothing.py

174 lines
7.6 KiB
Python

"""Build shared boundary fields and pinned position smoothing."""
from .nodes import evaluate_field, sample_field
def edge_boundary_field(nodes, links):
"""Report the points sitting on an open or loose boundary."""
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
loose_vertex = nodes.new("FunctionNodeCompare")
loose_vertex.data_type = "INT"
loose_vertex.operation = "EQUAL"
loose_vertex_a = next(
socket for socket in loose_vertex.inputs if socket.identifier == "A_INT"
)
loose_vertex_b = next(
socket for socket in loose_vertex.inputs if socket.identifier == "B_INT"
)
loose_vertex_b.default_value = 1
links.new(vertex_neighbors.outputs["Vertex Count"], loose_vertex_a)
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
boundary_edge = nodes.new("FunctionNodeCompare")
boundary_edge.data_type = "INT"
boundary_edge.operation = "EQUAL"
boundary_edge_a = next(
socket for socket in boundary_edge.inputs if socket.identifier == "A_INT"
)
boundary_edge_b = next(
socket for socket in boundary_edge.inputs if socket.identifier == "B_INT"
)
boundary_edge_b.default_value = 1
links.new(edge_neighbors.outputs["Face Count"], boundary_edge_a)
edge_domain = nodes.new("GeometryNodeFieldOnDomain")
edge_domain.data_type = "BOOLEAN"
edge_domain.domain = "EDGE"
links.new(boundary_edge.outputs["Result"], edge_domain.inputs["Value"])
is_boundary = nodes.new("FunctionNodeBooleanMath")
is_boundary.operation = "OR"
links.new(edge_domain.outputs["Value"], is_boundary.inputs[0])
links.new(loose_vertex.outputs["Result"], is_boundary.inputs[1])
return is_boundary.outputs["Boolean"]
def _multiply(group, a, b):
node = group.nodes.new("ShaderNodeMath")
node.operation = "MULTIPLY"
for index, value in enumerate((a, b)):
if isinstance(value, (int, float)):
node.inputs[index].default_value = value
else:
group.links.new(value, node.inputs[index])
return node.outputs[0]
def _boundary_target(group, geometry, current, boundary_points):
"""Blur the boundary strip alone and sample it back onto every point."""
nodes = group.nodes
links = group.links
boundary_geometry = nodes.new("GeometryNodeSeparateGeometry")
boundary_geometry.domain = "POINT"
links.new(current, boundary_geometry.inputs["Geometry"])
links.new(boundary_points, boundary_geometry.inputs["Selection"])
boundary_position = nodes.new("GeometryNodeInputPosition")
smooth_boundary = nodes.new("GeometryNodeBlurAttribute")
smooth_boundary.data_type = "FLOAT_VECTOR"
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"
# Both boundary meshes keep the same compact index order: only positions change,
# so the lookup can stay anchored on the untouched input geometry.
source_boundary = nodes.new("GeometryNodeSeparateGeometry")
source_boundary.domain = "POINT"
links.new(geometry, source_boundary.inputs["Geometry"])
links.new(boundary_points, source_boundary.inputs["Selection"])
links.new(source_boundary.outputs["Selection"], nearest_boundary.inputs["Geometry"])
fixed_position = sample_field(
group, geometry, boundary_position.outputs["Position"], "FLOAT_VECTOR"
)
links.new(fixed_position, nearest_boundary.inputs["Sample Position"])
sample_boundary = nodes.new("GeometryNodeSampleIndex")
sample_boundary.data_type = "FLOAT_VECTOR"
sample_boundary.domain = "POINT"
sample_boundary.clamp = False
links.new(boundary_geometry.outputs["Selection"], sample_boundary.inputs["Geometry"])
links.new(smooth_boundary.outputs["Value"], sample_boundary.inputs["Value"])
links.new(nearest_boundary.outputs["Index"], sample_boundary.inputs["Index"])
return sample_boundary.outputs["Value"]
def pinned_smooth(
group,
geometry,
iterations,
region,
weight=1.0,
pin_boundary=True,
pin_sharp=True,
):
"""Smooth positions toward a pinned target inside an influence region.
``weight`` is the smoothing weight: each point moves that share of the way
to the target every iteration, scaled further by the sharpness of the
normal field and the influence region.
``pin_boundary`` sends boundary points to a boundary-only blur so an outline
is never dragged inward. ``pin_sharp`` lowers the weight of creases through
the sharpness of the normal field. Junction bands keep the sharpness weight
without the boundary rule, where the influenced points are not a boundary.
"""
nodes, links = group.nodes, group.links
repeat_output = nodes.new("GeometryNodeRepeatOutput")
repeat_input = nodes.new("GeometryNodeRepeatInput")
repeat_input.pair_with_output(repeat_output)
links.new(iterations, repeat_input.inputs["Iterations"])
links.new(geometry, repeat_input.inputs["Geometry"])
current = repeat_input.outputs["Geometry"]
position = nodes.new("GeometryNodeInputPosition")
smooth_all = nodes.new("GeometryNodeBlurAttribute")
smooth_all.data_type = "FLOAT_VECTOR"
smooth_all.inputs["Iterations"].default_value = 1
smooth_all.inputs["Weight"].default_value = 1.0
links.new(position.outputs["Position"], smooth_all.inputs["Value"])
target = smooth_all.outputs["Value"]
if pin_boundary:
boundary_points = evaluate_field(
group, edge_boundary_field(nodes, links), "BOOLEAN", "POINT"
)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "VECTOR"
links.new(boundary_points, choose.inputs["Switch"])
links.new(smooth_all.outputs["Value"], choose.inputs["False"])
links.new(
_boundary_target(group, geometry, current, boundary_points),
choose.inputs["True"],
)
target = choose.outputs["Output"]
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"])
coherence = nodes.new("ShaderNodeVectorMath")
coherence.operation = "LENGTH"
links.new(smooth_normal.outputs["Value"], coherence.inputs[0])
weight = _multiply(group, weight, coherence.outputs["Value"])
weight = _multiply(group, weight, region)
difference = nodes.new("ShaderNodeVectorMath")
difference.operation = "SUBTRACT"
links.new(target, difference.inputs[0])
links.new(position.outputs["Position"], difference.inputs[1])
offset = nodes.new("ShaderNodeVectorMath")
offset.operation = "SCALE"
links.new(difference.outputs[0], offset.inputs[0])
links.new(weight, offset.inputs[3])
set_position = nodes.new("GeometryNodeSetPosition")
links.new(current, set_position.inputs["Geometry"])
links.new(offset.outputs[0], set_position.inputs["Offset"])
links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"])
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "GEOMETRY"
links.new(iterations, enabled.inputs["Switch"])
links.new(geometry, enabled.inputs["False"])
links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"])
return enabled.outputs["Output"]