Implement arrangement framework for node groups
- Introduced `frames.py` to manage field complex calculations and their arrangement within node groups. - Added functionality to create and remove field frames based on node arrangements. - Developed `passages.py` to find unobstructed paths for connections around calculation frames. - Implemented `references.py` to route shared results on peripheral tracks, ensuring efficient distribution of outputs. - Created `sections.py` to reserve space for field calculations adjacent to geometry consumers. - Added `sources.py` to position shared calculation sources effectively beside their distribution channels. - Enhanced outline smoothing with `outline_smoothing.py` to adapt camera depth along original outlines.
This commit is contained in:
@@ -55,6 +55,12 @@ def smooth_boundary_camera(group, camera, protected=False):
|
||||
links.new(camera, axes.inputs[0])
|
||||
depth = axes.outputs["Z"]
|
||||
smoothed = smooth_boundary_depth(group, depth, protected)
|
||||
return reconstruct_camera_depth(group, camera, depth, smoothed)
|
||||
|
||||
|
||||
def reconstruct_camera_depth(group, camera, depth, smoothed):
|
||||
"""Move each valid camera point along its original projection ray."""
|
||||
nodes, links = group.nodes, group.links
|
||||
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")
|
||||
|
||||
@@ -36,10 +36,8 @@ def taper_split_profile(group, original, geometry):
|
||||
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))
|
||||
controls = group_input(nodes, {"Split Threshold"})
|
||||
band = _math(group, "MULTIPLY", influence, _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)
|
||||
|
||||
@@ -155,15 +155,23 @@ def sample_face_camera(group, image):
|
||||
return texture.outputs["Color"]
|
||||
|
||||
|
||||
def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
face_center=None, triangles=False):
|
||||
"""Split adjacent face samples using their Z depth and base center spacing."""
|
||||
def ray_distance(group, camera):
|
||||
"""Measure the distance from the camera origin to a camera-space point."""
|
||||
length = group.nodes.new("ShaderNodeVectorMath")
|
||||
length.operation = "LENGTH"
|
||||
group.links.new(camera, length.inputs[0])
|
||||
return length.outputs["Value"]
|
||||
|
||||
|
||||
def split_depth_surface(group, geometry, face_camera, strength, reference_depth, *,
|
||||
face_distance=None, face_center=None, triangles=False):
|
||||
"""Split relative ray-distance jumps over the base face-center spacing."""
|
||||
nodes, links = group.nodes, group.links
|
||||
if face_center is None:
|
||||
face_center = nodes.new("GeometryNodeInputPosition").outputs[0]
|
||||
face_camera = evaluate_field(group, face_camera, "FLOAT_VECTOR", "FACE")
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(face_camera, axes.inputs[0])
|
||||
if face_distance is None:
|
||||
face_distance = ray_distance(group, face_camera)
|
||||
centers, depths = [], []
|
||||
for side in (0, 1):
|
||||
corner = nodes.new("GeometryNodeCornersOfEdge")
|
||||
@@ -172,7 +180,7 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
links.new(corner.outputs["Corner Index"], face.inputs["Corner Index"])
|
||||
for data_type, field, results in (
|
||||
("FLOAT_VECTOR", face_center, centers),
|
||||
("FLOAT", axes.outputs["Z"], depths),
|
||||
("FLOAT", face_distance, depths),
|
||||
):
|
||||
sample = nodes.new("GeometryNodeFieldAtIndex")
|
||||
sample.data_type = data_type
|
||||
@@ -185,13 +193,15 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
for index, center in enumerate(centers):
|
||||
links.new(center, distance.inputs[index])
|
||||
delta = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", *depths))
|
||||
scaled = _math(group, "MULTIPLY", delta, uniform)
|
||||
# Cross multiplication avoids division and stays invariant under uniform scaling.
|
||||
local_distance = _math(group, "MAXIMUM", _math(group, "MULTIPLY", _math(group, "ADD", *depths), 0.5), 1e-8)
|
||||
reference = _math(group, "MAXIMUM", reference_depth, 1e-8)
|
||||
# Uniform Scale cancels between the distance jump and local spacing correction.
|
||||
# Cross multiplication also avoids dividing by the local distance.
|
||||
selected = _math(
|
||||
group,
|
||||
"GREATER_THAN",
|
||||
_math(group, "MULTIPLY", scaled, strength),
|
||||
_math(group, "MULTIPLY", distance.outputs["Value"], 2.5),
|
||||
_math(group, "MULTIPLY", _math(group, "MULTIPLY", delta, reference), strength),
|
||||
_math(group, "MULTIPLY", _math(group, "MULTIPLY", distance.outputs["Value"], local_distance), 2.5),
|
||||
)
|
||||
selected = _math(
|
||||
group,
|
||||
@@ -352,7 +362,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
normal = nodes.new("GeometryNodeInputNormal")
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal_smooth, blur.inputs["Iterations"])
|
||||
blur.inputs["Iterations"].default_value = normal_smooth
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
normalize = nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
|
||||
@@ -11,6 +11,15 @@ def prepare_node_group(node_group):
|
||||
node_group.nodes.remove(node)
|
||||
|
||||
|
||||
def replace_link_source(node_group, link, source):
|
||||
"""Replace a source while preserving multi-input socket order."""
|
||||
multi = link.to_socket.is_multi_input
|
||||
replacement = node_group.links.new(source, link.to_socket)
|
||||
if multi:
|
||||
replacement.swap_multi_input_sort_id(link)
|
||||
node_group.links.remove(link)
|
||||
|
||||
|
||||
def interface_socket(group, name, in_out, socket_type, parent=None):
|
||||
socket = group.interface.new_socket(
|
||||
name=name,
|
||||
|
||||
98
tools/nodes/common/outline_smoothing.py
Normal file
98
tools/nodes/common/outline_smoothing.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Adapt camera depth along the original outline with batched weight updates."""
|
||||
|
||||
from .boundary_smoothing import boundary_influence, reconstruct_camera_depth
|
||||
from .depth_surface import CUT_ATTRIBUTE, _math, ray_distance
|
||||
from .nodes import evaluate_field, group_input, read_float_attribute, sample_field
|
||||
from .smoothing import edge_boundary_field
|
||||
|
||||
|
||||
def _outline_influence(group, original, geometry):
|
||||
"""Identify original outline points and protect split boundary intersections."""
|
||||
nodes, links = group.nodes, group.links
|
||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
nearest = nodes.new("GeometryNodeSampleNearest")
|
||||
nearest.domain = "POINT"
|
||||
links.new(original, nearest.inputs["Geometry"])
|
||||
position = nodes.new("GeometryNodeInputPosition").outputs["Position"]
|
||||
links.new(position, nearest.inputs["Sample Position"])
|
||||
original_rim = sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
||||
original_rim = _math(group, "GREATER_THAN", original_rim, 0)
|
||||
cut = _math(group, "GREATER_THAN", read_float_attribute(group, CUT_ATTRIBUTE), 0)
|
||||
protected = _math(group, "MULTIPLY", _math(group, "GREATER_THAN", rim, 0),
|
||||
_math(group, "MAXIMUM", cut, _math(group, "SUBTRACT", 1, original_rim)))
|
||||
# Resolve planar correspondence before replacing positions with camera depth.
|
||||
return sample_field(group, geometry, boundary_influence(group, protected), "FLOAT")
|
||||
|
||||
|
||||
def smooth_outline_camera(group, original, geometry, camera, uniform_scale):
|
||||
"""Update outline weights and blur ray distance in fixed-weight batches."""
|
||||
nodes, links = group.nodes, group.links
|
||||
controls = group_input(nodes, {"Outline Depth Fix", "Boundary Smooth"})
|
||||
influence = _outline_influence(group, original, geometry)
|
||||
depth = ray_distance(group, camera)
|
||||
|
||||
endpoints = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
||||
length = nodes.new("ShaderNodeVectorMath")
|
||||
length.operation = "DISTANCE"
|
||||
links.new(endpoints.outputs["Position 1"], length.inputs[0])
|
||||
links.new(endpoints.outputs["Position 2"], length.inputs[1])
|
||||
statistic = nodes.new("GeometryNodeAttributeStatistic")
|
||||
statistic.data_type, statistic.domain = "FLOAT", "EDGE"
|
||||
links.new(geometry, statistic.inputs["Geometry"])
|
||||
links.new(length.outputs["Value"], statistic.inputs["Attribute"])
|
||||
scale = _math(group, "DIVIDE", uniform_scale,
|
||||
_math(group, "MAXIMUM", statistic.outputs["Median"], 1e-8))
|
||||
|
||||
seed = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(depth, seed.inputs["Z"])
|
||||
carrier = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, carrier.inputs["Geometry"])
|
||||
links.new(seed.outputs[0], carrier.inputs["Position"])
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
links.new(carrier.outputs[0], repeat_input.inputs["Geometry"])
|
||||
links.new(controls.outputs["Outline Depth Fix"], repeat_input.inputs["Iterations"])
|
||||
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
current = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs[0], current.inputs[0])
|
||||
reference = nodes.new("GeometryNodeBlurAttribute")
|
||||
reference.data_type = "FLOAT"
|
||||
reference.inputs["Iterations"].default_value = 1
|
||||
reference.inputs["Weight"].default_value = 0.5
|
||||
links.new(current.outputs["Z"], reference.inputs["Value"])
|
||||
residual = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", current.outputs["Z"], reference.outputs[0]))
|
||||
ratio = _math(group, "MULTIPLY", residual, scale)
|
||||
weight = nodes.new("ShaderNodeMapRange")
|
||||
weight.interpolation_type, weight.clamp = "SMOOTHSTEP", True
|
||||
weight.inputs["From Min"].default_value = 0.25
|
||||
weight.inputs["From Max"].default_value = 0.5
|
||||
links.new(ratio, weight.inputs["Value"])
|
||||
active = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", depth, 1e-8))
|
||||
active = sample_field(group, geometry, active, "FLOAT")
|
||||
weighted = _math(group, "MULTIPLY", weight.outputs[0], active)
|
||||
# Sampling the round's input prevents the inner Blur from updating its weight.
|
||||
frozen = sample_field(group, repeat_input.outputs["Geometry"], weighted, "FLOAT")
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT"
|
||||
links.new(current.outputs["Z"], blur.inputs["Value"])
|
||||
links.new(frozen, blur.inputs["Weight"])
|
||||
links.new(controls.outputs["Boundary Smooth"], blur.inputs["Iterations"])
|
||||
updated = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(blur.outputs[0], updated.inputs["Z"])
|
||||
move = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(repeat_input.outputs["Geometry"], move.inputs["Geometry"])
|
||||
links.new(updated.outputs[0], move.inputs["Position"])
|
||||
links.new(move.outputs[0], repeat_output.inputs["Geometry"])
|
||||
|
||||
result = sample_field(group, repeat_output.outputs["Geometry"], current.outputs["Z"], "FLOAT")
|
||||
enabled = _math(group, "MULTIPLY", active,
|
||||
_math(group, "MULTIPLY", controls.outputs["Outline Depth Fix"],
|
||||
controls.outputs["Boundary Smooth"]))
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "FLOAT"
|
||||
links.new(enabled, choose.inputs["Switch"])
|
||||
links.new(depth, choose.inputs["False"])
|
||||
links.new(result, choose.inputs["True"])
|
||||
return reconstruct_camera_depth(group, camera, depth, choose.outputs[0])
|
||||
@@ -51,7 +51,6 @@ def expand_smooth(
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
||||
links.new(
|
||||
iteration_controls.outputs[iterations_name],
|
||||
repeat_input.inputs["Iterations"],
|
||||
@@ -109,11 +108,6 @@ def expand_smooth(
|
||||
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")
|
||||
boundary_mix.data_type = "VECTOR"
|
||||
boundary_mix.inputs[0].default_value = 1.0
|
||||
links.new(smooth_all.outputs["Value"], boundary_mix.inputs[4])
|
||||
links.new(choose_boundary.outputs["Output"], boundary_mix.inputs[5])
|
||||
|
||||
weight = weight_controls.outputs[weight_name]
|
||||
if pin_sharp:
|
||||
@@ -126,15 +120,10 @@ def expand_smooth(
|
||||
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])
|
||||
links.new(normal_coherence.outputs["Value"], smooth_weight.inputs[1])
|
||||
weight = smooth_weight.outputs[0]
|
||||
if influence is not None:
|
||||
masked_weight = nodes.new("ShaderNodeMath")
|
||||
@@ -143,17 +132,12 @@ def expand_smooth(
|
||||
links.new(influence, masked_weight.inputs[1])
|
||||
weight = masked_weight.outputs[0]
|
||||
|
||||
target_position = nodes.new("GeometryNodeSwitch")
|
||||
target_position.input_type = "VECTOR"
|
||||
target_position.inputs["Switch"].default_value = True
|
||||
links.new(smooth_all.outputs["Value"], target_position.inputs["False"])
|
||||
links.new(boundary_mix.outputs["Result"], target_position.inputs["True"])
|
||||
current_position = nodes.new("GeometryNodeInputPosition")
|
||||
mix_position = nodes.new("ShaderNodeMix")
|
||||
mix_position.data_type = "VECTOR"
|
||||
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])
|
||||
links.new(choose_boundary.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"])
|
||||
|
||||
Reference in New Issue
Block a user