- Implemented `build_mesh_plane.py` to create a geometry node group for mesh planes with customizable parameters. - Created `build_shadeless.py` for a shader node group that displays color without scene lighting while preserving shadows and reflections. - Added `validate.py` to validate the integrity of the newly created node groups and their inputs. - Introduced `validate_cutout.py` to ensure the functionality of cutout node groups and their dependencies. - Developed `validate_mesh_plane.py` to test the mesh plane creation and its modifier settings.
1005 lines
38 KiB
Python
1005 lines
38 KiB
Python
"""Build the four Cutout Geometry Nodes groups stored in O_AnyImage.blend."""
|
|
|
|
import math
|
|
|
|
import bpy
|
|
|
|
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,
|
|
"Reference 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,
|
|
"Reference 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",
|
|
"Reference 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["Reference 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["Reference 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, expand_smooth):
|
|
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,
|
|
"Reference Depth",
|
|
1.0,
|
|
0.0,
|
|
None,
|
|
subtype="DISTANCE",
|
|
parent=options,
|
|
)
|
|
reference_depth.description = (
|
|
"Metric reference depth shared by 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"},
|
|
)
|
|
geometry = _island_cleanup(
|
|
group,
|
|
base_controls.outputs["Geometry"],
|
|
base_controls.outputs["Cleanup"],
|
|
(-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
|