"""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)