- Added a new cutout tool for selecting regions in images, allowing users to choose mesh shapes based on selected areas. - Introduced depth generation capabilities for rasterized images, including normal data extraction. - Created jobs for generating depth planes and region data, enhancing the AI data generation workflow. - Developed a selection refinement process for cutout regions, improving accuracy in generated outputs. - Established a new region module for handling rasterized image regions and their associated AI generation tasks. - Enhanced node graph builders for asset generation, facilitating smoother integration of new features.
1008 lines
38 KiB
Python
1008 lines
38 KiB
Python
"""Build the four Cutout Geometry Nodes groups stored in O_AnyImage.blend."""
|
|
|
|
import math
|
|
|
|
import bpy
|
|
|
|
from node_graph import expand_smooth
|
|
|
|
from anyimage.operators.cutout_tool.shape_types import (
|
|
BALLOON_ATTRIBUTE_NAME,
|
|
CUTOUT_NODE_GROUP_NAME,
|
|
CUTOUT_NODE_GROUP_NAMES,
|
|
CUTOUT_SHAPE_LABELS,
|
|
)
|
|
|
|
|
|
def _socket(group, name, in_out, socket_type, parent=None):
|
|
socket = group.interface.new_socket(
|
|
name=name,
|
|
in_out=in_out,
|
|
socket_type=socket_type,
|
|
parent=parent,
|
|
)
|
|
if in_out == "INPUT" and hasattr(socket, "structure_type"):
|
|
socket.structure_type = "SINGLE"
|
|
return socket
|
|
|
|
|
|
def _group_input(nodes, location, visible_names):
|
|
node = nodes.new("NodeGroupInput")
|
|
node.location = location
|
|
visible = set(visible_names)
|
|
for output in node.outputs:
|
|
output.hide = output.name not in visible
|
|
return node
|
|
|
|
|
|
def _localize_group_inputs(group, controls):
|
|
"""Place short, socket-specific Group Inputs beside their consumers."""
|
|
links = group.links
|
|
outgoing = [link for link in links if link.from_node == controls]
|
|
by_socket = {}
|
|
for link in outgoing:
|
|
if link.from_socket.name == "Geometry":
|
|
continue
|
|
by_socket.setdefault(link.from_socket.name, []).append(link)
|
|
|
|
for socket_name, socket_links in by_socket.items():
|
|
ordered = sorted(socket_links, key=lambda link: link.to_node.location.x)
|
|
clusters = []
|
|
for link in ordered:
|
|
if (
|
|
not clusters
|
|
or link.to_node.location.x
|
|
- clusters[-1][-1].to_node.location.x
|
|
> 900.0
|
|
):
|
|
clusters.append([])
|
|
clusters[-1].append(link)
|
|
for cluster in clusters:
|
|
targets = [link.to_node for link in cluster]
|
|
location = [
|
|
min(node.location.x for node in targets) - 260.0,
|
|
max(node.location.y for node in targets) + 320.0,
|
|
]
|
|
while any(
|
|
abs(node.location.x - location[0]) < 190.0
|
|
and abs(node.location.y - location[1]) < 190.0
|
|
for node in group.nodes
|
|
):
|
|
location[1] += 220.0
|
|
local = _group_input(group.nodes, location, {socket_name})
|
|
for link in cluster:
|
|
target = link.to_socket
|
|
links.remove(link)
|
|
links.new(local.outputs[socket_name], target)
|
|
for output in controls.outputs:
|
|
output.hide = output.name != "Geometry"
|
|
|
|
|
|
def _float_input(
|
|
group,
|
|
name,
|
|
default,
|
|
minimum=0.0,
|
|
maximum=None,
|
|
subtype=None,
|
|
parent=None,
|
|
):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketFloat", parent)
|
|
socket.default_value = default
|
|
socket.min_value = minimum
|
|
if maximum is not None:
|
|
socket.max_value = maximum
|
|
if subtype is not None:
|
|
socket.subtype = subtype
|
|
return socket
|
|
|
|
|
|
def _bool_input(group, name, default=False, parent=None):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketBool", parent)
|
|
socket.default_value = default
|
|
return socket
|
|
|
|
|
|
def _vector_input(group, name, default, subtype=None, parent=None):
|
|
socket = _socket(group, name, "INPUT", "NodeSocketVector", parent)
|
|
socket.default_value = default
|
|
if subtype is not None:
|
|
socket.subtype = subtype
|
|
return socket
|
|
|
|
|
|
def _math(nodes, operation, location):
|
|
node = nodes.new("ShaderNodeMath")
|
|
node.operation = operation
|
|
node.location = location
|
|
return node
|
|
|
|
|
|
def _island_cleanup(group, geometry, cleanup, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
mesh_island = nodes.new("GeometryNodeInputMeshIsland")
|
|
mesh_island.location = (x, y + 250.0)
|
|
face_area = nodes.new("GeometryNodeInputMeshFaceArea")
|
|
face_area.location = (x, y + 100.0)
|
|
island_area = nodes.new("GeometryNodeAccumulateField")
|
|
island_area.data_type = "FLOAT"
|
|
island_area.domain = "FACE"
|
|
island_area.location = (x + 220.0, y + 200.0)
|
|
links.new(face_area.outputs["Area"], island_area.inputs["Value"])
|
|
links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"])
|
|
|
|
tiny = _math(nodes, "LESS_THAN", (x + 450.0, y + 200.0))
|
|
links.new(island_area.outputs["Total"], tiny.inputs[0])
|
|
links.new(cleanup, tiny.inputs[1])
|
|
delete = nodes.new("GeometryNodeDeleteGeometry")
|
|
delete.domain = "FACE"
|
|
delete.location = (x + 700.0, y)
|
|
links.new(geometry, delete.inputs["Geometry"])
|
|
links.new(tiny.outputs[0], delete.inputs["Selection"])
|
|
return delete.outputs["Geometry"]
|
|
|
|
|
|
def _profile_field(group, geometry, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
profile = nodes.new("GeometryNodeInputNamedAttribute")
|
|
profile.data_type = "FLOAT"
|
|
profile.inputs["Name"].default_value = BALLOON_ATTRIBUTE_NAME
|
|
profile.location = (x, y + 250.0)
|
|
profile_max = nodes.new("GeometryNodeAttributeStatistic")
|
|
profile_max.data_type = "FLOAT"
|
|
profile_max.domain = "POINT"
|
|
profile_max.location = (x + 220.0, y)
|
|
links.new(geometry, profile_max.inputs["Geometry"])
|
|
links.new(profile.outputs["Attribute"], profile_max.inputs["Attribute"])
|
|
safe_max = _math(nodes, "MAXIMUM", (x + 450.0, y))
|
|
safe_max.inputs[1].default_value = 1e-6
|
|
links.new(profile_max.outputs["Max"], safe_max.inputs[0])
|
|
normalized = _math(nodes, "DIVIDE", (x + 450.0, y + 250.0))
|
|
links.new(profile.outputs["Attribute"], normalized.inputs[0])
|
|
links.new(safe_max.outputs[0], normalized.inputs[1])
|
|
smooth = nodes.new("ShaderNodeMapRange")
|
|
smooth.data_type = "FLOAT"
|
|
smooth.interpolation_type = "SMOOTHERSTEP"
|
|
smooth.clamp = True
|
|
smooth.inputs["From Min"].default_value = 0.0
|
|
smooth.inputs["From Max"].default_value = 1.0
|
|
smooth.inputs["To Min"].default_value = 0.0
|
|
smooth.inputs["To Max"].default_value = 1.0
|
|
smooth.location = (x + 650.0, y + 250.0)
|
|
links.new(normalized.outputs[0], smooth.inputs["Value"])
|
|
return profile.outputs["Attribute"], smooth.outputs["Result"]
|
|
|
|
|
|
def _extrude_layers(group, geometry, location):
|
|
node = group.nodes.new("GeometryNodeExtrudeMesh")
|
|
node.mode = "FACES"
|
|
node.inputs["Individual"].default_value = False
|
|
node.inputs["Offset"].default_value = (1.0, 0.0, 0.0)
|
|
# Keep distinct front and back vertices before overriding their X positions.
|
|
node.inputs["Offset Scale"].default_value = 1.0
|
|
node.location = location
|
|
group.links.new(geometry, node.inputs["Mesh"])
|
|
return node
|
|
|
|
|
|
def _offset_geometry_x(group, geometry, x_value, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
offset = nodes.new("ShaderNodeCombineXYZ")
|
|
offset.location = location
|
|
links.new(x_value, offset.inputs["X"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 200.0, location[1])
|
|
links.new(geometry, set_position.inputs["Geometry"])
|
|
links.new(offset.outputs["Vector"], set_position.inputs["Offset"])
|
|
return set_position.outputs["Geometry"]
|
|
|
|
|
|
def _set_layer_x(group, geometry, extrude, front_x, back_x, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
choose_x = nodes.new("GeometryNodeSwitch")
|
|
choose_x.input_type = "FLOAT"
|
|
choose_x.location = (location[0], location[1] - 200.0)
|
|
links.new(extrude.outputs["Top"], choose_x.inputs["Switch"])
|
|
links.new(front_x, choose_x.inputs["False"])
|
|
links.new(back_x, choose_x.inputs["True"])
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = (location[0], location[1] + 150.0)
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (location[0] + 200.0, location[1] + 150.0)
|
|
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
|
combine = nodes.new("ShaderNodeCombineXYZ")
|
|
combine.location = (location[0] + 400.0, location[1])
|
|
links.new(choose_x.outputs["Output"], combine.inputs["X"])
|
|
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
|
links.new(separate.outputs["Z"], combine.inputs["Z"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 600.0, location[1])
|
|
links.new(extrude.outputs["Mesh"], set_position.inputs["Geometry"])
|
|
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
|
|
|
side = nodes.new("GeometryNodeSeparateGeometry")
|
|
side.domain = "FACE"
|
|
side.location = (location[0] + 850.0, location[1] + 200.0)
|
|
links.new(set_position.outputs["Geometry"], side.inputs["Geometry"])
|
|
links.new(extrude.outputs["Side"], side.inputs["Selection"])
|
|
flip_side = nodes.new("GeometryNodeFlipFaces")
|
|
flip_side.location = (location[0] + 850.0, location[1] + 400.0)
|
|
links.new(side.outputs["Selection"], flip_side.inputs["Mesh"])
|
|
front = _offset_geometry_x(
|
|
group, geometry, front_x, (location[0], location[1] - 450.0)
|
|
)
|
|
back = _offset_geometry_x(
|
|
group, geometry, back_x, (location[0], location[1] - 700.0)
|
|
)
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
flip_back.location = (location[0] + 500.0, location[1] - 700.0)
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
join.location = (location[0] + 1100.0, location[1])
|
|
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
|
|
merge.location = (location[0] + 1300.0, location[1])
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _set_segmented_layer_x(
|
|
group,
|
|
geometry,
|
|
front_x,
|
|
back_x,
|
|
thickness,
|
|
location,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
x, y = location
|
|
|
|
edge_vertices = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
|
edge_vertices.location = (x, y + 850.0)
|
|
edge_length = nodes.new("ShaderNodeVectorMath")
|
|
edge_length.operation = "DISTANCE"
|
|
edge_length.location = (x + 220.0, y + 850.0)
|
|
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")
|
|
edge_neighbors.location = (x, y + 650.0)
|
|
boundary = _math(nodes, "COMPARE", (x + 220.0, y + 650.0))
|
|
boundary.inputs[1].default_value = 1.0
|
|
boundary.inputs[2].default_value = 0.1
|
|
links.new(edge_neighbors.outputs["Face Count"], boundary.inputs[0])
|
|
statistics = nodes.new("GeometryNodeAttributeStatistic")
|
|
statistics.data_type = "FLOAT"
|
|
statistics.domain = "EDGE"
|
|
statistics.location = (x + 450.0, y + 750.0)
|
|
links.new(geometry, statistics.inputs["Geometry"])
|
|
links.new(boundary.outputs[0], statistics.inputs["Selection"])
|
|
links.new(edge_length.outputs["Value"], statistics.inputs["Attribute"])
|
|
safe_edge = _math(nodes, "MAXIMUM", (x + 680.0, y + 750.0))
|
|
safe_edge.inputs[1].default_value = 1e-6
|
|
links.new(statistics.outputs["Median"], safe_edge.inputs[0])
|
|
segment_ratio = _math(nodes, "DIVIDE", (x + 900.0, y + 750.0))
|
|
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"
|
|
segment_count.location = (x + 1120.0, y + 750.0)
|
|
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
|
|
at_least_one.location = (x + 1340.0, y + 750.0)
|
|
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
|
|
segment_limit.location = (x + 1560.0, y + 750.0)
|
|
links.new(at_least_one.outputs[0], segment_limit.inputs[0])
|
|
step = _math(nodes, "DIVIDE", (x + 1560.0, y + 500.0))
|
|
links.new(thickness, step.inputs[0])
|
|
links.new(segment_limit.outputs[0], step.inputs[1])
|
|
|
|
front = _offset_geometry_x(group, geometry, front_x, (x, y + 100.0))
|
|
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
|
repeat_output.location = (x + 2380.0, y + 100.0)
|
|
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")
|
|
repeat_input.location = (x + 1560.0, y + 100.0)
|
|
links.new(segment_limit.outputs[0], repeat_input.inputs["Iterations"])
|
|
links.new(geometry, repeat_input.inputs["Geometry"])
|
|
negative_step = _math(nodes, "MULTIPLY", (x + 1760.0, y - 100.0))
|
|
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)
|
|
extrude.location = (x + 1960.0, y + 100.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"
|
|
split_extrude.location = (x + 2160.0, y + 100.0)
|
|
links.new(extrude.outputs["Mesh"], split_extrude.inputs["Geometry"])
|
|
links.new(extrude.outputs["Side"], split_extrude.inputs["Selection"])
|
|
join_sides = nodes.new("GeometryNodeJoinGeometry")
|
|
join_sides.location = (x + 2160.0, y - 150.0)
|
|
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_x(
|
|
group,
|
|
repeat_output.outputs["Sides"],
|
|
front_x,
|
|
(x + 2450.0, y + 350.0),
|
|
)
|
|
flip_side = nodes.new("GeometryNodeFlipFaces")
|
|
flip_side.location = (x + 2850.0, y + 350.0)
|
|
links.new(positioned_side, flip_side.inputs["Mesh"])
|
|
|
|
back = _offset_geometry_x(
|
|
group, geometry, back_x, (x + 2450.0, y - 400.0)
|
|
)
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
flip_back.location = (x + 2850.0, y - 400.0)
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
join.location = (x + 3050.0, y + 50.0)
|
|
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
|
|
merge.location = (x + 3250.0, y + 50.0)
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _set_geometry_x(group, geometry, x_value, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = location
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (location[0] + 200.0, location[1])
|
|
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
|
combine = nodes.new("ShaderNodeCombineXYZ")
|
|
combine.location = (location[0] + 400.0, location[1])
|
|
links.new(x_value, combine.inputs["X"])
|
|
links.new(separate.outputs["Y"], combine.inputs["Y"])
|
|
links.new(separate.outputs["Z"], combine.inputs["Z"])
|
|
set_position = nodes.new("GeometryNodeSetPosition")
|
|
set_position.location = (location[0] + 600.0, location[1])
|
|
links.new(geometry, set_position.inputs["Geometry"])
|
|
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
|
|
return set_position.outputs["Geometry"]
|
|
|
|
|
|
def _remove_zero_thickness(
|
|
group,
|
|
zero_geometry,
|
|
shaped_geometry,
|
|
thickness,
|
|
location,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
zero = _math(nodes, "LESS_THAN", (location[0], location[1] - 200.0))
|
|
zero.inputs[1].default_value = 1e-6
|
|
links.new(thickness, zero.inputs[0])
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "GEOMETRY"
|
|
choose.location = location
|
|
links.new(zero.outputs[0], choose.inputs["Switch"])
|
|
links.new(shaped_geometry, choose.inputs["False"])
|
|
links.new(zero_geometry, choose.inputs["True"])
|
|
return choose.outputs["Output"]
|
|
|
|
|
|
def _position_field(group, depth_image, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
uv.data_type = "FLOAT_VECTOR"
|
|
uv.inputs["Name"].default_value = "UVMap"
|
|
uv.location = location
|
|
position_texture = nodes.new("GeometryNodeImageTexture")
|
|
position_texture.interpolation = "Linear"
|
|
position_texture.extension = "EXTEND"
|
|
position_texture.location = (location[0] + 220.0, location[1])
|
|
links.new(depth_image, position_texture.inputs["Image"])
|
|
links.new(uv.outputs["Attribute"], position_texture.inputs["Vector"])
|
|
return position_texture.outputs["Color"]
|
|
|
|
|
|
def _plane_depth(group, direction, reference_depth, location):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
normal = nodes.new("ShaderNodeVectorMath")
|
|
normal.operation = "NORMALIZE"
|
|
normal.location = location
|
|
links.new(direction, normal.inputs[0])
|
|
normal_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
normal_components.location = (location[0] + 220.0, location[1])
|
|
links.new(normal.outputs["Vector"], normal_components.inputs["Vector"])
|
|
position = nodes.new("GeometryNodeInputPosition")
|
|
position.location = (location[0], location[1] + 450.0)
|
|
position_components = nodes.new("ShaderNodeSeparateXYZ")
|
|
position_components.location = (location[0] + 220.0, location[1] + 450.0)
|
|
links.new(position.outputs["Position"], position_components.inputs["Vector"])
|
|
direction_x = _math(
|
|
nodes,
|
|
"MULTIPLY",
|
|
(location[0] + 440.0, location[1] + 850.0),
|
|
)
|
|
direction_x.inputs[1].default_value = -1.0
|
|
links.new(normal_components.outputs["X"], direction_x.inputs[0])
|
|
y_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 500.0))
|
|
links.new(position_components.outputs["Y"], y_term.inputs[0])
|
|
links.new(direction_x.outputs[0], y_term.inputs[1])
|
|
z_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 250.0))
|
|
links.new(position_components.outputs["Z"], z_term.inputs[0])
|
|
links.new(normal_components.outputs["Y"], z_term.inputs[1])
|
|
point_dot = _math(nodes, "ADD", (location[0] + 660.0, location[1] + 400.0))
|
|
links.new(y_term.outputs[0], point_dot.inputs[0])
|
|
links.new(z_term.outputs[0], point_dot.inputs[1])
|
|
local_x = _math(nodes, "MULTIPLY", (location[0] + 660.0, location[1]))
|
|
local_x.inputs[1].default_value = -1.0
|
|
links.new(normal_components.outputs["Z"], local_x.inputs[0])
|
|
divide = _math(nodes, "DIVIDE", (location[0] + 880.0, location[1] + 200.0))
|
|
links.new(point_dot.outputs[0], divide.inputs[0])
|
|
links.new(local_x.outputs[0], divide.inputs[1])
|
|
plane = _math(nodes, "SUBTRACT", (location[0] + 1100.0, location[1] + 200.0))
|
|
links.new(reference_depth, plane.inputs[0])
|
|
links.new(divide.outputs[0], plane.inputs[1])
|
|
return plane.outputs[0]
|
|
|
|
|
|
def _shade_output(group, geometry, location):
|
|
shade = group.nodes.new("GeometryNodeSetShadeSmooth")
|
|
shade.domain = "FACE"
|
|
shade.inputs["Shade Smooth"].default_value = True
|
|
shade.location = location
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
output.location = (location[0] + 250.0, location[1])
|
|
group.links.new(geometry, shade.inputs["Geometry"])
|
|
group.links.new(shade.outputs["Geometry"], output.inputs["Geometry"])
|
|
|
|
|
|
def _build_cutout(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
half = _math(nodes, "MULTIPLY", (-200.0, 50.0))
|
|
half.inputs[1].default_value = 0.5
|
|
links.new(thickness, half.inputs[0])
|
|
front = _math(nodes, "MULTIPLY", (20.0, 0.0))
|
|
front.inputs[1].default_value = -1.0
|
|
links.new(half.outputs[0], front.inputs[0])
|
|
shaped = _set_segmented_layer_x(
|
|
group,
|
|
geometry,
|
|
front.outputs[0],
|
|
half.outputs[0],
|
|
thickness,
|
|
(250.0, 300.0),
|
|
)
|
|
return _remove_zero_thickness(
|
|
group, geometry, shaped, thickness, (3750.0, 300.0)
|
|
)
|
|
|
|
|
|
def _build_balloon(group, geometry, thickness):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
profile, _profile_factor = _profile_field(
|
|
group, geometry, (-200.0, 0.0)
|
|
)
|
|
front = _math(nodes, "MULTIPLY", (2400.0, 500.0))
|
|
front.inputs[1].default_value = -1.0
|
|
links.new(profile, front.inputs[0])
|
|
front_scale = _math(nodes, "MULTIPLY", (2600.0, 500.0))
|
|
links.new(front.outputs[0], front_scale.inputs[0])
|
|
links.new(thickness, front_scale.inputs[1])
|
|
back = _math(nodes, "MULTIPLY", (2600.0, 300.0))
|
|
links.new(profile, back.inputs[0])
|
|
links.new(thickness, back.inputs[1])
|
|
extrude = _extrude_layers(group, geometry, (2400.0, 750.0))
|
|
shaped = _set_layer_x(
|
|
group,
|
|
geometry,
|
|
extrude,
|
|
front_scale.outputs[0],
|
|
back.outputs[0],
|
|
(2850.0, 650.0),
|
|
)
|
|
return _remove_zero_thickness(
|
|
group, geometry, shaped, thickness, (4400.0, 650.0)
|
|
)
|
|
|
|
|
|
def _build_depth_balloon(
|
|
group,
|
|
geometry,
|
|
thickness,
|
|
depth_scale,
|
|
double_sided,
|
|
uniform_scale,
|
|
depth_direction,
|
|
depth_image,
|
|
depth_reference,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
profile, profile_factor = _profile_field(
|
|
group, geometry, (-200.0, 250.0)
|
|
)
|
|
camera_position = _position_field(group, depth_image, (200.0, -250.0))
|
|
separate_camera = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate_camera.location = (650.0, -250.0)
|
|
links.new(camera_position, separate_camera.inputs["Vector"])
|
|
metric_depth = _math(nodes, "MULTIPLY", (900.0, -250.0))
|
|
links.new(separate_camera.outputs["Z"], metric_depth.inputs[0])
|
|
links.new(uniform_scale, metric_depth.inputs[1])
|
|
plane_depth = _plane_depth(
|
|
group,
|
|
depth_direction,
|
|
depth_reference,
|
|
(600.0, -850.0),
|
|
)
|
|
corrected_depth = _math(nodes, "SUBTRACT", (1750.0, -250.0))
|
|
links.new(plane_depth, corrected_depth.inputs[0])
|
|
links.new(metric_depth.outputs[0], corrected_depth.inputs[1])
|
|
nonnegative_depth = _math(nodes, "MAXIMUM", (1950.0, -250.0))
|
|
nonnegative_depth.inputs[1].default_value = 0.0
|
|
links.new(corrected_depth.outputs[0], nonnegative_depth.inputs[0])
|
|
scaled_depth = _math(nodes, "MULTIPLY", (2150.0, -250.0))
|
|
links.new(nonnegative_depth.outputs[0], scaled_depth.inputs[0])
|
|
links.new(depth_scale, scaled_depth.inputs[1])
|
|
front_depth = _math(nodes, "MULTIPLY", (2350.0, -250.0))
|
|
front_depth.inputs[1].default_value = -1.0
|
|
links.new(scaled_depth.outputs[0], front_depth.inputs[0])
|
|
base_front = _math(nodes, "MULTIPLY", (2400.0, 500.0))
|
|
links.new(profile, base_front.inputs[0])
|
|
links.new(thickness, base_front.inputs[1])
|
|
negate_front = _math(nodes, "MULTIPLY", (2600.0, 500.0))
|
|
negate_front.inputs[1].default_value = -1.0
|
|
links.new(base_front.outputs[0], negate_front.inputs[0])
|
|
base_back = _math(nodes, "MULTIPLY", (2600.0, 300.0))
|
|
links.new(profile, base_back.inputs[0])
|
|
links.new(thickness, base_back.inputs[1])
|
|
front_mix = nodes.new("ShaderNodeMix")
|
|
front_mix.data_type = "FLOAT"
|
|
front_mix.clamp_factor = True
|
|
front_mix.location = (2600.0, 50.0)
|
|
links.new(profile_factor, front_mix.inputs["Factor"])
|
|
links.new(negate_front.outputs[0], front_mix.inputs["A"])
|
|
links.new(front_depth.outputs[0], front_mix.inputs["B"])
|
|
mirrored_front = _math(nodes, "MULTIPLY", (2850.0, -150.0))
|
|
mirrored_front.inputs[1].default_value = -1.0
|
|
links.new(front_mix.outputs["Result"], mirrored_front.inputs[0])
|
|
back_choice = nodes.new("GeometryNodeSwitch")
|
|
back_choice.input_type = "FLOAT"
|
|
back_choice.location = (3050.0, 100.0)
|
|
links.new(double_sided, back_choice.inputs["Switch"])
|
|
links.new(base_back.outputs[0], back_choice.inputs["False"])
|
|
links.new(mirrored_front.outputs[0], back_choice.inputs["True"])
|
|
extrude = _extrude_layers(group, geometry, (2400.0, 750.0))
|
|
shaped = _set_layer_x(
|
|
group,
|
|
geometry,
|
|
extrude,
|
|
front_mix.outputs["Result"],
|
|
back_choice.outputs["Output"],
|
|
(3300.0, 650.0),
|
|
)
|
|
front_only = _set_geometry_x(
|
|
group, geometry, front_mix.outputs["Result"], (3500.0, 100.0)
|
|
)
|
|
return _remove_zero_thickness(
|
|
group, front_only, shaped, thickness, (4850.0, 600.0)
|
|
)
|
|
|
|
|
|
def _build_depth_surface(
|
|
group,
|
|
geometry,
|
|
thickness,
|
|
depth_scale,
|
|
uniform_scale,
|
|
reference_depth,
|
|
depth_image,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
camera_position = _position_field(group, depth_image, (-200.0, -200.0))
|
|
scaled = nodes.new("ShaderNodeVectorMath")
|
|
scaled.operation = "SCALE"
|
|
scaled.location = (3100.0, -100.0)
|
|
links.new(camera_position, scaled.inputs[0])
|
|
links.new(uniform_scale, scaled.inputs[3])
|
|
separate = nodes.new("ShaderNodeSeparateXYZ")
|
|
separate.location = (3300.0, -100.0)
|
|
links.new(scaled.outputs["Vector"], separate.inputs["Vector"])
|
|
relative_depth = _math(nodes, "SUBTRACT", (3500.0, -100.0))
|
|
links.new(separate.outputs["Z"], relative_depth.inputs[0])
|
|
links.new(reference_depth, relative_depth.inputs[1])
|
|
front = _math(nodes, "MULTIPLY", (3700.0, -100.0))
|
|
links.new(relative_depth.outputs[0], front.inputs[0])
|
|
links.new(depth_scale, front.inputs[1])
|
|
|
|
negative_x = _math(nodes, "MULTIPLY", (3500.0, -350.0))
|
|
negative_x.inputs[1].default_value = -1.0
|
|
links.new(separate.outputs["X"], negative_x.inputs[0])
|
|
negative_y = _math(nodes, "MULTIPLY", (3500.0, -600.0))
|
|
negative_y.inputs[1].default_value = -1.0
|
|
links.new(separate.outputs["Y"], negative_y.inputs[0])
|
|
projected = nodes.new("ShaderNodeCombineXYZ")
|
|
projected.location = (3700.0, -450.0)
|
|
links.new(negative_x.outputs[0], projected.inputs["Y"])
|
|
links.new(negative_y.outputs[0], projected.inputs["Z"])
|
|
current_position = nodes.new("GeometryNodeInputPosition")
|
|
current_position.location = (3500.0, -850.0)
|
|
blend = nodes.new("ShaderNodeMix")
|
|
blend.data_type = "VECTOR"
|
|
blend.clamp_factor = False
|
|
blend.location = (3900.0, -450.0)
|
|
links.new(depth_scale, blend.inputs[0])
|
|
links.new(current_position.outputs["Position"], blend.inputs[4])
|
|
links.new(projected.outputs["Vector"], blend.inputs[5])
|
|
project_geometry = nodes.new("GeometryNodeSetPosition")
|
|
project_geometry.location = (4100.0, 400.0)
|
|
links.new(geometry, project_geometry.inputs["Geometry"])
|
|
links.new(blend.outputs["Result"], project_geometry.inputs["Position"])
|
|
|
|
back = _math(nodes, "ADD", (3900.0, -100.0))
|
|
links.new(front.outputs[0], back.inputs[0])
|
|
links.new(thickness, back.inputs[1])
|
|
shaped = _set_segmented_layer_x(
|
|
group,
|
|
project_geometry.outputs["Geometry"],
|
|
front.outputs[0],
|
|
back.outputs[0],
|
|
thickness,
|
|
(4400.0, 400.0),
|
|
)
|
|
front_only = _set_geometry_x(
|
|
group,
|
|
project_geometry.outputs["Geometry"],
|
|
front.outputs[0],
|
|
(4600.0, -200.0),
|
|
)
|
|
geometry = _remove_zero_thickness(
|
|
group, front_only, shaped, thickness, (7900.0, 400.0)
|
|
)
|
|
return geometry
|
|
|
|
|
|
def build_node_group(shape):
|
|
group_name = CUTOUT_NODE_GROUP_NAMES[shape]
|
|
group = bpy.data.node_groups.get(group_name)
|
|
if group is not None:
|
|
return group
|
|
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
|
|
group.is_modifier = False
|
|
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
if shape in {"BALLOON", "DEPTH_BALLOON"}:
|
|
thickness = _float_input(group, "Thickness", 1.0)
|
|
thickness.description = (
|
|
"Multiplier for the injected Poisson Balloon profile; zero keeps "
|
|
"the source sheet"
|
|
)
|
|
else:
|
|
thickness = _float_input(
|
|
group, "Thickness", 0.0, 0.0, None, subtype="DISTANCE"
|
|
)
|
|
thickness.description = "Final thickness in scene distance"
|
|
|
|
depth_image = None
|
|
if shape == "DEPTH_BALLOON":
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0)
|
|
depth_scale.description = "Scale the fitted-plane-corrected metric depth"
|
|
double_sided = _bool_input(group, "Double Sided", False)
|
|
double_sided.description = (
|
|
"Mirror the complete front surface across the local YZ plane"
|
|
)
|
|
elif shape == "DEPTH_SURFACE":
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0)
|
|
|
|
if shape == "DEPTH_BALLOON":
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Base Plane Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = (
|
|
"Shared upper-percentile metric depth for outward placement"
|
|
)
|
|
depth_direction = _vector_input(
|
|
group,
|
|
"Depth Direction",
|
|
(0.0, 0.0, 1.0),
|
|
subtype="DIRECTION",
|
|
parent=options,
|
|
)
|
|
depth_direction.description = (
|
|
"Normal direction of the fitted metric depth plane"
|
|
)
|
|
elif shape == "DEPTH_SURFACE":
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Base Plane Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = (
|
|
"Scaled camera depth placed on the original image sheet"
|
|
)
|
|
|
|
if shape in {"DEPTH_BALLOON", "DEPTH_SURFACE"}:
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
uniform_scale = _float_input(
|
|
group, "Uniform Scale", 1.0, 0.0, parent=data
|
|
)
|
|
uniform_scale.description = (
|
|
"Internal uniform conversion from MoGe meters to local distance"
|
|
)
|
|
uniform_scale.hide_in_modifier = True
|
|
depth_image = _socket(
|
|
group, "Depth Image", "INPUT", "NodeSocketImage", data
|
|
)
|
|
depth_image.description = f"Internally generated {CUTOUT_SHAPE_LABELS[shape]} data"
|
|
depth_image.hide_in_modifier = True
|
|
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
|
|
controls = _group_input(
|
|
group.nodes,
|
|
(-1600.0, 500.0),
|
|
{
|
|
"Geometry",
|
|
"Thickness",
|
|
"Double Sided",
|
|
"Depth Image",
|
|
"Depth Scale",
|
|
"Uniform Scale",
|
|
"Base Plane Depth",
|
|
"Depth Direction",
|
|
},
|
|
)
|
|
geometry = controls.outputs["Geometry"]
|
|
if shape == "CUTOUT":
|
|
geometry = _build_cutout(group, geometry, controls.outputs["Thickness"])
|
|
output_location = (4050.0, 300.0)
|
|
elif shape == "BALLOON":
|
|
geometry = _build_balloon(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
)
|
|
output_location = (4650.0, 650.0)
|
|
elif shape == "DEPTH_BALLOON":
|
|
geometry = _build_depth_balloon(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
controls.outputs["Depth Scale"],
|
|
controls.outputs["Double Sided"],
|
|
controls.outputs["Uniform Scale"],
|
|
controls.outputs["Depth Direction"],
|
|
controls.outputs["Depth Image"],
|
|
controls.outputs["Base Plane Depth"],
|
|
)
|
|
output_location = (5100.0, 600.0)
|
|
else:
|
|
geometry = _build_depth_surface(
|
|
group,
|
|
geometry,
|
|
controls.outputs["Thickness"],
|
|
controls.outputs["Depth Scale"],
|
|
controls.outputs["Uniform Scale"],
|
|
controls.outputs["Base Plane Depth"],
|
|
controls.outputs["Depth Image"],
|
|
)
|
|
output_location = (8200.0, 400.0)
|
|
_localize_group_inputs(group, controls)
|
|
_shade_output(group, geometry, output_location)
|
|
group.color_tag = "GEOMETRY"
|
|
group.description = {
|
|
"CUTOUT": "Symmetrically thicken a Surface sheet.",
|
|
"BALLOON": "Inflate a Surface sheet from its boundary profile.",
|
|
"DEPTH_BALLOON": (
|
|
"Mix Balloon profile and metric image depth around a fitted plane."
|
|
),
|
|
"DEPTH_SURFACE": (
|
|
"Project and thicken a Surface sheet from hidden camera points."
|
|
),
|
|
}[shape]
|
|
group.use_fake_user = True
|
|
return group
|
|
|
|
|
|
def build_cutout_modifier_group(shape_groups):
|
|
group = bpy.data.node_groups.get(CUTOUT_NODE_GROUP_NAME)
|
|
if group is not None:
|
|
return group
|
|
group = bpy.data.node_groups.new(CUTOUT_NODE_GROUP_NAME, "GeometryNodeTree")
|
|
group.is_modifier = True
|
|
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
|
_socket(group, "Shape", "INPUT", "NodeSocketMenu")
|
|
|
|
thickness = _float_input(group, "Thickness", 0.0, 0.0)
|
|
thickness.description = (
|
|
"Scene thickness for Surface shapes or Balloon profile multiplier"
|
|
)
|
|
depth_scale = _float_input(group, "Depth Scale", 1.0, 0.0)
|
|
depth_scale.description = "Scale metric image depth"
|
|
double_sided = _bool_input(group, "Double Sided", False)
|
|
double_sided.description = (
|
|
"Mirror the complete Depth Balloon front surface across the local YZ plane"
|
|
)
|
|
|
|
iterations = _socket(
|
|
group, "Smooth Iterations", "INPUT", "NodeSocketInt"
|
|
)
|
|
iterations.default_value = 0
|
|
iterations.min_value = 0
|
|
iterations.max_value = 20
|
|
iterations.description = "Number of smoothing passes"
|
|
options = group.interface.new_panel(name="Options", default_closed=True)
|
|
cleanup = _float_input(
|
|
group,
|
|
"Cleanup",
|
|
0.0,
|
|
0.0,
|
|
None,
|
|
parent=options,
|
|
)
|
|
cleanup.description = (
|
|
"Delete mesh islands below this absolute local-area threshold"
|
|
)
|
|
smooth = _float_input(
|
|
group,
|
|
"Smooth",
|
|
1.0,
|
|
0.0,
|
|
1.0,
|
|
subtype="FACTOR",
|
|
parent=options,
|
|
)
|
|
smooth.description = "Blend each smoothing pass into the selected shape"
|
|
|
|
reference_depth = _float_input(
|
|
group,
|
|
"Base Plane Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = "Depth of the flat base plane for both Depth shapes"
|
|
depth_direction = _vector_input(
|
|
group,
|
|
"Depth Direction",
|
|
(0.0, 0.0, 1.0),
|
|
subtype="DIRECTION",
|
|
parent=options,
|
|
)
|
|
depth_direction.description = "Normal direction of the fitted metric depth plane"
|
|
|
|
|
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
|
uniform_scale = _float_input(
|
|
group, "Uniform Scale", 1.0, 0.0, parent=data
|
|
)
|
|
uniform_scale.description = (
|
|
"Internal uniform conversion from MoGe meters to local distance"
|
|
)
|
|
uniform_scale.hide_in_modifier = True
|
|
depth_image = _socket(
|
|
group, "Depth Image", "INPUT", "NodeSocketImage", data
|
|
)
|
|
depth_image.description = "Internally generated Cutout depth data"
|
|
depth_image.hide_in_modifier = True
|
|
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
|
|
|
base_controls = _group_input(
|
|
group.nodes,
|
|
(-2700.0, 500.0),
|
|
{"Geometry", "Cleanup"},
|
|
)
|
|
cleanup_area = _math(group.nodes, "MULTIPLY", (-2500.0, 750.0))
|
|
cleanup_area.inputs[1].default_value = 0.01
|
|
group.links.new(base_controls.outputs["Cleanup"], cleanup_area.inputs[0])
|
|
geometry = _island_cleanup(
|
|
group,
|
|
base_controls.outputs["Geometry"],
|
|
cleanup_area.outputs[0],
|
|
(-2350.0, 250.0),
|
|
)
|
|
|
|
branch_y = {
|
|
"CUTOUT": 1150.0,
|
|
"BALLOON": 550.0,
|
|
"DEPTH_BALLOON": -50.0,
|
|
"DEPTH_SURFACE": -650.0,
|
|
}
|
|
branch_nodes = {}
|
|
for shape, y in branch_y.items():
|
|
node = group.nodes.new("GeometryNodeGroup")
|
|
node.node_tree = shape_groups[shape]
|
|
node.location = (-850.0, y)
|
|
node.width = 220.0
|
|
group.links.new(geometry, node.inputs["Geometry"])
|
|
visible = {
|
|
socket.name
|
|
for socket in node.inputs
|
|
if socket.name and socket.name != "Geometry"
|
|
}
|
|
controls = _group_input(group.nodes, (-1200.0, y + 120.0), visible)
|
|
for socket in node.inputs:
|
|
if not socket.name or socket.name == "Geometry":
|
|
continue
|
|
group.links.new(controls.outputs[socket.name], socket)
|
|
branch_nodes[shape] = node
|
|
|
|
menu_switch = group.nodes.new("GeometryNodeMenuSwitch")
|
|
menu_switch.data_type = "GEOMETRY"
|
|
shapes = list(CUTOUT_NODE_GROUP_NAMES)
|
|
for item, shape in zip(menu_switch.enum_items, shapes[:2]):
|
|
item.name = CUTOUT_SHAPE_LABELS[shape]
|
|
for shape in shapes[2:]:
|
|
menu_switch.enum_items.new(CUTOUT_SHAPE_LABELS[shape])
|
|
menu_switch.location = (-350.0, 250.0)
|
|
|
|
shape_controls = _group_input(
|
|
group.nodes,
|
|
(-700.0, -1200.0),
|
|
{"Shape"},
|
|
)
|
|
group.links.new(shape_controls.outputs["Shape"], menu_switch.inputs["Menu"])
|
|
for shape, node in branch_nodes.items():
|
|
group.links.new(
|
|
node.outputs["Geometry"],
|
|
menu_switch.inputs[CUTOUT_SHAPE_LABELS[shape]],
|
|
)
|
|
|
|
geometry = expand_smooth(
|
|
group,
|
|
menu_switch.outputs["Output"],
|
|
iterations_name="Smooth Iterations",
|
|
)
|
|
_shade_output(group, geometry, (3350.0, 550.0))
|
|
group.color_tag = "GEOMETRY"
|
|
group.description = (
|
|
"Build Surface, Balloon, Depth Balloon, or Depth Surface from one "
|
|
"menu-controlled Cutout modifier."
|
|
)
|
|
group.use_fake_user = True
|
|
return group
|