Files
AnyImage/tools/nodes/common/depth_surface.py
nan 89087fc2e2 feat: Add new Geometry Nodes groups for image relief and shadeless shader
- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters.
- Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections.
- Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format.
- Added a template HTML file for rendering node previews with customizable views and controls.
2026-09-09 21:24:57 +08:00

323 lines
14 KiB
Python

"""Face separation and connected thickness for Depth Surface assets."""
TEMPORARY_ATTRIBUTES = {
"face_center": ("FLOAT_VECTOR", "FACE"),
"face_camera": ("FLOAT_VECTOR", "FACE"),
"corner_camera": ("FLOAT_VECTOR", "CORNER"),
"cut_vertex": ("BOOLEAN", "CORNER"),
"camera": ("FLOAT_VECTOR", "POINT"),
"surface_position": ("FLOAT_VECTOR", "POINT"),
"source_vertex": ("INT", "POINT"),
"surface_normal": ("FLOAT_VECTOR", "POINT"),
}
def _store(group, geometry, name, value):
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
node.data_type, node.domain = TEMPORARY_ATTRIBUTES[name]
node.inputs["Name"].default_value = "o_depth_" + name
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(value, node.inputs["Value"])
return node.outputs["Geometry"]
def _read(group, name):
node = group.nodes.new("GeometryNodeInputNamedAttribute")
node.data_type = TEMPORARY_ATTRIBUTES[name][0]
node.inputs["Name"].default_value = "o_depth_" + name
return node.outputs["Attribute"]
def remove_surface_attributes(group, geometry):
"""Clear the reserved temporary namespace with one wildcard operation."""
node = group.nodes.new("GeometryNodeRemoveAttribute")
node.pattern_mode = "WILDCARD"
node.inputs["Name"].default_value = "o_depth_*"
group.links.new(geometry, node.inputs["Geometry"])
return node.outputs["Geometry"]
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 split_depth_surface(group, geometry, image, strength, uniform):
"""Keep each face's camera sample on its own corners before splitting."""
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"])
position = nodes.new("GeometryNodeInputPosition")
faces = _store(group, geometry, "face_center", position.outputs[0])
faces = _store(group, faces, "face_camera", texture.outputs["Color"])
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(_read(group, "face_camera"), axes.inputs[0])
center_field = _read(group, "face_center")
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", center_field, centers),
("FLOAT", axes.outputs["Z"], 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))
scaled = _math(group, "MULTIPLY", delta, uniform)
# Cross multiplication avoids division and stays invariant under uniform scaling.
selected = _math(
group,
"GREATER_THAN",
_math(group, "MULTIPLY", scaled, strength),
_math(group, "MULTIPLY", distance.outputs["Value"], 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 = _store(group, faces, "corner_camera", _read(group, "face_camera"))
corners = _store(
group,
corners,
"cut_vertex",
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0),
)
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(split.outputs["Mesh"], choose.inputs["True"])
return choose.outputs["Output"]
def store_surface_camera(group, geometry, original, strength):
"""Change cut depth along the original ray, preserving every other vertex."""
separated = _read(group, "corner_camera")
enabled = _math(
group,
"MULTIPLY",
_math(group, "GREATER_THAN", strength, 0.0),
_read(group, "cut_vertex"),
)
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"])
geometry = _store(group, geometry, "camera", choose.outputs["Output"])
return geometry, _read(group, "camera")
def thicken_depth_surface(group, geometry, thickness, normal_smooth=None):
"""Connect caps with single quads by source vertex and cap identity."""
nodes, links = group.nodes, group.links
front_geometry = geometry
position = nodes.new("GeometryNodeInputPosition")
geometry = _store(group, geometry, "surface_position", position.outputs[0])
surface_store = geometry.node
flattened = nodes.new("ShaderNodeVectorMath")
flattened.operation = "MULTIPLY"
flattened.inputs[1].default_value = (1.0, 1.0, 0.0)
links.new(position.outputs[0], flattened.inputs[0])
sheet = nodes.new("GeometryNodeSetPosition")
links.new(geometry, sheet.inputs["Geometry"])
links.new(flattened.outputs[0], sheet.inputs["Position"])
index = nodes.new("GeometryNodeInputIndex")
geometry = _store(
group, sheet.outputs["Geometry"], "source_vertex", index.outputs[0]
)
saved = geometry
# Weld in an integer identity grid, never in scene coordinates. Nearby cuts
# retain distinct IDs; copies of one rim vertex share the same ID and layer.
identity = nodes.new("ShaderNodeCombineXYZ")
for operation, axis in (("MODULO", "X"), ("DIVIDE", "Y")):
part = nodes.new("FunctionNodeIntegerMath")
part.operation = operation
part.inputs[1].default_value = 4096
links.new(_read(group, "source_vertex"), part.inputs[0])
links.new(part.outputs[0], identity.inputs[axis])
extrude = nodes.new("GeometryNodeExtrudeMesh")
extrude.mode = "FACES"
extrude.inputs["Individual"].default_value = False
extrusion_offset = nodes.new("ShaderNodeCombineXYZ")
extrusion_offset.inputs["Z"].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["Z"].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")
for value in (
saved,
flip_back.outputs["Mesh"],
flip_sides.outputs["Mesh"],
):
links.new(value, join.inputs["Geometry"])
encoded = nodes.new("GeometryNodeSetPosition")
links.new(join.outputs["Geometry"], encoded.inputs["Geometry"])
links.new(identity.outputs[0], encoded.inputs["Position"])
merge = nodes.new("GeometryNodeMergeByDistance")
merge.mode = "ALL"
merge.inputs["Distance"].default_value = 0.1
links.new(encoded.outputs["Geometry"], merge.inputs["Geometry"])
displacement = nodes.new("ShaderNodeCombineXYZ")
layer_position = nodes.new("GeometryNodeInputPosition")
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(layer_position.outputs[0], layer_axes.inputs[0])
links.new(
_math(group, "MULTIPLY", layer_axes.outputs["Z"], thickness),
displacement.inputs["Z"],
)
restored = nodes.new("ShaderNodeVectorMath")
restored.operation = "ADD"
links.new(_read(group, "surface_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"])
local_position = nodes.new("GeometryNodeInputPosition")
local_axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(local_position.outputs[0], local_axes.inputs[0])
links.new(local_axes.outputs["Z"], identity.inputs["Z"])
if normal_smooth is not None:
normal = nodes.new("GeometryNodeInputNormal")
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT_VECTOR"
links.new(normal_smooth, blur.inputs["Iterations"])
links.new(normal.outputs[0], blur.inputs["Value"])
normalize = nodes.new("ShaderNodeVectorMath")
normalize.operation = "NORMALIZE"
links.new(blur.outputs[0], normalize.inputs[0])
front_geometry = _store(group, front_geometry, "surface_normal", normalize.outputs[0])
links.new(front_geometry, surface_store.inputs["Geometry"])
normal_offset = nodes.new("ShaderNodeVectorMath")
normal_offset.operation = "SCALE"
links.new(_read(group, "surface_normal"), normal_offset.inputs[0])
links.new(displacement.inputs["Z"].links[0].from_socket, normal_offset.inputs[3])
links.new(normal_offset.outputs[0], restored.inputs[1])
nodes.remove(displacement)
return result.outputs["Geometry"]
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(reference_depth, relative_depth.inputs[0])
links.new(separate.outputs["Z"], 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_y = _projection_math(nodes, "MULTIPLY")
negative_y.inputs[1].default_value = -1.0
links.new(separate.outputs["Y"], negative_y.inputs[0])
projected = nodes.new("ShaderNodeCombineXYZ")
links.new(separate.outputs["X"], projected.inputs["X"])
links.new(negative_y.outputs[0], projected.inputs["Y"])
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(blended_axes.outputs["Y"], surface_position.inputs["Y"])
links.new(front.outputs[0], 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 project_geometry.outputs["Geometry"]