Add tests and implement boundary smoothing and cutout functionality
- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity. - Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending. - Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised. - Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry. - Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity. - Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles. - Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
"""Face separation and connected thickness for Depth Surface assets."""
|
||||
|
||||
from .nodes import capture_fields, evaluate_field, store_float_attribute
|
||||
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):
|
||||
@@ -211,10 +214,9 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
cut_vertices.data_type = "FLOAT"
|
||||
cut_vertices.domain = "POINT"
|
||||
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
|
||||
corners, captured = capture_fields(
|
||||
group, geometry, "CORNER",
|
||||
Camera=("VECTOR", face_camera),
|
||||
Cut=("BOOLEAN", _math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0)),
|
||||
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"])
|
||||
@@ -225,17 +227,16 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
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"], captured["Camera"], captured["Cut"]
|
||||
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 = _math(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
_math(group, "GREATER_THAN", strength, 0.0),
|
||||
cut_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])
|
||||
@@ -249,7 +250,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength):
|
||||
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(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")
|
||||
@@ -261,36 +262,32 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
index = nodes.new("GeometryNodeInputIndex")
|
||||
fields = {"Position": ("VECTOR", position.outputs[0]), "Vertex": ("INT", index.outputs[0])}
|
||||
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])
|
||||
fields["Normal"] = ("VECTOR", normalize.outputs[0])
|
||||
geometry, captured = capture_fields(group, geometry, "POINT", **fields)
|
||||
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"])
|
||||
saved = sheet.outputs["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.
|
||||
# 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(captured["Vertex"], part.inputs[0])
|
||||
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"
|
||||
@@ -315,53 +312,62 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
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, 0.0, domain="FACE")
|
||||
rim = store_float_attribute(group, rim, back_mask, 0.0, domain="FACE")
|
||||
for value in (
|
||||
saved,
|
||||
rear,
|
||||
rim,
|
||||
):
|
||||
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"])
|
||||
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(encoded.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
links.new(construction, 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])
|
||||
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], thickness)
|
||||
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(captured["Position"], restored.inputs[0])
|
||||
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"])
|
||||
local_position = nodes.new("GeometryNodeInputPosition")
|
||||
local_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(local_position.outputs[0], local_axes.inputs[0])
|
||||
layer = local_axes.outputs["Z"]
|
||||
if weld_zero_thickness:
|
||||
positive = _math(group, "GREATER_THAN", thickness, 1e-12)
|
||||
layer = _math(group, "MULTIPLY", positive, layer)
|
||||
links.new(layer, 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])
|
||||
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(captured["Normal"], normal_offset.inputs[0])
|
||||
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": captured["Position"],
|
||||
"front": source_position,
|
||||
"source_index": source_index,
|
||||
"position": restored.outputs[0],
|
||||
"position_input": result.inputs["Position"],
|
||||
}
|
||||
@@ -374,7 +380,7 @@ def _projection_math(nodes, operation):
|
||||
|
||||
|
||||
def project_depth_surface(
|
||||
group, geometry, camera_position, uniform_scale, reference_depth, depth_scale
|
||||
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
|
||||
@@ -414,4 +420,7 @@ def project_depth_surface(
|
||||
links.new(geometry, project_geometry.inputs["Geometry"])
|
||||
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])
|
||||
|
||||
return project_geometry.outputs["Geometry"]
|
||||
return remove_attributes(
|
||||
group, project_geometry.outputs["Geometry"], CUT_ATTRIBUTE,
|
||||
enabled=_math(group, "GREATER_THAN", split_strength, 0.0),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user