- 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.
323 lines
14 KiB
Python
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"]
|