Refactor depth node groups and simplify options
- Removed `Smooth Weight` input from `O Image Depth Plane`, `O Image Depth Cutout`, and `O Image Depth Panorama` to streamline the interface and maintain consistent boundary smoothing behavior. - Adjusted the `Reference Depth` input to appear last in the options panel for better organization, reflecting its role as a calibration value. - Consolidated the `expand_smooth()` function to only accept necessary parameters, eliminating unused branches related to `Smooth Weight` and `pin_sharp`. - Updated related tests and node assets to ensure functionality remains intact and consistent with the new structure. - Introduced new specifications to document the changes and ensure compliance with the updated requirements.
This commit is contained in:
@@ -92,12 +92,9 @@ 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):
|
||||
"""Blend O Smooth through split and outline masks with configurable weights."""
|
||||
"""Move boundary vertices toward the smoothed 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, iterations_name="Boundary Smooth", weight_name="Smooth Weight",
|
||||
influence=influence, pin_sharp=False,
|
||||
)
|
||||
return expand_smooth(group, geometry, "Boundary Smooth", influence)
|
||||
|
||||
@@ -40,14 +40,10 @@ def edge_boundary_field(nodes, links):
|
||||
return is_boundary.outputs["Boolean"]
|
||||
|
||||
|
||||
def expand_smooth(
|
||||
group, geometry, iterations_name, weight_name, *,
|
||||
influence=None, pin_sharp=True,
|
||||
):
|
||||
def expand_smooth(group, geometry, iterations_name, influence):
|
||||
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)
|
||||
@@ -69,18 +65,15 @@ def expand_smooth(
|
||||
links.new(geometry, repeat_input.inputs["Geometry"])
|
||||
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
|
||||
nearest_boundary.domain = "POINT"
|
||||
if influence is not None:
|
||||
# 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"])
|
||||
else:
|
||||
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
|
||||
# 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")
|
||||
@@ -109,44 +102,19 @@ def expand_smooth(
|
||||
links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"])
|
||||
links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"])
|
||||
|
||||
weight = weight_controls.outputs[weight_name]
|
||||
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"])
|
||||
normal_coherence = nodes.new("ShaderNodeVectorMath")
|
||||
normal_coherence.operation = "LENGTH"
|
||||
links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0])
|
||||
smooth_weight = nodes.new("ShaderNodeMath")
|
||||
smooth_weight.operation = "MULTIPLY"
|
||||
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
|
||||
links.new(normal_coherence.outputs["Value"], smooth_weight.inputs[1])
|
||||
weight = smooth_weight.outputs[0]
|
||||
if influence is not None:
|
||||
masked_weight = nodes.new("ShaderNodeMath")
|
||||
masked_weight.operation = "MULTIPLY"
|
||||
links.new(weight, masked_weight.inputs[0])
|
||||
links.new(influence, masked_weight.inputs[1])
|
||||
weight = masked_weight.outputs[0]
|
||||
|
||||
current_position = nodes.new("GeometryNodeInputPosition")
|
||||
mix_position = nodes.new("ShaderNodeMix")
|
||||
mix_position.data_type = "VECTOR"
|
||||
links.new(weight, mix_position.inputs[0])
|
||||
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])
|
||||
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"])
|
||||
if influence is not None:
|
||||
enabled = nodes.new("GeometryNodeSwitch")
|
||||
enabled.input_type = "GEOMETRY"
|
||||
links.new(iteration_controls.outputs[iterations_name], enabled.inputs["Switch"])
|
||||
links.new(geometry, enabled.inputs["False"])
|
||||
links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"])
|
||||
return enabled.outputs["Output"]
|
||||
return repeat_output.outputs["Geometry"]
|
||||
enabled = nodes.new("GeometryNodeSwitch")
|
||||
enabled.input_type = "GEOMETRY"
|
||||
links.new(iteration_controls.outputs[iterations_name], enabled.inputs["Switch"])
|
||||
links.new(geometry, enabled.inputs["False"])
|
||||
links.new(repeat_output.outputs["Geometry"], enabled.inputs["True"])
|
||||
return enabled.outputs["Output"]
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout Symmetry"
|
||||
|
||||
@@ -55,20 +56,101 @@ def _align_front(group, geometry, controls):
|
||||
return store.outputs['Geometry']
|
||||
|
||||
|
||||
def _delete_positive_faces(group, front, tolerance):
|
||||
"""Discard faces touching positive Y without leaving wires or isolated vertices."""
|
||||
position = group.nodes.new('GeometryNodeInputPosition')
|
||||
components = group.nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
positive = _math(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
|
||||
point_positive = evaluate_field(group, positive, 'FLOAT', 'POINT')
|
||||
face_positive = evaluate_field(group, point_positive, 'FLOAT', 'FACE')
|
||||
delete = group.nodes.new('GeometryNodeDeleteGeometry')
|
||||
delete.domain = 'FACE'
|
||||
delete.mode = 'ALL'
|
||||
group.links.new(front, delete.inputs['Geometry'])
|
||||
group.links.new(_math(group, 'GREATER_THAN', face_positive, 0), delete.inputs['Selection'])
|
||||
return delete.outputs[0]
|
||||
def _boundary_edge_field(group):
|
||||
"""Report one on edges that carry a single face."""
|
||||
neighbors = group.nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
return _math(group, 'COMPARE', neighbors.outputs['Face Count'], 1)
|
||||
|
||||
|
||||
def _fold_to_plane(group, geometry, tolerance):
|
||||
"""Move over-plane points onto the plane and drop the faces that collapse."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
beyond = _math(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
|
||||
target = nodes.new('ShaderNodeCombineXYZ')
|
||||
links.new(components.outputs['X'], target.inputs['X'])
|
||||
links.new(components.outputs['Z'], target.inputs['Z'])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(geometry, place.inputs['Geometry'])
|
||||
links.new(beyond, place.inputs['Selection'])
|
||||
links.new(target.outputs[0], place.inputs['Position'])
|
||||
# A face lying on the plane would be mirrored onto itself.
|
||||
center = evaluate_field(group, _math(group, 'ABSOLUTE', components.outputs['Y']), 'FLOAT', 'FACE')
|
||||
normal = nodes.new('GeometryNodeInputNormal')
|
||||
normal_axes = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(normal.outputs[0], normal_axes.inputs[0])
|
||||
flat = nodes.new('FunctionNodeBooleanMath')
|
||||
flat.operation = 'AND'
|
||||
links.new(_math(group, 'LESS_THAN', center, tolerance), flat.inputs[0])
|
||||
links.new(_math(group, 'GREATER_THAN', _math(group, 'ABSOLUTE', normal_axes.outputs['Y']), 1 - 1e-3), flat.inputs[1])
|
||||
delete = nodes.new('GeometryNodeDeleteGeometry')
|
||||
delete.domain, delete.mode = 'FACE', 'ALL'
|
||||
links.new(place.outputs[0], delete.inputs['Geometry'])
|
||||
links.new(flat.outputs[0], delete.inputs['Selection'])
|
||||
return _clean_crease(group, delete.outputs[0], tolerance)
|
||||
|
||||
|
||||
def _clean_crease(group, geometry, tolerance):
|
||||
"""Open interior in-plane edges left by folding a closed surface."""
|
||||
nodes, links = group.nodes, group.links
|
||||
heights = _edge_abs_y(group)
|
||||
edge_plane = nodes.new('FunctionNodeBooleanMath')
|
||||
edge_plane.operation = 'AND'
|
||||
links.new(_math(group, 'LESS_THAN', heights[0], tolerance), edge_plane.inputs[0])
|
||||
links.new(_math(group, 'LESS_THAN', heights[1], tolerance), edge_plane.inputs[1])
|
||||
neighbors = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
interior = _math(group, 'COMPARE', neighbors.outputs['Face Count'], 2)
|
||||
fold = nodes.new('FunctionNodeBooleanMath')
|
||||
fold.operation = 'AND'
|
||||
links.new(edge_plane.outputs[0], fold.inputs[0])
|
||||
links.new(interior, fold.inputs[1])
|
||||
|
||||
face_index = nodes.new('GeometryNodeInputIndex')
|
||||
corners = nodes.new('GeometryNodeCornersOfFace')
|
||||
links.new(face_index.outputs[0], corners.inputs['Face Index'])
|
||||
face_fold = None
|
||||
for offset in range(4):
|
||||
corner = nodes.new('GeometryNodeOffsetCornerInFace')
|
||||
links.new(corners.outputs['Corner Index'], corner.inputs['Corner Index'])
|
||||
corner.inputs['Offset'].default_value = offset
|
||||
edge_of_corner = nodes.new('GeometryNodeEdgesOfCorner')
|
||||
links.new(corner.outputs['Corner Index'], edge_of_corner.inputs['Corner Index'])
|
||||
sample = nodes.new('GeometryNodeFieldAtIndex')
|
||||
sample.data_type, sample.domain = 'BOOLEAN', 'EDGE'
|
||||
links.new(fold.outputs[0], sample.inputs['Value'])
|
||||
links.new(edge_of_corner.outputs['Next Edge Index'], sample.inputs['Index'])
|
||||
if face_fold is None:
|
||||
face_fold = sample.outputs['Value']
|
||||
else:
|
||||
combined = nodes.new('FunctionNodeBooleanMath')
|
||||
combined.operation = 'OR'
|
||||
links.new(face_fold, combined.inputs[0])
|
||||
links.new(sample.outputs['Value'], combined.inputs[1])
|
||||
face_fold = combined.outputs['Boolean']
|
||||
cleanup = nodes.new('GeometryNodeDeleteGeometry')
|
||||
cleanup.domain, cleanup.mode = 'FACE', 'ALL'
|
||||
links.new(geometry, cleanup.inputs['Geometry'])
|
||||
links.new(face_fold, cleanup.inputs['Selection'])
|
||||
return cleanup.outputs[0]
|
||||
|
||||
|
||||
def _off_plane_edge_field(group, tolerance):
|
||||
"""Report one on edges that keep an endpoint off the symmetry plane."""
|
||||
heights = _edge_abs_y(group)
|
||||
return _math(group, 'GREATER_THAN', _math(group, 'MAXIMUM', heights[0], heights[1]), tolerance)
|
||||
|
||||
|
||||
def _edge_abs_y(group):
|
||||
"""Return both endpoint heights on the symmetry axis for an edge."""
|
||||
edge = group.nodes.new('GeometryNodeInputMeshEdgeVertices')
|
||||
heights = []
|
||||
for socket in ('Position 1', 'Position 2'):
|
||||
axis = group.nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(edge.outputs[socket], axis.inputs[0])
|
||||
heights.append(_math(group, 'ABSOLUTE', axis.outputs['Y']))
|
||||
return heights
|
||||
|
||||
|
||||
def _mirror(group, geometry):
|
||||
@@ -80,6 +162,67 @@ def _mirror(group, geometry):
|
||||
return flip.outputs[0]
|
||||
|
||||
|
||||
def _cut_fields(group, tolerance):
|
||||
"""Return point-domain seam and outline fields for the cut."""
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
def _falloff(group, center, rings=2):
|
||||
"""Blur a 0/1 center into a falloff over the requested rings."""
|
||||
blur = group.nodes.new('GeometryNodeBlurAttribute')
|
||||
blur.data_type = 'FLOAT'
|
||||
blur.inputs['Iterations'].default_value = rings
|
||||
group.links.new(center, blur.inputs['Value'])
|
||||
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_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)
|
||||
|
||||
|
||||
def _extrude_boundary(group, geometry, selection, y_factor):
|
||||
"""Extrude selected edges to a fraction of their current height."""
|
||||
nodes, links = group.nodes, group.links
|
||||
@@ -106,50 +249,11 @@ def _extrude_boundary(group, geometry, selection, y_factor):
|
||||
return store.outputs[0], extrude.outputs['Top']
|
||||
|
||||
|
||||
def _walls(group, front, boundary, smooth, tolerance, *, curved):
|
||||
"""Build straight walls or a smoothed half-strip followed by its reflection."""
|
||||
nodes, links = group.nodes, group.links
|
||||
geometry, top = _extrude_boundary(group, front, boundary, .5 if curved else -1)
|
||||
if curved:
|
||||
geometry, _ = _extrude_boundary(group, geometry, top, 0)
|
||||
sides = nodes.new('GeometryNodeSeparateGeometry')
|
||||
sides.domain = 'FACE'
|
||||
links.new(geometry, sides.inputs['Geometry'])
|
||||
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 3), sides.inputs['Selection'])
|
||||
geometry = sides.outputs['Selection']
|
||||
if curved:
|
||||
# Query the retained front to pin the original seam; only new ring XY moves.
|
||||
proximity = nodes.new('GeometryNodeProximity')
|
||||
proximity.target_element = 'POINTS'
|
||||
links.new(front, proximity.inputs['Target'])
|
||||
movable = _math(group, 'GREATER_THAN', proximity.outputs['Distance'], tolerance)
|
||||
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'])
|
||||
# Keep heights fixed so neither the seam nor the symmetry plane can drift.
|
||||
difference = nodes.new('ShaderNodeVectorMath')
|
||||
difference.operation = 'SUBTRACT'
|
||||
links.new(blur.outputs[0], difference.inputs[0])
|
||||
links.new(position.outputs[0], difference.inputs[1])
|
||||
mask = nodes.new('ShaderNodeVectorMath')
|
||||
mask.operation = 'MULTIPLY'
|
||||
mask.inputs[1].default_value = (.5, 0, .5)
|
||||
links.new(difference.outputs[0], mask.inputs[0])
|
||||
place = nodes.new('GeometryNodeSetPosition')
|
||||
links.new(geometry, place.inputs['Geometry'])
|
||||
links.new(movable, place.inputs['Selection'])
|
||||
links.new(mask.outputs[0], place.inputs['Offset'])
|
||||
join = nodes.new('GeometryNodeJoinGeometry')
|
||||
links.new(place.outputs[0], join.inputs[0])
|
||||
links.new(_mirror(group, place.outputs[0]), join.inputs[0])
|
||||
geometry = join.outputs[0]
|
||||
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
shade.domain = 'FACE'
|
||||
shade.inputs['Shade Smooth'].default_value = curved
|
||||
links.new(geometry, shade.inputs['Geometry'])
|
||||
return shade.outputs[0]
|
||||
def _build_wall(group, front, boundary):
|
||||
"""Attach the curved half wall to the front and mark its faces."""
|
||||
geometry, top = _extrude_boundary(group, front, boundary, .5)
|
||||
geometry, _ = _extrude_boundary(group, geometry, top, 0)
|
||||
return geometry
|
||||
|
||||
|
||||
def build_image_cutout_symmetry_group():
|
||||
@@ -167,36 +271,54 @@ 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 only the generated wall interiors. Zero keeps straight walls.'
|
||||
smooth.description = 'Smooth the outline and the wall it builds through a two-ring falloff. 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.'
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
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(inputs['Geometry'], bounds.inputs['Geometry'])
|
||||
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)
|
||||
front = _delete_positive_faces(group, _align_front(group, inputs['Geometry'], inputs), tolerance)
|
||||
is_boundary = _boundary_edge_field(group)
|
||||
seam_field, outline_field = _cut_fields(group, tolerance)
|
||||
front = _fold_to_plane(group, aligned, tolerance)
|
||||
shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
shade.domain = 'FACE'
|
||||
links.new(front, shade.inputs['Geometry'])
|
||||
front = store_float_attribute(group, shade.outputs[0], FACE_ATTRIBUTE, 1., domain='FACE')
|
||||
count = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
boundary = _math(group, 'COMPARE', count.outputs['Face Count'], 1)
|
||||
walls = nodes.new('GeometryNodeSwitch')
|
||||
walls.input_type = 'GEOMETRY'
|
||||
links.new(_math(group, 'GREATER_THAN', inputs['Fill Smooth'], 0), walls.inputs['Switch'])
|
||||
for curved, socket in ((False, 'False'), (True, 'True')):
|
||||
links.new(_walls(group, front, boundary, inputs['Fill Smooth'], tolerance, curved=curved), walls.inputs[socket])
|
||||
enabled = nodes.new('GeometryNodeSwitch')
|
||||
enabled.input_type = 'GEOMETRY'
|
||||
links.new(inputs['Fill Sides'], enabled.inputs['Switch'])
|
||||
links.new(walls.outputs[0], enabled.inputs['True'])
|
||||
back = store_float_attribute(group, _mirror(group, front), FACE_ATTRIBUTE, 2., domain='FACE')
|
||||
front = _relax_seam(group, front, seam_field, inputs['Seam Smooth'])
|
||||
boundary = nodes.new('FunctionNodeBooleanMath')
|
||||
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])
|
||||
fill_smooth = _math(group, 'MULTIPLY', inputs['Fill Smooth'], inputs['Fill Sides'])
|
||||
front = _apply_planar_relax(group, front, _falloff(group, outline_field), fill_smooth)
|
||||
walled = _build_wall(group, front, boundary.outputs[0])
|
||||
wall_shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
wall_shade.domain = 'FACE'
|
||||
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 3), wall_shade.inputs['Selection'])
|
||||
links.new(walled, wall_shade.inputs['Geometry'])
|
||||
body = nodes.new('GeometryNodeSwitch')
|
||||
body.input_type = 'GEOMETRY'
|
||||
links.new(inputs['Fill Sides'], body.inputs['Switch'])
|
||||
links.new(front, body.inputs['False'])
|
||||
links.new(wall_shade.outputs[0], body.inputs['True'])
|
||||
mark_back = nodes.new('GeometryNodeStoreNamedAttribute')
|
||||
mark_back.data_type, mark_back.domain = 'FLOAT', 'FACE'
|
||||
mark_back.inputs['Name'].default_value = FACE_ATTRIBUTE
|
||||
mark_back.inputs['Value'].default_value = 2
|
||||
links.new(_mirror(group, body.outputs[0]), mark_back.inputs['Geometry'])
|
||||
links.new(_math(group, 'COMPARE', read_float_attribute(group, FACE_ATTRIBUTE), 1), mark_back.inputs['Selection'])
|
||||
join = nodes.new('GeometryNodeJoinGeometry')
|
||||
for geometry in (front, back, enabled.outputs[0]):
|
||||
for geometry in (body.outputs[0], mark_back.outputs['Geometry']):
|
||||
links.new(geometry, join.inputs[0])
|
||||
merge = nodes.new('GeometryNodeMergeByDistance')
|
||||
links.new(tolerance, merge.inputs['Distance'])
|
||||
|
||||
@@ -361,8 +361,6 @@ def build_image_depth_cutout_group():
|
||||
float_input(group, 'Depth Scale', 1.0, maximum=2.0)
|
||||
float_input(group, 'Depth Split', 0.1, maximum=1, subtype='FACTOR').description = 'Split the mesh where depth changes abruptly. Higher values detect smaller depth jumps. Zero disables splitting.'
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
float_input(group, 'Smooth Weight', 1.0, maximum=1, subtype='FACTOR', parent=options).description = 'Control the strength of boundary smoothing. Higher values move boundary vertices more.'
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||
boundary_smooth.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
|
||||
@@ -370,6 +368,7 @@ def build_image_depth_cutout_group():
|
||||
normal_smooth.default_value, normal_smooth.min_value, normal_smooth.max_value = 64, 16, 512
|
||||
normal_smooth.description = 'Control smoothing of front and rear displacement normals. Higher values produce smoother directions.'
|
||||
float_input(group, 'Front Inflation', 0.2, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode. Zero keeps the original front surface.'
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
data = group.interface.new_panel(name='Data', default_closed=True)
|
||||
float_input(group, 'Uniform Scale', 1.0, parent=data).hide_in_modifier = True
|
||||
interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True
|
||||
|
||||
@@ -51,7 +51,6 @@ def build_image_depth_panorama_group():
|
||||
boundary_smooth = interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 2, 0, 16
|
||||
boundary_smooth.description = "Smooth depth and geometry near cut edges on the panorama surface. Zero disables boundary smoothing."
|
||||
float_input(group, "Smooth Weight", 1, maximum=1, subtype="FACTOR", parent=options).description = "Control the strength of boundary smoothing. Higher values move boundary vertices more."
|
||||
float_input(group, "Mask Threshold", 0.9, maximum=1, subtype="FACTOR", parent=options).description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
||||
float_input(group, "Invalid Distance", 100, minimum=1e-5, subtype="DISTANCE", parent=options).description = "Set the fallback distance from the panorama center for invalid depth areas kept when Depth Mask is disabled."
|
||||
data = group.interface.new_panel("Data", default_closed=True)
|
||||
|
||||
@@ -45,8 +45,6 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
valid_only.description = "Remove mesh areas marked invalid by the depth mask."
|
||||
valid_only.default_value = True
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options).description = "Control the strength of boundary smoothing. Higher values move boundary vertices more."
|
||||
float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
||||
boundary_smooth = _interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", parent=options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||
boundary_smooth.description = "Smooth depth and geometry near cut edges while preserving the original image border. Zero disables boundary smoothing."
|
||||
@@ -55,6 +53,7 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
||||
data = group.interface.new_panel(name="Data", default_closed=True)
|
||||
uniform_scale = _interface_socket(
|
||||
group,
|
||||
|
||||
@@ -55,6 +55,11 @@ def build_image_relief_plane_group(image_plane_group):
|
||||
valid_only.description = "Treat depth-mask-invalid areas as zero depth instead of deleting them."
|
||||
valid_only.default_value = True
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", options)
|
||||
validity_threshold.description = "Set the minimum depth mask value treated as valid."
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
base_plane_depth = _interface_socket(
|
||||
group,
|
||||
"Reference Depth",
|
||||
@@ -65,11 +70,6 @@ def build_image_relief_plane_group(image_plane_group):
|
||||
base_plane_depth.default_value = 1.0
|
||||
base_plane_depth.min_value = 0.0
|
||||
base_plane_depth.subtype = "DISTANCE"
|
||||
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", options)
|
||||
validity_threshold.description = "Set the minimum depth mask value treated as valid."
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
data = group.interface.new_panel(name="Data", default_closed=True)
|
||||
uniform_scale = _interface_socket(
|
||||
group,
|
||||
|
||||
Reference in New Issue
Block a user