- Added a new proposal for boundary smoothing in the weight cutout process, differentiating between Split edges and Outline smoothing requirements. - Introduced a new capability `cutout-boundary-smoothing` to control smoothing strength and default values. - Updated specifications to define the behavior of boundary-specific smoothing strength and default settings. - Created tasks to verify the implementation of boundary behavior and asset updates. - Developed comprehensive tests for depth symmetry and normal scaling to ensure expected behavior and performance. - Introduced new attributes for normal mapping to facilitate persistent face attributes sharing between depth symmetry and image materials. - Built a new node group for O Image Depth Symmetry, integrating the new smoothing capabilities and ensuring compatibility with existing workflows.
437 lines
21 KiB
Python
437 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):
|
|
"""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"])
|
|
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.outputs["Output"], 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", "Y")):
|
|
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["Y"], 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["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")
|
|
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", "Y"):
|
|
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
|
|
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Z"]), flattened.inputs["Z"])
|
|
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["Z"], source_thickness)
|
|
links.new(distance, displacement.inputs["Z"])
|
|
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(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 remove_attributes(
|
|
group, project_geometry.outputs["Geometry"], CUT_ATTRIBUTE,
|
|
enabled=_math(group, "GREATER_THAN", split_strength, 0.0),
|
|
)
|