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:
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'}:
|
||||
|
||||
Reference in New Issue
Block a user