- Introduced new test files for image layer, image plane, normal scale, object normal rotation, shadeless, and temporary attributes. - Implemented tests to validate the behavior of alpha curves, depth layers, and normal scales in various rendering engines. - Refactored import paths for node groups to align with the new directory structure. - Added checks for temporary attribute lifetimes and ensured proper cleanup without warnings. - Enhanced CLI commands to require the Blender group and validate the presence of generated node assets before packaging. - Updated packaging scripts to enforce the existence of necessary assets, preventing build failures.
174 lines
7.6 KiB
Python
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"]
|