Refactor depth-related tests and improve parameter handling in image cutout symmetry
This commit is contained in:
@@ -97,12 +97,14 @@ def pinned_smooth(
|
||||
iterations,
|
||||
region,
|
||||
weight=1.0,
|
||||
movement=None,
|
||||
pin_boundary=True,
|
||||
pin_sharp=True,
|
||||
):
|
||||
"""Smooth positions toward a pinned target inside an influence region.
|
||||
|
||||
``weight`` is the smoothing weight: each point moves that share of the way
|
||||
to the target every iteration, scaled further by the sharpness of the
|
||||
normal field and the 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
|
||||
@@ -158,12 +160,6 @@ def pinned_smooth(
|
||||
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(current, set_position.inputs["Geometry"])
|
||||
links.new(offset.outputs[0], set_position.inputs["Offset"])
|
||||
|
||||
@@ -6,8 +6,9 @@ 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,
|
||||
compare_node, evaluate_field, store_float_attribute, read_float_attribute,
|
||||
prepare_node_group,
|
||||
compare_node, boolean_node, evaluate_field,
|
||||
store_boolean_attribute, store_float_attribute, read_float_attribute,
|
||||
remove_attribute_pattern, prepare_node_group,
|
||||
)
|
||||
from ..common.depth_surface import _math
|
||||
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
|
||||
@@ -16,8 +17,12 @@ from ..common.smoothing import edge_boundary_field, pinned_smooth
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout Symmetry"
|
||||
|
||||
# 衔接带从轮廓向内覆盖的圈数。
|
||||
FILL_SMOOTH_RINGS = 4
|
||||
# 切口环与衔接带各自向内覆盖的圈数。
|
||||
SMOOTH_RINGS = 4
|
||||
|
||||
# 衔接带要覆盖镜像焊好的平面环,焊完之后先把这些点记在内部属性上。
|
||||
WELD_ATTRIBUTE = '_o_anyimage_symmetry_weld'
|
||||
|
||||
|
||||
# 对称面附近按中位边长的倍数降低法线贴图强度。
|
||||
SEAM_NORMAL_BAND = 4
|
||||
@@ -71,78 +76,137 @@ def _boundary_edge_field(group):
|
||||
return compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 1, data_type='INT')
|
||||
|
||||
|
||||
def _fold_to_plane(group, geometry, tolerance):
|
||||
"""Move over-plane points onto the plane and drop the faces that collapse."""
|
||||
def _beyond_plane_field(group, tolerance):
|
||||
"""Report the points sitting beyond the symmetry plane."""
|
||||
nodes = group.nodes
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
return compare_node(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
|
||||
|
||||
|
||||
def _on_plane_field(group, tolerance):
|
||||
"""Report the points sitting on the symmetry plane."""
|
||||
nodes = group.nodes
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
group.links.new(position.outputs[0], components.inputs[0])
|
||||
return compare_node(group, 'LESS_THAN', _math(group, 'ABSOLUTE', components.outputs['Y']), tolerance)
|
||||
|
||||
|
||||
def _merge_points(group, geometry, selection, distance):
|
||||
"""Weld the selected points that share a position."""
|
||||
nodes, links = group.nodes, group.links
|
||||
merge = nodes.new('GeometryNodeMergeByDistance')
|
||||
links.new(geometry, merge.inputs['Geometry'])
|
||||
links.new(selection, merge.inputs['Selection'])
|
||||
if isinstance(distance, (int, float)):
|
||||
merge.inputs['Distance'].default_value = distance
|
||||
else:
|
||||
links.new(distance, merge.inputs['Distance'])
|
||||
return merge.outputs[0]
|
||||
|
||||
|
||||
def _delete_faces(group, geometry, selection):
|
||||
"""Delete the selected faces and the geometry only they used."""
|
||||
nodes, links = group.nodes, group.links
|
||||
delete = nodes.new('GeometryNodeDeleteGeometry')
|
||||
delete.domain, delete.mode = 'FACE', 'ALL'
|
||||
links.new(geometry, delete.inputs['Geometry'])
|
||||
links.new(selection, delete.inputs['Selection'])
|
||||
return delete.outputs[0]
|
||||
|
||||
|
||||
def _delete_beyond_plane(group, geometry, beyond):
|
||||
"""Delete the faces whose points all sit beyond the symmetry plane."""
|
||||
# 点域求值让面域选择按「全部点越界」删除,跨界的面连几何一起留给切口回拉。
|
||||
return _delete_faces(group, geometry, evaluate_field(group, beyond, 'BOOLEAN', 'POINT'))
|
||||
|
||||
|
||||
def _corner_edge(group, corner, offset, value):
|
||||
"""Read an edge value at one corner around a face."""
|
||||
nodes, links = group.nodes, group.links
|
||||
shifted = nodes.new('GeometryNodeOffsetCornerInFace')
|
||||
links.new(corner, shifted.inputs['Corner Index'])
|
||||
shifted.inputs['Offset'].default_value = offset
|
||||
edges = nodes.new('GeometryNodeEdgesOfCorner')
|
||||
links.new(shifted.outputs['Corner Index'], edges.inputs['Corner Index'])
|
||||
sample = nodes.new('GeometryNodeFieldAtIndex')
|
||||
sample.data_type, sample.domain = 'BOOLEAN', 'EDGE'
|
||||
links.new(value, sample.inputs['Value'])
|
||||
links.new(edges.outputs['Next Edge Index'], sample.inputs['Index'])
|
||||
return sample.outputs['Value']
|
||||
|
||||
|
||||
def _boundary_edge_count(group):
|
||||
"""Count the edges carrying a single face around each face."""
|
||||
nodes, links = group.nodes, group.links
|
||||
boundary = _boundary_edge_field(group)
|
||||
index = nodes.new('GeometryNodeInputIndex')
|
||||
corners = nodes.new('GeometryNodeCornersOfFace')
|
||||
links.new(index.outputs[0], corners.inputs['Face Index'])
|
||||
# 第四个偏移在三角形上绕回第一条边,按角数把它归零。
|
||||
quad = compare_node(group, 'GREATER_THAN', corners.outputs['Total'], 3, data_type='INT')
|
||||
count = None
|
||||
for offset in range(4):
|
||||
term = _corner_edge(group, corners.outputs['Corner Index'], offset, boundary)
|
||||
if offset == 3:
|
||||
masked = nodes.new('GeometryNodeSwitch')
|
||||
masked.input_type = 'FLOAT'
|
||||
masked.inputs['False'].default_value = 0.0
|
||||
links.new(quad, masked.inputs['Switch'])
|
||||
links.new(term, masked.inputs['True'])
|
||||
term = masked.outputs['Output']
|
||||
count = term if count is None else _math(group, 'ADD', count, term)
|
||||
return count
|
||||
|
||||
|
||||
def _delete_bridge_faces(group, geometry):
|
||||
"""Delete the faces left bridging two openings of a cut."""
|
||||
return _delete_faces(group, geometry, compare_node(group, 'GREATER_THAN', _boundary_edge_count(group), 1.5))
|
||||
|
||||
|
||||
def _delete_flat_faces(group, geometry, tolerance):
|
||||
"""Delete the faces lying on the symmetry plane; they mirror onto themselves."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
beyond = compare_node(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(compare_node(group, 'LESS_THAN', center, tolerance), flat.inputs[0])
|
||||
links.new(compare_node(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)
|
||||
center = evaluate_field(group, _math(group, 'ABSOLUTE', components.outputs['Y']), 'FLOAT', 'FACE')
|
||||
return _delete_faces(
|
||||
group, geometry,
|
||||
boolean_node(
|
||||
group, 'AND',
|
||||
compare_node(group, 'LESS_THAN', center, tolerance),
|
||||
compare_node(group, 'GREATER_THAN', _math(group, 'ABSOLUTE', normal_axes.outputs['Y']), 1 - 1e-3),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _clean_crease(group, geometry, tolerance):
|
||||
"""Open interior in-plane edges left by folding a closed surface."""
|
||||
def _delete_crease_faces(group, geometry, tolerance):
|
||||
"""Delete the faces the fold leaves sharing an interior edge on the plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
heights = _edge_abs_y(group)
|
||||
edge_plane = nodes.new('FunctionNodeBooleanMath')
|
||||
edge_plane.operation = 'AND'
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[0], tolerance), edge_plane.inputs[0])
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[1], tolerance), edge_plane.inputs[1])
|
||||
plane = nodes.new('FunctionNodeBooleanMath')
|
||||
plane.operation = 'AND'
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[0], tolerance), plane.inputs[0])
|
||||
links.new(compare_node(group, 'LESS_THAN', heights[1], tolerance), plane.inputs[1])
|
||||
neighbors = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
|
||||
interior = compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 2, data_type='INT')
|
||||
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')
|
||||
crease = nodes.new('FunctionNodeBooleanMath')
|
||||
crease.operation = 'AND'
|
||||
links.new(plane.outputs[0], crease.inputs[0])
|
||||
links.new(compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 2, data_type='INT'), crease.inputs[1])
|
||||
index = nodes.new('GeometryNodeInputIndex')
|
||||
corners = nodes.new('GeometryNodeCornersOfFace')
|
||||
links.new(face_index.outputs[0], corners.inputs['Face Index'])
|
||||
face_fold = None
|
||||
links.new(index.outputs[0], corners.inputs['Face Index'])
|
||||
selected = 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]
|
||||
term = _corner_edge(group, corners.outputs['Corner Index'], offset, crease.outputs[0])
|
||||
selected = term if selected is None else boolean_node(group, 'OR', selected, term)
|
||||
return _delete_faces(group, geometry, selected)
|
||||
|
||||
|
||||
def _off_plane_edge_field(group, tolerance):
|
||||
@@ -171,24 +235,13 @@ def _mirror(group, geometry):
|
||||
return flip.outputs[0]
|
||||
|
||||
|
||||
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])
|
||||
seam = nodes.new('FunctionNodeBooleanMath')
|
||||
seam.operation = 'AND'
|
||||
links.new(compare_node(group, 'GREATER_THAN', boundary, 0), seam.inputs[0])
|
||||
links.new(
|
||||
compare_node(group, 'LESS_THAN', _math(group, 'ABSOLUTE', components.outputs['Y']), tolerance),
|
||||
seam.inputs[1],
|
||||
)
|
||||
return boundary, seam.outputs[0]
|
||||
def _seam_field(group, tolerance):
|
||||
"""Report the cut ring the folded front leaves on the symmetry plane."""
|
||||
boundary = evaluate_field(group, edge_boundary_field(group.nodes, group.links), 'BOOLEAN', 'POINT')
|
||||
return boolean_node(group, 'AND', boundary, _on_plane_field(group, tolerance))
|
||||
|
||||
|
||||
def _falloff(group, center, rings=2):
|
||||
def _falloff(group, center, rings):
|
||||
"""Blur a 0/1 center into a falloff over the requested rings."""
|
||||
blur = group.nodes.new('GeometryNodeBlurAttribute')
|
||||
blur.data_type = 'FLOAT'
|
||||
@@ -197,28 +250,37 @@ def _falloff(group, center, rings=2):
|
||||
return _math(group, 'MAXIMUM', center, blur.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 _snap_to_plane(group, geometry, selection):
|
||||
"""Move the selected points onto the symmetry plane."""
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new('GeometryNodeInputPosition')
|
||||
components = nodes.new('ShaderNodeSeparateXYZ')
|
||||
links.new(position.outputs[0], components.inputs[0])
|
||||
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(selection, place.inputs['Selection'])
|
||||
links.new(target.outputs[0], place.inputs['Position'])
|
||||
return place.outputs[0]
|
||||
|
||||
|
||||
def _relax_seam(group, geometry, seam, beyond, smooth):
|
||||
"""Relax the cut seam in three dimensions and keep the cut on the plane."""
|
||||
relaxed = pinned_smooth(
|
||||
group, geometry, smooth, _falloff(group, seam, SMOOTH_RINGS),
|
||||
pin_boundary=True,
|
||||
)
|
||||
return _snap_to_plane(group, relaxed, boolean_node(group, 'OR', seam, beyond))
|
||||
|
||||
|
||||
def _relax_seam(group, geometry, seam_field, smooth):
|
||||
"""Relax the cut seam in plane through a two-ring falloff."""
|
||||
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 _relax_fill(group, geometry, weld, smooth):
|
||||
"""Relax the junction band on the welded mesh, where the seam needs no pinning."""
|
||||
return pinned_smooth(
|
||||
group, geometry, smooth, _falloff(group, weld, SMOOTH_RINGS),
|
||||
weight=0.1, pin_boundary=False,
|
||||
)
|
||||
|
||||
|
||||
def _extrude_boundary(group, geometry, selection, y_factor):
|
||||
@@ -267,12 +329,9 @@ def build_image_cutout_symmetry_group():
|
||||
float_input(group, 'Scale', 1, maximum=2).description = 'Scale the offset geometry along the aligned symmetry axis.'
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
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 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.'
|
||||
smooth = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
smooth.default_value, smooth.min_value, smooth.max_value = 4, 0, 16
|
||||
smooth.description = 'Smooth the cut ring and the junction band around the outline and the fill, each over four rings. Zero keeps both raw.'
|
||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||
nodes, links = group.nodes, group.links
|
||||
inputs = group_input(nodes, set()).outputs
|
||||
@@ -289,26 +348,33 @@ def build_image_cutout_symmetry_group():
|
||||
snap_tolerance = _math(
|
||||
group, 'MULTIPLY', _math(group, 'MAXIMUM', statistics.outputs['Median'], 1e-6), 1e-6,
|
||||
)
|
||||
beyond = _beyond_plane_field(group, snap_tolerance)
|
||||
on_plane = _on_plane_field(group, snap_tolerance)
|
||||
is_boundary = _boundary_edge_field(group)
|
||||
boundary_field, seam_field = _boundary_fields(group, snap_tolerance)
|
||||
front = _fold_to_plane(group, aligned, snap_tolerance)
|
||||
front = _delete_beyond_plane(group, aligned, beyond)
|
||||
front = _delete_bridge_faces(group, front)
|
||||
# 跨界保留的面折回对称面,切口环因此落在平面上。
|
||||
front = _snap_to_plane(group, front, beyond)
|
||||
seam_field = _seam_field(group, snap_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')
|
||||
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, snap_tolerance), boundary.inputs[1])
|
||||
fill_smooth = nodes.new('GeometryNodeSwitch')
|
||||
fill_smooth.input_type = 'INT'
|
||||
fill_smooth.inputs['False'].default_value = 0
|
||||
links.new(inputs['Fill Sides'], fill_smooth.inputs['Switch'])
|
||||
links.new(inputs['Fill Smooth'], fill_smooth.inputs['True'])
|
||||
front = _relax_fill(group, front, boundary_field, fill_smooth.outputs[0])
|
||||
walled = _build_wall(group, front, boundary.outputs[0])
|
||||
front = _relax_seam(group, front, seam_field, beyond, inputs['Smooth'])
|
||||
# 松弛会把点压回平面,贴平的面和内部贴平边要在松弛之后再清一次。
|
||||
front = _delete_flat_faces(group, front, snap_tolerance)
|
||||
front = _delete_crease_faces(group, front, snap_tolerance)
|
||||
fill_iterations = nodes.new('GeometryNodeSwitch')
|
||||
fill_iterations.input_type = 'INT'
|
||||
fill_iterations.inputs['False'].default_value = 0
|
||||
links.new(inputs['Fill Sides'], fill_iterations.inputs['Switch'])
|
||||
links.new(inputs['Smooth'], fill_iterations.inputs['True'])
|
||||
# 切口环本身贴在对称面上并靠镜像焊住,只有不落在这条环上的边界边才需要补面。
|
||||
wall = nodes.new('FunctionNodeBooleanMath')
|
||||
wall.operation = 'AND'
|
||||
links.new(is_boundary, wall.inputs[0])
|
||||
links.new(_off_plane_edge_field(group, snap_tolerance), wall.inputs[1])
|
||||
walled = _build_wall(group, front, wall.outputs[0])
|
||||
wall_shade = nodes.new('GeometryNodeSetShadeSmooth')
|
||||
wall_shade.domain = 'FACE'
|
||||
links.new(compare_node(group, 'EQUAL', read_float_attribute(group, FACE_ATTRIBUTE), 3), wall_shade.inputs['Selection'])
|
||||
@@ -327,15 +393,12 @@ 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 = compare_node(group, 'LESS_THAN', _math(group, 'ABSOLUTE', axis.outputs['Y']), snap_tolerance)
|
||||
merge = nodes.new('GeometryNodeMergeByDistance')
|
||||
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')
|
||||
# 接缝两侧的位置本来就重合,合并距离收到容差量级,别把平面附近相邻而不同的点连起来。
|
||||
merged = _merge_points(group, join.outputs[0], on_plane, snap_tolerance)
|
||||
# 先让镜像焊住接缝,再对焊好的网格松弛衔接带。
|
||||
geometry, weld = store_boolean_attribute(group, merged, WELD_ATTRIBUTE, on_plane, domain='POINT')
|
||||
geometry = _relax_fill(group, geometry, weld, fill_iterations.outputs[0])
|
||||
geometry = store_float_attribute(group, geometry, 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'])
|
||||
@@ -358,6 +421,7 @@ def build_image_cutout_symmetry_group():
|
||||
geometry = store_float_attribute(
|
||||
group, orient.outputs[0], NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction,
|
||||
)
|
||||
geometry = remove_attribute_pattern(group, geometry, '_o_*')
|
||||
links.new(geometry, nodes.new('NodeGroupOutput').inputs['Geometry'])
|
||||
prepare_node_group(group)
|
||||
return group
|
||||
|
||||
Reference in New Issue
Block a user