Files
AnyImage/tools/nodes/common/depth_surface.py

446 lines
21 KiB
Python

"""Face separation and connected thickness for Depth Surface assets."""
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attributes, 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."""
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)
if triangles:
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)
# Evaluate each internal edge as a symmetric pair of triangles.
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)
# Explicit edge-to-face sampling marks both members without averaging
# through neighboring vertices or iterating over newly exposed edges.
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)
triangle_selected = combine("MULTIPLY", equals(corners.outputs["Total"], 3),
combine("MAXIMUM", paired, equals(boundary_count, 3)))
selected = combine("MAXIMUM", selected, triangle_selected)
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, domain="FACE")
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, split_strength
):
"""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_attributes(
group, project_geometry.outputs["Geometry"], CUT_ATTRIBUTE,
enabled=_math(group, "GREATER_THAN", split_strength, 0.0),
)