"""Face separation and connected thickness for Depth Surface assets.""" from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attribute_pattern, sample_field CUT_ATTRIBUTE = "_o_anyimage_depth_cut" def _math(group, operation, a, b=None): node = group.nodes.new("ShaderNodeMath") node.operation = operation for index, value in enumerate((a, b)): if value is None: continue if isinstance(value, (int, float)): node.inputs[index].default_value = value else: group.links.new(value, node.inputs[index]) return node.outputs[0] def _remove_strip_faces(group, geometry, *, triangles=False): """Delete boundary strips from one unchanged topology snapshot.""" if triangles: return _remove_triangle_strip_faces(group, geometry) return _remove_quad_strip_faces(group, geometry) def _remove_quad_strip_faces(group, geometry): """Delete boundary quad strips while preserving open boundary edges.""" nodes, links = group.nodes, group.links def equals(value, count): compare = nodes.new("FunctionNodeCompare") compare.data_type, compare.operation = "INT", "EQUAL" links.new(value, compare.inputs[2]) if isinstance(count, int): compare.inputs[3].default_value = count else: links.new(count, compare.inputs[3]) return compare.outputs["Result"] def combine(operation, a, b): node = nodes.new("FunctionNodeIntegerMath") node.operation = operation links.new(a, node.inputs[0]) links.new(b, node.inputs[1]) return node.outputs[0] boundaries = [] for offset in range(4): index = nodes.new("GeometryNodeInputIndex") corners = nodes.new("GeometryNodeCornersOfFace") links.new(index.outputs[0], corners.inputs["Face Index"]) corner = nodes.new("GeometryNodeOffsetCornerInFace") corner.inputs["Offset"].default_value = offset links.new(corners.outputs["Corner Index"], corner.inputs["Corner Index"]) edge = nodes.new("GeometryNodeEdgesOfCorner") links.new(corner.outputs["Corner Index"], edge.inputs["Corner Index"]) sample = nodes.new("GeometryNodeFieldAtIndex") sample.data_type, sample.domain = "INT", "EDGE" links.new(edge.outputs["Next Edge Index"], sample.inputs["Index"]) neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") links.new(neighbors.outputs["Face Count"], sample.inputs["Value"]) boundaries.append(equals(sample.outputs["Value"], 1)) opposite = combine( "MAXIMUM", combine("MULTIPLY", boundaries[0], boundaries[2]), combine("MULTIPLY", boundaries[1], boundaries[3]), ) face = nodes.new("GeometryNodeInputMeshFaceNeighbors") selected = combine("MULTIPLY", equals(face.outputs["Vertex Count"], 4), opposite) delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain, delete.mode = "FACE", "ALL" links.new(geometry, delete.inputs["Geometry"]) links.new(selected, delete.inputs["Selection"]) return delete.outputs["Geometry"] def _remove_triangle_strip_faces(group, geometry): """Delete symmetric triangle strip pairs and all-boundary ears.""" nodes, links = group.nodes, group.links def equals(value, count): compare = nodes.new("FunctionNodeCompare") compare.data_type, compare.operation = "INT", "EQUAL" links.new(value, compare.inputs[2]) if isinstance(count, int): compare.inputs[3].default_value = count else: links.new(count, compare.inputs[3]) return compare.outputs["Result"] def combine(operation, a, b): node = nodes.new("FunctionNodeIntegerMath") node.operation = operation links.new(a, node.inputs[0]) links.new(b, node.inputs[1]) return node.outputs[0] def sample(value, index, domain): node = nodes.new("GeometryNodeFieldAtIndex") node.data_type, node.domain = "INT", domain links.new(value, node.inputs["Value"]) links.new(index, node.inputs["Index"]) return node.outputs["Value"] def offset_corner(corner, offset): node = nodes.new("GeometryNodeOffsetCornerInFace") links.new(corner, node.inputs["Corner Index"]) node.inputs["Offset"].default_value = offset return node.outputs["Corner Index"] def vertex(corner): node = nodes.new("GeometryNodeVertexOfCorner") links.new(corner, node.inputs["Corner Index"]) return node.outputs["Vertex Index"] def boundary(corner): edge = nodes.new("GeometryNodeEdgesOfCorner") links.new(corner, edge.inputs["Corner Index"]) count = nodes.new("GeometryNodeInputMeshEdgeNeighbors") return equals(sample(count.outputs["Face Count"], edge.outputs["Next Edge Index"], "EDGE"), 1) edge_index = nodes.new("GeometryNodeInputIndex") sides = [] for side in (0, 1): corners = nodes.new("GeometryNodeCornersOfEdge") links.new(edge_index.outputs[0], corners.inputs["Edge Index"]) corners.inputs["Sort Index"].default_value = side corner = corners.outputs["Corner Index"] face_index = nodes.new("GeometryNodeFaceOfCorner") links.new(corner, face_index.inputs["Corner Index"]) face_size = nodes.new("GeometryNodeInputMeshFaceNeighbors") triangle = equals(sample(face_size.outputs["Vertex Count"], face_index.outputs["Face Index"], "FACE"), 3) end, tip = offset_corner(corner, 1), offset_corner(corner, 2) sides.append((triangle, vertex(corner), vertex(tip), boundary(end), boundary(tip))) a, b = sides same_direction = equals(a[1], b[1]) same_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[3]), combine("MULTIPLY", a[4], b[4])) reverse_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[4]), combine("MULTIPLY", a[4], b[3])) pair = combine( "MAXIMUM", combine("MULTIPLY", same_direction, reverse_order), combine("MULTIPLY", equals(same_direction, 0), same_order), ) neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") for condition in (equals(neighbors.outputs["Face Count"], 2), a[0], b[0], equals(equals(a[2], b[2]), 0)): pair = combine("MULTIPLY", pair, condition) face_index = nodes.new("GeometryNodeInputIndex") corners = nodes.new("GeometryNodeCornersOfFace") links.new(face_index.outputs[0], corners.inputs["Face Index"]) boundary_count, paired = None, None for offset in range(3): corner = offset_corner(corners.outputs["Corner Index"], offset) edge = nodes.new("GeometryNodeEdgesOfCorner") links.new(corner, edge.inputs["Corner Index"]) match = sample(pair, edge.outputs["Next Edge Index"], "EDGE") paired = match if paired is None else combine("MAXIMUM", paired, match) rim = boundary(corner) boundary_count = rim if boundary_count is None else combine("ADD", boundary_count, rim) selected = combine( "MULTIPLY", equals(corners.outputs["Total"], 3), combine("MAXIMUM", paired, equals(boundary_count, 3)), ) delete = nodes.new("GeometryNodeDeleteGeometry") delete.domain, delete.mode = "FACE", "ALL" links.new(geometry, delete.inputs["Geometry"]) links.new(selected, delete.inputs["Selection"]) return delete.outputs["Geometry"] def sample_face_camera(group, image): """Sample camera XYZ at each planar face's UV center.""" nodes, links = group.nodes, group.links uv = nodes.new("GeometryNodeInputNamedAttribute") uv.data_type = "FLOAT_VECTOR" uv.inputs["Name"].default_value = "UVMap" face_uv = nodes.new("GeometryNodeFieldOnDomain") face_uv.data_type = "FLOAT_VECTOR" face_uv.domain = "FACE" links.new(uv.outputs["Attribute"], face_uv.inputs["Value"]) texture = nodes.new("GeometryNodeImageTexture") texture.interpolation = "Linear" texture.extension = "EXTEND" links.new(image, texture.inputs["Image"]) links.new(face_uv.outputs["Value"], texture.inputs["Vector"]) return texture.outputs["Color"] def ray_distance(group, camera): """Measure the distance from the camera origin to a camera-space point.""" length = group.nodes.new("ShaderNodeVectorMath") length.operation = "LENGTH" group.links.new(camera, length.inputs[0]) return length.outputs["Value"] def split_depth_surface(group, geometry, face_camera, strength, reference_depth, *, face_distance=None, face_center=None, triangles=False): """Split relative ray-distance jumps over the base face-center spacing.""" nodes, links = group.nodes, group.links if face_center is None: face_center = nodes.new("GeometryNodeInputPosition").outputs[0] face_camera = evaluate_field(group, face_camera, "FLOAT_VECTOR", "FACE") if face_distance is None: face_distance = ray_distance(group, face_camera) centers, depths = [], [] for side in (0, 1): corner = nodes.new("GeometryNodeCornersOfEdge") corner.inputs["Sort Index"].default_value = side face = nodes.new("GeometryNodeFaceOfCorner") links.new(corner.outputs["Corner Index"], face.inputs["Corner Index"]) for data_type, field, results in ( ("FLOAT_VECTOR", face_center, centers), ("FLOAT", face_distance, depths), ): sample = nodes.new("GeometryNodeFieldAtIndex") sample.data_type = data_type sample.domain = "FACE" links.new(face.outputs["Face Index"], sample.inputs["Index"]) links.new(field, sample.inputs["Value"]) results.append(sample.outputs["Value"]) distance = nodes.new("ShaderNodeVectorMath") distance.operation = "DISTANCE" for index, center in enumerate(centers): links.new(center, distance.inputs[index]) delta = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", *depths)) local_distance = _math(group, "MAXIMUM", _math(group, "MULTIPLY", _math(group, "ADD", *depths), 0.5), 1e-8) reference = _math(group, "MAXIMUM", reference_depth, 1e-8) # Uniform Scale cancels between the distance jump and local spacing correction. # Cross multiplication also avoids dividing by the local distance. selected = _math( group, "GREATER_THAN", _math(group, "MULTIPLY", _math(group, "MULTIPLY", delta, reference), strength), _math(group, "MULTIPLY", _math(group, "MULTIPLY", distance.outputs["Value"], local_distance), 2.5), ) selected = _math( group, "MULTIPLY", selected, _math(group, "GREATER_THAN", distance.outputs["Value"], 0.0), ) neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") selected = _math( group, "MULTIPLY", selected, _math(group, "COMPARE", neighbors.outputs["Face Count"], 2.0), ) cut_edges = nodes.new("GeometryNodeFieldOnDomain") cut_edges.data_type = "FLOAT" cut_edges.domain = "EDGE" links.new(selected, cut_edges.inputs["Value"]) cut_vertices = nodes.new("GeometryNodeFieldOnDomain") cut_vertices.data_type = "FLOAT" cut_vertices.domain = "POINT" links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"]) corners, cut = store_boolean_attribute( group, geometry, CUT_ATTRIBUTE, _math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER", ) split = nodes.new("GeometryNodeSplitEdges") links.new(corners, split.inputs["Mesh"]) links.new(selected, split.inputs["Selection"]) enabled = _math(group, "GREATER_THAN", strength, 0.0) choose = nodes.new("GeometryNodeSwitch") choose.input_type = "GEOMETRY" links.new(enabled, choose.inputs["Switch"]) links.new(geometry, choose.inputs["False"]) links.new(_remove_strip_faces(group, split.outputs["Mesh"], triangles=triangles), choose.inputs["True"]) return choose.outputs["Output"], face_camera, cut def build_surface_camera(group, original, separated, cut_vertex, strength, *, always_vertex=None): """Change cut depth along the original ray, preserving every other vertex.""" enabled = group.nodes.new("GeometryNodeSwitch") enabled.input_type = "BOOLEAN" group.links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"]) enabled.inputs["False"].default_value = False group.links.new(cut_vertex, enabled.inputs["True"]) if always_vertex is not None: combined = group.nodes.new("FunctionNodeBooleanMath") combined.operation = "OR" group.links.new(enabled.outputs["Output"], combined.inputs[0]) group.links.new(always_vertex, combined.inputs[1]) enabled = combined.outputs["Boolean"] else: enabled = enabled.outputs["Output"] original_axes = group.nodes.new("ShaderNodeSeparateXYZ") separated_axes = group.nodes.new("ShaderNodeSeparateXYZ") group.links.new(original, original_axes.inputs[0]) group.links.new(separated, separated_axes.inputs[0]) ratio = _math( group, "DIVIDE", separated_axes.outputs["Z"], original_axes.outputs["Z"] ) projected = group.nodes.new("ShaderNodeVectorMath") projected.operation = "SCALE" group.links.new(original, projected.inputs[0]) group.links.new(ratio, projected.inputs[3]) choose = group.nodes.new("GeometryNodeSwitch") choose.input_type = "VECTOR" group.links.new(enabled, choose.inputs["Switch"]) group.links.new(original, choose.inputs["False"]) group.links.new(projected.outputs[0], choose.inputs["True"]) return evaluate_field(group, choose.outputs["Output"], "FLOAT_VECTOR", "POINT") def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *, back_mask=None, weld_zero_thickness=False): """Connect caps with single quads by source vertex and cap identity.""" nodes, links = group.nodes, group.links position = nodes.new("GeometryNodeInputPosition") index = nodes.new("GeometryNodeInputIndex") # Encode identity in the construction geometry before extrusion. The source # surface remains a separate sampling input; no position or ID is stored. identity = nodes.new("ShaderNodeCombineXYZ") for operation, axis in (("MODULO", "X"), ("DIVIDE", "Z")): part = nodes.new("FunctionNodeIntegerMath") part.operation = operation part.inputs[1].default_value = 4096 links.new(index.outputs[0], part.inputs[0]) links.new(part.outputs[0], identity.inputs[axis]) sheet = nodes.new("GeometryNodeSetPosition") links.new(geometry, sheet.inputs["Geometry"]) links.new(identity.outputs[0], sheet.inputs["Position"]) saved = sheet.outputs["Geometry"] layer_axes = nodes.new("ShaderNodeSeparateXYZ") links.new(position.outputs[0], layer_axes.inputs[0]) row = nodes.new("FunctionNodeIntegerMath") row.operation = "MULTIPLY" row.inputs[1].default_value = 4096 links.new(layer_axes.outputs["Z"], row.inputs[0]) vertex = nodes.new("FunctionNodeIntegerMath") vertex.operation = "ADD" links.new(layer_axes.outputs["X"], vertex.inputs[0]) links.new(row.outputs[0], vertex.inputs[1]) source_index = vertex.outputs[0] source_position = sample_field(group, geometry, position.outputs[0], "FLOAT_VECTOR", index=source_index) source_thickness = sample_field(group, geometry, thickness, "FLOAT", index=source_index) extrude = nodes.new("GeometryNodeExtrudeMesh") extrude.mode = "FACES" extrude.inputs["Individual"].default_value = False extrusion_offset = nodes.new("ShaderNodeCombineXYZ") extrusion_offset.inputs["Y"].default_value = -1.0 links.new(extrusion_offset.outputs[0], extrude.inputs["Offset"]) links.new(saved, extrude.inputs["Mesh"]) sides = nodes.new("GeometryNodeSeparateGeometry") sides.domain = "FACE" links.new(extrude.outputs["Mesh"], sides.inputs["Geometry"]) links.new(extrude.outputs["Side"], sides.inputs["Selection"]) offset = nodes.new("ShaderNodeCombineXYZ") offset.inputs["Y"].default_value = -1.0 back = nodes.new("GeometryNodeSetPosition") links.new(saved, back.inputs["Geometry"]) links.new(offset.outputs[0], back.inputs["Offset"]) flip_back = nodes.new("GeometryNodeFlipFaces") links.new(back.outputs["Geometry"], flip_back.inputs["Mesh"]) flip_sides = nodes.new("GeometryNodeFlipFaces") links.new(sides.outputs["Selection"], flip_sides.inputs["Mesh"]) join = nodes.new("GeometryNodeJoinGeometry") rear = flip_back.outputs["Mesh"] rim = flip_sides.outputs["Mesh"] back_parts = nodes.new("GeometryNodeJoinGeometry") links.new(rear, back_parts.inputs["Geometry"]) links.new(rim, back_parts.inputs["Geometry"]) rear = back_parts.outputs["Geometry"] if back_mask is not None: rear = store_float_attribute(group, rear, back_mask, 1.0) for value in ( saved, rear, ): links.new(value, join.inputs["Geometry"]) construction = join.outputs["Geometry"] if weld_zero_thickness: positive = _math(group, "GREATER_THAN", source_thickness, 1e-12) flattened = nodes.new("ShaderNodeCombineXYZ") for axis in ("X", "Z"): links.new(layer_axes.outputs[axis], flattened.inputs[axis]) links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Y"]), flattened.inputs["Y"]) collapse = nodes.new("GeometryNodeSetPosition") links.new(construction, collapse.inputs["Geometry"]) links.new(flattened.outputs[0], collapse.inputs["Position"]) construction = collapse.outputs["Geometry"] merge = nodes.new("GeometryNodeMergeByDistance") merge.mode = "ALL" merge.inputs["Distance"].default_value = 0.1 links.new(construction, merge.inputs["Geometry"]) displacement = nodes.new("ShaderNodeCombineXYZ") distance = _math(group, "MULTIPLY", layer_axes.outputs["Y"], source_thickness) links.new(distance, displacement.inputs["Y"]) restored = nodes.new("ShaderNodeVectorMath") restored.operation = "ADD" links.new(source_position, restored.inputs[0]) links.new(displacement.outputs[0], restored.inputs[1]) result = nodes.new("GeometryNodeSetPosition") links.new(merge.outputs["Geometry"], result.inputs["Geometry"]) links.new(restored.outputs[0], result.inputs["Position"]) if normal_smooth is not None: normal = nodes.new("GeometryNodeInputNormal") blur = nodes.new("GeometryNodeBlurAttribute") blur.data_type = "FLOAT_VECTOR" blur.inputs["Iterations"].default_value = normal_smooth links.new(normal.outputs[0], blur.inputs["Value"]) normalize = nodes.new("ShaderNodeVectorMath") normalize.operation = "NORMALIZE" links.new(blur.outputs[0], normalize.inputs[0]) source_normal = sample_field(group, geometry, normalize.outputs[0], "FLOAT_VECTOR", index=source_index) normal_offset = nodes.new("ShaderNodeVectorMath") normal_offset.operation = "SCALE" links.new(source_normal, normal_offset.inputs[0]) links.new(distance, normal_offset.inputs[3]) links.new(normal_offset.outputs[0], restored.inputs[1]) nodes.remove(displacement) return { "geometry": result.outputs["Geometry"], "front": source_position, "source_index": source_index, "position": restored.outputs[0], "position_input": result.inputs["Position"], } def _projection_math(nodes, operation): node = nodes.new("ShaderNodeMath") node.operation = operation return node def project_depth_surface( group, geometry, camera_position, uniform_scale, reference_depth, depth_scale ): """Interpolate the rectangular plane into the model camera coordinates.""" nodes, links = group.nodes, group.links scaled = nodes.new("ShaderNodeVectorMath") scaled.operation = "SCALE" links.new(camera_position, scaled.inputs[0]) links.new(uniform_scale, scaled.inputs[3]) separate = nodes.new("ShaderNodeSeparateXYZ") links.new(scaled.outputs["Vector"], separate.inputs["Vector"]) relative_depth = _projection_math(nodes, "SUBTRACT") links.new(separate.outputs["Z"], relative_depth.inputs[0]) links.new(reference_depth, relative_depth.inputs[1]) front = _projection_math(nodes, "MULTIPLY") links.new(relative_depth.outputs[0], front.inputs[0]) links.new(depth_scale, front.inputs[1]) negative_z = _projection_math(nodes, "MULTIPLY") negative_z.inputs[1].default_value = -1.0 links.new(separate.outputs["Y"], negative_z.inputs[0]) projected = nodes.new("ShaderNodeCombineXYZ") links.new(separate.outputs["X"], projected.inputs["X"]) links.new(front.outputs[0], projected.inputs["Y"]) links.new(negative_z.outputs[0], projected.inputs["Z"]) current_position = nodes.new("GeometryNodeInputPosition") blend = nodes.new("ShaderNodeMix") blend.data_type = "VECTOR" blend.clamp_factor = False links.new(depth_scale, blend.inputs[0]) links.new(current_position.outputs["Position"], blend.inputs[4]) links.new(projected.outputs["Vector"], blend.inputs[5]) blended_axes = nodes.new("ShaderNodeSeparateXYZ") links.new(blend.outputs["Result"], blended_axes.inputs["Vector"]) surface_position = nodes.new("ShaderNodeCombineXYZ") links.new(blended_axes.outputs["X"], surface_position.inputs["X"]) links.new(front.outputs[0], surface_position.inputs["Y"]) links.new(blended_axes.outputs["Z"], surface_position.inputs["Z"]) project_geometry = nodes.new("GeometryNodeSetPosition") links.new(geometry, project_geometry.inputs["Geometry"]) links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"]) return remove_attribute_pattern(group, project_geometry.outputs["Geometry"], "_o_*")