Refactor depth surface handling and unify depth sampling rules

- Removed the `always_vertex` parameter from `build_surface_camera` to streamline depth sampling for cutouts and ensure consistent behavior across depth plane and panorama.
- Updated depth sampling logic to ensure that all free boundary points, including the original frame of the depth plane, use neighboring face depth instead of their own texture sampling.
- Adjusted the depth mask deletion process to operate on faces rather than points, eliminating the need for additional cleanup of dangling edges.
- Ensured that the cutout rules are consistently applied across all relevant nodes, removing exceptions and simplifying the logic.
- Updated tests to verify the new behavior, ensuring that cutout points follow the depth of adjacent faces and that the original frame maintains its depth sampling.
- Refactored geometry handling in multiple depth-related nodes to improve clarity and maintainability.
This commit is contained in:
2026-09-16 16:30:56 +08:00
parent 7f69630371
commit c62ec2cd48
28 changed files with 880 additions and 173 deletions

View File

@@ -2,7 +2,7 @@
from .nodes import evaluate_field, group_input
from .depth_surface import _math
from .smoothing import edge_boundary_field, expand_smooth
from .smoothing import edge_boundary_field, pinned_smooth
def boundary_influence(group, protected=False, *, boundary=None):
@@ -92,9 +92,10 @@ def cut_boundary_influence(group, split_boundary, protected=False, *, outline_sc
def smooth_cut_boundary(group, geometry, split_boundary=False, *, protected=False, outline_scale=0.1, boundary=None):
"""Move boundary vertices toward the smoothed position through split and outline influence."""
"""Move boundary vertices toward the pinned position through split and outline influence."""
influence = cut_boundary_influence(
group, split_boundary, protected, outline_scale=outline_scale, boundary=boundary,
)
# The support band depends on topology, which stays unchanged in the loop.
return expand_smooth(group, geometry, "Boundary Smooth", influence)
iterations = group_input(group.nodes, {"Boundary Smooth"}).outputs["Boundary Smooth"]
return pinned_smooth(group, geometry, iterations, influence)

View File

@@ -1,6 +1,7 @@
"""Face separation and connected thickness for Depth Surface assets."""
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attribute_pattern, sample_field
from .smoothing import edge_boundary_field
CUT_ATTRIBUTE = "_o_anyimage_depth_cut"
@@ -275,21 +276,24 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
return choose.outputs["Output"], face_camera, cut
def build_surface_camera(group, original, separated, cut_vertex, strength, *, always_vertex=None):
"""Change cut depth along the original ray, preserving every other vertex."""
enabled = group.nodes.new("GeometryNodeSwitch")
def build_surface_camera(group, original, separated, cut_vertex, strength):
"""Take cut depth from the adjacent face sample along each unchanged ray.
Depth Split edges and every open boundary left by Depth Mask deletion share
one replacement, so each cut edge sits on the surface it belongs to.
"""
nodes, links = group.nodes, group.links
cut_boundary = evaluate_field(group, edge_boundary_field(nodes, links), "BOOLEAN", "POINT")
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "BOOLEAN"
group.links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
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"])
if always_vertex is not None:
combined = group.nodes.new("FunctionNodeBooleanMath")
combined.operation = "OR"
group.links.new(enabled.outputs["Output"], combined.inputs[0])
group.links.new(always_vertex, combined.inputs[1])
enabled = combined.outputs["Boolean"]
else:
enabled = enabled.outputs["Output"]
links.new(cut_vertex, enabled.inputs["True"])
combined = nodes.new("FunctionNodeBooleanMath")
combined.operation = "OR"
links.new(enabled.outputs["Output"], combined.inputs[0])
links.new(cut_boundary, combined.inputs[1])
enabled = combined.outputs["Boolean"]
original_axes = group.nodes.new("ShaderNodeSeparateXYZ")
separated_axes = group.nodes.new("ShaderNodeSeparateXYZ")
group.links.new(original, original_axes.inputs[0])

View File

@@ -1,8 +1,9 @@
"""Build shared boundary fields and iterative smoothing."""
from .nodes import group_input, sample_field
"""Build shared boundary fields and pinned position smoothing."""
from .nodes import evaluate_field, sample_field
def edge_boundary_field(nodes, links):
"""Report the points sitting on an open or loose boundary."""
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
loose_vertex = nodes.new("FunctionNodeCompare")
loose_vertex.data_type = "INT"
@@ -40,54 +41,46 @@ def edge_boundary_field(nodes, links):
return is_boundary.outputs["Boolean"]
def expand_smooth(group, geometry, iterations_name, influence):
def _multiply(group, a, b):
node = group.nodes.new("ShaderNodeMath")
node.operation = "MULTIPLY"
for index, value in enumerate((a, b)):
if isinstance(value, (int, float)):
node.inputs[index].default_value = value
else:
group.links.new(value, node.inputs[index])
return node.outputs[0]
def _boundary_target(group, geometry, current, boundary_points):
"""Blur the boundary strip alone and sample it back onto every point."""
nodes = group.nodes
links = group.links
iteration_controls = group_input(nodes, {iterations_name})
repeat_output = nodes.new("GeometryNodeRepeatOutput")
repeat_input = nodes.new("GeometryNodeRepeatInput")
repeat_input.pair_with_output(repeat_output)
links.new(
iteration_controls.outputs[iterations_name],
repeat_input.inputs["Iterations"],
)
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"])
links.new(geometry, repeat_input.inputs["Geometry"])
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
nearest_boundary.domain = "POINT"
# Both boundary meshes keep the same compact index order: only positions change.
source_boundary = nodes.new("GeometryNodeSeparateGeometry")
source_boundary.domain = "POINT"
links.new(geometry, source_boundary.inputs["Geometry"])
links.new(boundary_points.outputs["Value"], source_boundary.inputs["Selection"])
links.new(source_boundary.outputs["Selection"], nearest_boundary.inputs["Geometry"])
source_position = nodes.new("GeometryNodeInputPosition").outputs["Position"]
fixed_position = sample_field(group, geometry, source_position, "FLOAT_VECTOR")
links.new(fixed_position, nearest_boundary.inputs["Sample Position"])
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"])
links.new(current, boundary_geometry.inputs["Geometry"])
links.new(boundary_points, boundary_geometry.inputs["Selection"])
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"
# Both boundary meshes keep the same compact index order: only positions change,
# so the lookup can stay anchored on the untouched input geometry.
source_boundary = nodes.new("GeometryNodeSeparateGeometry")
source_boundary.domain = "POINT"
links.new(geometry, source_boundary.inputs["Geometry"])
links.new(boundary_points, source_boundary.inputs["Selection"])
links.new(source_boundary.outputs["Selection"], nearest_boundary.inputs["Geometry"])
fixed_position = sample_field(
group, geometry, boundary_position.outputs["Position"], "FLOAT_VECTOR"
)
links.new(fixed_position, nearest_boundary.inputs["Sample Position"])
sample_boundary = nodes.new("GeometryNodeSampleIndex")
sample_boundary.data_type = "FLOAT_VECTOR"
sample_boundary.domain = "POINT"
@@ -95,26 +88,90 @@ def expand_smooth(group, geometry, iterations_name, influence):
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"])
return sample_boundary.outputs["Value"]
choose_boundary = nodes.new("GeometryNodeSwitch")
choose_boundary.input_type = "VECTOR"
links.new(boundary_points.outputs["Value"], choose_boundary.inputs["Switch"])
links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"])
links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"])
current_position = nodes.new("GeometryNodeInputPosition")
mix_position = nodes.new("ShaderNodeMix")
mix_position.data_type = "VECTOR"
links.new(influence, mix_position.inputs[0])
links.new(current_position.outputs["Position"], mix_position.inputs[4])
links.new(choose_boundary.outputs["Output"], mix_position.inputs[5])
def pinned_smooth(
group,
geometry,
iterations,
region,
weight=1.0,
movement=None,
pin_boundary=True,
pin_sharp=True,
):
"""Smooth positions toward a pinned target inside an influence region.
``pin_boundary`` sends boundary points to a boundary-only blur so an outline
is never dragged inward. ``pin_sharp`` lowers the weight of creases through
the sharpness of the normal field. Junction bands keep the sharpness weight
without the boundary rule, where the influenced points are not a boundary.
"""
nodes, links = group.nodes, group.links
repeat_output = nodes.new("GeometryNodeRepeatOutput")
repeat_input = nodes.new("GeometryNodeRepeatInput")
repeat_input.pair_with_output(repeat_output)
links.new(iterations, repeat_input.inputs["Iterations"])
links.new(geometry, repeat_input.inputs["Geometry"])
current = repeat_input.outputs["Geometry"]
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"])
target = smooth_all.outputs["Value"]
if pin_boundary:
boundary_points = evaluate_field(
group, edge_boundary_field(nodes, links), "BOOLEAN", "POINT"
)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "VECTOR"
links.new(boundary_points, choose.inputs["Switch"])
links.new(smooth_all.outputs["Value"], choose.inputs["False"])
links.new(
_boundary_target(group, geometry, current, boundary_points),
choose.inputs["True"],
)
target = choose.outputs["Output"]
if pin_sharp:
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"])
coherence = nodes.new("ShaderNodeVectorMath")
coherence.operation = "LENGTH"
links.new(smooth_normal.outputs["Value"], coherence.inputs[0])
weight = _multiply(group, weight, coherence.outputs["Value"])
weight = _multiply(group, weight, region)
difference = nodes.new("ShaderNodeVectorMath")
difference.operation = "SUBTRACT"
links.new(target, difference.inputs[0])
links.new(position.outputs["Position"], difference.inputs[1])
offset = nodes.new("ShaderNodeVectorMath")
offset.operation = "SCALE"
links.new(difference.outputs[0], offset.inputs[0])
links.new(weight, offset.inputs[3])
if movement is not None:
masked = nodes.new("ShaderNodeVectorMath")
masked.operation = "MULTIPLY"
links.new(offset.outputs[0], masked.inputs[0])
masked.inputs[1].default_value = movement
offset = masked
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(current, set_position.inputs["Geometry"])
links.new(offset.outputs[0], set_position.inputs["Offset"])
links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"])
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "GEOMETRY"
links.new(iteration_controls.outputs[iterations_name], enabled.inputs["Switch"])
links.new(iterations, enabled.inputs["Switch"])
links.new(geometry, enabled.inputs["False"])
links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"])
return enabled.outputs["Output"]

View File

@@ -1,21 +1,10 @@
"""Remove invalid vertices and the wire edges left by point deletion."""
"""Delete the faces the depth mask excludes."""
def delete_invalid_points(group, geometry, invalid):
def delete_invalid_faces(group, geometry, invalid):
nodes, links = group.nodes, group.links
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "POINT", "ALL"
delete.domain, delete.mode = "FACE", "ALL"
links.new(geometry, delete.inputs["Geometry"])
links.new(invalid, delete.inputs["Selection"])
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
wire = nodes.new("FunctionNodeCompare")
wire.data_type, wire.operation = "INT", "EQUAL"
a = next(socket for socket in wire.inputs if socket.identifier == "A_INT")
b = next(socket for socket in wire.inputs if socket.identifier == "B_INT")
b.default_value = 0
links.new(neighbors.outputs["Face Count"], a)
cleanup = nodes.new("GeometryNodeDeleteGeometry")
cleanup.domain, cleanup.mode = "EDGE", "ALL"
links.new(delete.outputs["Geometry"], cleanup.inputs["Geometry"])
links.new(wire.outputs["Result"], cleanup.inputs["Selection"])
return cleanup.outputs["Geometry"]
return delete.outputs["Geometry"]

View File

@@ -2,6 +2,8 @@
import bpy
from mathutils import Matrix
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import (
interface_socket, group_input, float_input, bool_input, vector_input,
evaluate_field, store_float_attribute, read_float_attribute, prepare_node_group,
@@ -9,10 +11,16 @@ from ..common.nodes import (
from ..common.depth_surface import _math
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
from ..common.direction import symmetry_legacy_direction_to_canonical
from ..common.smoothing import edge_boundary_field
from ..common.smoothing import edge_boundary_field, pinned_smooth
NODE_GROUP_NAME = "O Image Cutout Symmetry"
# 衔接带从轮廓向内覆盖的圈数。
FILL_SMOOTH_RINGS = 4
# 对称面附近按中位边长的倍数降低法线贴图强度。
SEAM_NORMAL_BAND = 4
def _align_front(group, geometry, controls):
"""Align existing geometry with the fixed XY symmetry plane."""
@@ -162,25 +170,26 @@ def _mirror(group, geometry):
return flip.outputs[0]
def _cut_fields(group, tolerance):
"""Return point-domain seam and outline fields for the cut."""
def _boundary_fields(group, tolerance):
"""Return the folded front's boundary field and its on-plane seam."""
nodes, links = group.nodes, group.links
boundary = evaluate_field(group, edge_boundary_field(nodes, links), 'FLOAT', 'POINT')
position = nodes.new('GeometryNodeInputPosition')
components = nodes.new('ShaderNodeSeparateXYZ')
links.new(position.outputs[0], components.inputs[0])
absolute = _math(group, 'ABSOLUTE', components.outputs['Y'])
def split(side):
node = nodes.new('FunctionNodeBooleanMath')
node.operation = 'AND'
links.new(_math(group, 'GREATER_THAN', boundary, 0), node.inputs[0])
links.new(side, node.inputs[1])
return node.outputs[0]
return split(_math(group, 'LESS_THAN', absolute, tolerance)), split(
_math(group, 'GREATER_THAN', absolute, tolerance)
seam = nodes.new('FunctionNodeBooleanMath')
seam.operation = 'AND'
links.new(_math(group, 'GREATER_THAN', boundary, 0), seam.inputs[0])
links.new(
_math(
group,
'LESS_THAN',
_math(group, 'ABSOLUTE', components.outputs['Y']),
tolerance,
),
seam.inputs[1],
)
return boundary, seam.outputs[0]
def _falloff(group, center, rings=2):
@@ -192,35 +201,28 @@ def _falloff(group, center, rings=2):
return _math(group, 'MAXIMUM', center, blur.outputs[0])
def _apply_planar_relax(group, geometry, influence, smooth):
"""Move points inside the plane by their blurred position, gated by influence."""
nodes, links = group.nodes, group.links
position = nodes.new('GeometryNodeInputPosition')
blur = nodes.new('GeometryNodeBlurAttribute')
blur.data_type = 'FLOAT_VECTOR'
links.new(position.outputs[0], blur.inputs['Value'])
links.new(smooth, blur.inputs['Iterations'])
difference = nodes.new('ShaderNodeVectorMath')
difference.operation = 'SUBTRACT'
links.new(blur.outputs[0], difference.inputs[0])
links.new(position.outputs[0], difference.inputs[1])
in_plane = nodes.new('ShaderNodeVectorMath')
in_plane.operation = 'MULTIPLY'
in_plane.inputs[1].default_value = (.5, 0, .5)
links.new(difference.outputs[0], in_plane.inputs[0])
scaled = nodes.new('ShaderNodeVectorMath')
scaled.operation = 'SCALE'
links.new(in_plane.outputs[0], scaled.inputs[0])
links.new(influence, scaled.inputs[3])
place = nodes.new('GeometryNodeSetPosition')
links.new(geometry, place.inputs['Geometry'])
links.new(scaled.outputs[0], place.inputs['Offset'])
return place.outputs[0]
def _relax_in_plane(group, geometry, region, iterations, pin_boundary):
"""Relax points without leaving the symmetry plane."""
return pinned_smooth(
group,
geometry,
iterations,
region,
weight=0.5,
movement=(1, 0, 1),
pin_boundary=pin_boundary,
)
def _relax_seam(group, geometry, seam_field, smooth):
"""Relax the cut seam in plane through a two-ring falloff."""
return _apply_planar_relax(group, geometry, _falloff(group, seam_field), smooth)
return _relax_in_plane(group, geometry, _falloff(group, seam_field), smooth, pin_boundary=True)
def _relax_fill(group, geometry, boundary_field, smooth):
"""Smooth the junction band the wall builds from, including the symmetry axis."""
region = _falloff(group, boundary_field, FILL_SMOOTH_RINGS)
return _relax_in_plane(group, geometry, region, smooth, pin_boundary=False)
def _extrude_boundary(group, geometry, selection, y_factor):
@@ -271,7 +273,7 @@ def build_image_cutout_symmetry_group():
bool_input(group, 'Fill Sides', True, parent=options).description = 'Connect matching front and mirrored boundaries.'
smooth = interface_socket(group, 'Fill Smooth', 'INPUT', 'NodeSocketInt', options)
smooth.default_value, smooth.min_value, smooth.max_value = 2, 0, 16
smooth.description = 'Smooth the outline and the wall it builds through a two-ring falloff. Zero keeps the raw outline.'
smooth.description = 'Smooth the band around the front boundary and the symmetry seam over four rings. Zero keeps the raw outline.'
seam = interface_socket(group, 'Seam Smooth', 'INPUT', 'NodeSocketInt', options)
seam.default_value, seam.min_value, seam.max_value = 2, 0, 16
seam.description = 'Smooth the cut seam inside the symmetry plane. Zero keeps the raw seam.'
@@ -279,16 +281,21 @@ def build_image_cutout_symmetry_group():
nodes, links = group.nodes, group.links
inputs = group_input(nodes, set()).outputs
aligned = _align_front(group, inputs['Geometry'], inputs)
bounds = nodes.new('GeometryNodeBoundBox')
links.new(aligned, bounds.inputs['Geometry'])
size = nodes.new('ShaderNodeVectorMath')
size.operation = 'DISTANCE'
links.new(bounds.outputs['Min'], size.inputs[0])
links.new(bounds.outputs['Max'], size.inputs[1])
tolerance = _math(group, 'MULTIPLY', _math(group, 'MAXIMUM', size.outputs['Value'], 1e-8), 1e-7)
edge = nodes.new('GeometryNodeInputMeshEdgeVertices')
edge_length = nodes.new('ShaderNodeVectorMath')
edge_length.operation = 'DISTANCE'
links.new(edge.outputs['Position 1'], edge_length.inputs[0])
links.new(edge.outputs['Position 2'], edge_length.inputs[1])
statistics = nodes.new('GeometryNodeAttributeStatistic')
statistics.data_type, statistics.domain = 'FLOAT', 'EDGE'
links.new(aligned, statistics.inputs['Geometry'])
links.new(edge_length.outputs['Value'], statistics.inputs['Attribute'])
snap_tolerance = _math(
group, 'MULTIPLY', _math(group, 'MAXIMUM', statistics.outputs['Median'], 1e-6), 1e-6,
)
is_boundary = _boundary_edge_field(group)
seam_field, outline_field = _cut_fields(group, tolerance)
front = _fold_to_plane(group, aligned, tolerance)
boundary_field, seam_field = _boundary_fields(group, snap_tolerance)
front = _fold_to_plane(group, aligned, snap_tolerance)
shade = nodes.new('GeometryNodeSetShadeSmooth')
shade.domain = 'FACE'
links.new(front, shade.inputs['Geometry'])
@@ -298,9 +305,9 @@ def build_image_cutout_symmetry_group():
boundary.operation = 'AND'
links.new(is_boundary, boundary.inputs[0])
# The seam sits on the plane and welds through the merge, so only off-plane edges need walls.
links.new(_off_plane_edge_field(group, tolerance), boundary.inputs[1])
links.new(_off_plane_edge_field(group, snap_tolerance), boundary.inputs[1])
fill_smooth = _math(group, 'MULTIPLY', inputs['Fill Smooth'], inputs['Fill Sides'])
front = _apply_planar_relax(group, front, _falloff(group, outline_field), fill_smooth)
front = _relax_fill(group, front, boundary_field, fill_smooth)
walled = _build_wall(group, front, boundary.outputs[0])
wall_shade = nodes.new('GeometryNodeSetShadeSmooth')
wall_shade.domain = 'FACE'
@@ -320,13 +327,37 @@ def build_image_cutout_symmetry_group():
join = nodes.new('GeometryNodeJoinGeometry')
for geometry in (body.outputs[0], mark_back.outputs['Geometry']):
links.new(geometry, join.inputs[0])
position = nodes.new('GeometryNodeInputPosition')
axis = nodes.new('ShaderNodeSeparateXYZ')
links.new(position.outputs[0], axis.inputs[0])
on_axis = _math(group, 'LESS_THAN', _math(group, 'ABSOLUTE', axis.outputs['Y']), snap_tolerance)
merge = nodes.new('GeometryNodeMergeByDistance')
links.new(tolerance, merge.inputs['Distance'])
merge.inputs['Distance'].default_value = 0.01
links.new(join.outputs[0], merge.inputs['Geometry'])
links.new(on_axis, merge.inputs['Selection'])
geometry = store_float_attribute(group, merge.outputs[0], AXIS_ATTRIBUTE, 1., domain='FACE')
orient = nodes.new('GeometryNodeTransform')
orient.inputs['Rotation'].default_value = Matrix.Rotation(1.5707963267948966, 4, 'Z').to_euler()
links.new(geometry, orient.inputs['Geometry'])
links.new(orient.outputs[0], nodes.new('NodeGroupOutput').inputs['Geometry'])
# Fade the material normal map near the symmetry plane so the weld shades smoother.
position = nodes.new('GeometryNodeInputPosition')
components = nodes.new('ShaderNodeSeparateXYZ')
links.new(position.outputs[0], components.inputs[0])
ramp = nodes.new('ShaderNodeMapRange')
ramp.interpolation_type = 'SMOOTHSTEP'
ramp.clamp = True
links.new(_math(group, 'ABSOLUTE', components.outputs['X']), ramp.inputs['Value'])
ramp.inputs['From Min'].default_value = 0.0
links.new(_math(group, 'MULTIPLY', statistics.outputs['Median'], SEAM_NORMAL_BAND), ramp.inputs['From Max'])
ramp.inputs['To Min'].default_value = 1.0
ramp.inputs['To Max'].default_value = 0.0
reduction = _math(
group, 'MAXIMUM',
read_float_attribute(group, NORMAL_REDUCTION_ATTRIBUTE_NAME), ramp.outputs['Result'],
)
geometry = store_float_attribute(
group, orient.outputs[0], NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction,
)
links.new(geometry, nodes.new('NodeGroupOutput').inputs['Geometry'])
prepare_node_group(group)
return group

View File

@@ -276,10 +276,9 @@ def _build_depth_surface(group, geometry, controls):
)
geometry = remove_boundary_triangles(group, geometry)
boundary = edge_boundary_field(group.nodes, group.links)
outline = evaluate_field(group, boundary, "BOOLEAN", "POINT")
rim = evaluate_field(group, boundary, "FLOAT", "POINT")
camera = build_surface_camera(
group, _position_field(group, image), corner_camera, cut_vertex, split, always_vertex=outline,
group, _position_field(group, image), corner_camera, cut_vertex, split,
)
projection = project_depth_surface(
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"],

View File

@@ -4,7 +4,7 @@ import math
import bpy
from ..common.validity import delete_invalid_points
from ..common.validity import delete_invalid_faces
from ..common.depth_surface import (
_math, split_depth_surface, build_surface_camera,
@@ -138,7 +138,7 @@ def build_image_depth_panorama_group():
mixed = _math(group, "MULTIPLY", any_invalid,
_math(group, "LESS_THAN", face_invalid, 1))
protected = uv_map.outputs["Geometry"]
culled = delete_invalid_points(group, protected, any_invalid)
culled = delete_invalid_faces(group, protected, any_invalid)
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(protected, delete.inputs["Geometry"])

View File

@@ -2,7 +2,7 @@
import bpy
from ..common.validity import delete_invalid_points
from ..common.validity import delete_invalid_faces
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
from ..common.nodes import prepare_node_group, float_input, evaluate_field
@@ -107,7 +107,7 @@ def build_image_depth_plane_group(image_plane_group):
_math(group, "MINIMUM", _math(group, "SUBTRACT", 1, u), _math(group, "SUBTRACT", 1, v)))
original_boundary = evaluate_field(group, _math(group, "LESS_THAN", margin, 1e-6), "BOOLEAN", "POINT")
protected = plane.outputs["Geometry"]
culled = delete_invalid_points(group, protected, invalid.outputs["Result"])
culled = delete_invalid_faces(group, protected, invalid.outputs["Result"])
cull_controls = _group_input(nodes, {"Depth Mask"})
cull = nodes.new("GeometryNodeSwitch")
cull.input_type = "GEOMETRY"

View File

@@ -19,7 +19,7 @@ def build_image_layer_group():
input_sockets.insert(4, ('Object Space', 'NodeSocketBool'))
input_sockets.insert(2, ('Alpha Fix', 'NodeSocketFloat'))
options = interface.new_panel(name='Options', default_closed=True)
option_names = {'Alpha Fix', 'Normal Scale', 'Object Space'}
option_names = {'Alpha Fix', 'Object Space'}
for name, socket_type in input_sockets:
socket = interface.new_socket(name=name, in_out='INPUT', socket_type=socket_type, parent=options if name in option_names else None)
if name in {'Alpha', 'Alpha Fix'}: