Add tests and implement boundary smoothing and cutout functionality
- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity. - Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending. - Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised. - Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry. - Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity. - Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles. - Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
@@ -82,7 +82,8 @@ def _localize_peer_inputs(node_group, ancestors, children):
|
||||
if source.bl_idname == "NodeReroute":
|
||||
source["arrangement_input"] = True
|
||||
continue
|
||||
if source.bl_idname != "NodeGroupInput" and len(children[source]) <= 1 and sources.count(source) == 1 and source not in upstream:
|
||||
# Zone exits keep their own lane when a later peer merge is placed.
|
||||
if source.bl_idname not in {"NodeGroupInput", "GeometryNodeRepeatOutput"} and len(children[source]) <= 1 and sources.count(source) == 1 and source not in upstream:
|
||||
continue
|
||||
port = node_group.nodes.new("NodeReroute")
|
||||
port["arrangement_input"] = True
|
||||
@@ -262,9 +263,9 @@ def _place_geometry_paths(paths, geometry_x, sizes, order):
|
||||
weights /= weights.max()
|
||||
constraints = []
|
||||
if equalities:
|
||||
constraints.append(LinearConstraint(np.array(equalities), centers, centers))
|
||||
constraints.append(LinearConstraint(np.array(equalities), np.array(centers) / pitch, np.array(centers) / pitch))
|
||||
if inequalities:
|
||||
constraints.append(LinearConstraint(np.array(inequalities), separations, np.inf))
|
||||
constraints.append(LinearConstraint(np.array(inequalities), np.array(separations) / pitch, np.inf))
|
||||
compact = []
|
||||
for node in order:
|
||||
if node not in path_of:
|
||||
@@ -279,14 +280,14 @@ def _place_geometry_paths(paths, geometry_x, sizes, order):
|
||||
matrix = np.array(compact)
|
||||
metric += matrix.T @ matrix * 4
|
||||
solution = minimize(lambda values: values @ metric @ values,
|
||||
np.array([-rows[index] * pitch for index in range(len(paths))], dtype=float),
|
||||
np.array([-rows[index] for index in range(len(paths))], dtype=float),
|
||||
jac=lambda values: 2 * metric @ values, constraints=constraints,
|
||||
method="SLSQP", options={"ftol": 1e-5, "maxiter": 300})
|
||||
method="SLSQP", options={"ftol": 1e-12, "maxiter": 300})
|
||||
if not solution.success:
|
||||
raise ValueError(f"Geometry lane constraints could not be placed: {solution.message}")
|
||||
for index, path in enumerate(paths):
|
||||
for node in path:
|
||||
positions[node] = (positions[node][0], float(solution.x[index]))
|
||||
positions[node] = (positions[node][0], float(solution.x[index] * pitch))
|
||||
for node in order:
|
||||
if node not in positions:
|
||||
continue
|
||||
@@ -383,7 +384,7 @@ class BranchSpace:
|
||||
if link.to_node != other.to_node and link.from_socket != other.from_socket)
|
||||
|
||||
|
||||
def _place_field_region(local, width, height, destinations, owners, positions, sizes, occupied, limits, core, trunks, space, horizontal=None):
|
||||
def _place_field_region(local, width, height, destinations, owners, positions, sizes, occupied, limits, core, trunks, space, horizontal=None, *, allow_opposite=False):
|
||||
"""Place an upstream module together with its forward-facing exit channels."""
|
||||
right = min(positions[node][0] for node in destinations) - FIELD_GAP
|
||||
baseline = sum(positions[node][1] for node in owners) / len(owners)
|
||||
@@ -394,6 +395,8 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
|
||||
axis_bottom = axis_top - sizes[output].height
|
||||
core_left = min(positions[node][0] for node in core)
|
||||
side = 1 if floor > axis_top else -1 if ceiling < axis_bottom else 0
|
||||
if allow_opposite:
|
||||
side = 0
|
||||
desired = right - width
|
||||
front_allowed = not horizontal and all(link.from_node in local
|
||||
for node in local for socket in node.inputs for link in socket.links)
|
||||
@@ -433,6 +436,8 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
|
||||
limits[0], limits[1] + height,
|
||||
*(top + BRANCH_GAP + height for _, _, top, _ in occupied),
|
||||
*(bottom - BRANCH_GAP for _, _, _, bottom in occupied)}
|
||||
tops.update(top + BRANCH_GAP + height for _, boxes in space.exits for _, _, top, _ in boxes)
|
||||
tops.update(bottom - BRANCH_GAP for _, boxes in space.exits for _, _, _, bottom in boxes)
|
||||
if front_allowed:
|
||||
tops.add(baseline + height / 2)
|
||||
for top in sorted(tops, key=lambda y: abs(y - height / 2 - baseline)):
|
||||
@@ -443,6 +448,9 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
|
||||
placed_left = max(left, minimum) if horizontal else min(left, desired)
|
||||
placed_left = min([placed_left, *(end - max(GEOMETRY_GAP, abs(target_y - top - source_y) * 0.8)
|
||||
for end, source_y, target_y in constraints)])
|
||||
if not horizontal:
|
||||
# Leave clearance at saturated link constraints for solver roundoff.
|
||||
placed_left -= 0.01
|
||||
if horizontal and not minimum <= placed_left <= horizontal[1] - width:
|
||||
continue
|
||||
in_front = placed_left + width + BRANCH_GAP <= core_left
|
||||
@@ -478,6 +486,11 @@ def _place_field_region(local, width, height, destinations, owners, positions, s
|
||||
best = candidate
|
||||
best_exits, best_routes = exits, routes
|
||||
if best is None:
|
||||
if not allow_opposite:
|
||||
return _place_field_region(
|
||||
local, width, height, destinations, owners, positions, sizes,
|
||||
occupied, limits, core, trunks, space, horizontal, allow_opposite=True,
|
||||
)
|
||||
raise ValueError("The field region does not fit its reserved scope")
|
||||
_, _, _, left, top = best
|
||||
for node, (x, y) in local.items():
|
||||
@@ -645,7 +658,7 @@ def _route_references(node_group, sizes, positions, regions, paths, trunks, prot
|
||||
_replace_link(node_group, link, trunk)
|
||||
|
||||
|
||||
def _zone_width(entry, interior, layout, sizes):
|
||||
def _zone_width(interior, layout, sizes):
|
||||
"""Reserve the body and the actual socket runs at each end of a paired scope."""
|
||||
body_height = max(sizes[node].height for node in interior)
|
||||
spans = []
|
||||
@@ -654,8 +667,8 @@ def _zone_width(entry, interior, layout, sizes):
|
||||
for node, (x, y) in layout.positions.items():
|
||||
for socket in node.inputs:
|
||||
for link in socket.links:
|
||||
if link.from_node == entry:
|
||||
delta = top + y - sizes[node].sockets[socket] + sizes[entry].sockets[link.from_socket]
|
||||
if link.from_node not in layout.positions and link.from_node not in interior:
|
||||
delta = top + y - sizes[node].sockets[socket] + sizes[link.from_node].sockets[link.from_socket]
|
||||
left = max(left, max(GEOMETRY_GAP, abs(delta) * 0.8) - x)
|
||||
for socket in node.outputs:
|
||||
for link in socket.links:
|
||||
@@ -663,7 +676,7 @@ def _zone_width(entry, interior, layout, sizes):
|
||||
delta = top + y - sizes[node].sockets[socket] + sizes[link.to_node].sockets[link.to_socket]
|
||||
right = max(right, x + sizes[node].width + max(GEOMETRY_GAP, abs(delta) * 0.8))
|
||||
spans.append(left + right)
|
||||
return max(spans)
|
||||
return max(spans) + BRANCH_GAP * 2
|
||||
|
||||
|
||||
def arrange_nodes(node_group):
|
||||
@@ -699,7 +712,7 @@ def arrange_nodes(node_group):
|
||||
layout = field_layout(fields, parents, peers, sizes, gap=FIELD_GAP, target_height=FIELD_HEIGHT)
|
||||
field_layouts.append(layout)
|
||||
local, width, height = layout.positions, layout.width, layout.height
|
||||
span = _zone_width(entry, interior, layout, sizes)
|
||||
span = _zone_width(interior, layout, sizes)
|
||||
for node in core:
|
||||
if entry in geometry_parents[node]:
|
||||
spacing[node] = max(spacing.get(node, 0), span)
|
||||
@@ -724,6 +737,7 @@ def arrange_nodes(node_group):
|
||||
layouts.append((remaining, targets, layout.positions, layout.width, layout.height))
|
||||
reserve = max([FIELD_HEIGHT, *(height for _, _, _, _, height in layouts),
|
||||
*(height for _, _, _, _, _, _, height in zones)]) * 2 + BRANCH_GAP * 2
|
||||
reserve += sum(height + BRANCH_GAP * 2 for _, _, _, _, _, _, height in zones)
|
||||
limits = (max(y for _, y in positions.values()) + reserve,
|
||||
min(y - sizes[node].height for node, (_, y) in positions.items()) - reserve)
|
||||
for entry, end, interior, fields, local, width, height in zones:
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"FunctionNodeSeparateColor": 1,
|
||||
"GeometryNodeAccumulateField": 2,
|
||||
"GeometryNodeAttributeStatistic": 2,
|
||||
"GeometryNodeAttributeDomainSize": 1,
|
||||
"GeometryNodeBlurAttribute": 1,
|
||||
"GeometryNodeBoundBox": 0,
|
||||
"GeometryNodeCaptureAttribute": 3,
|
||||
"GeometryNodeCornersOfEdge": 0,
|
||||
"GeometryNodeCornersOfFace": 0,
|
||||
"GeometryNodeDeleteGeometry": 2,
|
||||
|
||||
@@ -40,7 +40,7 @@ def estimate_node_dimensions(node):
|
||||
|
||||
# Conservative reserves include spacing between UI sections.
|
||||
controls = RULES["controls"][node.bl_idname]
|
||||
for collection in ("repeat_items", "state_items", "input_items", "main_items", "generation_items", "capture_items", "enum_items"):
|
||||
for collection in ("repeat_items", "state_items", "input_items", "main_items", "generation_items", "enum_items"):
|
||||
controls += len(getattr(node, collection, ()))
|
||||
if node.bl_idname == "GeometryNodeGroup" and node.node_tree:
|
||||
controls += sum(item.item_type == "PANEL" for item in node.node_tree.interface.items_tree)
|
||||
|
||||
@@ -19,6 +19,16 @@ def check_asset_library(asset_path):
|
||||
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
|
||||
assert set(source.node_groups) == GEOMETRY_NAMES | SHADER_NAMES
|
||||
target.node_groups = sorted(GEOMETRY_NAMES | SHADER_NAMES)
|
||||
pending, checked = list(target.node_groups), set()
|
||||
while pending:
|
||||
group = pending.pop()
|
||||
if group in checked or group.bl_idname != "GeometryNodeTree":
|
||||
continue
|
||||
checked.add(group)
|
||||
for node in group.nodes:
|
||||
assert node.bl_idname != "GeometryNodeCaptureAttribute", group.name
|
||||
if node.bl_idname == "GeometryNodeGroup" and node.node_tree:
|
||||
pending.append(node.node_tree)
|
||||
depth_image = bpy.data.images.new("Asset Check Depth", width=8, height=8, float_buffer=True)
|
||||
depth_image.colorspace_settings.name = "Non-Color"
|
||||
depth_image.pixels.foreach_set([
|
||||
@@ -31,7 +41,11 @@ def check_asset_library(asset_path):
|
||||
if expected_type == "ShaderNodeTree":
|
||||
continue
|
||||
mesh = bpy.data.meshes.new("Asset Check Plane")
|
||||
mesh.from_pydata([(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)], [], [(0, 1, 2, 3)])
|
||||
mesh.from_pydata(
|
||||
[(x / 3 - 0.5, y / 3 - 0.5, 0) for y in range(4) for x in range(4)], [],
|
||||
[(y * 4 + x, y * 4 + x + 1, (y + 1) * 4 + x + 1, (y + 1) * 4 + x)
|
||||
for y in range(3) for x in range(3)],
|
||||
)
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
position = mesh.vertices[loop.vertex_index].co
|
||||
@@ -55,6 +69,12 @@ def check_asset_library(asset_path):
|
||||
try:
|
||||
assert len(result.vertices) and len(result.polygons), node_group.name
|
||||
assert all(isfinite(value) for vertex in result.vertices for value in vertex.co), node_group.name
|
||||
assert not list(modifier.node_warnings), (
|
||||
node_group.name, [warning.message for warning in modifier.node_warnings],
|
||||
)
|
||||
assert not {attribute.name for attribute in result.attributes} & {
|
||||
".o_anyimage_depth_cut", ".o_anyimage_original_boundary",
|
||||
}, node_group.name
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
79
tools/nodes/common/boundary_smoothing.py
Normal file
79
tools/nodes/common/boundary_smoothing.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Apply boundary-preserving smoothing within a narrow cut-edge band."""
|
||||
|
||||
from .nodes import evaluate_field, group_input
|
||||
from .depth_surface import _math
|
||||
from .smoothing import edge_boundary_field, expand_smooth
|
||||
|
||||
|
||||
def boundary_influence(group, protected=False):
|
||||
"""Fade a boundary field through two rings while excluding protected points."""
|
||||
nodes, links = group.nodes, group.links
|
||||
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
unprotected = _math(group, "SUBTRACT", 1, protected)
|
||||
cut = _math(group, "MULTIPLY", boundary, unprotected)
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT"
|
||||
blur.inputs["Iterations"].default_value = 2
|
||||
blur.inputs["Weight"].default_value = 1
|
||||
links.new(cut, blur.inputs["Value"])
|
||||
return _math(group, "MULTIPLY", _math(group, "MAXIMUM", cut, blur.outputs["Value"]), unprotected)
|
||||
|
||||
|
||||
def smooth_boundary_depth(group, depth, protected=False):
|
||||
"""Blur point depth on the current mesh and apply it only in the edge band."""
|
||||
nodes, links = group.nodes, group.links
|
||||
depth = evaluate_field(group, depth, "FLOAT", "POINT")
|
||||
iterations = group_input(nodes, {"Boundary Smooth"}).outputs["Boundary Smooth"]
|
||||
influence = boundary_influence(group, protected)
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT"
|
||||
blur.inputs["Weight"].default_value = 0.5
|
||||
links.new(depth, blur.inputs["Value"])
|
||||
links.new(iterations, blur.inputs["Iterations"])
|
||||
mix = nodes.new("ShaderNodeMix")
|
||||
mix.data_type = "FLOAT"
|
||||
links.new(influence, mix.inputs[0])
|
||||
links.new(depth, mix.inputs[2])
|
||||
links.new(blur.outputs["Value"], mix.inputs[3])
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "FLOAT"
|
||||
links.new(influence, choose.inputs["Switch"])
|
||||
links.new(depth, choose.inputs["False"])
|
||||
links.new(mix.outputs[0], choose.inputs["True"])
|
||||
enabled = nodes.new("GeometryNodeSwitch")
|
||||
enabled.input_type = "FLOAT"
|
||||
links.new(iterations, enabled.inputs["Switch"])
|
||||
links.new(depth, enabled.inputs["False"])
|
||||
links.new(choose.outputs[0], enabled.inputs["True"])
|
||||
return enabled.outputs[0]
|
||||
|
||||
|
||||
def smooth_boundary_camera(group, camera, protected=False):
|
||||
"""Reconstruct blurred camera depth along each unchanged projection ray."""
|
||||
nodes, links = group.nodes, group.links
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(camera, axes.inputs[0])
|
||||
depth = axes.outputs["Z"]
|
||||
smoothed = smooth_boundary_depth(group, depth, protected)
|
||||
changed = _math(group, "GREATER_THAN", _math(group, "ABSOLUTE", _math(group, "SUBTRACT", smoothed, depth)), 0)
|
||||
enabled = _math(group, "MULTIPLY", changed, _math(group, "GREATER_THAN", depth, 1e-8))
|
||||
projected = nodes.new("ShaderNodeVectorMath")
|
||||
projected.operation = "SCALE"
|
||||
links.new(camera, projected.inputs[0])
|
||||
links.new(_math(group, "DIVIDE", smoothed, depth), projected.inputs[3])
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "VECTOR"
|
||||
links.new(enabled, choose.inputs["Switch"])
|
||||
links.new(camera, choose.inputs["False"])
|
||||
links.new(projected.outputs[0], choose.inputs["True"])
|
||||
return choose.outputs[0]
|
||||
|
||||
|
||||
def smooth_cut_boundary(group, geometry, original_boundary=False):
|
||||
"""Blend O Smooth's pinned-boundary result through a two-ring edge mask."""
|
||||
influence = boundary_influence(group, original_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,
|
||||
)
|
||||
45
tools/nodes/common/cutout_boundary.py
Normal file
45
tools/nodes/common/cutout_boundary.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Clean cutout boundary triangles and taper the split thickness profile."""
|
||||
|
||||
from .depth_surface import _math
|
||||
from .nodes import evaluate_field, group_input, read_float_attribute, sample_field, store_float_attribute
|
||||
from .smoothing import edge_boundary_field
|
||||
from .boundary_smoothing import boundary_influence
|
||||
|
||||
|
||||
def remove_boundary_triangles(group, geometry):
|
||||
"""Remove all-boundary triangles once on the current topology."""
|
||||
nodes, links = group.nodes, group.links
|
||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
all_boundary = _math(group, "GREATER_THAN", evaluate_field(group, rim, "FLOAT", "FACE"), 0.99999)
|
||||
count = nodes.new("GeometryNodeInputMeshFaceNeighbors").outputs["Vertex Count"]
|
||||
selected = _math(group, "MULTIPLY", all_boundary, _math(group, "COMPARE", count, 3))
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
links.new(geometry, delete.inputs["Geometry"])
|
||||
links.new(selected, delete.inputs["Selection"])
|
||||
return delete.outputs["Geometry"]
|
||||
|
||||
|
||||
def taper_split_profile(group, original, geometry):
|
||||
"""Taper balloon thickness through the shared two-ring influence builder."""
|
||||
nodes, links = group.nodes, group.links
|
||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
||||
uv.data_type = "FLOAT_VECTOR"
|
||||
uv.inputs["Name"].default_value = "UVMap"
|
||||
original_uv = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(original, original_uv.inputs["Geometry"])
|
||||
links.new(uv.outputs["Attribute"], original_uv.inputs["Position"])
|
||||
nearest = nodes.new("GeometryNodeSampleNearest")
|
||||
nearest.domain = "POINT"
|
||||
links.new(original_uv.outputs["Geometry"], nearest.inputs["Geometry"])
|
||||
links.new(uv.outputs["Attribute"], nearest.inputs["Sample Position"])
|
||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||
protected = sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
||||
influence = boundary_influence(group, protected)
|
||||
controls = group_input(nodes, {"Edge Round", "Split Threshold"})
|
||||
strength = _math(group, "ADD", 1, _math(group, "MULTIPLY", controls.outputs["Edge Round"], 4))
|
||||
band = _math(group, "MINIMUM", _math(group, "MULTIPLY", influence, strength), 1)
|
||||
band = _math(group, "MULTIPLY", band, _math(group, "GREATER_THAN", controls.outputs["Split Threshold"], 0))
|
||||
retained = _math(group, "SQRT", _math(group, "MAXIMUM", _math(group, "SUBTRACT", 1, band), 0))
|
||||
profile = _math(group, "MULTIPLY", read_float_attribute(group, "o_balloon"), retained)
|
||||
return store_float_attribute(group, geometry, "o_balloon", profile)
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Face separation and connected thickness for Depth Surface assets."""
|
||||
|
||||
from .nodes import capture_fields, evaluate_field, store_float_attribute
|
||||
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attributes, sample_field
|
||||
|
||||
|
||||
CUT_ATTRIBUTE = ".o_anyimage_depth_cut"
|
||||
|
||||
|
||||
def _math(group, operation, a, b=None):
|
||||
@@ -211,10 +214,9 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
cut_vertices.data_type = "FLOAT"
|
||||
cut_vertices.domain = "POINT"
|
||||
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
|
||||
corners, captured = capture_fields(
|
||||
group, geometry, "CORNER",
|
||||
Camera=("VECTOR", face_camera),
|
||||
Cut=("BOOLEAN", _math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0)),
|
||||
corners, cut = store_boolean_attribute(
|
||||
group, geometry, CUT_ATTRIBUTE,
|
||||
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
|
||||
)
|
||||
split = nodes.new("GeometryNodeSplitEdges")
|
||||
links.new(corners, split.inputs["Mesh"])
|
||||
@@ -225,17 +227,16 @@ def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
|
||||
links.new(enabled, choose.inputs["Switch"])
|
||||
links.new(geometry, choose.inputs["False"])
|
||||
links.new(_remove_strip_faces(group, split.outputs["Mesh"], triangles=triangles), choose.inputs["True"])
|
||||
return choose.outputs["Output"], captured["Camera"], captured["Cut"]
|
||||
return choose.outputs["Output"], face_camera, cut
|
||||
|
||||
|
||||
def build_surface_camera(group, original, separated, cut_vertex, strength):
|
||||
"""Change cut depth along the original ray, preserving every other vertex."""
|
||||
enabled = _math(
|
||||
group,
|
||||
"MULTIPLY",
|
||||
_math(group, "GREATER_THAN", strength, 0.0),
|
||||
cut_vertex,
|
||||
)
|
||||
enabled = group.nodes.new("GeometryNodeSwitch")
|
||||
enabled.input_type = "BOOLEAN"
|
||||
group.links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
|
||||
enabled.inputs["False"].default_value = False
|
||||
group.links.new(cut_vertex, enabled.inputs["True"])
|
||||
original_axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
separated_axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
group.links.new(original, original_axes.inputs[0])
|
||||
@@ -249,7 +250,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength):
|
||||
group.links.new(ratio, projected.inputs[3])
|
||||
choose = group.nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "VECTOR"
|
||||
group.links.new(enabled, choose.inputs["Switch"])
|
||||
group.links.new(enabled.outputs["Output"], choose.inputs["Switch"])
|
||||
group.links.new(original, choose.inputs["False"])
|
||||
group.links.new(projected.outputs[0], choose.inputs["True"])
|
||||
return evaluate_field(group, choose.outputs["Output"], "FLOAT_VECTOR", "POINT")
|
||||
@@ -261,36 +262,32 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
nodes, links = group.nodes, group.links
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
index = nodes.new("GeometryNodeInputIndex")
|
||||
fields = {"Position": ("VECTOR", position.outputs[0]), "Vertex": ("INT", index.outputs[0])}
|
||||
if normal_smooth is not None:
|
||||
normal = nodes.new("GeometryNodeInputNormal")
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal_smooth, blur.inputs["Iterations"])
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
normalize = nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(blur.outputs[0], normalize.inputs[0])
|
||||
fields["Normal"] = ("VECTOR", normalize.outputs[0])
|
||||
geometry, captured = capture_fields(group, geometry, "POINT", **fields)
|
||||
flattened = nodes.new("ShaderNodeVectorMath")
|
||||
flattened.operation = "MULTIPLY"
|
||||
flattened.inputs[1].default_value = (1.0, 1.0, 0.0)
|
||||
links.new(position.outputs[0], flattened.inputs[0])
|
||||
sheet = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, sheet.inputs["Geometry"])
|
||||
links.new(flattened.outputs[0], sheet.inputs["Position"])
|
||||
saved = sheet.outputs["Geometry"]
|
||||
|
||||
# Weld in an integer identity grid, never in scene coordinates. Nearby cuts
|
||||
# retain distinct IDs; copies of one rim vertex share the same ID and layer.
|
||||
# Encode identity in the construction geometry before extrusion. The source
|
||||
# surface remains a separate sampling input; no position or ID is stored.
|
||||
identity = nodes.new("ShaderNodeCombineXYZ")
|
||||
for operation, axis in (("MODULO", "X"), ("DIVIDE", "Y")):
|
||||
part = nodes.new("FunctionNodeIntegerMath")
|
||||
part.operation = operation
|
||||
part.inputs[1].default_value = 4096
|
||||
links.new(captured["Vertex"], part.inputs[0])
|
||||
links.new(index.outputs[0], part.inputs[0])
|
||||
links.new(part.outputs[0], identity.inputs[axis])
|
||||
sheet = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, sheet.inputs["Geometry"])
|
||||
links.new(identity.outputs[0], sheet.inputs["Position"])
|
||||
saved = sheet.outputs["Geometry"]
|
||||
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs[0], layer_axes.inputs[0])
|
||||
row = nodes.new("FunctionNodeIntegerMath")
|
||||
row.operation = "MULTIPLY"
|
||||
row.inputs[1].default_value = 4096
|
||||
links.new(layer_axes.outputs["Y"], row.inputs[0])
|
||||
vertex = nodes.new("FunctionNodeIntegerMath")
|
||||
vertex.operation = "ADD"
|
||||
links.new(layer_axes.outputs["X"], vertex.inputs[0])
|
||||
links.new(row.outputs[0], vertex.inputs[1])
|
||||
source_index = vertex.outputs[0]
|
||||
source_position = sample_field(group, geometry, position.outputs[0], "FLOAT_VECTOR", index=source_index)
|
||||
source_thickness = sample_field(group, geometry, thickness, "FLOAT", index=source_index)
|
||||
|
||||
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
||||
extrude.mode = "FACES"
|
||||
@@ -315,53 +312,62 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
rear = flip_back.outputs["Mesh"]
|
||||
rim = flip_sides.outputs["Mesh"]
|
||||
back_parts = nodes.new("GeometryNodeJoinGeometry")
|
||||
links.new(rear, back_parts.inputs["Geometry"])
|
||||
links.new(rim, back_parts.inputs["Geometry"])
|
||||
rear = back_parts.outputs["Geometry"]
|
||||
if back_mask is not None:
|
||||
rear = store_float_attribute(group, rear, back_mask, 0.0, domain="FACE")
|
||||
rim = store_float_attribute(group, rim, back_mask, 0.0, domain="FACE")
|
||||
for value in (
|
||||
saved,
|
||||
rear,
|
||||
rim,
|
||||
):
|
||||
links.new(value, join.inputs["Geometry"])
|
||||
encoded = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(join.outputs["Geometry"], encoded.inputs["Geometry"])
|
||||
links.new(identity.outputs[0], encoded.inputs["Position"])
|
||||
construction = join.outputs["Geometry"]
|
||||
if weld_zero_thickness:
|
||||
positive = _math(group, "GREATER_THAN", source_thickness, 1e-12)
|
||||
flattened = nodes.new("ShaderNodeCombineXYZ")
|
||||
for axis in ("X", "Y"):
|
||||
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
|
||||
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Z"]), flattened.inputs["Z"])
|
||||
collapse = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(construction, collapse.inputs["Geometry"])
|
||||
links.new(flattened.outputs[0], collapse.inputs["Position"])
|
||||
construction = collapse.outputs["Geometry"]
|
||||
merge = nodes.new("GeometryNodeMergeByDistance")
|
||||
merge.mode = "ALL"
|
||||
merge.inputs["Distance"].default_value = 0.1
|
||||
links.new(encoded.outputs["Geometry"], merge.inputs["Geometry"])
|
||||
links.new(construction, merge.inputs["Geometry"])
|
||||
displacement = nodes.new("ShaderNodeCombineXYZ")
|
||||
layer_position = nodes.new("GeometryNodeInputPosition")
|
||||
layer_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(layer_position.outputs[0], layer_axes.inputs[0])
|
||||
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], thickness)
|
||||
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], source_thickness)
|
||||
links.new(distance, displacement.inputs["Z"])
|
||||
restored = nodes.new("ShaderNodeVectorMath")
|
||||
restored.operation = "ADD"
|
||||
links.new(captured["Position"], restored.inputs[0])
|
||||
links.new(source_position, restored.inputs[0])
|
||||
links.new(displacement.outputs[0], restored.inputs[1])
|
||||
result = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(merge.outputs["Geometry"], result.inputs["Geometry"])
|
||||
links.new(restored.outputs[0], result.inputs["Position"])
|
||||
local_position = nodes.new("GeometryNodeInputPosition")
|
||||
local_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(local_position.outputs[0], local_axes.inputs[0])
|
||||
layer = local_axes.outputs["Z"]
|
||||
if weld_zero_thickness:
|
||||
positive = _math(group, "GREATER_THAN", thickness, 1e-12)
|
||||
layer = _math(group, "MULTIPLY", positive, layer)
|
||||
links.new(layer, identity.inputs["Z"])
|
||||
if normal_smooth is not None:
|
||||
normal = nodes.new("GeometryNodeInputNormal")
|
||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal_smooth, blur.inputs["Iterations"])
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
normalize = nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(blur.outputs[0], normalize.inputs[0])
|
||||
source_normal = sample_field(group, geometry, normalize.outputs[0], "FLOAT_VECTOR", index=source_index)
|
||||
normal_offset = nodes.new("ShaderNodeVectorMath")
|
||||
normal_offset.operation = "SCALE"
|
||||
links.new(captured["Normal"], normal_offset.inputs[0])
|
||||
links.new(source_normal, normal_offset.inputs[0])
|
||||
links.new(distance, normal_offset.inputs[3])
|
||||
links.new(normal_offset.outputs[0], restored.inputs[1])
|
||||
nodes.remove(displacement)
|
||||
return {
|
||||
"geometry": result.outputs["Geometry"],
|
||||
"front": captured["Position"],
|
||||
"front": source_position,
|
||||
"source_index": source_index,
|
||||
"position": restored.outputs[0],
|
||||
"position_input": result.inputs["Position"],
|
||||
}
|
||||
@@ -374,7 +380,7 @@ def _projection_math(nodes, operation):
|
||||
|
||||
|
||||
def project_depth_surface(
|
||||
group, geometry, camera_position, uniform_scale, reference_depth, depth_scale
|
||||
group, geometry, camera_position, uniform_scale, reference_depth, depth_scale, split_strength
|
||||
):
|
||||
"""Interpolate the rectangular plane into the model camera coordinates."""
|
||||
nodes, links = group.nodes, group.links
|
||||
@@ -414,4 +420,7 @@ def project_depth_surface(
|
||||
links.new(geometry, project_geometry.inputs["Geometry"])
|
||||
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])
|
||||
|
||||
return project_geometry.outputs["Geometry"]
|
||||
return remove_attributes(
|
||||
group, project_geometry.outputs["Geometry"], CUT_ATTRIBUTE,
|
||||
enabled=_math(group, "GREATER_THAN", split_strength, 0.0),
|
||||
)
|
||||
|
||||
@@ -3,20 +3,23 @@
|
||||
|
||||
def inherit_material_slots(group, geometry, source):
|
||||
nodes, links = group.nodes, group.links
|
||||
capture = nodes.new("GeometryNodeCaptureAttribute")
|
||||
capture.domain = "POINT"
|
||||
capture.capture_items.clear()
|
||||
capture.capture_items.new("BOOLEAN", "Source")
|
||||
capture.inputs["Source"].default_value = True
|
||||
links.new(source, capture.inputs["Geometry"])
|
||||
size = nodes.new("GeometryNodeAttributeDomainSize")
|
||||
size.component = "MESH"
|
||||
links.new(source, size.inputs["Geometry"])
|
||||
index = nodes.new("GeometryNodeInputIndex")
|
||||
source_vertices = nodes.new("FunctionNodeCompare")
|
||||
source_vertices.data_type, source_vertices.operation = "INT", "LESS_THAN"
|
||||
links.new(index.outputs["Index"], source_vertices.inputs[2])
|
||||
links.new(size.outputs["Point Count"], source_vertices.inputs[3])
|
||||
join = nodes.new("GeometryNodeJoinGeometry")
|
||||
# Multi-input links are prepended: source occupies the first point range.
|
||||
links.new(geometry, join.inputs["Geometry"])
|
||||
links.new(capture.outputs["Geometry"], join.inputs["Geometry"])
|
||||
links.new(source, join.inputs["Geometry"])
|
||||
material = nodes.new("GeometryNodeSetMaterialIndex")
|
||||
material.inputs["Material Index"].default_value = 0
|
||||
links.new(join.outputs["Geometry"], material.inputs["Geometry"])
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain = "POINT"
|
||||
links.new(material.outputs["Geometry"], delete.inputs["Geometry"])
|
||||
links.new(capture.outputs["Source"], delete.inputs["Selection"])
|
||||
links.new(source_vertices.outputs["Result"], delete.inputs["Selection"])
|
||||
return delete.outputs["Geometry"]
|
||||
|
||||
@@ -114,13 +114,45 @@ def evaluate_field(group, value, data_type, domain):
|
||||
return node.outputs["Value"]
|
||||
|
||||
|
||||
def capture_fields(group, geometry, domain, **fields):
|
||||
"""Keep source fields attached to geometry through later changes."""
|
||||
node = group.nodes.new("GeometryNodeCaptureAttribute")
|
||||
node.domain = domain
|
||||
node.capture_items.clear()
|
||||
def sample_field(group, geometry, value, data_type, domain="POINT", index=None):
|
||||
"""Evaluate a field on an explicit source geometry and index domain."""
|
||||
node = group.nodes.new("GeometryNodeSampleIndex")
|
||||
node.data_type, node.domain = data_type, domain
|
||||
node.clamp = False
|
||||
if index is None:
|
||||
index = group.nodes.new("GeometryNodeInputIndex").outputs["Index"]
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
for name, (data_type, value) in fields.items():
|
||||
node.capture_items.new(data_type, name)
|
||||
group.links.new(value, node.inputs[name])
|
||||
return node.outputs["Geometry"], {name: node.outputs[name] for name in fields}
|
||||
group.links.new(value, node.inputs["Value"])
|
||||
group.links.new(index, node.inputs["Index"])
|
||||
return node.outputs["Value"]
|
||||
|
||||
|
||||
def store_boolean_attribute(group, geometry, name, value, domain="POINT"):
|
||||
"""Store a topology marker needed after destructive mesh operations."""
|
||||
node = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
node.domain, node.data_type = domain, "BOOLEAN"
|
||||
node.inputs["Name"].default_value = name
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
group.links.new(value, node.inputs["Value"])
|
||||
read = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
read.data_type = "BOOLEAN"
|
||||
read.inputs["Name"].default_value = name
|
||||
return node.outputs["Geometry"], read.outputs["Attribute"]
|
||||
|
||||
|
||||
def remove_attributes(group, geometry, *names, enabled=None):
|
||||
"""Remove only the explicitly owned temporary attributes."""
|
||||
original = geometry
|
||||
for name in names:
|
||||
node = group.nodes.new("GeometryNodeRemoveAttribute")
|
||||
node.inputs["Name"].default_value = name
|
||||
group.links.new(geometry, node.inputs["Geometry"])
|
||||
geometry = node.outputs["Geometry"]
|
||||
if enabled is not None:
|
||||
choose = group.nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
group.links.new(enabled, choose.inputs["Switch"])
|
||||
group.links.new(original, choose.inputs["False"])
|
||||
group.links.new(geometry, choose.inputs["True"])
|
||||
geometry = choose.outputs["Output"]
|
||||
return geometry
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Build shared boundary fields and iterative smoothing."""
|
||||
from .nodes import group_input
|
||||
from .nodes import group_input, sample_field
|
||||
|
||||
|
||||
def edge_boundary_field(nodes, links):
|
||||
@@ -41,7 +41,8 @@ def edge_boundary_field(nodes, links):
|
||||
|
||||
|
||||
def expand_smooth(
|
||||
group, geometry, iterations_name="Iterations", weight_name="Smooth"
|
||||
group, geometry, iterations_name="Iterations", weight_name="Smooth", *,
|
||||
influence=None, pin_sharp=True,
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
@@ -55,7 +56,6 @@ def expand_smooth(
|
||||
iteration_controls.outputs[iterations_name],
|
||||
repeat_input.inputs["Iterations"],
|
||||
)
|
||||
links.new(geometry, repeat_input.inputs["Geometry"])
|
||||
|
||||
boundary_field = edge_boundary_field(nodes, links)
|
||||
boundary_points = nodes.new("GeometryNodeFieldOnDomain")
|
||||
@@ -67,6 +67,21 @@ def expand_smooth(
|
||||
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"
|
||||
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"])
|
||||
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
smooth_all = nodes.new("GeometryNodeBlurAttribute")
|
||||
@@ -81,9 +96,6 @@ def expand_smooth(
|
||||
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"
|
||||
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
|
||||
sample_boundary = nodes.new("GeometryNodeSampleIndex")
|
||||
sample_boundary.data_type = "FLOAT_VECTOR"
|
||||
sample_boundary.domain = "POINT"
|
||||
@@ -94,8 +106,7 @@ def expand_smooth(
|
||||
|
||||
choose_boundary = nodes.new("GeometryNodeSwitch")
|
||||
choose_boundary.input_type = "VECTOR"
|
||||
direct_boundary = edge_boundary_field(nodes, links)
|
||||
links.new(direct_boundary, choose_boundary.inputs["Switch"])
|
||||
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"])
|
||||
boundary_mix = nodes.new("ShaderNodeMix")
|
||||
@@ -104,24 +115,33 @@ def expand_smooth(
|
||||
links.new(smooth_all.outputs["Value"], boundary_mix.inputs[4])
|
||||
links.new(choose_boundary.outputs["Output"], boundary_mix.inputs[5])
|
||||
|
||||
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])
|
||||
sharp_switch = nodes.new("GeometryNodeSwitch")
|
||||
sharp_switch.input_type = "FLOAT"
|
||||
sharp_switch.inputs["Switch"].default_value = True
|
||||
sharp_switch.inputs["False"].default_value = 1.0
|
||||
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
|
||||
smooth_weight = nodes.new("ShaderNodeMath")
|
||||
smooth_weight.operation = "MULTIPLY"
|
||||
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
|
||||
links.new(sharp_switch.outputs["Output"], smooth_weight.inputs[1])
|
||||
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])
|
||||
sharp_switch = nodes.new("GeometryNodeSwitch")
|
||||
sharp_switch.input_type = "FLOAT"
|
||||
sharp_switch.inputs["Switch"].default_value = pin_sharp
|
||||
sharp_switch.inputs["False"].default_value = 1.0
|
||||
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
|
||||
smooth_weight = nodes.new("ShaderNodeMath")
|
||||
smooth_weight.operation = "MULTIPLY"
|
||||
links.new(weight_controls.outputs[weight_name], smooth_weight.inputs[0])
|
||||
links.new(sharp_switch.outputs["Output"], 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]
|
||||
|
||||
target_position = nodes.new("GeometryNodeSwitch")
|
||||
target_position.input_type = "VECTOR"
|
||||
@@ -131,11 +151,18 @@ def expand_smooth(
|
||||
current_position = nodes.new("GeometryNodeInputPosition")
|
||||
mix_position = nodes.new("ShaderNodeMix")
|
||||
mix_position.data_type = "VECTOR"
|
||||
links.new(smooth_weight.outputs[0], mix_position.inputs[0])
|
||||
links.new(weight, mix_position.inputs[0])
|
||||
links.new(current_position.outputs["Position"], mix_position.inputs[4])
|
||||
links.new(target_position.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"]
|
||||
|
||||
21
tools/nodes/common/validity.py
Normal file
21
tools/nodes/common/validity.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Remove invalid vertices and the wire edges left by point deletion."""
|
||||
|
||||
|
||||
def delete_invalid_points(group, geometry, invalid):
|
||||
nodes, links = group.nodes, group.links
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "POINT", "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"]
|
||||
@@ -2,16 +2,51 @@
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group, capture_fields, evaluate_field
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.boundary_smoothing import smooth_boundary_camera, smooth_cut_boundary
|
||||
from ..common.cutout import _island_cleanup, _position_field, _remove_zero_thickness, _shade_output
|
||||
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera, thicken_depth_surface
|
||||
from ..common.smoothing import edge_boundary_field
|
||||
from ..common.cutout_boundary import remove_boundary_triangles, taper_split_profile
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Depth Cutout"
|
||||
|
||||
|
||||
def build_normal_weight(group, uniform):
|
||||
"""Scale normal smoothing by the active mode's relative thickness."""
|
||||
control = group_input(group.nodes, {"Thickness", "Uniform Thickness", "Reference Depth"})
|
||||
amount = group.nodes.new("GeometryNodeSwitch")
|
||||
amount.input_type = "FLOAT"
|
||||
group.links.new(uniform, amount.inputs["Switch"])
|
||||
group.links.new(control.outputs["Thickness"], amount.inputs["False"])
|
||||
ratio = _math(group, "DIVIDE", control.outputs["Uniform Thickness"],
|
||||
_math(group, "MAXIMUM", control.outputs["Reference Depth"], 1e-8))
|
||||
group.links.new(ratio, amount.inputs["True"])
|
||||
ramp = group.nodes.new("ShaderNodeMapRange")
|
||||
ramp.interpolation_type = "SMOOTHSTEP"
|
||||
ramp.clamp = True
|
||||
group.links.new(amount.outputs[0], ramp.inputs["Value"])
|
||||
return ramp.outputs["Result"]
|
||||
|
||||
|
||||
def blend_normal(group, original, smoothed, weight):
|
||||
"""Normalize the thickness-weighted blend on the consumer geometry."""
|
||||
mix = group.nodes.new("ShaderNodeMix")
|
||||
mix.data_type = "VECTOR"
|
||||
if isinstance(weight, (int, float)):
|
||||
mix.inputs[0].default_value = weight
|
||||
else:
|
||||
group.links.new(weight, mix.inputs[0])
|
||||
group.links.new(original, mix.inputs[4])
|
||||
group.links.new(smoothed, mix.inputs[5])
|
||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
group.links.new(mix.outputs["Result"], normalize.inputs[0])
|
||||
return normalize.outputs[0]
|
||||
|
||||
|
||||
def build_volume_fields(group):
|
||||
"""Evaluate the center and thickness on the original front surface."""
|
||||
links = group.links
|
||||
@@ -37,7 +72,7 @@ def build_volume_fields(group):
|
||||
return {"rim": rim, "center": center, "thickness": thickness}
|
||||
|
||||
|
||||
def build_volume_rounding(group, geometry, fields, uniform):
|
||||
def build_volume_rounding(group, geometry, fields, uniform, normal_weight=1):
|
||||
links = group.links
|
||||
|
||||
control = group.nodes.new("NodeGroupInput")
|
||||
@@ -87,39 +122,36 @@ def build_volume_rounding(group, geometry, fields, uniform):
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
local = group.nodes.new("NodeGroupInput")
|
||||
links.new(local.outputs["Normal Smooth"], blur.inputs["Iterations"])
|
||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(blur.outputs[0], normalize.inputs[0])
|
||||
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
||||
index = group.nodes.new("GeometryNodeInputIndex")
|
||||
sample = group.nodes.new("GeometryNodeSampleIndex")
|
||||
sample.data_type, sample.domain = "FLOAT_VECTOR", "POINT"
|
||||
links.new(surface.outputs[0], sample.inputs["Geometry"])
|
||||
links.new(normalize.outputs[0], sample.inputs["Value"])
|
||||
links.new(normalized, sample.inputs["Value"])
|
||||
links.new(index.outputs[0], sample.inputs["Index"])
|
||||
thickness = group.nodes.new("GeometryNodeSwitch")
|
||||
thickness.input_type = "FLOAT"
|
||||
links.new(uniform, thickness.inputs["Switch"])
|
||||
links.new(total, thickness.inputs["False"])
|
||||
links.new(control.outputs["Uniform Thickness"], thickness.inputs["True"])
|
||||
carrier, captured = capture_fields(
|
||||
group, geometry, "POINT",
|
||||
edge_weight=("FLOAT", weight), front_delta=("FLOAT", delta),
|
||||
thickness=("FLOAT", thickness.outputs[0]), normal=("VECTOR", sample.outputs[0]),
|
||||
)
|
||||
rounding = {
|
||||
"edge_weight": weight, "front_delta": delta,
|
||||
"thickness": thickness.outputs[0], "normal": sample.outputs[0],
|
||||
}
|
||||
displacement = group.nodes.new("ShaderNodeCombineXYZ")
|
||||
links.new(captured["front_delta"], displacement.inputs["Z"])
|
||||
links.new(delta, displacement.inputs["Z"])
|
||||
front = group.nodes.new("GeometryNodeSetPosition")
|
||||
links.new(carrier, front.inputs["Geometry"])
|
||||
links.new(geometry, front.inputs["Geometry"])
|
||||
links.new(displacement.outputs[0], front.inputs["Offset"])
|
||||
choose = group.nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(uniform, choose.inputs["Switch"])
|
||||
links.new(front.outputs[0], choose.inputs["False"])
|
||||
links.new(carrier, choose.inputs["True"])
|
||||
return choose.outputs[0], captured
|
||||
links.new(geometry, choose.inputs["True"])
|
||||
return choose.outputs[0], rounding
|
||||
|
||||
|
||||
def build_front_fields(group, rim, front_normal):
|
||||
def build_front_fields(group, rim, front_normal, normal_weight=1):
|
||||
"""Fit structural amplitude over neighbouring points before creating the shell."""
|
||||
links = group.links
|
||||
|
||||
@@ -162,10 +194,8 @@ def build_front_fields(group, rim, front_normal):
|
||||
smooth.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal, smooth.inputs["Value"])
|
||||
links.new(spread, smooth.inputs["Iterations"])
|
||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(smooth.outputs[0], normalize.inputs[0])
|
||||
return {"structure": structure, "inflation_normal": normalize.outputs[0]}
|
||||
normalized = blend_normal(group, normal, smooth.outputs[0], normal_weight)
|
||||
return {"structure": structure, "inflation_normal": normalized}
|
||||
|
||||
|
||||
def build_inflation_position(group, position, distance, uniform, fields):
|
||||
@@ -220,7 +250,7 @@ def build_inflation_position(group, position, distance, uniform, fields):
|
||||
return result.outputs[0]
|
||||
|
||||
|
||||
def build_front_normal(group):
|
||||
def build_front_normal(group, normal_weight):
|
||||
links = group.links
|
||||
normal = group.nodes.new("GeometryNodeInputNormal")
|
||||
control = group.nodes.new("NodeGroupInput")
|
||||
@@ -228,10 +258,8 @@ def build_front_normal(group):
|
||||
blur.data_type = "FLOAT_VECTOR"
|
||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||
links.new(control.outputs["Normal Smooth"], blur.inputs["Iterations"])
|
||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||
normalize.operation = "NORMALIZE"
|
||||
links.new(blur.outputs[0], normalize.inputs[0])
|
||||
return evaluate_field(group, normalize.outputs[0], "FLOAT_VECTOR", "POINT")
|
||||
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
||||
return evaluate_field(group, normalized, "FLOAT_VECTOR", "POINT")
|
||||
|
||||
|
||||
def build_shell_position(group, position, front, uniform, fields):
|
||||
@@ -316,36 +344,49 @@ def build_shell_position(group, position, front, uniform, fields):
|
||||
|
||||
|
||||
def _build_depth_surface(group, geometry, controls):
|
||||
"""Project the cleaned mesh, capture surface fields, and connect the shell."""
|
||||
"""Project the mesh and sample front-surface fields while shaping the shell."""
|
||||
links = group.links
|
||||
split = controls.outputs["Split Threshold"]
|
||||
image = controls.outputs["Depth Image"]
|
||||
scale = controls.outputs["Uniform Scale"]
|
||||
original = geometry
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||
group, geometry, sample_face_camera(group, image), split, scale, triangles=True,
|
||||
)
|
||||
geometry = remove_boundary_triangles(group, geometry)
|
||||
camera = build_surface_camera(group, _position_field(group, image), corner_camera, cut_vertex, split)
|
||||
camera = smooth_boundary_camera(group, camera)
|
||||
projection = project_depth_surface(
|
||||
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"],
|
||||
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split,
|
||||
)
|
||||
projection = smooth_cut_boundary(group, projection)
|
||||
projection = taper_split_profile(group, original, projection)
|
||||
# The mode field is shared by the geometry and thickness choices.
|
||||
mode = menu_switch(group, "Mode", ("Balloon", "Uniform"), "BOOLEAN")
|
||||
mode.inputs["Balloon"].default_value = False
|
||||
mode.inputs["Uniform"].default_value = True
|
||||
uniform = mode.outputs[0]
|
||||
volume = build_volume_fields(group)
|
||||
normal = build_front_normal(group)
|
||||
front = build_front_fields(group, volume["rim"], normal)
|
||||
shared, fields = capture_fields(
|
||||
group, projection, "POINT",
|
||||
center=("FLOAT", volume["center"]), front_normal=("VECTOR", normal),
|
||||
structure=("FLOAT", front["structure"]), inflation_normal=("VECTOR", front["inflation_normal"]),
|
||||
)
|
||||
rounded, rounding = build_volume_rounding(group, shared, {**volume, **fields}, uniform)
|
||||
fields.update(rounding)
|
||||
normal_weight = build_normal_weight(group, uniform)
|
||||
normal = build_front_normal(group, normal_weight)
|
||||
front = build_front_fields(group, volume["rim"], normal, normal_weight)
|
||||
rounded, rounding = build_volume_rounding(group, projection, volume, uniform, normal_weight)
|
||||
thickness = sample_field(group, projection, rounding["thickness"], "FLOAT")
|
||||
shell = thicken_depth_surface(
|
||||
group, rounded, fields["thickness"], back_mask=NORMAL_SCALE_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
group, rounded, thickness, back_mask=NORMAL_SCALE_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
)
|
||||
fields = {
|
||||
name: sample_field(group, projection, value, data_type, index=shell["source_index"])
|
||||
for name, value, data_type in (
|
||||
("center", volume["center"], "FLOAT"), ("front_normal", normal, "FLOAT_VECTOR"),
|
||||
("structure", front["structure"], "FLOAT"), ("inflation_normal", front["inflation_normal"], "FLOAT_VECTOR"),
|
||||
)
|
||||
}
|
||||
fields.update({
|
||||
name: sample_field(group, projection, value, "FLOAT_VECTOR" if name == "normal" else "FLOAT",
|
||||
index=shell["source_index"])
|
||||
for name, value in rounding.items()
|
||||
})
|
||||
oriented = build_shell_position(group, shell["position"], shell["front"], uniform, fields)
|
||||
links.new(oriented, shell["position_input"])
|
||||
amount = group.nodes.new("GeometryNodeSwitch")
|
||||
@@ -364,17 +405,20 @@ def build_image_depth_cutout_group():
|
||||
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
|
||||
group.is_modifier, group.use_fake_user = (True, True)
|
||||
group.color_tag = 'GEOMETRY'
|
||||
group['anyimage_asset_revision'] = 11
|
||||
group['anyimage_asset_revision'] = 18
|
||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
||||
float_input(group, 'Thickness', 1.0).description = 'Balloon profile multiplier; zero returns the sheet'
|
||||
float_input(group, 'Uniform Thickness', 0.0, subtype='DISTANCE').description = 'Distance used only by Uniform thickness mode'
|
||||
float_input(group, 'Depth Scale', 1.0)
|
||||
float_input(group, 'Split Threshold', 0.0, maximum=1, subtype='FACTOR')
|
||||
float_input(group, 'Split Threshold', 0.1, maximum=1, subtype='FACTOR')
|
||||
iterations = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt')
|
||||
iterations.default_value = 0
|
||||
iterations.min_value, iterations.max_value = (0, 20)
|
||||
options = group.interface.new_panel(name='Options', default_closed=True)
|
||||
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 5, 0, 20
|
||||
boundary_smooth.description = 'Smooth boundary depth and geometry along the original outline and split edges'
|
||||
float_input(group, 'Cleanup Threshold', 0.0, parent=options)
|
||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||
float_input(group, 'Edge Round', 0.2, maximum=1, subtype='FACTOR', parent=options)
|
||||
|
||||
@@ -4,12 +4,15 @@ import math
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.validity import delete_invalid_points
|
||||
|
||||
from ..common.depth_surface import (
|
||||
_math, split_depth_surface, build_surface_camera,
|
||||
_math, split_depth_surface, build_surface_camera, CUT_ATTRIBUTE,
|
||||
)
|
||||
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field
|
||||
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field, remove_attributes
|
||||
from ..common.material import inherit_material_slots
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.boundary_smoothing import smooth_boundary_depth, smooth_cut_boundary
|
||||
|
||||
|
||||
def _quad_sphere(group, segments):
|
||||
@@ -36,6 +39,7 @@ def _quad_sphere(group, segments):
|
||||
def build_image_depth_panorama_group():
|
||||
group = bpy.data.node_groups.new("O Image Depth Panorama", "GeometryNodeTree")
|
||||
group.is_modifier = group.use_fake_user = True
|
||||
group["anyimage_asset_revision"] = 14
|
||||
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
subdivide.default_value, subdivide.min_value, subdivide.max_value = 7, 0, 8
|
||||
@@ -45,6 +49,9 @@ def build_image_depth_panorama_group():
|
||||
smooth.default_value, smooth.min_value, smooth.max_value = 0, 0, 20
|
||||
interface_socket(group, "Valid Only", "INPUT", "NodeSocketBool").default_value = True
|
||||
options = group.interface.new_panel("Options", default_closed=True)
|
||||
boundary_smooth = interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 5, 0, 20
|
||||
boundary_smooth.description = "Smooth boundary distance and cut-edge geometry on the sphere"
|
||||
float_input(group, "Smooth Weight", 1, maximum=1, subtype="FACTOR", parent=options)
|
||||
float_input(group, "Validity Threshold", 0.9, maximum=1, subtype="FACTOR", parent=options)
|
||||
float_input(group, "Invalid Distance", 100, minimum=1e-5, subtype="DISTANCE", parent=options)
|
||||
@@ -133,18 +140,20 @@ def build_image_depth_panorama_group():
|
||||
any_invalid = _math(group, "GREATER_THAN", face_invalid, 0)
|
||||
mixed = _math(group, "MULTIPLY", any_invalid,
|
||||
_math(group, "LESS_THAN", face_invalid, 1))
|
||||
selection = nodes.new("GeometryNodeSwitch")
|
||||
selection.input_type = "BOOLEAN"
|
||||
links.new(group_input(nodes, {"Valid Only"}).outputs["Valid Only"], selection.inputs["Switch"])
|
||||
links.new(mixed, selection.inputs["False"])
|
||||
links.new(any_invalid, selection.inputs["True"])
|
||||
protected = uv_map.outputs["Geometry"]
|
||||
culled = delete_invalid_points(group, protected, any_invalid)
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
links.new(uv_map.outputs["Geometry"], delete.inputs["Geometry"])
|
||||
links.new(selection.outputs[0], delete.inputs["Selection"])
|
||||
links.new(protected, delete.inputs["Geometry"])
|
||||
links.new(mixed, delete.inputs["Selection"])
|
||||
selection = nodes.new("GeometryNodeSwitch")
|
||||
selection.input_type = "GEOMETRY"
|
||||
links.new(group_input(nodes, {"Valid Only"}).outputs["Valid Only"], selection.inputs["Switch"])
|
||||
links.new(delete.outputs[0], selection.inputs["False"])
|
||||
links.new(culled, selection.inputs["True"])
|
||||
controls = group_input(nodes, {"Split Threshold", "Reference Distance"})
|
||||
strength = controls.outputs["Split Threshold"]
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(group, delete.outputs[0], depth(combine(face_u, face_v))[0], strength,
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(group, selection.outputs[0], depth(combine(face_u, face_v))[0], strength,
|
||||
_math(group, "DIVIDE", 1, controls.outputs["Reference Distance"]),
|
||||
face_center=face.outputs["Value"])
|
||||
distance = build_surface_camera(group, pixel, corner_camera, cut_vertex, strength)
|
||||
@@ -153,7 +162,7 @@ def build_image_depth_panorama_group():
|
||||
distance = nodes.new("GeometryNodeSwitch")
|
||||
distance.input_type = "FLOAT"
|
||||
links.new(face_invalid, distance.inputs["Switch"])
|
||||
links.new(axes.outputs["X"], distance.inputs["False"])
|
||||
links.new(smooth_boundary_depth(group, axes.outputs["X"], face_invalid), distance.inputs["False"])
|
||||
links.new(group_input(nodes, {"Invalid Distance"}).outputs["Invalid Distance"], distance.inputs["True"])
|
||||
controls = group_input(nodes, {"Depth Scale"})
|
||||
radius = _math(group, "ADD", 1, _math(group, "MULTIPLY", controls.outputs["Depth Scale"], _math(group, "SUBTRACT", distance.outputs[0], 1)))
|
||||
@@ -164,7 +173,12 @@ def build_image_depth_panorama_group():
|
||||
deform = nodes.new("GeometryNodeSetPosition")
|
||||
links.new(geometry, deform.inputs["Geometry"])
|
||||
links.new(radial.outputs[0], deform.inputs["Position"])
|
||||
geometry = expand_smooth(group, deform.outputs[0], iterations_name="Smooth", weight_name="Smooth Weight")
|
||||
geometry = remove_attributes(
|
||||
group, deform.outputs[0], CUT_ATTRIBUTE,
|
||||
enabled=_math(group, "GREATER_THAN", strength, 0.0),
|
||||
)
|
||||
geometry = smooth_cut_boundary(group, geometry)
|
||||
geometry = expand_smooth(group, geometry, iterations_name="Smooth", weight_name="Smooth Weight")
|
||||
flip = nodes.new("GeometryNodeFlipFaces")
|
||||
links.new(geometry, flip.inputs["Mesh"])
|
||||
smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.validity import delete_invalid_points
|
||||
|
||||
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
|
||||
from ..common.nodes import prepare_node_group, float_input
|
||||
from ..common.nodes import prepare_node_group, float_input, evaluate_field
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.boundary_smoothing import smooth_boundary_camera, smooth_cut_boundary
|
||||
from ..common.depth_surface import (
|
||||
project_depth_surface, thicken_depth_surface,
|
||||
_math, project_depth_surface, thicken_depth_surface,
|
||||
split_depth_surface, sample_face_camera, build_surface_camera,
|
||||
)
|
||||
|
||||
@@ -22,6 +25,7 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
"GeometryNodeTree",
|
||||
)
|
||||
group.is_modifier = True
|
||||
group["anyimage_asset_revision"] = 14
|
||||
geometry = _interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
|
||||
geometry.structure_type = "SINGLE"
|
||||
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
|
||||
@@ -43,6 +47,9 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
valid_only = _interface_socket(group, "Valid Only", "INPUT", "NodeSocketBool")
|
||||
valid_only.default_value = True
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
boundary_smooth = _interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", parent=options)
|
||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 5, 0, 20
|
||||
boundary_smooth.description = "Smooth boundary depth and cut-edge geometry while preserving the original image border"
|
||||
base_plane_depth = _interface_socket(
|
||||
group,
|
||||
"Reference Depth",
|
||||
@@ -111,16 +118,21 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
|
||||
links.new(threshold_input.outputs["Validity Threshold"], invalid.inputs["B"])
|
||||
links.new(channels.outputs["Alpha"], invalid.inputs["A"])
|
||||
delete = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete.domain, delete.mode = "FACE", "ALL"
|
||||
links.new(plane.outputs["Geometry"], delete.inputs["Geometry"])
|
||||
links.new(invalid.outputs["Result"], delete.inputs["Selection"])
|
||||
# Image Plane's normalized UV rectangle retains its original outline after cuts.
|
||||
uv_axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(uv.outputs["Attribute"], uv_axes.inputs[0])
|
||||
u, v = uv_axes.outputs["X"], uv_axes.outputs["Y"]
|
||||
margin = _math(group, "MINIMUM", _math(group, "MINIMUM", u, v),
|
||||
_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"])
|
||||
cull_controls = _group_input(nodes, {"Valid Only"})
|
||||
cull = nodes.new("GeometryNodeSwitch")
|
||||
cull.input_type = "GEOMETRY"
|
||||
links.new(cull_controls.outputs["Valid Only"], cull.inputs["Switch"])
|
||||
links.new(delete.outputs["Geometry"], cull.inputs["True"])
|
||||
links.new(plane.outputs["Geometry"], cull.inputs["False"])
|
||||
links.new(culled, cull.inputs["True"])
|
||||
links.new(protected, cull.inputs["False"])
|
||||
split_input = _group_input(nodes, {"Split Threshold"})
|
||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||
group, cull.outputs["Output"], sample_face_camera(group, texture_input.outputs["Depth Image"]),
|
||||
@@ -131,11 +143,13 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
split_input.outputs["Split Threshold"],
|
||||
)
|
||||
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
|
||||
camera = smooth_boundary_camera(group, camera, original_boundary)
|
||||
surface = project_depth_surface(
|
||||
group, geometry, camera,
|
||||
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
|
||||
depth_input.outputs["Depth Scale"],
|
||||
depth_input.outputs["Depth Scale"], split_input.outputs["Split Threshold"],
|
||||
)
|
||||
surface = smooth_cut_boundary(group, surface, original_boundary)
|
||||
shell_input = _group_input(nodes, {"Thickness", "Normal Smooth"})
|
||||
shell = thicken_depth_surface(
|
||||
group, surface, shell_input.outputs["Thickness"],
|
||||
|
||||
Reference in New Issue
Block a user