"""Build the four Cutout Geometry Nodes groups stored in O_AnyImage.blend.""" import bpy from node_graph import expand_smooth from anyimage.operators.cutout_tool.shape 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, "Base Plane 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, "Base Plane 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", "Base Plane Depth", "Depth Direction", }, ) geometry = controls.outputs["Geometry"] if shape == "SURFACE": 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["Base Plane 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["Base Plane 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 = { "SURFACE": "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): 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, "Base Plane Depth", 1.0, 0.0, None, subtype="DISTANCE", parent=options, ) reference_depth.description = "Depth of the flat base plane for 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"}, ) cleanup_area = _math(group.nodes, "MULTIPLY", (-2500.0, 750.0)) cleanup_area.inputs[1].default_value = 0.01 group.links.new(base_controls.outputs["Cleanup"], cleanup_area.inputs[0]) geometry = _island_cleanup( group, base_controls.outputs["Geometry"], cleanup_area.outputs[0], (-2350.0, 250.0), ) branch_y = { "SURFACE": 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