import math import bpy CUTOUT = "CUTOUT" BALLOON = "BALLOON" DEPTH_BALLOON = "DEPTH_BALLOON" DEPTH_SURFACE = "DEPTH_SURFACE" BALLOON_ATTRIBUTE = "o_balloon" NODE_GROUP_VERSION = 18 GROUP_NAMES = { CUTOUT: "O Image Surface", BALLOON: "O Image Balloon", DEPTH_BALLOON: "O Image Depth Balloon", DEPTH_SURFACE: "O Image Depth Surface", } 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 _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"]) statistics = nodes.new("GeometryNodeAttributeStatistic") statistics.data_type = "FLOAT" statistics.domain = "FACE" statistics.location = (x + 220.0, y - 100.0) links.new(geometry, statistics.inputs["Geometry"]) links.new(face_area.outputs["Area"], statistics.inputs["Attribute"]) reference_scale = _math(nodes, "MULTIPLY", (x + 450.0, y - 50.0)) reference_scale.inputs[1].default_value = 4.0 / math.sqrt(3.0) links.new(statistics.outputs["Median"], reference_scale.inputs[0]) threshold = _math(nodes, "MULTIPLY", (x + 650.0, y - 50.0)) links.new(reference_scale.outputs[0], threshold.inputs[0]) links.new(cleanup, threshold.inputs[1]) tiny = _math(nodes, "LESS_THAN", (x + 650.0, y + 200.0)) links.new(island_area.outputs["Total"], tiny.inputs[0]) links.new(threshold.outputs[0], tiny.inputs[1]) delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain = "FACE" delete.location = (x + 900.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 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", "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 _image_fields(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 texture = nodes.new("GeometryNodeImageTexture") texture.interpolation = "Linear" texture.extension = "EXTEND" texture.location = (location[0] + 220.0, location[1]) links.new(depth_image, texture.inputs["Image"]) links.new(uv.outputs["Attribute"], texture.inputs["Vector"]) separate = nodes.new("ShaderNodeSeparateXYZ") separate.location = (location[0] + 450.0, location[1]) links.new(texture.outputs["Color"], separate.inputs["Vector"]) return separate.outputs["X"], separate.outputs["Y"], texture.outputs["Alpha"] def _plane_x(group, base_plane, location): nodes = group.nodes links = group.links object_info = nodes.new("GeometryNodeObjectInfo") object_info.transform_space = "RELATIVE" object_info.location = location links.new(base_plane, object_info.inputs["Object"]) plane_normal = nodes.new("FunctionNodeRotateVector") plane_normal.inputs["Vector"].default_value = (0.0, 0.0, 1.0) plane_normal.location = (location[0] + 220.0, location[1]) links.new(object_info.outputs["Rotation"], plane_normal.inputs["Rotation"]) position = nodes.new("GeometryNodeInputPosition") position.location = (location[0], location[1] + 250.0) separate = nodes.new("ShaderNodeSeparateXYZ") separate.location = (location[0] + 220.0, location[1] + 250.0) links.new(position.outputs["Position"], separate.inputs["Vector"]) zero_x = nodes.new("ShaderNodeCombineXYZ") zero_x.location = (location[0] + 440.0, location[1] + 250.0) links.new(separate.outputs["Y"], zero_x.inputs["Y"]) links.new(separate.outputs["Z"], zero_x.inputs["Z"]) point_dot = nodes.new("ShaderNodeVectorMath") point_dot.operation = "DOT_PRODUCT" point_dot.location = (location[0] + 660.0, location[1] + 250.0) links.new(zero_x.outputs["Vector"], point_dot.inputs[0]) links.new(plane_normal.outputs["Vector"], point_dot.inputs[1]) offset_dot = nodes.new("ShaderNodeVectorMath") offset_dot.operation = "DOT_PRODUCT" offset_dot.location = (location[0] + 660.0, location[1]) links.new(object_info.outputs["Location"], offset_dot.inputs[0]) links.new(plane_normal.outputs["Vector"], offset_dot.inputs[1]) numerator = _math(nodes, "SUBTRACT", (location[0] + 880.0, location[1] + 150.0)) links.new(offset_dot.outputs["Value"], numerator.inputs[0]) links.new(point_dot.outputs["Value"], numerator.inputs[1]) normal_x = nodes.new("ShaderNodeSeparateXYZ") normal_x.location = (location[0] + 440.0, location[1] - 100.0) links.new(plane_normal.outputs["Vector"], normal_x.inputs["Vector"]) divide = _math(nodes, "DIVIDE", (location[0] + 1100.0, location[1] + 150.0)) links.new(numerator.outputs[0], divide.inputs[0]) links.new(normal_x.outputs["X"], divide.inputs[1]) return divide.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, base_plane, depth_image, depth_reference, ): nodes = group.nodes links = group.links profile, profile_factor = _profile_field( group, geometry, (-200.0, 250.0) ) sampled_depth, _auxiliary, _alpha = _image_fields( group, depth_image, (200.0, -250.0) ) depth_distance = _math(nodes, "MULTIPLY", (850.0, -250.0)) links.new(sampled_depth, depth_distance.inputs[0]) links.new(depth_reference, depth_distance.inputs[1]) calibrated = _math(nodes, "MULTIPLY", (1050.0, -250.0)) calibrated.inputs[1].default_value = 0.5 links.new(depth_distance.outputs[0], calibrated.inputs[0]) depth_base = _math(nodes, "ADD", (1250.0, -200.0)) links.new(calibrated.outputs[0], depth_base.inputs[0]) links.new(depth_reference, depth_base.inputs[1]) plane_x = _plane_x(group, base_plane, (200.0, -650.0)) corrected_depth = _math(nodes, "SUBTRACT", (1450.0, -150.0)) links.new(plane_x, corrected_depth.inputs[0]) links.new(depth_base.outputs[0], corrected_depth.inputs[1]) scaled_depth = _math(nodes, "MULTIPLY", (1650.0, -150.0)) links.new(corrected_depth.outputs[0], scaled_depth.inputs[0]) links.new(depth_scale, scaled_depth.inputs[1]) 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(scaled_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 _depth_continuity_threshold(group, continuity, location): nodes = group.nodes links = group.links percent = _math(nodes, "MULTIPLY", location) percent.inputs[1].default_value = 0.01 links.new(continuity, percent.inputs[0]) inverse = _math(nodes, "SUBTRACT", (location[0] + 200.0, location[1])) inverse.inputs[0].default_value = 0.5 links.new(percent.outputs[0], inverse.inputs[1]) exponent = _math(nodes, "MULTIPLY", (location[0] + 400.0, location[1])) exponent.inputs[1].default_value = 10.0 links.new(inverse.outputs[0], exponent.inputs[0]) power = _math(nodes, "POWER", (location[0] + 600.0, location[1])) power.inputs[0].default_value = 2.0 links.new(exponent.outputs[0], power.inputs[1]) scale = _math(nodes, "MULTIPLY", (location[0] + 800.0, location[1])) scale.inputs[1].default_value = 0.04 links.new(power.outputs[0], scale.inputs[0]) return scale.outputs[0] def _build_depth_surface( group, geometry, thickness, depth_scale, continuity, cleanup, depth_image, expand_smooth, ): nodes = group.nodes links = group.links depth, discontinuity, _alpha = _image_fields( group, depth_image, (-200.0, -200.0) ) threshold = _depth_continuity_threshold(group, continuity, (450.0, -250.0)) jump = _math(nodes, "GREATER_THAN", (1450.0, -100.0)) links.new(discontinuity, jump.inputs[0]) links.new(threshold, jump.inputs[1]) delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain = "FACE" delete.location = (1650.0, 400.0) links.new(geometry, delete.inputs["Geometry"]) links.new(jump.outputs[0], delete.inputs["Selection"]) geometry = _island_cleanup( group, delete.outputs["Geometry"], cleanup, (1900.0, 0.0) ) scaled = _math(nodes, "MULTIPLY", (2900.0, -100.0)) links.new(depth, scaled.inputs[0]) links.new(depth_scale, scaled.inputs[1]) front = _math(nodes, "MULTIPLY", (3100.0, -100.0)) front.inputs[1].default_value = -1.0 links.new(scaled.outputs[0], front.inputs[0]) back = _math(nodes, "ADD", (3300.0, -100.0)) links.new(front.outputs[0], back.inputs[0]) links.new(thickness, back.inputs[1]) shaped = _set_segmented_layer_x( group, geometry, front.outputs[0], back.outputs[0], thickness, (3500.0, 400.0), ) front_only = _set_geometry_x( group, geometry, front.outputs[0], (3700.0, -200.0) ) geometry = _remove_zero_thickness( group, front_only, shaped, thickness, (7000.0, 400.0) ) return expand_smooth(group, geometry, x_offset=7300.0) def node_group(shape, expand_smooth): group_name = GROUP_NAMES[shape] if shape == CUTOUT and bpy.data.node_groups.get(group_name) is None: legacy = bpy.data.node_groups.get("O Image Cutout") if legacy is not None: legacy.name = group_name group = bpy.data.node_groups.get(group_name) if group is not None and group.get("anyimage_cutout_version") == NODE_GROUP_VERSION: return group if group is None: group = bpy.data.node_groups.new(group_name, "GeometryNodeTree") else: group.nodes.clear() group.interface.clear() group.is_modifier = True _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 Base Plane-corrected image depth" double_sided = _bool_input(group, "Double Sided", False) double_sided.description = ( "Mirror the complete front surface across the local YZ plane" ) depth_reference = _float_input( group, "Depth Reference", 0.25, 0.0, None, subtype="DISTANCE", ) depth_reference.description = "Internal scale for image depth" depth_reference.hide_in_modifier = True depth_image = _socket(group, "Depth Image", "INPUT", "NodeSocketImage") depth_image.description = "Internally generated Depth Balloon data" depth_image.hide_in_modifier = True elif shape == DEPTH_SURFACE: depth_scale = _float_input(group, "Depth Scale", 1.0) iterations = _socket(group, "Iterations", "INPUT", "NodeSocketInt") iterations.default_value = 1 iterations.min_value = 0 iterations.max_value = 20 depth_image = _socket(group, "Depth Image", "INPUT", "NodeSocketImage") depth_image.description = "Internally generated Depth Surface data" depth_image.hide_in_modifier = True options = group.interface.new_panel(name="Options", default_closed=True) cleanup = _float_input(group, "Cleanup", 1.0, 0.0, 100.0, parent=options) cleanup.description = ( "Remove disconnected islands smaller than this multiple of the " "estimated point-spacing area" ) if shape == DEPTH_BALLOON: base_plane = _socket( group, "Base Plane", "INPUT", "NodeSocketObject", options ) base_plane.description = ( "Fitted depth plane; its arrow points in the deformation direction" ) elif shape == DEPTH_SURFACE: continuity = _float_input( group, "Depth Continuity", 0.0, 0.0, 100.0, subtype="PERCENTAGE", parent=options, ) smooth = _float_input( group, "Smooth", 1.0, 0.0, 1.0, subtype="FACTOR", parent=options, ) _socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry") controls = _group_input( group.nodes, (-1600.0, 500.0), { "Geometry", "Cleanup", "Thickness", "Double Sided", "Base Plane", "Depth Image", "Depth Scale", "Depth Continuity", "Depth Reference", }, ) geometry = _island_cleanup( group, controls.outputs["Geometry"], controls.outputs["Cleanup"], (-1300.0, 300.0), ) 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["Base Plane"], controls.outputs["Depth Image"], controls.outputs["Depth Reference"], ) output_location = (5100.0, 600.0) else: geometry = _build_depth_surface( group, geometry, controls.outputs["Thickness"], controls.outputs["Depth Scale"], controls.outputs["Depth Continuity"], controls.outputs["Cleanup"], controls.outputs["Depth Image"], expand_smooth, ) output_location = (10700.0, 400.0) _localize_group_inputs(group, controls) _shade_output(group, geometry, output_location) group.color_tag = "GEOMETRY" group.description = { CUTOUT: "Clean and symmetrically thicken a Surface sheet.", BALLOON: "Inflate a Surface sheet from its boundary profile.", DEPTH_BALLOON: ( "Mix Balloon profile and hidden image depth around a Base Plane." ), DEPTH_SURFACE: ( "Displace a Surface sheet from hidden image depth, cull, " "thicken, and smooth." ), }[shape] group["anyimage_cutout_version"] = NODE_GROUP_VERSION group.use_fake_user = True return group