Add cutout shell and volume node assets for depth volume processing
- Implemented `cutout_shell.py` to build front normals and shell positions for balloon geometry. - Created `cutout_volume.py` to establish smooth center fields and manage balloon surface attributes. - Introduced `pisa_sr_export.py` for exporting PiSA-SR components with dependencies and asset finalization.
This commit is contained in:
252
tools/node_assets/build_cutout_modifiers.py
Normal file
252
tools/node_assets/build_cutout_modifiers.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""Build the three directly editable Cutout modifier assets."""
|
||||
import bpy
|
||||
from types import SimpleNamespace
|
||||
from mathutils import Matrix
|
||||
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_NODE_GROUP_NAMES, CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from cutout_shapes import (
|
||||
_socket, _float_input, _bool_input, _vector_input, _group_input, _math,
|
||||
_island_cleanup, _build_cutout, _build_balloon, _build_depth_balloon,
|
||||
_build_depth_surface, _shade_output,
|
||||
)
|
||||
from cutout_shape_layouts import LAYOUTS
|
||||
from cutout_depth_surface_layout import POSITIONS, INPUT_REGIONS, layout_geometry_references
|
||||
from cutout_layout import layout_stage, finish_layout
|
||||
from cutout_volume import build_volume_fields, shift_stage
|
||||
from cutout_rounding import build_volume_rounding
|
||||
from cutout_inflation import build_front_fields
|
||||
from cutout_shell import build_front_normals, build_shell_position
|
||||
from node_graph import expand_smooth
|
||||
|
||||
|
||||
def _menu(group, control, names, kind, location):
|
||||
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:
|
||||
selector = group.nodes.new("GeometryNodeMenuSwitch")
|
||||
selector.data_type = "BOOLEAN"
|
||||
selector.location = (location[0] - 240, location[1] + 450)
|
||||
for item, name in zip(selector.enum_items, names):
|
||||
item.name = name
|
||||
selector.inputs[names[0]].default_value = False
|
||||
selector.inputs[names[1]].default_value = True
|
||||
local = _group_input(group.nodes, (location[0] - 480, location[1] + 450), {control})
|
||||
group.links.new(local.outputs[control], selector.inputs["Menu"])
|
||||
node = group.nodes.new("GeometryNodeSwitch")
|
||||
node.input_type = kind
|
||||
node.location = location
|
||||
group.links.new(selector.outputs[0], node.inputs["Switch"])
|
||||
return SimpleNamespace(inputs={names[0]: node.inputs["False"], names[1]: node.inputs["True"]}, outputs=node.outputs)
|
||||
|
||||
|
||||
def _read(group, name, location):
|
||||
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
node.data_type = "FLOAT"
|
||||
node.inputs["Name"].default_value = name
|
||||
node.location = location
|
||||
return node.outputs["Attribute"]
|
||||
|
||||
|
||||
def _store(group, geometry, name, value, location, domain="POINT"):
|
||||
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
node.domain, node.data_type = domain, "FLOAT"
|
||||
node.location = location
|
||||
node.inputs["Name"].default_value = name
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
if isinstance(value, (int, float)):
|
||||
node.inputs["Value"].default_value = value
|
||||
else:
|
||||
group.links.new(value, node.inputs["Value"])
|
||||
return node.outputs["Geometry"]
|
||||
|
||||
|
||||
def _depth_volume(group, stage):
|
||||
"""Insert the volume field ahead of identity-based shell construction."""
|
||||
links = group.links
|
||||
projection = stage["Set Position"].outputs["Geometry"]
|
||||
shell_input = stage["Store Named Attribute.005"].inputs["Geometry"]
|
||||
for node in group.nodes:
|
||||
if node.location.x >= 4150:
|
||||
node.location.x += 5100
|
||||
projection = build_front_normals(group, projection)
|
||||
volume = build_volume_fields(group, projection, (4150, 0))
|
||||
shared = build_front_fields(group, volume.node.inputs["Geometry"].links[0].from_socket)
|
||||
links.new(shared, volume.node.inputs["Geometry"])
|
||||
uniform_control = _group_input(group.nodes, (8210, -330), {"Uniform Thickness"})
|
||||
uniform = _store(group, shared, "o_balloon_surface_thickness", uniform_control.outputs["Uniform Thickness"], (8490, -330))
|
||||
volume.node.location.x = uniform.node.location.x
|
||||
choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (9000, 0))
|
||||
links.new(volume, choose.inputs["Balloon"])
|
||||
links.new(uniform, choose.inputs["Uniform"])
|
||||
links.new(choose.outputs[0], shell_input)
|
||||
thickness = _read(group, "o_balloon_surface_thickness", (12000, -1100))
|
||||
links.new(thickness, stage["Math.018"].inputs[1])
|
||||
control = _group_input(group.nodes, (13300, 700), {"Thickness", "Uniform Thickness"})
|
||||
amount = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (13600, 500))
|
||||
links.new(control.outputs["Thickness"], amount.inputs["Balloon"])
|
||||
links.new(control.outputs["Uniform Thickness"], amount.inputs["Uniform"])
|
||||
links.new(amount.outputs[0], stage["Math.019"].inputs[0])
|
||||
identity_thickness = _read(group, "o_balloon_surface_thickness", (11100, 1500))
|
||||
positive = _math(group.nodes, "GREATER_THAN", (11340, 1500))
|
||||
positive.inputs[1].default_value = 1e-12
|
||||
links.new(identity_thickness, positive.inputs[0])
|
||||
layer = _math(group.nodes, "MULTIPLY", (11570, 1200))
|
||||
links.new(positive.outputs[0], layer.inputs[0])
|
||||
links.new(stage["Separate XYZ.006"].outputs["Z"], layer.inputs[1])
|
||||
links.new(layer.outputs[0], stage["Combine XYZ.002"].inputs["Z"])
|
||||
for node in group.nodes:
|
||||
if node.location.x >= 12070:
|
||||
node.location.x += 400
|
||||
for name in ("Flip Faces", "Flip Faces.001"):
|
||||
source = stage[name].outputs["Mesh"]
|
||||
outgoing = [link for link in links if link.from_socket == source]
|
||||
targets = [link.to_socket for link in outgoing]
|
||||
for link in outgoing:
|
||||
links.remove(link)
|
||||
masked = _store(group, source, NORMAL_SCALE_ATTRIBUTE_NAME, 0.0, (12100, stage[name].location.y), domain="FACE")
|
||||
for target in targets:
|
||||
links.new(masked, target)
|
||||
|
||||
for node in group.nodes:
|
||||
if node.location.x >= 8490:
|
||||
node.location.x += 3200
|
||||
volume.node.location.x = 8290
|
||||
rounded = build_volume_rounding(group, volume, (8600, 0))
|
||||
rounded.node.location.x = uniform.node.location.x + 40
|
||||
links.new(rounded, choose.inputs["Balloon"])
|
||||
restored = stage["Vector Math.004"].outputs[0]
|
||||
targets = [link.to_socket for link in restored.links]
|
||||
uniform_mode = choose.outputs[0].node.inputs["Switch"].links[0].from_socket
|
||||
oriented = build_shell_position(group, restored, uniform_mode, (14100, -2100))
|
||||
for target in targets:
|
||||
links.new(oriented, target)
|
||||
|
||||
|
||||
def build_cutout_group(shape):
|
||||
name = CUTOUT_NODE_GROUP_NAMES[shape]
|
||||
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"
|
||||
group["anyimage_asset_revision"] = 11
|
||||
depth = shape in {"DEPTH_SOLID", "DEPTH_BALLOON"}
|
||||
dual = shape != "DEPTH_BALLOON"
|
||||
_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
if dual:
|
||||
_socket(group, "Mode", "INPUT", "NodeSocketMenu")
|
||||
_float_input(group, "Thickness", 1.0).description = "Balloon profile multiplier; zero returns the sheet"
|
||||
if dual:
|
||||
_float_input(group, "Uniform Thickness", 0.0, subtype="DISTANCE").description = "Distance used only by Uniform thickness mode"
|
||||
if depth:
|
||||
_float_input(group, "Depth Scale", 1.0)
|
||||
if shape == "DEPTH_BALLOON":
|
||||
_bool_input(group, "Double Sided", True)
|
||||
iterations = _socket(group, "Smooth", "INPUT", "NodeSocketInt")
|
||||
iterations.default_value = 0
|
||||
iterations.min_value, iterations.max_value = 0, 20
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
_float_input(group, "Cleanup Threshold", 0.0, parent=options)
|
||||
if shape == "DEPTH_SOLID":
|
||||
_float_input(group, "Split Threshold", 0.0, maximum=1, subtype="FACTOR", parent=options)
|
||||
if depth:
|
||||
_float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
||||
if shape == "DEPTH_SOLID":
|
||||
_float_input(group, "Edge Round", 0.2, maximum=1, subtype="FACTOR", parent=options)
|
||||
_float_input(group, "Front Inflation", 1.0, maximum=32, parent=options)
|
||||
normal_iterations = _socket(group, "Normal Smooth", "INPUT", "NodeSocketInt", options)
|
||||
normal_iterations.default_value = 50
|
||||
normal_iterations.min_value, normal_iterations.max_value = 0, 50
|
||||
_float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options)
|
||||
if shape == "DEPTH_BALLOON":
|
||||
_vector_input(group, "Depth Direction", (0, 0, 1), subtype="DIRECTION", parent=options)
|
||||
data = group.interface.new_panel(name="Data", default_closed=True)
|
||||
_socket(group, "Inward Axis", "INPUT", "NodeSocketMenu", data).hide_in_modifier = True
|
||||
if depth:
|
||||
_float_input(group, "Uniform Scale", 1.0, parent=data).hide_in_modifier = True
|
||||
_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
|
||||
_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
|
||||
controls = _group_input(group.nodes, (-5000, 0), set())
|
||||
previous = set(group.nodes)
|
||||
if shape == "DEPTH_SOLID":
|
||||
geometry = _build_depth_surface(group, controls.outputs["Geometry"], controls.outputs["Thickness"], controls.outputs["Depth Scale"], controls.outputs["Split Threshold"], controls.outputs["Uniform Scale"], controls.outputs["Reference Depth"], controls.outputs["Depth Image"])
|
||||
stage = layout_stage(group, previous, POSITIONS, INPUT_REGIONS)
|
||||
_depth_volume(group, stage)
|
||||
end = 18000
|
||||
elif shape == "DEPTH_BALLOON":
|
||||
geometry = _build_depth_balloon(group, controls.outputs["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"])
|
||||
layout_stage(group, previous, *LAYOUTS["DEPTH_BALLOON"])
|
||||
end = 3000
|
||||
else:
|
||||
balloon = _build_balloon(group, controls.outputs["Geometry"], controls.outputs["Thickness"])
|
||||
layout_stage(group, previous, *LAYOUTS["BALLOON"])
|
||||
previous = set(group.nodes)
|
||||
uniform = _build_cutout(group, controls.outputs["Geometry"], controls.outputs["Uniform Thickness"])
|
||||
layout_stage(group, previous, *LAYOUTS["UNIFORM"], offset=(0, -2400))
|
||||
balloon.node.location.x = uniform.node.location.x
|
||||
choose = _menu(group, "Mode", ("Balloon", "Uniform"), "GEOMETRY", (3200, 0))
|
||||
group.links.new(balloon, choose.inputs["Balloon"])
|
||||
group.links.new(uniform, choose.inputs["Uniform"])
|
||||
geometry, end = choose.outputs[0], 3700
|
||||
planar_inputs = [link.to_socket for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
|
||||
base = _group_input(group.nodes, (-4800, 0), {"Geometry", "Cleanup Threshold"})
|
||||
area = _math(group.nodes, "MULTIPLY", (-4520, 500))
|
||||
area.inputs[1].default_value = 0.01
|
||||
group.links.new(base.outputs["Cleanup Threshold"], area.inputs[0])
|
||||
cleaned = _island_cleanup(group, base.outputs["Geometry"], area.outputs[0], (-4520, 0))
|
||||
if shape == "DEPTH_SOLID":
|
||||
cleaned = _store(group, cleaned, NORMAL_SCALE_ATTRIBUTE_NAME, 1.0, (-3540, 0), domain="FACE")
|
||||
for target in planar_inputs:
|
||||
group.links.new(cleaned, target)
|
||||
previous = set(group.nodes)
|
||||
geometry = expand_smooth(group, geometry, iterations_name="Smooth", weight_name="Smooth Weight")
|
||||
shift_stage(group, previous, (end, -550))
|
||||
end += 3450
|
||||
if shape == "DEPTH_SOLID":
|
||||
remove = group.nodes.new("GeometryNodeRemoveAttribute")
|
||||
remove.pattern_mode = "WILDCARD"
|
||||
remove.inputs["Name"].default_value = "o_balloon_surface_*"
|
||||
remove.location = (end, 0)
|
||||
group.links.new(geometry, remove.inputs["Geometry"])
|
||||
geometry, end = remove.outputs[0], end + 300
|
||||
else:
|
||||
profile = _read(group, "o_balloon", (end, 800))
|
||||
scale = profile
|
||||
if dual:
|
||||
amount = _group_input(group.nodes, (end, 1100), {"Thickness"})
|
||||
enabled = _math(group.nodes, "GREATER_THAN", (end + 250, 1100))
|
||||
enabled.inputs[1].default_value = 1e-6
|
||||
group.links.new(amount.outputs["Thickness"], enabled.inputs[0])
|
||||
inflated = group.nodes.new("GeometryNodeSwitch")
|
||||
inflated.input_type = "FLOAT"
|
||||
inflated.location = (end + 480, 800)
|
||||
inflated.inputs["False"].default_value = 1
|
||||
group.links.new(enabled.outputs[0], inflated.inputs["Switch"])
|
||||
group.links.new(profile, inflated.inputs["True"])
|
||||
mode = _menu(group, "Mode", ("Balloon", "Uniform"), "FLOAT", (end + 750, 800))
|
||||
mode.inputs["Uniform"].default_value = 1
|
||||
group.links.new(inflated.outputs[0], mode.inputs["Balloon"])
|
||||
scale = mode.outputs[0]
|
||||
geometry = _store(group, geometry, NORMAL_SCALE_ATTRIBUTE_NAME, scale, (end + 1050, 0))
|
||||
end += 1350
|
||||
orient = group.nodes.new("GeometryNodeTransform")
|
||||
orient.location = (end, -350)
|
||||
orient.inputs["Rotation"].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
|
||||
group.links.new(geometry, orient.inputs["Geometry"])
|
||||
axis = _menu(group, "Inward Axis", ("-Z", "+X"), "GEOMETRY", (end + 300, 0))
|
||||
group.links.new(geometry, axis.inputs["-Z"])
|
||||
group.links.new(orient.outputs[0], axis.inputs["+X"])
|
||||
_shade_output(group, axis.outputs[0], (end + 600, 0))
|
||||
if shape == "DEPTH_SOLID":
|
||||
layout_geometry_references(group, stage)
|
||||
finish_layout(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 == "Inward Axis":
|
||||
item.default_value = "-Z"
|
||||
elif item.name == "Uniform Thickness":
|
||||
item.name = "Thickness"
|
||||
return group
|
||||
Reference in New Issue
Block a user