Refactor comparison operations in geometry nodes
- Introduced a new `compare_node` function to streamline comparison operations across various geometry nodes. - Replaced instances of mathematical comparison nodes with the new `compare_node` function in multiple files, enhancing code readability and maintainability. - Updated related logic in cutout, depth surface, and image processing groups to utilize the new comparison method. - Ensured consistent handling of boolean logic through the introduction of a `boolean_node` function for combining boolean inputs.
This commit is contained in:
@@ -1,24 +1,37 @@
|
||||
"""Apply boundary-preserving smoothing within a narrow cut-edge band."""
|
||||
|
||||
from .nodes import evaluate_field, group_input
|
||||
from .nodes import boolean_node, compare_node, evaluate_field, group_input
|
||||
from .depth_surface import _math
|
||||
from .smoothing import edge_boundary_field, pinned_smooth
|
||||
|
||||
|
||||
def _gated(group, value, blocked, *, default):
|
||||
"""Return the value unless the blocked control is true."""
|
||||
node = group.nodes.new("GeometryNodeSwitch")
|
||||
node.input_type = "FLOAT"
|
||||
node.inputs["True"].default_value = default
|
||||
if isinstance(blocked, bool):
|
||||
node.inputs["Switch"].default_value = blocked
|
||||
else:
|
||||
group.links.new(blocked, node.inputs["Switch"])
|
||||
group.links.new(value, node.inputs["False"])
|
||||
return node.outputs[0]
|
||||
|
||||
|
||||
def boundary_influence(group, protected=False, *, boundary=None):
|
||||
"""Fade a boundary field through two rings while excluding protected points."""
|
||||
nodes, links = group.nodes, group.links
|
||||
if boundary is None:
|
||||
boundary = edge_boundary_field(nodes, links)
|
||||
boundary = evaluate_field(group, boundary, "FLOAT", "POINT")
|
||||
unprotected = _math(group, "SUBTRACT", 1, protected)
|
||||
cut = _math(group, "MULTIPLY", boundary, unprotected)
|
||||
cut = _gated(group, boundary, protected, default=0.0)
|
||||
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)
|
||||
mixed = _math(group, "MAXIMUM", cut, blur.outputs["Value"])
|
||||
return _gated(group, mixed, protected, default=0.0)
|
||||
|
||||
|
||||
def smooth_boundary_depth(group, depth, protected=False):
|
||||
@@ -63,8 +76,8 @@ def smooth_boundary_camera(group, camera, protected=False):
|
||||
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))
|
||||
changed = compare_node(group, "GREATER_THAN", _math(group, "ABSOLUTE", _math(group, "SUBTRACT", smoothed, depth)), 0)
|
||||
enabled = boolean_node(group, "AND", changed, compare_node(group, "GREATER_THAN", depth, 1e-8))
|
||||
projected = nodes.new("ShaderNodeVectorMath")
|
||||
projected.operation = "SCALE"
|
||||
links.new(camera, projected.inputs[0])
|
||||
@@ -82,7 +95,7 @@ def cut_boundary_influence(group, split_boundary, protected=False, *, outline_sc
|
||||
nodes, links = group.nodes, group.links
|
||||
if boundary is None:
|
||||
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
split = _math(group, "MULTIPLY", boundary, split_boundary)
|
||||
split = _gated(group, boundary, boolean_node(group, "NOT", split_boundary), default=0.0)
|
||||
outline = _math(group, "MAXIMUM", _math(group, "SUBTRACT", boundary, split), 0)
|
||||
split_influence = boundary_influence(group, protected, boundary=split)
|
||||
outline_influence = _math(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Build geometry calculations shared by Cutout assets."""
|
||||
from .nodes import math_node
|
||||
from .nodes import compare_node, math_node
|
||||
from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME
|
||||
|
||||
|
||||
@@ -107,12 +107,10 @@ def _remove_zero_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])
|
||||
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(zero.outputs[0], choose.inputs["Switch"])
|
||||
links.new(zero, choose.inputs["Switch"])
|
||||
links.new(shaped_geometry, choose.inputs["False"])
|
||||
links.new(zero_geometry, choose.inputs["True"])
|
||||
return choose.outputs["Output"]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""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 .nodes import (
|
||||
boolean_node, compare_node, evaluate_field, group_input,
|
||||
read_float_attribute, sample_field, store_float_attribute,
|
||||
)
|
||||
from .smoothing import edge_boundary_field
|
||||
from .boundary_smoothing import boundary_influence
|
||||
|
||||
@@ -10,9 +13,9 @@ 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)
|
||||
all_boundary = compare_node(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))
|
||||
selected = boolean_node(group, "AND", all_boundary, compare_node(group, "EQUAL", count, 3, data_type="INT"))
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
links.new(geometry, delete.inputs["Geometry"])
|
||||
@@ -41,9 +44,16 @@ def original_boundary_field(group, original, rim=None):
|
||||
def taper_split_profile(group, geometry, original_boundary):
|
||||
"""Taper balloon thickness through the shared two-ring influence builder."""
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
influence = boundary_influence(group, original_boundary)
|
||||
controls = group_input(nodes, {"Depth Split"})
|
||||
band = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", controls.outputs["Depth Split"], 0))
|
||||
has_split = compare_node(group, "GREATER_THAN", controls.outputs["Depth Split"], 0)
|
||||
band = nodes.new("GeometryNodeSwitch")
|
||||
band.input_type = "FLOAT"
|
||||
band.inputs["False"].default_value = 0.0
|
||||
links.new(has_split, band.inputs["Switch"])
|
||||
links.new(influence, band.inputs["True"])
|
||||
band = band.outputs[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)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Face separation and connected thickness for Depth Surface assets."""
|
||||
|
||||
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attribute_pattern, sample_field
|
||||
from .nodes import (
|
||||
boolean_node, compare_node, evaluate_field, store_float_attribute,
|
||||
store_boolean_attribute, remove_attribute_pattern, sample_field,
|
||||
)
|
||||
from .smoothing import edge_boundary_field
|
||||
|
||||
|
||||
@@ -32,21 +35,7 @@ def _remove_quad_strip_faces(group, geometry):
|
||||
nodes, links = group.nodes, group.links
|
||||
|
||||
def equals(value, count):
|
||||
compare = nodes.new("FunctionNodeCompare")
|
||||
compare.data_type, compare.operation = "INT", "EQUAL"
|
||||
links.new(value, compare.inputs[2])
|
||||
if isinstance(count, int):
|
||||
compare.inputs[3].default_value = count
|
||||
else:
|
||||
links.new(count, compare.inputs[3])
|
||||
return compare.outputs["Result"]
|
||||
|
||||
def combine(operation, a, b):
|
||||
node = nodes.new("FunctionNodeIntegerMath")
|
||||
node.operation = operation
|
||||
links.new(a, node.inputs[0])
|
||||
links.new(b, node.inputs[1])
|
||||
return node.outputs[0]
|
||||
return compare_node(group, "EQUAL", value, count, data_type="INT")
|
||||
|
||||
boundaries = []
|
||||
for offset in range(4):
|
||||
@@ -64,13 +53,13 @@ def _remove_quad_strip_faces(group, geometry):
|
||||
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
links.new(neighbors.outputs["Face Count"], sample.inputs["Value"])
|
||||
boundaries.append(equals(sample.outputs["Value"], 1))
|
||||
opposite = combine(
|
||||
"MAXIMUM",
|
||||
combine("MULTIPLY", boundaries[0], boundaries[2]),
|
||||
combine("MULTIPLY", boundaries[1], boundaries[3]),
|
||||
opposite = boolean_node(
|
||||
group, "OR",
|
||||
boolean_node(group, "AND", boundaries[0], boundaries[2]),
|
||||
boolean_node(group, "AND", boundaries[1], boundaries[3]),
|
||||
)
|
||||
face = nodes.new("GeometryNodeInputMeshFaceNeighbors")
|
||||
selected = combine("MULTIPLY", equals(face.outputs["Vertex Count"], 4), opposite)
|
||||
selected = boolean_node(group, "AND", equals(face.outputs["Vertex Count"], 4), opposite)
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
links.new(geometry, delete.inputs["Geometry"])
|
||||
@@ -83,25 +72,11 @@ def _remove_triangle_strip_faces(group, geometry):
|
||||
nodes, links = group.nodes, group.links
|
||||
|
||||
def equals(value, count):
|
||||
compare = nodes.new("FunctionNodeCompare")
|
||||
compare.data_type, compare.operation = "INT", "EQUAL"
|
||||
links.new(value, compare.inputs[2])
|
||||
if isinstance(count, int):
|
||||
compare.inputs[3].default_value = count
|
||||
else:
|
||||
links.new(count, compare.inputs[3])
|
||||
return compare.outputs["Result"]
|
||||
return compare_node(group, "EQUAL", value, count, data_type="INT")
|
||||
|
||||
def combine(operation, a, b):
|
||||
node = nodes.new("FunctionNodeIntegerMath")
|
||||
node.operation = operation
|
||||
links.new(a, node.inputs[0])
|
||||
links.new(b, node.inputs[1])
|
||||
return node.outputs[0]
|
||||
|
||||
def sample(value, index, domain):
|
||||
def sample(value, index, domain, data_type="INT"):
|
||||
node = nodes.new("GeometryNodeFieldAtIndex")
|
||||
node.data_type, node.domain = "INT", domain
|
||||
node.data_type, node.domain = data_type, domain
|
||||
links.new(value, node.inputs["Value"])
|
||||
links.new(index, node.inputs["Index"])
|
||||
return node.outputs["Value"]
|
||||
@@ -138,16 +113,29 @@ def _remove_triangle_strip_faces(group, geometry):
|
||||
sides.append((triangle, vertex(corner), vertex(tip), boundary(end), boundary(tip)))
|
||||
a, b = sides
|
||||
same_direction = equals(a[1], b[1])
|
||||
same_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[3]), combine("MULTIPLY", a[4], b[4]))
|
||||
reverse_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[4]), combine("MULTIPLY", a[4], b[3]))
|
||||
pair = combine(
|
||||
"MAXIMUM",
|
||||
combine("MULTIPLY", same_direction, reverse_order),
|
||||
combine("MULTIPLY", equals(same_direction, 0), same_order),
|
||||
same_order = boolean_node(
|
||||
group, "OR",
|
||||
boolean_node(group, "AND", a[3], b[3]),
|
||||
boolean_node(group, "AND", a[4], b[4]),
|
||||
)
|
||||
reverse_order = boolean_node(
|
||||
group, "OR",
|
||||
boolean_node(group, "AND", a[3], b[4]),
|
||||
boolean_node(group, "AND", a[4], b[3]),
|
||||
)
|
||||
pair = boolean_node(
|
||||
group, "OR",
|
||||
boolean_node(group, "AND", same_direction, reverse_order),
|
||||
boolean_node(group, "AND", boolean_node(group, "NOT", same_direction), same_order),
|
||||
)
|
||||
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
for condition in (equals(neighbors.outputs["Face Count"], 2), a[0], b[0], equals(equals(a[2], b[2]), 0)):
|
||||
pair = combine("MULTIPLY", pair, condition)
|
||||
for condition in (
|
||||
equals(neighbors.outputs["Face Count"], 2),
|
||||
a[0],
|
||||
b[0],
|
||||
boolean_node(group, "NOT", equals(a[2], b[2])),
|
||||
):
|
||||
pair = boolean_node(group, "AND", pair, condition)
|
||||
|
||||
face_index = nodes.new("GeometryNodeInputIndex")
|
||||
corners = nodes.new("GeometryNodeCornersOfFace")
|
||||
@@ -157,14 +145,21 @@ def _remove_triangle_strip_faces(group, geometry):
|
||||
corner = offset_corner(corners.outputs["Corner Index"], offset)
|
||||
edge = nodes.new("GeometryNodeEdgesOfCorner")
|
||||
links.new(corner, edge.inputs["Corner Index"])
|
||||
match = sample(pair, edge.outputs["Next Edge Index"], "EDGE")
|
||||
paired = match if paired is None else combine("MAXIMUM", paired, match)
|
||||
match = sample(pair, edge.outputs["Next Edge Index"], "EDGE", "BOOLEAN")
|
||||
paired = match if paired is None else boolean_node(group, "OR", paired, match)
|
||||
rim = boundary(corner)
|
||||
boundary_count = rim if boundary_count is None else combine("ADD", boundary_count, rim)
|
||||
selected = combine(
|
||||
"MULTIPLY",
|
||||
if boundary_count is None:
|
||||
boundary_count = rim
|
||||
else:
|
||||
add = nodes.new("FunctionNodeIntegerMath")
|
||||
add.operation = "ADD"
|
||||
links.new(boundary_count, add.inputs[0])
|
||||
links.new(rim, add.inputs[1])
|
||||
boundary_count = add.outputs[0]
|
||||
selected = boolean_node(
|
||||
group, "AND",
|
||||
equals(corners.outputs["Total"], 3),
|
||||
combine("MAXIMUM", paired, equals(boundary_count, 3)),
|
||||
boolean_node(group, "OR", paired, equals(boundary_count, 3)),
|
||||
)
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
@@ -233,24 +228,24 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
|
||||
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(
|
||||
selected = compare_node(
|
||||
group,
|
||||
"GREATER_THAN",
|
||||
_math(group, "MULTIPLY", _math(group, "MULTIPLY", delta, reference), strength),
|
||||
_math(group, "MULTIPLY", _math(group, "MULTIPLY", distance.outputs["Value"], local_distance), 2.5),
|
||||
)
|
||||
selected = _math(
|
||||
selected = boolean_node(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
"AND",
|
||||
selected,
|
||||
_math(group, "GREATER_THAN", distance.outputs["Value"], 0.0),
|
||||
compare_node(group, "GREATER_THAN", distance.outputs["Value"], 0.0),
|
||||
)
|
||||
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
selected = _math(
|
||||
selected = boolean_node(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
"AND",
|
||||
selected,
|
||||
_math(group, "COMPARE", neighbors.outputs["Face Count"], 2.0),
|
||||
compare_node(group, "EQUAL", neighbors.outputs["Face Count"], 2, data_type="INT"),
|
||||
)
|
||||
cut_edges = nodes.new("GeometryNodeFieldOnDomain")
|
||||
cut_edges.data_type = "FLOAT"
|
||||
@@ -262,12 +257,12 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
|
||||
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
|
||||
corners, cut = store_boolean_attribute(
|
||||
group, geometry, CUT_ATTRIBUTE,
|
||||
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
|
||||
compare_node(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
|
||||
)
|
||||
split = nodes.new("GeometryNodeSplitEdges")
|
||||
links.new(corners, split.inputs["Mesh"])
|
||||
links.new(selected, split.inputs["Selection"])
|
||||
enabled = _math(group, "GREATER_THAN", strength, 0.0)
|
||||
enabled = compare_node(group, "GREATER_THAN", strength, 0.0)
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(enabled, choose.inputs["Switch"])
|
||||
@@ -286,7 +281,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength):
|
||||
cut_boundary = evaluate_field(group, edge_boundary_field(nodes, links), "BOOLEAN", "POINT")
|
||||
enabled = nodes.new("GeometryNodeSwitch")
|
||||
enabled.input_type = "BOOLEAN"
|
||||
links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
|
||||
links.new(compare_node(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
|
||||
enabled.inputs["False"].default_value = False
|
||||
links.new(cut_vertex, enabled.inputs["True"])
|
||||
combined = nodes.new("FunctionNodeBooleanMath")
|
||||
@@ -382,11 +377,16 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
links.new(value, join.inputs["Geometry"])
|
||||
construction = join.outputs["Geometry"]
|
||||
if weld_zero_thickness:
|
||||
positive = _math(group, "GREATER_THAN", source_thickness, 1e-12)
|
||||
positive = compare_node(group, "GREATER_THAN", source_thickness, 1e-12)
|
||||
flattened = nodes.new("ShaderNodeCombineXYZ")
|
||||
for axis in ("X", "Z"):
|
||||
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
|
||||
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Y"]), flattened.inputs["Y"])
|
||||
collapsed = nodes.new("GeometryNodeSwitch")
|
||||
collapsed.input_type = "FLOAT"
|
||||
collapsed.inputs["False"].default_value = 0.0
|
||||
links.new(positive, collapsed.inputs["Switch"])
|
||||
links.new(layer_axes.outputs["Y"], collapsed.inputs["True"])
|
||||
links.new(collapsed.outputs[0], flattened.inputs["Y"])
|
||||
collapse = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(construction, collapse.inputs["Geometry"])
|
||||
links.new(flattened.outputs[0], collapse.inputs["Position"])
|
||||
|
||||
@@ -79,6 +79,41 @@ def math_node(nodes, operation):
|
||||
return node
|
||||
|
||||
|
||||
_COMPARE_INPUTS = {
|
||||
"FLOAT": ("A", "B"),
|
||||
"INT": ("A_INT", "B_INT"),
|
||||
"VECTOR": ("A_VEC3", "B_VEC3"),
|
||||
"RGBA": ("A_COL", "B_COL"),
|
||||
}
|
||||
|
||||
|
||||
def compare_node(group, operation, a, b, data_type="FLOAT"):
|
||||
"""Compare two inputs on the given data type and return the boolean result."""
|
||||
node = group.nodes.new("FunctionNodeCompare")
|
||||
node.data_type, node.operation = data_type, operation
|
||||
for value, identifier in zip((a, b), _COMPARE_INPUTS[data_type]):
|
||||
socket = next(item for item in node.inputs if item.identifier == identifier)
|
||||
if isinstance(value, (bool, int, float)):
|
||||
socket.default_value = value
|
||||
else:
|
||||
group.links.new(value, socket)
|
||||
return node.outputs["Result"]
|
||||
|
||||
|
||||
def boolean_node(group, operation, a=None, b=None):
|
||||
"""Combine boolean inputs and return the boolean result."""
|
||||
node = group.nodes.new("FunctionNodeBooleanMath")
|
||||
node.operation = operation
|
||||
for value, index in ((a, 0), (b, 1)):
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, bool):
|
||||
node.inputs[index].default_value = value
|
||||
else:
|
||||
group.links.new(value, node.inputs[index])
|
||||
return node.outputs["Boolean"]
|
||||
|
||||
|
||||
def menu_switch(group, control, names, kind):
|
||||
selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None)
|
||||
if selector is None:
|
||||
|
||||
Reference in New Issue
Block a user