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.
This commit is contained in:
1
tools/nodes/common/__init__.py
Normal file
1
tools/nodes/common/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
162
tools/nodes/common/cutout.py
Normal file
162
tools/nodes/common/cutout.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""Build geometry calculations shared by Cutout assets."""
|
||||
from .nodes import math_node
|
||||
from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME
|
||||
|
||||
|
||||
def _island_cleanup(group, geometry, cleanup):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
mesh_island = nodes.new("GeometryNodeInputMeshIsland")
|
||||
face_area = nodes.new("GeometryNodeInputMeshFaceArea")
|
||||
island_area = nodes.new("GeometryNodeAccumulateField")
|
||||
island_area.data_type = "FLOAT"
|
||||
island_area.domain = "FACE"
|
||||
links.new(face_area.outputs["Area"], island_area.inputs["Value"])
|
||||
links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"])
|
||||
|
||||
tiny = math_node(nodes, "LESS_THAN")
|
||||
links.new(island_area.outputs["Total"], tiny.inputs[0])
|
||||
links.new(cleanup, tiny.inputs[1])
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain = "FACE"
|
||||
links.new(geometry, delete.inputs["Geometry"])
|
||||
links.new(tiny.outputs[0], delete.inputs["Selection"])
|
||||
return delete.outputs["Geometry"]
|
||||
|
||||
|
||||
def _profile_field(group, geometry):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
profile = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
profile.data_type = "FLOAT"
|
||||
profile.inputs["Name"].default_value = BALLOON_ATTRIBUTE_NAME
|
||||
profile_max = nodes.new("GeometryNodeAttributeStatistic")
|
||||
profile_max.data_type = "FLOAT"
|
||||
profile_max.domain = "POINT"
|
||||
links.new(geometry, profile_max.inputs["Geometry"])
|
||||
links.new(profile.outputs["Attribute"], profile_max.inputs["Attribute"])
|
||||
safe_max = math_node(nodes, "MAXIMUM")
|
||||
safe_max.inputs[1].default_value = 1e-6
|
||||
links.new(profile_max.outputs["Max"], safe_max.inputs[0])
|
||||
normalized = math_node(nodes, "DIVIDE")
|
||||
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
|
||||
links.new(normalized.outputs[0], smooth.inputs["Value"])
|
||||
return profile.outputs["Attribute"], smooth.outputs["Result"]
|
||||
|
||||
|
||||
def _extrude_layers(group, geometry):
|
||||
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
|
||||
group.links.new(geometry, node.inputs["Mesh"])
|
||||
return node
|
||||
|
||||
|
||||
def _offset_geometry_z(group, geometry, z_value):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
offset = nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(z_value, offset.inputs["Z"])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
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):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
choose_z = nodes.new("GeometryNodeSwitch")
|
||||
choose_z.input_type = "FLOAT"
|
||||
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")
|
||||
separate = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs["Position"], separate.inputs["Vector"])
|
||||
combine = nodes.new("ShaderNodeCombineXYZ")
|
||||
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")
|
||||
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"
|
||||
links.new(set_position.outputs["Geometry"], side.inputs["Geometry"])
|
||||
links.new(extrude.outputs["Side"], side.inputs["Selection"])
|
||||
flip_side = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(side.outputs["Selection"], flip_side.inputs["Mesh"])
|
||||
front = _offset_geometry_z(
|
||||
group, geometry, front_z
|
||||
)
|
||||
back = _offset_geometry_z(
|
||||
group, geometry, back_z
|
||||
)
|
||||
flip_back = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(back, flip_back.inputs["Mesh"])
|
||||
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
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
|
||||
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
return merge.outputs["Geometry"]
|
||||
|
||||
|
||||
def _remove_zero_thickness(
|
||||
group,
|
||||
zero_geometry,
|
||||
shaped_geometry,
|
||||
thickness,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
zero = math_node(nodes, "LESS_THAN")
|
||||
zero.inputs[1].default_value = 1e-6
|
||||
links.new(thickness, zero.inputs[0])
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
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):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
uv.data_type = "FLOAT_VECTOR"
|
||||
uv.inputs["Name"].default_value = "UVMap"
|
||||
position_texture = nodes.new("GeometryNodeImageTexture")
|
||||
position_texture.interpolation = "Linear"
|
||||
position_texture.extension = "EXTEND"
|
||||
links.new(depth_image, position_texture.inputs["Image"])
|
||||
links.new(uv.outputs["Attribute"], position_texture.inputs["Vector"])
|
||||
return position_texture.outputs["Color"]
|
||||
|
||||
|
||||
def _shade_output(group, geometry):
|
||||
shade = group.nodes.new("GeometryNodeSetShadeSmooth")
|
||||
shade.domain = "FACE"
|
||||
shade.inputs["Shade Smooth"].default_value = True
|
||||
output = group.nodes.new("NodeGroupOutput")
|
||||
group.links.new(geometry, shade.inputs["Geometry"])
|
||||
group.links.new(shade.outputs["Geometry"], output.inputs["Geometry"])
|
||||
322
tools/nodes/common/depth_surface.py
Normal file
322
tools/nodes/common/depth_surface.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""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"]
|
||||
39
tools/nodes/common/materials.py
Normal file
39
tools/nodes/common/materials.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Build shared material inputs, roughness, and bump outputs."""
|
||||
BUMP_BASE_DISTANCE = 0.001
|
||||
|
||||
|
||||
def build_material_nodes(group):
|
||||
nodes = group.nodes
|
||||
group_input = nodes.new("NodeGroupInput")
|
||||
group_input.location = (-500.0, 100.0)
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
group_output.location = (300.0, 100.0)
|
||||
roughness_curve = nodes.new("ShaderNodeFloatCurve")
|
||||
roughness_curve.mapping.initialize()
|
||||
curve_points = roughness_curve.mapping.curves[0].points
|
||||
curve_points[0].location = (0.0, 0.5)
|
||||
curve_points[-1].location = (1.0, 0.1)
|
||||
roughness_curve.mapping.update()
|
||||
roughness_curve.location = (-250.0, 350.0)
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
bump.location = (100.0, 0.0)
|
||||
return group_input, group_output, roughness_curve, bump
|
||||
|
||||
|
||||
def connect_material_outputs(group, group_input, group_output, roughness_curve, bump, processed_alpha, bump_location):
|
||||
nodes, links = group.nodes, group.links
|
||||
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
|
||||
links.new(group_input.outputs["Color"], roughness_curve.inputs["Value"])
|
||||
links.new(
|
||||
roughness_curve.outputs["Value"],
|
||||
group_output.inputs["Roughness"],
|
||||
)
|
||||
links.new(processed_alpha, group_output.inputs["Alpha"])
|
||||
links.new(group_input.outputs["Color"], bump.inputs["Height"])
|
||||
bump_distance = nodes.new("ShaderNodeMath")
|
||||
bump_distance.operation = "MULTIPLY"
|
||||
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
|
||||
bump_distance.location = bump_location
|
||||
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
|
||||
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
|
||||
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
|
||||
106
tools/nodes/common/nodes.py
Normal file
106
tools/nodes/common/nodes.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Build interfaces and basic nodes shared by asset definitions."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def prepare_node_group(node_group):
|
||||
"""Hide unused outputs and discard disconnected interface nodes."""
|
||||
for node in list(node_group.nodes):
|
||||
for socket in node.outputs:
|
||||
socket.hide = not socket.is_linked
|
||||
if node.bl_idname == "NodeGroupInput" and not any(socket.is_linked for socket in node.outputs):
|
||||
node_group.nodes.remove(node)
|
||||
|
||||
|
||||
def interface_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, visible_names):
|
||||
node = nodes.new("NodeGroupInput")
|
||||
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 = interface_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 = interface_socket(group, name, "INPUT", "NodeSocketBool", parent)
|
||||
socket.default_value = default
|
||||
return socket
|
||||
|
||||
|
||||
def vector_input(group, name, default, subtype=None, parent=None):
|
||||
socket = interface_socket(group, name, "INPUT", "NodeSocketVector", parent)
|
||||
socket.default_value = default
|
||||
if subtype is not None:
|
||||
socket.subtype = subtype
|
||||
return socket
|
||||
|
||||
|
||||
def math_node(nodes, operation):
|
||||
node = nodes.new("ShaderNodeMath")
|
||||
node.operation = operation
|
||||
return node
|
||||
|
||||
|
||||
def menu_switch(group, control, names, kind):
|
||||
selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None)
|
||||
if selector is None:
|
||||
selector = group.nodes.new("GeometryNodeMenuSwitch")
|
||||
selector.data_type = "BOOLEAN"
|
||||
for item, name in zip(selector.enum_items, names):
|
||||
item.name = name
|
||||
selector.inputs[names[0]].default_value = False
|
||||
selector.inputs[names[1]].default_value = True
|
||||
local = group_input(group.nodes, {control})
|
||||
group.links.new(local.outputs[control], selector.inputs["Menu"])
|
||||
node = group.nodes.new("GeometryNodeSwitch")
|
||||
node.input_type = kind
|
||||
group.links.new(selector.outputs[0], node.inputs["Switch"])
|
||||
return SimpleNamespace(inputs={names[0]: node.inputs["False"], names[1]: node.inputs["True"]}, outputs=node.outputs)
|
||||
|
||||
|
||||
def read_float_attribute(group, name):
|
||||
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
node.data_type = "FLOAT"
|
||||
node.inputs["Name"].default_value = name
|
||||
return node.outputs["Attribute"]
|
||||
|
||||
|
||||
def store_float_attribute(group, geometry, name, value, domain="POINT"):
|
||||
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
node.domain, node.data_type = domain, "FLOAT"
|
||||
node.inputs["Name"].default_value = name
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
if isinstance(value, (int, float)):
|
||||
node.inputs["Value"].default_value = value
|
||||
else:
|
||||
group.links.new(value, node.inputs["Value"])
|
||||
return node.outputs["Geometry"]
|
||||
141
tools/nodes/common/smoothing.py
Normal file
141
tools/nodes/common/smoothing.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Build shared boundary fields and iterative smoothing."""
|
||||
from .nodes import group_input
|
||||
|
||||
|
||||
def edge_boundary_field(nodes, links):
|
||||
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
|
||||
loose_vertex = nodes.new("FunctionNodeCompare")
|
||||
loose_vertex.data_type = "INT"
|
||||
loose_vertex.operation = "EQUAL"
|
||||
loose_vertex_a = next(
|
||||
socket for socket in loose_vertex.inputs if socket.identifier == "A_INT"
|
||||
)
|
||||
loose_vertex_b = next(
|
||||
socket for socket in loose_vertex.inputs if socket.identifier == "B_INT"
|
||||
)
|
||||
loose_vertex_b.default_value = 1
|
||||
links.new(vertex_neighbors.outputs["Vertex Count"], loose_vertex_a)
|
||||
|
||||
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
boundary_edge = nodes.new("FunctionNodeCompare")
|
||||
boundary_edge.data_type = "INT"
|
||||
boundary_edge.operation = "EQUAL"
|
||||
boundary_edge_a = next(
|
||||
socket for socket in boundary_edge.inputs if socket.identifier == "A_INT"
|
||||
)
|
||||
boundary_edge_b = next(
|
||||
socket for socket in boundary_edge.inputs if socket.identifier == "B_INT"
|
||||
)
|
||||
boundary_edge_b.default_value = 1
|
||||
links.new(edge_neighbors.outputs["Face Count"], boundary_edge_a)
|
||||
edge_domain = nodes.new("GeometryNodeFieldOnDomain")
|
||||
edge_domain.data_type = "BOOLEAN"
|
||||
edge_domain.domain = "EDGE"
|
||||
links.new(boundary_edge.outputs["Result"], edge_domain.inputs["Value"])
|
||||
|
||||
is_boundary = nodes.new("FunctionNodeBooleanMath")
|
||||
is_boundary.operation = "OR"
|
||||
links.new(edge_domain.outputs["Value"], is_boundary.inputs[0])
|
||||
links.new(loose_vertex.outputs["Result"], is_boundary.inputs[1])
|
||||
return is_boundary.outputs["Boolean"]
|
||||
|
||||
|
||||
def expand_smooth(
|
||||
group, geometry, iterations_name="Iterations", weight_name="Smooth"
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
iteration_controls = group_input(nodes, {iterations_name})
|
||||
weight_controls = group_input(nodes, {weight_name})
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
||||
links.new(
|
||||
iteration_controls.outputs[iterations_name],
|
||||
repeat_input.inputs["Iterations"],
|
||||
)
|
||||
links.new(geometry, repeat_input.inputs["Geometry"])
|
||||
|
||||
boundary_field = edge_boundary_field(nodes, links)
|
||||
boundary_points = nodes.new("GeometryNodeFieldOnDomain")
|
||||
boundary_points.data_type = "BOOLEAN"
|
||||
boundary_points.domain = "POINT"
|
||||
links.new(boundary_field, boundary_points.inputs["Value"])
|
||||
|
||||
boundary_geometry = nodes.new("GeometryNodeSeparateGeometry")
|
||||
boundary_geometry.domain = "POINT"
|
||||
links.new(repeat_input.outputs["Geometry"], boundary_geometry.inputs["Geometry"])
|
||||
links.new(boundary_points.outputs["Value"], boundary_geometry.inputs["Selection"])
|
||||
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
smooth_all = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_all.data_type = "FLOAT_VECTOR"
|
||||
smooth_all.inputs["Iterations"].default_value = 1
|
||||
smooth_all.inputs["Weight"].default_value = 1.0
|
||||
links.new(position.outputs["Position"], smooth_all.inputs["Value"])
|
||||
|
||||
boundary_position = nodes.new("GeometryNodeInputPosition")
|
||||
smooth_boundary = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_boundary.data_type = "FLOAT_VECTOR"
|
||||
smooth_boundary.inputs["Iterations"].default_value = 1
|
||||
smooth_boundary.inputs["Weight"].default_value = 0.5
|
||||
links.new(boundary_position.outputs["Position"], smooth_boundary.inputs["Value"])
|
||||
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
|
||||
nearest_boundary.domain = "POINT"
|
||||
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
|
||||
sample_boundary = nodes.new("GeometryNodeSampleIndex")
|
||||
sample_boundary.data_type = "FLOAT_VECTOR"
|
||||
sample_boundary.domain = "POINT"
|
||||
sample_boundary.clamp = False
|
||||
links.new(boundary_geometry.outputs["Selection"], sample_boundary.inputs["Geometry"])
|
||||
links.new(smooth_boundary.outputs["Value"], sample_boundary.inputs["Value"])
|
||||
links.new(nearest_boundary.outputs["Index"], sample_boundary.inputs["Index"])
|
||||
|
||||
choose_boundary = nodes.new("GeometryNodeSwitch")
|
||||
choose_boundary.input_type = "VECTOR"
|
||||
direct_boundary = edge_boundary_field(nodes, links)
|
||||
links.new(direct_boundary, choose_boundary.inputs["Switch"])
|
||||
links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"])
|
||||
links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"])
|
||||
boundary_mix = nodes.new("ShaderNodeMix")
|
||||
boundary_mix.data_type = "VECTOR"
|
||||
boundary_mix.inputs[0].default_value = 1.0
|
||||
links.new(smooth_all.outputs["Value"], boundary_mix.inputs[4])
|
||||
links.new(choose_boundary.outputs["Output"], boundary_mix.inputs[5])
|
||||
|
||||
normal = nodes.new("GeometryNodeInputNormal")
|
||||
smooth_normal = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_normal.data_type = "FLOAT_VECTOR"
|
||||
smooth_normal.inputs["Iterations"].default_value = 10
|
||||
smooth_normal.inputs["Weight"].default_value = 1.0
|
||||
links.new(normal.outputs["Normal"], smooth_normal.inputs["Value"])
|
||||
normal_coherence = nodes.new("ShaderNodeVectorMath")
|
||||
normal_coherence.operation = "LENGTH"
|
||||
links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0])
|
||||
sharp_switch = nodes.new("GeometryNodeSwitch")
|
||||
sharp_switch.input_type = "FLOAT"
|
||||
sharp_switch.inputs["Switch"].default_value = True
|
||||
sharp_switch.inputs["False"].default_value = 1.0
|
||||
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
|
||||
smooth_weight = nodes.new("ShaderNodeMath")
|
||||
smooth_weight.operation = "MULTIPLY"
|
||||
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
|
||||
links.new(sharp_switch.outputs["Output"], smooth_weight.inputs[1])
|
||||
|
||||
target_position = nodes.new("GeometryNodeSwitch")
|
||||
target_position.input_type = "VECTOR"
|
||||
target_position.inputs["Switch"].default_value = True
|
||||
links.new(smooth_all.outputs["Value"], target_position.inputs["False"])
|
||||
links.new(boundary_mix.outputs["Result"], target_position.inputs["True"])
|
||||
current_position = nodes.new("GeometryNodeInputPosition")
|
||||
mix_position = nodes.new("ShaderNodeMix")
|
||||
mix_position.data_type = "VECTOR"
|
||||
links.new(smooth_weight.outputs[0], mix_position.inputs[0])
|
||||
links.new(current_position.outputs["Position"], mix_position.inputs[4])
|
||||
links.new(target_position.outputs["Output"], mix_position.inputs[5])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(repeat_input.outputs["Geometry"], set_position.inputs["Geometry"])
|
||||
links.new(mix_position.outputs["Result"], set_position.inputs["Position"])
|
||||
links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"])
|
||||
return repeat_output.outputs["Geometry"]
|
||||
Reference in New Issue
Block a user