"""Construct the planar and depth-based Cutout shape stages.""" from depth_surface import project_depth_surface, store_surface_camera, remove_surface_attributes, split_depth_surface, thicken_depth_surface from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME 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 _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 = (0.0, 0.0, 1.0) # Keep distinct front and back vertices before overriding their Z positions. node.inputs["Offset Scale"].default_value = 1.0 node.location = location group.links.new(geometry, node.inputs["Mesh"]) return node def _offset_geometry_z(group, geometry, z_value, location): nodes = group.nodes links = group.links offset = nodes.new("ShaderNodeCombineXYZ") offset.location = location links.new(z_value, offset.inputs["Z"]) 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_z(group, geometry, extrude, front_z, back_z, location): nodes = group.nodes links = group.links choose_z = nodes.new("GeometryNodeSwitch") choose_z.input_type = "FLOAT" choose_z.location = (location[0], location[1] - 200.0) links.new(extrude.outputs["Top"], choose_z.inputs["Switch"]) links.new(front_z, choose_z.inputs["False"]) links.new(back_z, choose_z.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(separate.outputs["X"], combine.inputs["X"]) links.new(separate.outputs["Y"], combine.inputs["Y"]) links.new(choose_z.outputs["Output"], 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_z( group, geometry, front_z, (location[0], location[1] - 450.0) ) back = _offset_geometry_z( group, geometry, back_z, (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_z( group, geometry, front_z, back_z, 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_z(group, geometry, front_z, (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_z( group, repeat_output.outputs["Sides"], front_z, (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_z(group, geometry, back_z, (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_z(group, geometry, z_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(separate.outputs["X"], combine.inputs["X"]) links.new(separate.outputs["Y"], combine.inputs["Y"]) links.new(z_value, 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"]) x_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 850.0)) links.new(position_components.outputs["X"], x_term.inputs[0]) links.new(normal_components.outputs["X"], x_term.inputs[1]) y_term = _math(nodes, "MULTIPLY", (location[0] + 440.0, location[1] + 350.0)) links.new(position_components.outputs["Y"], y_term.inputs[0]) links.new(normal_components.outputs["Y"], y_term.inputs[1]) point_dot = _math(nodes, "ADD", (location[0] + 660.0, location[1] + 400.0)) links.new(x_term.outputs[0], point_dot.inputs[0]) links.new(y_term.outputs[0], point_dot.inputs[1]) divide = _math(nodes, "DIVIDE", (location[0] + 880.0, location[1] + 200.0)) links.new(point_dot.outputs[0], divide.inputs[0]) links.new(normal_components.outputs["Z"], divide.inputs[1]) plane = _math(nodes, "ADD", (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]) back = _math(nodes, "MULTIPLY", (20.0, 0.0)) back.inputs[1].default_value = -1.0 links.new(half.outputs[0], back.inputs[0]) shaped = _set_segmented_layer_z( group, geometry, half.outputs[0], back.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_scale = _math(nodes, "MULTIPLY", (2600.0, 500.0)) links.new(profile, 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]) negative_back = _math(nodes, "MULTIPLY", (2800.0, 300.0)) negative_back.inputs[1].default_value = -1.0 links.new(back.outputs[0], negative_back.inputs[0]) extrude = _extrude_layers(group, geometry, (2400.0, 750.0)) shaped = _set_layer_z( group, geometry, extrude, front_scale.outputs[0], negative_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 = scaled_depth.outputs[0] base_front = _math(nodes, "MULTIPLY", (2400.0, 500.0)) links.new(profile, base_front.inputs[0]) links.new(thickness, base_front.inputs[1]) base_back = _math(nodes, "MULTIPLY", (2600.0, 300.0)) links.new(profile, base_back.inputs[0]) links.new(thickness, base_back.inputs[1]) negate_back = _math(nodes, "MULTIPLY", (2800.0, 300.0)) negate_back.inputs[1].default_value = -1.0 links.new(base_back.outputs[0], negate_back.inputs[0]) 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(base_front.outputs[0], front_mix.inputs["A"]) links.new(front_depth, 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(negate_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_z( group, geometry, extrude, front_mix.outputs["Result"], back_choice.outputs["Output"], (3300.0, 650.0), ) front_only = _set_geometry_z( 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, split, uniform_scale, reference_depth, depth_image, ): nodes = group.nodes links = group.links geometry = split_depth_surface( group, geometry, depth_image, split, uniform_scale ) original_camera = _position_field(group, depth_image, (-200.0, -200.0)) geometry, camera_position = store_surface_camera( group, geometry, original_camera, split ) geometry = project_depth_surface( group, geometry, camera_position, uniform_scale, reference_depth, depth_scale ) shaped = thicken_depth_surface(group, geometry, thickness) geometry = _remove_zero_thickness( group, geometry, shaped, thickness, (11800.0, 400.0) ) return remove_surface_attributes(group, geometry)