Files
AnyImage/tools/node_assets/cutout_shell.py
nan 196b964efc 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.
2026-09-08 17:18:03 +08:00

123 lines
5.7 KiB
Python

"""Continue a front-normal thin shell into the rounded depth volume."""
from cutout_inflation import build_inflation_position
from cutout_volume import node, shift_stage
def build_front_normals(group, geometry):
links = group.links
normal = node(group, "GeometryNodeInputNormal", (3400, -650))
control = node(group, "NodeGroupInput", (3400, -950))
blur = node(group, "GeometryNodeBlurAttribute", (3640, -650))
blur.data_type = "FLOAT_VECTOR"
links.new(normal.outputs[0], blur.inputs["Value"])
links.new(control.outputs["Normal Smooth"], blur.inputs["Iterations"])
normalize = node(group, "ShaderNodeVectorMath", (3880, -650))
normalize.operation = "NORMALIZE"
links.new(blur.outputs[0], normalize.inputs[0])
store = node(group, "GeometryNodeStoreNamedAttribute", (4110, 0))
store.data_type, store.domain = "FLOAT_VECTOR", "POINT"
store.inputs["Name"].default_value = "o_balloon_surface_front_normal"
links.new(geometry, store.inputs["Geometry"])
links.new(normalize.outputs[0], store.inputs["Value"])
return store.outputs[0]
def build_shell_position(group, position, uniform, offset):
previous = set(group.nodes)
links = group.links
def connect(value, target):
if hasattr(value, "node"):
links.new(value, target)
else:
target.default_value = value
def math(operation, a, b, location):
result = node(group, "ShaderNodeMath", location)
result.operation = operation
connect(a, result.inputs[0])
connect(b, result.inputs[1])
return result.outputs[0]
def vector(operation, a, b, location):
result = node(group, "ShaderNodeVectorMath", location)
result.operation = operation
connect(a, result.inputs[0])
connect(b, result.inputs[3 if operation == "SCALE" else 1])
return result.outputs[0]
def read(name, location, kind="FLOAT"):
result = node(group, "GeometryNodeInputNamedAttribute", location)
result.data_type = kind
result.inputs["Name"].default_value = name
return result.outputs["Attribute"]
def z(value, location):
result = node(group, "ShaderNodeSeparateXYZ", location)
links.new(value, result.inputs[0])
return result.outputs["Z"]
# Uniform and the thin end of Balloon share the actual front normal.
front = read("o_depth_surface_position", (0, 0), "FLOAT_VECTOR")
front_z = z(front, (240, 0))
raw_z = z(position, (240, 250))
distance = math("SUBTRACT", raw_z, front_z, (480, 250))
normal = read("o_balloon_surface_front_normal", (480, 550), "FLOAT_VECTOR")
thin_offset = vector("SCALE", normal, distance, (720, 550))
uniform_position = vector("ADD", front, thin_offset, (3120, 550))
# R = t * (front_z - center) + (1 - t) * front_delta, with t clamped.
# This avoids division by Thickness and removes the old zero-thickness drift.
control = node(group, "NodeGroupInput", (0, -400))
clamp = node(group, "ShaderNodeClamp", (240, -400))
links.new(control.outputs["Thickness"], clamp.inputs["Value"])
t = clamp.outputs[0]
center = read("o_balloon_surface_center", (0, -750))
delta = read("o_balloon_surface_front_delta", (480, -1050))
radius = math("SUBTRACT", front_z, center, (480, -750))
radius = math("MULTIPLY", radius, t, (720, -750))
remaining = math("SUBTRACT", 1, t, (480, -400))
remainder = math("MULTIPLY", remaining, delta, (720, -1050))
radius = math("ADD", radius, remainder, (960, -750))
# Front and rear use the same edge field, evaluated on their own caps.
edge = read("o_balloon_surface_edge_weight", (0, -1450))
mask = read("o_normal_scale", (240, -1800))
remaining = math("SUBTRACT", 1, edge, (240, -1450))
remaining = math("MULTIPLY", remaining, mask, (480, -1450))
weight = math("SUBTRACT", 1, remaining, (720, -1450))
difference = math("SUBTRACT", weight, edge, (960, -1450))
base = math("MULTIPLY", difference, radius, (1200, -1450))
layer = math("MULTIPLY", weight, distance, (1200, -1800))
rear_radius = math("ADD", base, layer, (1440, -1450))
front_radius = math("MULTIPLY", edge, radius, (1440, -750))
normal = read("o_balloon_surface_normal", (1200, -300), "FLOAT_VECTOR")
direction = vector("SUBTRACT", normal, (0, 0, 1), (1440, -300))
front_shift = vector("SCALE", direction, front_radius, (1680, -750))
front_position = vector("ADD", front, front_shift, (1920, -750))
rear_shift = vector("SCALE", direction, rear_radius, (1680, -1450))
axis_offset = node(group, "ShaderNodeCombineXYZ", (1680, -1800))
links.new(distance, axis_offset.inputs["Z"])
full_offset = vector("ADD", axis_offset.outputs[0], rear_shift, (1920, -1450))
# Smoothly reach the existing full-volume shape at Thickness >= 1.
transition = node(group, "ShaderNodeMapRange", (1920, 0))
transition.interpolation_type = "SMOOTHERSTEP"
links.new(t, transition.inputs["Value"])
mix = node(group, "ShaderNodeMix", (2400, -1100))
mix.data_type = "VECTOR"
links.new(transition.outputs[0], mix.inputs[0])
links.new(thin_offset, mix.inputs[4])
links.new(full_offset, mix.inputs[5])
balloon_position = vector("ADD", front_position, mix.outputs["Result"], (3120, -750))
choose = node(group, "GeometryNodeSwitch", (3400, 0))
choose.input_type = "VECTOR"
links.new(uniform, choose.inputs["Switch"])
links.new(balloon_position, choose.inputs["False"])
links.new(uniform_position, choose.inputs["True"])
for item in set(group.nodes) - previous:
item.location.x *= 0.9
item.location.y *= 0.65
shift_stage(group, previous, offset)
return build_inflation_position(group, choose.outputs[0], distance, uniform)