- 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.
206 lines
9.0 KiB
Python
206 lines
9.0 KiB
Python
"""Build O Image Cutout."""
|
|
import bpy
|
|
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
|
from ..common.nodes import (
|
|
interface_socket, group_input, compare_node, math_node, float_input,
|
|
menu_switch, read_float_attribute, store_float_attribute, prepare_node_group,
|
|
)
|
|
from ..common.cutout import _extrude_layers, _offset_geometry_y, _set_layer_y, _shade_output
|
|
|
|
|
|
NODE_GROUP_NAME = "O Image Cutout"
|
|
|
|
|
|
def _set_segmented_layer_y(
|
|
group,
|
|
geometry,
|
|
front_z,
|
|
back_z,
|
|
thickness,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
|
|
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
|
edge_length = nodes.new("ShaderNodeVectorMath")
|
|
edge_length.operation = "DISTANCE"
|
|
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
|
|
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
|
|
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
|
boundary = compare_node(group, "EQUAL", edge_neighbors.outputs["Face Count"], 1, data_type="INT")
|
|
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
|
statistics.data_type = "FLOAT"
|
|
statistics.domain = "EDGE"
|
|
links.new(geometry, statistics.inputs["Geometry"])
|
|
links.new(boundary, statistics.inputs["Selection"])
|
|
links.new(edge_length.outputs["Value"], statistics.inputs["Attribute"])
|
|
safe_edge = math_node(nodes, "MAXIMUM")
|
|
safe_edge.inputs[1].default_value = 1e-6
|
|
links.new(statistics.outputs["Median"], safe_edge.inputs[0])
|
|
segment_ratio = math_node(nodes, "DIVIDE")
|
|
links.new(thickness, segment_ratio.inputs[0])
|
|
links.new(safe_edge.outputs[0], segment_ratio.inputs[1])
|
|
segment_count = nodes.new("FunctionNodeFloatToInt")
|
|
segment_count.rounding_mode = "CEILING"
|
|
links.new(segment_ratio.outputs[0], segment_count.inputs["Float"])
|
|
at_least_one = nodes.new("FunctionNodeIntegerMath")
|
|
at_least_one.operation = "MAXIMUM"
|
|
at_least_one.inputs[1].default_value = 1
|
|
links.new(segment_count.outputs["Integer"], at_least_one.inputs[0])
|
|
segment_limit = nodes.new("FunctionNodeIntegerMath")
|
|
segment_limit.operation = "MINIMUM"
|
|
segment_limit.inputs[1].default_value = 128
|
|
links.new(at_least_one.outputs[0], segment_limit.inputs[0])
|
|
step = math_node(nodes, "DIVIDE")
|
|
links.new(thickness, step.inputs[0])
|
|
links.new(segment_limit.outputs[0], step.inputs[1])
|
|
|
|
front = _offset_geometry_y(group, geometry, front_z)
|
|
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
|
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
|
repeat_input.pair_with_output(repeat_output)
|
|
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
|
repeat_output.repeat_items.new("GEOMETRY", "Sides")
|
|
links.new(segment_limit.outputs[0], repeat_input.inputs["Iterations"])
|
|
links.new(geometry, repeat_input.inputs["Geometry"])
|
|
negative_step = math_node(nodes, "MULTIPLY")
|
|
negative_step.inputs[1].default_value = -1.0
|
|
links.new(step.outputs[0], negative_step.inputs[0])
|
|
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
|
extrude.mode = "FACES"
|
|
extrude.inputs["Individual"].default_value = False
|
|
extrude.inputs["Selection"].default_value = True
|
|
extrude.inputs["Offset"].default_value = (0.0, 0.0, 0.0)
|
|
links.new(negative_step.outputs[0], extrude.inputs["Offset Scale"])
|
|
links.new(repeat_input.outputs["Geometry"], extrude.inputs["Mesh"])
|
|
split_extrude = nodes.new("GeometryNodeSeparateGeometry")
|
|
split_extrude.domain = "FACE"
|
|
links.new(extrude.outputs["Mesh"], split_extrude.inputs["Geometry"])
|
|
links.new(extrude.outputs["Side"], split_extrude.inputs["Selection"])
|
|
join_sides = nodes.new("GeometryNodeJoinGeometry")
|
|
links.new(repeat_input.outputs["Sides"], join_sides.inputs["Geometry"])
|
|
links.new(split_extrude.outputs["Selection"], join_sides.inputs["Geometry"])
|
|
links.new(split_extrude.outputs["Inverted"], repeat_output.inputs["Geometry"])
|
|
links.new(join_sides.outputs["Geometry"], repeat_output.inputs["Sides"])
|
|
|
|
positioned_side = _offset_geometry_y(
|
|
group,
|
|
repeat_output.outputs["Sides"],
|
|
front_z,
|
|
)
|
|
flip_side = nodes.new("GeometryNodeFlipFaces")
|
|
links.new(positioned_side, flip_side.inputs["Mesh"])
|
|
|
|
back = _offset_geometry_y(group, geometry, back_z)
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
links.new(front, join.inputs["Geometry"])
|
|
links.new(flip_back.outputs["Mesh"], join.inputs["Geometry"])
|
|
links.new(flip_side.outputs["Mesh"], join.inputs["Geometry"])
|
|
merge = nodes.new("GeometryNodeMergeByDistance")
|
|
merge.mode = "ALL"
|
|
merge.inputs["Distance"].default_value = 1e-6
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _build_shell(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "GEOMETRY"
|
|
links.new(zero, choose.inputs["Switch"])
|
|
links.new(geometry, choose.inputs["True"])
|
|
front = math_node(nodes, "MULTIPLY")
|
|
front.inputs[1].default_value = 0.0
|
|
links.new(thickness, front.inputs[0])
|
|
back = math_node(nodes, "MULTIPLY")
|
|
back.inputs[1].default_value = 1.0
|
|
links.new(thickness, back.inputs[0])
|
|
shaped = _set_segmented_layer_y(
|
|
group,
|
|
geometry,
|
|
front.outputs[0],
|
|
back.outputs[0],
|
|
thickness,
|
|
)
|
|
links.new(shaped, choose.inputs["False"])
|
|
return choose.outputs[0]
|
|
|
|
|
|
def _build_balloon(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "GEOMETRY"
|
|
links.new(zero, choose.inputs["Switch"])
|
|
links.new(geometry, choose.inputs["True"])
|
|
profile = read_float_attribute(group, 'o_balloon')
|
|
front_scale = math_node(nodes, "MULTIPLY")
|
|
links.new(profile, front_scale.inputs[0])
|
|
links.new(thickness, front_scale.inputs[1])
|
|
negative_front = math_node(nodes, "MULTIPLY")
|
|
negative_front.inputs[1].default_value = -1.0
|
|
links.new(front_scale.outputs[0], negative_front.inputs[0])
|
|
extrude = _extrude_layers(group, geometry)
|
|
shaped = _set_layer_y(
|
|
group,
|
|
geometry,
|
|
extrude,
|
|
negative_front.outputs[0],
|
|
front_scale.outputs[0],
|
|
)
|
|
links.new(shaped, choose.inputs["False"])
|
|
return choose.outputs[0]
|
|
|
|
|
|
def build_image_cutout_group():
|
|
name = NODE_GROUP_NAME
|
|
existing = bpy.data.node_groups.get(name)
|
|
if existing is not None:
|
|
return existing
|
|
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
|
|
group.is_modifier, group.use_fake_user = (True, True)
|
|
group.color_tag = 'GEOMETRY'
|
|
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
|
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
|
float_input(group, 'Thickness', 1.0, maximum=2.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
|
|
float_input(group, 'Shell Thickness', 0.0, maximum=1.0, subtype='DISTANCE').description = 'Add thickness behind the original surface while keeping the front surface in place.'
|
|
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
|
controls = group_input(group.nodes, set())
|
|
balloon = _build_balloon(group, controls.outputs['Geometry'], controls.outputs['Thickness'])
|
|
shell = _build_shell(group, controls.outputs['Geometry'], controls.outputs['Shell Thickness'])
|
|
choose = menu_switch(group, 'Mode', ('Balloon', 'Shell'), 'GEOMETRY')
|
|
group.links.new(balloon, choose.inputs['Balloon'])
|
|
group.links.new(shell, choose.inputs['Shell'])
|
|
geometry = choose.outputs[0]
|
|
profile = read_float_attribute(group, 'o_balloon')
|
|
amount = group_input(group.nodes, {'Thickness'})
|
|
enabled = compare_node(group, 'GREATER_THAN', amount.outputs['Thickness'], 1e-6)
|
|
inflated = group.nodes.new('GeometryNodeSwitch')
|
|
inflated.input_type = 'FLOAT'
|
|
inflated.inputs['False'].default_value = 1
|
|
group.links.new(enabled, inflated.inputs['Switch'])
|
|
group.links.new(profile, inflated.inputs['True'])
|
|
mode = menu_switch(group, 'Mode', ('Balloon', 'Shell'), 'FLOAT')
|
|
mode.inputs['Shell'].default_value = 1
|
|
group.links.new(inflated.outputs[0], mode.inputs['Balloon'])
|
|
scale = mode.outputs[0]
|
|
reduction = math_node(group.nodes, 'SUBTRACT')
|
|
reduction.inputs[0].default_value = 1.0
|
|
group.links.new(scale, reduction.inputs[1])
|
|
geometry = store_float_attribute(group, geometry, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction.outputs[0])
|
|
_shade_output(group, geometry)
|
|
prepare_node_group(group)
|
|
for item in group.interface.items_tree:
|
|
if item.item_type != 'SOCKET' or item.in_out != 'INPUT':
|
|
continue
|
|
if item.name == 'Mode':
|
|
item.default_value = 'Balloon'
|
|
elif item.name == 'Shell Thickness':
|
|
item.name = 'Thickness'
|
|
return group
|