Refactor comparison operations in geometry nodes

- Introduced a new `compare_node` function to streamline comparison operations across various geometry nodes.
- Replaced instances of mathematical comparison nodes with the new `compare_node` function in multiple files, enhancing code readability and maintainability.
- Updated related logic in cutout, depth surface, and image processing groups to utilize the new comparison method.
- Ensured consistent handling of boolean logic through the introduction of a `boolean_node` function for combining boolean inputs.
This commit is contained in:
2026-09-16 21:42:02 +08:00
parent c62ec2cd48
commit 78bbff6351
19 changed files with 282 additions and 200 deletions

View File

@@ -31,6 +31,7 @@
- 构建阶段通过明确的节点或 socket 引用接线
- 保留节点默认 `name` 和标题,不用 `label` 覆盖,不用 String 节点注解。Frame 仅用于确有包裹语义的区域
- 全接线的简单数学运算可折叠;需要阅读的常数、运算域和模式保持可见
- 比较统一用 Compare 节点并按操作数数据类型选择输入,布尔运算统一用 Boolean Math,不用 Math 节点做比较、也不用数值乘法代替布尔运算;材质树不支持这两个节点,仍用 Math
- 修改器 Geometry Nodes 的输入 socket 默认使用 `SINGLE`
- socket `subtype` 按业务语义设置:长度用 `DISTANCE`,`0–1` 比例用 `FACTOR`,`0–100` 用 `PERCENTAGE`;无对应范围语义时保留默认
- 接口或布局变更须验证 socket 顺序与结构、未使用输出隐藏及几何求值;纯布局变更比较运算、默认值和实际连接,确认功能不变

View File

@@ -61,7 +61,9 @@ build_surface_camera(切口点取相邻面深)
删除改用 FACE 域,并去掉随后的残留边清理。理由是节点链按消费域求值:Plane 的判定链没有固定到点域,切到 FACE 后 UV 属性取该面四角平均,等于在面中心采样深度,因此面中心仍有效的面保留下来,切口不再比阈值轮廓多退一圈;点删除遗留的悬空边也随面删除一并消失,不需要第二次 Delete Geometry。
Panorama 的判定链在 `evaluate_field(..., "POINT")` 处已固定到点域,面域消费时按四角平均,得到的是「任一顶点无效即删」,删除范围与改动前相同。要让它也按面心判定,需要改用 `face_u` / `face_v` 那套面中心采样(直接平均点域经度会跨 seam 折返),本轮不做。
Panorama 的判定链同样改在面心求值:无效采样用面中心经纬(`combine(face_u, face_v)`)取 Closest 纹理,再固定到 FACE 域供删面使用,点域判定只留给「有效区与无效区之间的过渡带」和深度回退。面心经纬由面中心位置反算,不经过点域经度的平均,因此不会在 seam 处折返。判据统一后 Panorama 的切口与 Plane 一样落在面中心轮廓上,不再比阈值轮廓多退一圈。
面心判定让切口点可能落在无效侧:自身采样无效、但所属面中心有效。这些点由切口规则取相邻面深,所以 `Invalid Distance` 只在 Depth Mask 关闭时生效,判据改为「相邻面过半无效」。无效区连同过渡带一起保留时,无效区的点仍落到背景距离,切口点不再被背景距离顶出去。
## Risks / Trade-offs

View File

@@ -15,14 +15,15 @@
## 3. 测试
- [ ] 3.1 Plane:在无效侧写入与有效侧相差一个数量级的深度,断言 mask 切口点跟随相邻面深、不出现尖刺
- [ ] 3.2 Panorama:在无效侧写入差异距离,断言切口圈径向距离跟随相邻有效面
- [ ] 3.3 断言切口点保持自身射线方向(Plane 相机横纵坐标不变、Panorama 经纬方向不变)
- [ ] 3.4 重写 `test_valid_only_deletes_invalid_points_and_wire_edges` 中「删剩点是原几何子集」的断言,改为「切口取相邻面深」,保留 Depth Mask 关闭时逐位不变的对比
- [ ] 3.5 断言 Plane 原画幅外框保持自身采样深度,以及不存在相邻保留面的自由点输出坐标有限
- [ ] 3.6 运行 `uv run pytest`(至少覆盖 `tests/tools/nodes/test_depth_relief_planes.py`、`test_boundary_depth.py`、`test_depth_surface_split.py`、`test_image_depth_panorama.py`)
- [x] 3.1 Plane:在无效侧写入与有效侧相差一个数量级的深度,断言 mask 切口点跟随相邻面深、不出现尖刺(`test_plane_validity_hole_blurs_depth_and_protects_rectangle`)
- [x] 3.2 Panorama:在无效侧写入差异距离,断言切口圈径向距离跟随相邻有效面(`test_panorama_blurs_opening_radially_and_keeps_invalid_distance`)
- [x] 3.3 断言切口点保持自身射线方向(Plane 相机横纵坐标不变、Panorama 经纬方向不变)
- [x] 3.4 把 `test_valid_only_deletes_invalid_points_and_wire_edges` 的保留点断言改为「面中心采样低于 Mask Threshold 才删面」(改名 `test_valid_only_keeps_faces_covering_valid_samples`),保留 Depth Mask 关闭时逐位不变的对比
- [x] 3.5 断言 Plane 原画幅外框取相邻面深(`test_outline_points_use_adjacent_face_depth_without_split`),以及不存在相邻保留面的自由点输出坐标有限(`test_validity.py`)
- [x] 3.6 运行 `uv run pytest`(至少覆盖 `tests/tools/nodes/test_depth_relief_planes.py`、`test_boundary_depth.py`、`test_depth_surface_split.py`、`test_image_depth_panorama.py`)
## 4. 资产与收尾
- [x] 4.1 运行 `uv run node-group build`,确认节点资产构建与校验通过、Capture Attribute 为零
- [x] 4.2 检查源码与测试中不再有 `always_vertex` 及旧路径引用,确认最终差异只涉及本次范围
- [x] 4.3 `O Image Depth Panorama` 的无效判定改在面心求值,`Invalid Distance` 只在 Depth Mask 关闭时按相邻面生效,并补面心轮廓与切口点半径断言

Binary file not shown.

View File

@@ -37,9 +37,21 @@ def test_camera_projection_and_reference_depth(surface, strength):
base = evaluated(obj)
uv = (base[0][:, [0, 2]] + (2, 1)) / (4, 2)
texel = np.clip(uv * (16, 8), (0.5, 0.5), (15.5, 7.5))
camera = np.column_stack(((texel[:, 0] - 5) / 8,
(2 + texel[:, 0] / 16) * 0.5 - 1.2,
(texel[:, 1] - 3) / 8))
depth = 2 + texel[:, 0] / 16
# Outline vertices keep the border ray and adopt the adjacent face-centre
# depth, the same rule cut edges use to inherit their face sample.
face_depth = np.zeros((len(base[0]), 2))
for face in base[1]:
center = np.mean(base[0][list(face)], axis=0)
value = 2 + np.clip((center[0] + 2) / 4 * 16, 0.5, 15.5) / 16
for index in face:
face_depth[index] += (value, 1)
face_depth = face_depth[:, 0] / face_depth[:, 1]
on_outline = np.isclose(uv, 0).any(axis=1) | np.isclose(uv, 1).any(axis=1)
ratio = np.where(on_outline, face_depth / depth, 1.0)
camera = np.column_stack(((texel[:, 0] - 5) / 8 * ratio,
(depth * 0.5) * ratio - 1.2,
(texel[:, 1] - 3) / 8 * ratio))
expected = base[0] * (1 - strength) + camera * strength
set_value("Depth Scale", float(strength))
projected = evaluated(obj)
@@ -297,7 +309,7 @@ def test_relief_direction_adjusts_depth_and_keeps_base(relief, thickness, direct
("valid", 1.0), ("invalid", 0.0), ("invalid", 0.9),
("partial", 0.4), ("partial", 0.9), ("half", 0.9), ("stripe", 0.9),
])
def test_valid_only_deletes_invalid_points_and_wire_edges(surface, pattern, threshold):
def test_valid_only_keeps_faces_covering_valid_samples(surface, pattern, threshold):
obj, set_value, _, inputs, _ = surface
set_value("Mask Threshold", threshold)
depth_image = obj.modifiers[0][inputs["Depth Image"].identifier]
@@ -315,13 +327,16 @@ def test_valid_only_deletes_invalid_points_and_wire_edges(surface, pattern, thre
pixels[..., 3] = alpha
depth_image.pixels.foreach_set(pixels.ravel())
depth_image.update()
def sampled(x):
return np.interp((x + 2) / 4 * 16 - 0.5, np.arange(16), alpha)
for subdivisions in (0, 2, 5):
set_value("Subdivide", subdivisions)
set_value("Depth Mask", False)
base = evaluated(obj)
u = (base[0][:, 0] + 2) / 4
sampled = np.interp(u * 16 - 0.5, np.arange(16), alpha)
expected = [face for face in base[1] if np.all(sampled[list(face)] >= threshold)]
expected = [face for face in base[1]
if sampled(np.mean(base[0][list(face)][:, 0])) >= threshold]
set_value("Depth Mask", True)
culled = evaluated(obj)
assert len(culled[1]) == len(expected)

View File

@@ -78,6 +78,19 @@ def test_depth_alpha_culls_faces_without_double_multiplication():
assert len(evaluated(obj)[1]) == 384
def test_alpha_cut_keeps_the_ring_sharing_the_face_centre_contour():
"""Faces are judged at their centre, so the ring straddling the sky stays."""
alpha = np.ones((64, 128))
alpha[:, 64:] = 0
obj, _ = panorama(3.0, alpha, subdivide=4)
points, faces, _ = evaluated(obj)
plain, _ = panorama(3.0, 1.0, subdivide=4)
_, full, _ = evaluated(plain)
# Judging faces by their corners would drop one more ring of the half sphere.
assert len(faces) == len(full) // 2
np.testing.assert_allclose(np.linalg.norm(points, axis=1), 3, atol=1e-5)
def test_invalid_depth_does_not_bleed_into_alpha_boundary():
alpha = np.ones((64, 128))
alpha[:, 64:] = 0

View File

@@ -66,6 +66,9 @@ def test_saved_parameter_descriptions_match_definitions():
}:
assert "Smooth" not in input_names
assert "Smooth Weight" not in input_names
if "Mask Threshold" in parameters[group.name]:
threshold = next(item for item in input_items if item.name == "Mask Threshold")
assert (threshold.min_value, threshold.max_value) == pytest.approx((0.0, 0.99))
if group.name in {
"O Image Depth Cutout", "O Image Depth Plane", "O Image Relief Plane",
}:

View File

@@ -1,12 +1,12 @@
"""Verify point culling removes its dangling wire geometry."""
"""Verify face culling removes the faces the depth mask excludes."""
import bpy
from tools.nodes.common.nodes import interface_socket
from tools.nodes.common.validity import delete_invalid_points
from tools.nodes.common.nodes import compare_node, interface_socket
from tools.nodes.common.validity import delete_invalid_faces
def test_point_culling_removes_wire_edges_without_removing_adjacent_surface():
def test_face_culling_removes_invalid_faces_without_leaving_wire_geometry():
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("Two Quads")
mesh.from_pydata([(0, 0, 0), (1, 0, 0), (2, 0, 0),
@@ -14,17 +14,14 @@ def test_point_culling_removes_wire_edges_without_removing_adjacent_surface():
[(0, 1, 4, 3), (1, 2, 5, 4)])
obj = bpy.data.objects.new("Surface", mesh)
bpy.context.collection.objects.link(obj)
group = bpy.data.node_groups.new("Point Culling", "GeometryNodeTree")
group = bpy.data.node_groups.new("Face Culling", "GeometryNodeTree")
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
inlet = group.nodes.new("NodeGroupInput")
outlet = group.nodes.new("NodeGroupOutput")
index = group.nodes.new("GeometryNodeInputIndex")
invalid = group.nodes.new("ShaderNodeMath")
invalid.operation = "LESS_THAN"
invalid.inputs[1].default_value = 1
group.links.new(index.outputs[0], invalid.inputs[0])
geometry = delete_invalid_points(group, inlet.outputs["Geometry"], invalid.outputs[0])
invalid = compare_node(group, "LESS_THAN", index.outputs["Index"], 1, data_type="INT")
geometry = delete_invalid_faces(group, inlet.outputs["Geometry"], invalid)
group.links.new(geometry, outlet.inputs["Geometry"])
modifier = obj.modifiers.new("Cull", "NODES")
modifier.node_group = group

View File

@@ -1,24 +1,37 @@
"""Apply boundary-preserving smoothing within a narrow cut-edge band."""
from .nodes import evaluate_field, group_input
from .nodes import boolean_node, compare_node, evaluate_field, group_input
from .depth_surface import _math
from .smoothing import edge_boundary_field, pinned_smooth
def _gated(group, value, blocked, *, default):
"""Return the value unless the blocked control is true."""
node = group.nodes.new("GeometryNodeSwitch")
node.input_type = "FLOAT"
node.inputs["True"].default_value = default
if isinstance(blocked, bool):
node.inputs["Switch"].default_value = blocked
else:
group.links.new(blocked, node.inputs["Switch"])
group.links.new(value, node.inputs["False"])
return node.outputs[0]
def boundary_influence(group, protected=False, *, boundary=None):
"""Fade a boundary field through two rings while excluding protected points."""
nodes, links = group.nodes, group.links
if boundary is None:
boundary = edge_boundary_field(nodes, links)
boundary = evaluate_field(group, boundary, "FLOAT", "POINT")
unprotected = _math(group, "SUBTRACT", 1, protected)
cut = _math(group, "MULTIPLY", boundary, unprotected)
cut = _gated(group, boundary, protected, default=0.0)
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)
mixed = _math(group, "MAXIMUM", cut, blur.outputs["Value"])
return _gated(group, mixed, protected, default=0.0)
def smooth_boundary_depth(group, depth, protected=False):
@@ -63,8 +76,8 @@ def smooth_boundary_camera(group, camera, protected=False):
def reconstruct_camera_depth(group, camera, depth, smoothed):
"""Move each valid camera point along its original projection ray."""
nodes, links = group.nodes, group.links
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))
changed = compare_node(group, "GREATER_THAN", _math(group, "ABSOLUTE", _math(group, "SUBTRACT", smoothed, depth)), 0)
enabled = boolean_node(group, "AND", changed, compare_node(group, "GREATER_THAN", depth, 1e-8))
projected = nodes.new("ShaderNodeVectorMath")
projected.operation = "SCALE"
links.new(camera, projected.inputs[0])
@@ -82,7 +95,7 @@ def cut_boundary_influence(group, split_boundary, protected=False, *, outline_sc
nodes, links = group.nodes, group.links
if boundary is None:
boundary = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
split = _math(group, "MULTIPLY", boundary, split_boundary)
split = _gated(group, boundary, boolean_node(group, "NOT", split_boundary), default=0.0)
outline = _math(group, "MAXIMUM", _math(group, "SUBTRACT", boundary, split), 0)
split_influence = boundary_influence(group, protected, boundary=split)
outline_influence = _math(

View File

@@ -1,5 +1,5 @@
"""Build geometry calculations shared by Cutout assets."""
from .nodes import math_node
from .nodes import compare_node, math_node
from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME
@@ -107,12 +107,10 @@ def _remove_zero_thickness(
):
nodes = group.nodes
links = group.links
zero = math_node(nodes, "LESS_THAN")
zero.inputs[1].default_value = 1e-6
links.new(thickness, zero.inputs[0])
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(zero.outputs[0], choose.inputs["Switch"])
links.new(zero, choose.inputs["Switch"])
links.new(shaped_geometry, choose.inputs["False"])
links.new(zero_geometry, choose.inputs["True"])
return choose.outputs["Output"]

View File

@@ -1,7 +1,10 @@
"""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 .nodes import (
boolean_node, compare_node, evaluate_field, group_input,
read_float_attribute, sample_field, store_float_attribute,
)
from .smoothing import edge_boundary_field
from .boundary_smoothing import boundary_influence
@@ -10,9 +13,9 @@ 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)
all_boundary = compare_node(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))
selected = boolean_node(group, "AND", all_boundary, compare_node(group, "EQUAL", count, 3, data_type="INT"))
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(geometry, delete.inputs["Geometry"])
@@ -41,9 +44,16 @@ def original_boundary_field(group, original, rim=None):
def taper_split_profile(group, geometry, original_boundary):
"""Taper balloon thickness through the shared two-ring influence builder."""
nodes = group.nodes
links = group.links
influence = boundary_influence(group, original_boundary)
controls = group_input(nodes, {"Depth Split"})
band = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", controls.outputs["Depth Split"], 0))
has_split = compare_node(group, "GREATER_THAN", controls.outputs["Depth Split"], 0)
band = nodes.new("GeometryNodeSwitch")
band.input_type = "FLOAT"
band.inputs["False"].default_value = 0.0
links.new(has_split, band.inputs["Switch"])
links.new(influence, band.inputs["True"])
band = band.outputs[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)

View File

@@ -1,6 +1,9 @@
"""Face separation and connected thickness for Depth Surface assets."""
from .nodes import evaluate_field, store_float_attribute, store_boolean_attribute, remove_attribute_pattern, sample_field
from .nodes import (
boolean_node, compare_node, evaluate_field, store_float_attribute,
store_boolean_attribute, remove_attribute_pattern, sample_field,
)
from .smoothing import edge_boundary_field
@@ -32,21 +35,7 @@ def _remove_quad_strip_faces(group, geometry):
nodes, links = group.nodes, group.links
def equals(value, count):
compare = nodes.new("FunctionNodeCompare")
compare.data_type, compare.operation = "INT", "EQUAL"
links.new(value, compare.inputs[2])
if isinstance(count, int):
compare.inputs[3].default_value = count
else:
links.new(count, compare.inputs[3])
return compare.outputs["Result"]
def combine(operation, a, b):
node = nodes.new("FunctionNodeIntegerMath")
node.operation = operation
links.new(a, node.inputs[0])
links.new(b, node.inputs[1])
return node.outputs[0]
return compare_node(group, "EQUAL", value, count, data_type="INT")
boundaries = []
for offset in range(4):
@@ -64,13 +53,13 @@ def _remove_quad_strip_faces(group, geometry):
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
links.new(neighbors.outputs["Face Count"], sample.inputs["Value"])
boundaries.append(equals(sample.outputs["Value"], 1))
opposite = combine(
"MAXIMUM",
combine("MULTIPLY", boundaries[0], boundaries[2]),
combine("MULTIPLY", boundaries[1], boundaries[3]),
opposite = boolean_node(
group, "OR",
boolean_node(group, "AND", boundaries[0], boundaries[2]),
boolean_node(group, "AND", boundaries[1], boundaries[3]),
)
face = nodes.new("GeometryNodeInputMeshFaceNeighbors")
selected = combine("MULTIPLY", equals(face.outputs["Vertex Count"], 4), opposite)
selected = boolean_node(group, "AND", equals(face.outputs["Vertex Count"], 4), opposite)
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(geometry, delete.inputs["Geometry"])
@@ -83,25 +72,11 @@ def _remove_triangle_strip_faces(group, geometry):
nodes, links = group.nodes, group.links
def equals(value, count):
compare = nodes.new("FunctionNodeCompare")
compare.data_type, compare.operation = "INT", "EQUAL"
links.new(value, compare.inputs[2])
if isinstance(count, int):
compare.inputs[3].default_value = count
else:
links.new(count, compare.inputs[3])
return compare.outputs["Result"]
return compare_node(group, "EQUAL", value, count, data_type="INT")
def combine(operation, a, b):
node = nodes.new("FunctionNodeIntegerMath")
node.operation = operation
links.new(a, node.inputs[0])
links.new(b, node.inputs[1])
return node.outputs[0]
def sample(value, index, domain):
def sample(value, index, domain, data_type="INT"):
node = nodes.new("GeometryNodeFieldAtIndex")
node.data_type, node.domain = "INT", domain
node.data_type, node.domain = data_type, domain
links.new(value, node.inputs["Value"])
links.new(index, node.inputs["Index"])
return node.outputs["Value"]
@@ -138,16 +113,29 @@ def _remove_triangle_strip_faces(group, geometry):
sides.append((triangle, vertex(corner), vertex(tip), boundary(end), boundary(tip)))
a, b = sides
same_direction = equals(a[1], b[1])
same_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[3]), combine("MULTIPLY", a[4], b[4]))
reverse_order = combine("MAXIMUM", combine("MULTIPLY", a[3], b[4]), combine("MULTIPLY", a[4], b[3]))
pair = combine(
"MAXIMUM",
combine("MULTIPLY", same_direction, reverse_order),
combine("MULTIPLY", equals(same_direction, 0), same_order),
same_order = boolean_node(
group, "OR",
boolean_node(group, "AND", a[3], b[3]),
boolean_node(group, "AND", a[4], b[4]),
)
reverse_order = boolean_node(
group, "OR",
boolean_node(group, "AND", a[3], b[4]),
boolean_node(group, "AND", a[4], b[3]),
)
pair = boolean_node(
group, "OR",
boolean_node(group, "AND", same_direction, reverse_order),
boolean_node(group, "AND", boolean_node(group, "NOT", same_direction), same_order),
)
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
for condition in (equals(neighbors.outputs["Face Count"], 2), a[0], b[0], equals(equals(a[2], b[2]), 0)):
pair = combine("MULTIPLY", pair, condition)
for condition in (
equals(neighbors.outputs["Face Count"], 2),
a[0],
b[0],
boolean_node(group, "NOT", equals(a[2], b[2])),
):
pair = boolean_node(group, "AND", pair, condition)
face_index = nodes.new("GeometryNodeInputIndex")
corners = nodes.new("GeometryNodeCornersOfFace")
@@ -157,14 +145,21 @@ def _remove_triangle_strip_faces(group, geometry):
corner = offset_corner(corners.outputs["Corner Index"], offset)
edge = nodes.new("GeometryNodeEdgesOfCorner")
links.new(corner, edge.inputs["Corner Index"])
match = sample(pair, edge.outputs["Next Edge Index"], "EDGE")
paired = match if paired is None else combine("MAXIMUM", paired, match)
match = sample(pair, edge.outputs["Next Edge Index"], "EDGE", "BOOLEAN")
paired = match if paired is None else boolean_node(group, "OR", paired, match)
rim = boundary(corner)
boundary_count = rim if boundary_count is None else combine("ADD", boundary_count, rim)
selected = combine(
"MULTIPLY",
if boundary_count is None:
boundary_count = rim
else:
add = nodes.new("FunctionNodeIntegerMath")
add.operation = "ADD"
links.new(boundary_count, add.inputs[0])
links.new(rim, add.inputs[1])
boundary_count = add.outputs[0]
selected = boolean_node(
group, "AND",
equals(corners.outputs["Total"], 3),
combine("MAXIMUM", paired, equals(boundary_count, 3)),
boolean_node(group, "OR", paired, equals(boundary_count, 3)),
)
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
@@ -233,24 +228,24 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
reference = _math(group, "MAXIMUM", reference_depth, 1e-8)
# Uniform Scale cancels between the distance jump and local spacing correction.
# Cross multiplication also avoids dividing by the local distance.
selected = _math(
selected = compare_node(
group,
"GREATER_THAN",
_math(group, "MULTIPLY", _math(group, "MULTIPLY", delta, reference), strength),
_math(group, "MULTIPLY", _math(group, "MULTIPLY", distance.outputs["Value"], local_distance), 2.5),
)
selected = _math(
selected = boolean_node(
group,
"MULTIPLY",
"AND",
selected,
_math(group, "GREATER_THAN", distance.outputs["Value"], 0.0),
compare_node(group, "GREATER_THAN", distance.outputs["Value"], 0.0),
)
neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
selected = _math(
selected = boolean_node(
group,
"MULTIPLY",
"AND",
selected,
_math(group, "COMPARE", neighbors.outputs["Face Count"], 2.0),
compare_node(group, "EQUAL", neighbors.outputs["Face Count"], 2, data_type="INT"),
)
cut_edges = nodes.new("GeometryNodeFieldOnDomain")
cut_edges.data_type = "FLOAT"
@@ -262,12 +257,12 @@ def split_depth_surface(group, geometry, face_camera, strength, reference_depth,
links.new(cut_edges.outputs["Value"], cut_vertices.inputs["Value"])
corners, cut = store_boolean_attribute(
group, geometry, CUT_ATTRIBUTE,
_math(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
compare_node(group, "GREATER_THAN", cut_vertices.outputs["Value"], 0.0), domain="CORNER",
)
split = nodes.new("GeometryNodeSplitEdges")
links.new(corners, split.inputs["Mesh"])
links.new(selected, split.inputs["Selection"])
enabled = _math(group, "GREATER_THAN", strength, 0.0)
enabled = compare_node(group, "GREATER_THAN", strength, 0.0)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(enabled, choose.inputs["Switch"])
@@ -286,7 +281,7 @@ def build_surface_camera(group, original, separated, cut_vertex, strength):
cut_boundary = evaluate_field(group, edge_boundary_field(nodes, links), "BOOLEAN", "POINT")
enabled = nodes.new("GeometryNodeSwitch")
enabled.input_type = "BOOLEAN"
links.new(_math(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
links.new(compare_node(group, "GREATER_THAN", strength, 0.0), enabled.inputs["Switch"])
enabled.inputs["False"].default_value = False
links.new(cut_vertex, enabled.inputs["True"])
combined = nodes.new("FunctionNodeBooleanMath")
@@ -382,11 +377,16 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
links.new(value, join.inputs["Geometry"])
construction = join.outputs["Geometry"]
if weld_zero_thickness:
positive = _math(group, "GREATER_THAN", source_thickness, 1e-12)
positive = compare_node(group, "GREATER_THAN", source_thickness, 1e-12)
flattened = nodes.new("ShaderNodeCombineXYZ")
for axis in ("X", "Z"):
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Y"]), flattened.inputs["Y"])
collapsed = nodes.new("GeometryNodeSwitch")
collapsed.input_type = "FLOAT"
collapsed.inputs["False"].default_value = 0.0
links.new(positive, collapsed.inputs["Switch"])
links.new(layer_axes.outputs["Y"], collapsed.inputs["True"])
links.new(collapsed.outputs[0], flattened.inputs["Y"])
collapse = nodes.new("GeometryNodeSetPosition")
links.new(construction, collapse.inputs["Geometry"])
links.new(flattened.outputs[0], collapse.inputs["Position"])

View File

@@ -79,6 +79,41 @@ def math_node(nodes, operation):
return node
_COMPARE_INPUTS = {
"FLOAT": ("A", "B"),
"INT": ("A_INT", "B_INT"),
"VECTOR": ("A_VEC3", "B_VEC3"),
"RGBA": ("A_COL", "B_COL"),
}
def compare_node(group, operation, a, b, data_type="FLOAT"):
"""Compare two inputs on the given data type and return the boolean result."""
node = group.nodes.new("FunctionNodeCompare")
node.data_type, node.operation = data_type, operation
for value, identifier in zip((a, b), _COMPARE_INPUTS[data_type]):
socket = next(item for item in node.inputs if item.identifier == identifier)
if isinstance(value, (bool, int, float)):
socket.default_value = value
else:
group.links.new(value, socket)
return node.outputs["Result"]
def boolean_node(group, operation, a=None, b=None):
"""Combine boolean inputs and return the boolean result."""
node = group.nodes.new("FunctionNodeBooleanMath")
node.operation = operation
for value, index in ((a, 0), (b, 1)):
if value is None:
continue
if isinstance(value, bool):
node.inputs[index].default_value = value
else:
group.links.new(value, node.inputs[index])
return node.outputs["Boolean"]
def menu_switch(group, control, names, kind):
selector = next((node for node in group.nodes if node.bl_idname == "GeometryNodeMenuSwitch" and node.inputs["Menu"].is_linked and node.inputs["Menu"].links[0].from_socket.name == control), None)
if selector is None:

View File

@@ -1,7 +1,10 @@
"""Build O Image Cutout."""
import bpy
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_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
from ..common.nodes import (
interface_socket, group_input, compare_node, math_node, float_input,
menu_switch, read_float_attribute, store_float_attribute, prepare_node_group,
)
from ..common.cutout import _extrude_layers, _offset_geometry_y, _set_layer_y, _shade_output
@@ -24,15 +27,12 @@ def _set_segmented_layer_y(
links.new(edge_vertices.outputs["Position 1"], edge_length.inputs[0])
links.new(edge_vertices.outputs["Position 2"], edge_length.inputs[1])
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
boundary = math_node(nodes, "COMPARE")
boundary.inputs[1].default_value = 1.0
boundary.inputs[2].default_value = 0.1
links.new(edge_neighbors.outputs["Face Count"], boundary.inputs[0])
boundary = compare_node(group, "EQUAL", edge_neighbors.outputs["Face Count"], 1, data_type="INT")
statistics = nodes.new("GeometryNodeAttributeStatistic")
statistics.data_type = "FLOAT"
statistics.domain = "EDGE"
links.new(geometry, statistics.inputs["Geometry"])
links.new(boundary.outputs[0], statistics.inputs["Selection"])
links.new(boundary, statistics.inputs["Selection"])
links.new(edge_length.outputs["Value"], statistics.inputs["Attribute"])
safe_edge = math_node(nodes, "MAXIMUM")
safe_edge.inputs[1].default_value = 1e-6
@@ -108,12 +108,10 @@ def _set_segmented_layer_y(
def _build_shell(group, geometry, thickness):
nodes = group.nodes
links = group.links
zero = math_node(nodes, "LESS_THAN")
zero.inputs[1].default_value = 1e-6
links.new(thickness, zero.inputs[0])
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(zero.outputs[0], choose.inputs["Switch"])
links.new(zero, choose.inputs["Switch"])
links.new(geometry, choose.inputs["True"])
front = math_node(nodes, "MULTIPLY")
front.inputs[1].default_value = 0.0
@@ -135,12 +133,10 @@ def _build_shell(group, geometry, thickness):
def _build_balloon(group, geometry, thickness):
nodes = group.nodes
links = group.links
zero = math_node(nodes, "LESS_THAN")
zero.inputs[1].default_value = 1e-6
links.new(thickness, zero.inputs[0])
zero = compare_node(group, "LESS_THAN", thickness, 1e-6)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(zero.outputs[0], choose.inputs["Switch"])
links.new(zero, choose.inputs["Switch"])
links.new(geometry, choose.inputs["True"])
profile = read_float_attribute(group, 'o_balloon')
front_scale = math_node(nodes, "MULTIPLY")
@@ -183,13 +179,11 @@ def build_image_cutout_group():
geometry = choose.outputs[0]
profile = read_float_attribute(group, 'o_balloon')
amount = group_input(group.nodes, {'Thickness'})
enabled = math_node(group.nodes, 'GREATER_THAN')
enabled.inputs[1].default_value = 1e-06
group.links.new(amount.outputs['Thickness'], enabled.inputs[0])
enabled = compare_node(group, 'GREATER_THAN', amount.outputs['Thickness'], 1e-6)
inflated = group.nodes.new('GeometryNodeSwitch')
inflated.input_type = 'FLOAT'
inflated.inputs['False'].default_value = 1
group.links.new(enabled.outputs[0], inflated.inputs['Switch'])
group.links.new(enabled, inflated.inputs['Switch'])
group.links.new(profile, inflated.inputs['True'])
mode = menu_switch(group, 'Mode', ('Balloon', 'Shell'), 'FLOAT')
mode.inputs['Shell'].default_value = 1

View File

@@ -6,7 +6,8 @@ from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import (
interface_socket, group_input, float_input, bool_input, vector_input,
evaluate_field, store_float_attribute, read_float_attribute, prepare_node_group,
compare_node, evaluate_field, store_float_attribute, read_float_attribute,
prepare_node_group,
)
from ..common.depth_surface import _math
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
@@ -67,7 +68,7 @@ def _align_front(group, geometry, controls):
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)
return compare_node(group, 'EQUAL', neighbors.outputs['Face Count'], 1, data_type='INT')
def _fold_to_plane(group, geometry, tolerance):
@@ -76,7 +77,7 @@ def _fold_to_plane(group, geometry, tolerance):
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)
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'])
@@ -91,8 +92,8 @@ def _fold_to_plane(group, geometry, tolerance):
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])
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'])
@@ -106,10 +107,10 @@ def _clean_crease(group, geometry, tolerance):
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])
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])
neighbors = nodes.new('GeometryNodeInputMeshEdgeNeighbors')
interior = _math(group, 'COMPARE', neighbors.outputs['Face Count'], 2)
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])
@@ -147,7 +148,7 @@ def _clean_crease(group, geometry, tolerance):
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)
return compare_node(group, 'GREATER_THAN', _math(group, 'MAXIMUM', heights[0], heights[1]), tolerance)
def _edge_abs_y(group):
@@ -179,14 +180,9 @@ def _boundary_fields(group, tolerance):
links.new(position.outputs[0], components.inputs[0])
seam = nodes.new('FunctionNodeBooleanMath')
seam.operation = 'AND'
links.new(_math(group, 'GREATER_THAN', boundary, 0), seam.inputs[0])
links.new(compare_node(group, 'GREATER_THAN', boundary, 0), seam.inputs[0])
links.new(
_math(
group,
'LESS_THAN',
_math(group, 'ABSOLUTE', components.outputs['Y']),
tolerance,
),
compare_node(group, 'LESS_THAN', _math(group, 'ABSOLUTE', components.outputs['Y']), tolerance),
seam.inputs[1],
)
return boundary, seam.outputs[0]
@@ -306,12 +302,16 @@ def build_image_cutout_symmetry_group():
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 = _math(group, 'MULTIPLY', inputs['Fill Smooth'], inputs['Fill Sides'])
front = _relax_fill(group, front, boundary_field, fill_smooth)
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])
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(compare_node(group, 'EQUAL', 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'
@@ -323,14 +323,14 @@ def build_image_cutout_symmetry_group():
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'])
links.new(compare_node(group, 'EQUAL', read_float_attribute(group, FACE_ATTRIBUTE), 1), mark_back.inputs['Selection'])
join = nodes.new('GeometryNodeJoinGeometry')
for geometry in (body.outputs[0], mark_back.outputs['Geometry']):
links.new(geometry, join.inputs[0])
position = nodes.new('GeometryNodeInputPosition')
axis = nodes.new('ShaderNodeSeparateXYZ')
links.new(position.outputs[0], axis.inputs[0])
on_axis = _math(group, 'LESS_THAN', _math(group, 'ABSOLUTE', axis.outputs['Y']), snap_tolerance)
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'])

View File

@@ -1,7 +1,11 @@
"""Build O Image Depth Cutout."""
import bpy
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import interface_socket, group_input, float_input, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
from ..common.nodes import (
interface_socket, group_input, float_input, read_float_attribute,
store_float_attribute, prepare_node_group, sample_field, evaluate_field,
compare_node,
)
from ..common.boundary_smoothing import smooth_cut_boundary
from ..common.cutout import _position_field, _shade_output
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera, thicken_depth_surface
@@ -314,7 +318,7 @@ def _build_depth_surface(group, geometry, controls):
links.new(is_shell, selected_thickness.inputs["Switch"])
links.new(controls.outputs["Thickness"], selected_thickness.inputs["False"])
links.new(controls.outputs["Shell Thickness"], selected_thickness.inputs["True"])
has_thickness = _math(group, "GREATER_THAN", selected_thickness.outputs[0], 1e-6)
has_thickness = compare_node(group, "GREATER_THAN", selected_thickness.outputs[0], 1e-6)
zero_shell = group.nodes.new("GeometryNodeSwitch")
zero_shell.input_type = "GEOMETRY"
links.new(has_thickness, zero_shell.inputs["Switch"])

View File

@@ -9,7 +9,10 @@ from ..common.validity import delete_invalid_faces
from ..common.depth_surface import (
_math, split_depth_surface, build_surface_camera,
)
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group, evaluate_field, remove_attribute_pattern
from ..common.nodes import (
compare_node, float_input, group_input, interface_socket,
prepare_node_group, evaluate_field, remove_attribute_pattern,
)
from ..common.material_slots import inherit_material_slots
from ..common.boundary_smoothing import smooth_boundary_depth, smooth_cut_boundary
@@ -42,6 +45,8 @@ def build_image_depth_panorama_group():
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."
subdivide.default_value, subdivide.min_value, subdivide.max_value = 8, 0, 8
dome = float_input(group, "Dome Radius", 50, minimum=1, maximum=100, subtype="DISTANCE")
dome.description = "Radius of the sphere kept at Depth Scale 0, and where mask areas below Mask Threshold land."
float_input(group, "Depth Scale", 1, maximum=1, subtype="FACTOR").description = "Blend between a sphere at zero and the estimated depth surface at one."
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."
valid_only = interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
@@ -51,8 +56,7 @@ 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, "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."
float_input(group, "Mask Threshold", 0.9, maximum=0.99, subtype="FACTOR", parent=options).description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
data = group.interface.new_panel("Data", default_closed=True)
interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
@@ -99,8 +103,8 @@ def build_image_depth_panorama_group():
alphas.append(separate.outputs["Alpha"])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "RGBA"
links.new(_math(group, "LESS_THAN", alphas[0],
_math(group, "SUBTRACT", alphas[1], 1e-6)), choose.inputs["Switch"])
links.new(compare_node(group, "LESS_THAN", alphas[0],
_math(group, "SUBTRACT", alphas[1], 1e-6)), choose.inputs["Switch"])
links.new(linear, choose.inputs["False"])
links.new(nearest, choose.inputs["True"])
return choose.outputs[0], alphas[1]
@@ -117,7 +121,7 @@ def build_image_depth_panorama_group():
correction = _math(group, "ROUND", _math(group, "SUBTRACT", face_u, longitude))
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "FLOAT"
links.new(_math(group, "GREATER_THAN", _math(group, "ABSOLUTE", z), 0.999999), choose.inputs["Switch"])
links.new(compare_node(group, "GREATER_THAN", _math(group, "ABSOLUTE", z), 0.999999), choose.inputs["Switch"])
links.new(_math(group, "ADD", longitude, correction), choose.inputs["False"])
links.new(face_u, choose.inputs["True"])
uv_map = nodes.new("GeometryNodeStoreNamedAttribute")
@@ -127,46 +131,43 @@ def build_image_depth_panorama_group():
links.new(combine(choose.outputs[0], latitude), uv_map.inputs["Value"])
pixel, _ = depth(coordinates)
pixel = evaluate_field(group, pixel, "FLOAT_VECTOR", "POINT")
nearest = nodes.new("FunctionNodeSeparateColor")
links.new(texture(coordinates, "Closest"), nearest.inputs["Color"])
controls = group_input(nodes, {"Mask Threshold"})
invalid = _math(group, "MAXIMUM",
_math(group, "LESS_THAN", nearest.outputs["Alpha"], controls.outputs["Mask Threshold"]),
_math(group, "LESS_THAN", nearest.outputs["Red"], 1e-5))
face_invalid = evaluate_field(group, invalid, "FLOAT", "POINT")
any_invalid = _math(group, "GREATER_THAN", face_invalid, 0)
mixed = _math(group, "MULTIPLY", any_invalid,
_math(group, "LESS_THAN", face_invalid, 1))
# Both the mask and the depth of a face are read at its centre, like Depth Plane.
face_camera, face_alpha = depth(combine(face_u, face_v))
face_alpha = evaluate_field(group, face_alpha, "FLOAT", "FACE")
threshold_input = group_input(nodes, {"Mask Threshold"})
invalid = compare_node(group, "LESS_THAN", face_alpha, threshold_input.outputs["Mask Threshold"])
protected = uv_map.outputs["Geometry"]
culled = delete_invalid_faces(group, protected, any_invalid)
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
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, {"Depth Mask"}).outputs["Depth Mask"], selection.inputs["Switch"])
links.new(delete.outputs[0], selection.inputs["False"])
links.new(culled, selection.inputs["True"])
culled = delete_invalid_faces(group, protected, invalid)
cull_controls = group_input(nodes, {"Depth Mask"})
cull = nodes.new("GeometryNodeSwitch")
cull.input_type = "GEOMETRY"
links.new(cull_controls.outputs["Depth Mask"], cull.inputs["Switch"])
links.new(culled, cull.inputs["True"])
links.new(protected, cull.inputs["False"])
controls = group_input(nodes, {"Depth Split"})
strength = controls.outputs["Depth Split"]
face_camera = depth(combine(face_u, face_v))[0]
radial = nodes.new("ShaderNodeSeparateXYZ")
links.new(face_camera, radial.inputs[0])
geometry, corner_camera, cut_vertex = split_depth_surface(
group, selection.outputs[0], face_camera, strength, 1.0,
group, cull.outputs["Output"], face_camera, strength, 1.0,
face_distance=radial.outputs["X"], face_center=face.outputs["Value"],
)
distance = build_surface_camera(group, pixel, corner_camera, cut_vertex, strength)
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(distance, axes.inputs[0])
distance = nodes.new("GeometryNodeSwitch")
distance.input_type = "FLOAT"
links.new(face_invalid, distance.inputs["Switch"])
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"])
distance = smooth_boundary_depth(group, axes.outputs["X"])
dome_radius = group_input(nodes, {"Dome Radius"}).outputs["Dome Radius"]
fallback = nodes.new("GeometryNodeSwitch")
fallback.input_type = "FLOAT"
links.new(invalid, fallback.inputs["Switch"])
links.new(distance, fallback.inputs["False"])
links.new(dome_radius, fallback.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)))
radius = _math(
group, "ADD", dome_radius,
_math(group, "MULTIPLY", controls.outputs["Depth Scale"],
_math(group, "SUBTRACT", fallback.outputs[0], dome_radius)),
)
radial = nodes.new("ShaderNodeVectorMath")
radial.operation = "SCALE"
links.new(position, radial.inputs[0])

View File

@@ -5,7 +5,7 @@ import bpy
from ..common.validity import delete_invalid_faces
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
from ..common.nodes import prepare_node_group, float_input, evaluate_field
from ..common.nodes import compare_node, prepare_node_group, float_input, evaluate_field
from ..common.boundary_smoothing import smooth_boundary_camera, smooth_cut_boundary
from ..common.depth_surface import (
_math, project_depth_surface, thicken_depth_surface,
@@ -51,7 +51,7 @@ def build_image_depth_plane_group(image_plane_group):
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", parent=options)
validity_threshold.description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
validity_threshold.default_value = 0.9
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
validity_threshold.min_value, validity_threshold.max_value = 0.0, 0.99
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)
@@ -97,15 +97,15 @@ def build_image_depth_plane_group(image_plane_group):
links.new(texture.outputs["Color"], channels.inputs["Color"])
invalid = nodes.new("FunctionNodeCompare")
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
links.new(threshold_input.outputs["Mask Threshold"], invalid.inputs["B"])
links.new(channels.outputs["Alpha"], invalid.inputs["A"])
links.new(threshold_input.outputs["Mask Threshold"], invalid.inputs["B"])
# 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")
original_boundary = evaluate_field(group, compare_node(group, "LESS_THAN", margin, 1e-6), "BOOLEAN", "POINT")
protected = plane.outputs["Geometry"]
culled = delete_invalid_faces(group, protected, invalid.outputs["Result"])
cull_controls = _group_input(nodes, {"Depth Mask"})
@@ -139,13 +139,10 @@ def build_image_depth_plane_group(image_plane_group):
group, surface, shell_input.outputs["Thickness"],
normal_smooth=100,
)
positive = nodes.new("ShaderNodeMath")
positive.operation = "GREATER_THAN"
links.new(shell_input.outputs["Thickness"], positive.inputs[0])
positive.inputs[1].default_value = 0
positive = compare_node(group, "GREATER_THAN", shell_input.outputs["Thickness"], 0)
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(positive.outputs[0], choose.inputs["Switch"])
links.new(positive, choose.inputs["Switch"])
links.new(surface, choose.inputs["False"])
links.new(shell["geometry"], choose.inputs["True"])
smooth = nodes.new("GeometryNodeSetShadeSmooth")

View File

@@ -5,7 +5,7 @@ import bpy
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
from ..common.nodes import store_float_attribute
from ..common.nodes import prepare_node_group, float_input, vector_input
from ..common.nodes import compare_node, prepare_node_group, float_input, vector_input
from ..common.reference_plane import reference_plane_depth
from ..common.boundary_smoothing import boundary_influence
@@ -58,7 +58,7 @@ def build_image_relief_plane_group(image_plane_group):
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.min_value, validity_threshold.max_value = 0.0, 0.99
validity_threshold.subtype = "FACTOR"
base_plane_depth = _interface_socket(
group,
@@ -136,10 +136,10 @@ def build_image_relief_plane_group(image_plane_group):
links.new(texture.outputs["Alpha"], invalid.inputs["A"])
links.new(image_input.outputs["Mask Threshold"], invalid.inputs["B"])
mask_enabled = image_input.outputs["Depth Mask"]
valid_mask = nodes.new("ShaderNodeMath")
valid_mask.operation = "SUBTRACT"
valid_mask.inputs[0].default_value = 1.0
links.new(invalid.outputs["Result"], valid_mask.inputs[1])
valid_mask = nodes.new("GeometryNodeSwitch")
valid_mask.input_type = "FLOAT"
valid_mask.inputs["False"].default_value = 1.0
links.new(invalid.outputs["Result"], valid_mask.inputs["Switch"])
mask_switch = nodes.new("GeometryNodeSwitch")
mask_switch.input_type = "FLOAT"
mask_switch.inputs["False"].default_value = 1.0
@@ -165,9 +165,7 @@ def build_image_relief_plane_group(image_plane_group):
links.new(full_offset.outputs[0], masked_full_offset.inputs[0])
links.new(mask_switch.outputs[0], masked_full_offset.inputs[1])
has_thickness = _math(nodes, "GREATER_THAN")
has_thickness.inputs[1].default_value = 1e-6
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
has_thickness = compare_node(group, "GREATER_THAN", shape_input.outputs["Thickness"], 1e-6)
safe_offset = _math(nodes, "MINIMUM")
safe_offset.inputs[1].default_value = 0.0
links.new(masked_full_offset.outputs[0], safe_offset.inputs[0])
@@ -201,7 +199,7 @@ def build_image_relief_plane_group(image_plane_group):
layer_weight = nodes.new("GeometryNodeSwitch")
layer_weight.input_type = "FLOAT"
layer_weight.inputs["False"].default_value = 1.0
links.new(has_thickness.outputs[0], layer_weight.inputs["Switch"])
links.new(has_thickness, layer_weight.inputs["Switch"])
links.new(one_minus_ratio.outputs[0], layer_weight.inputs["True"])
weighted_offset = _math(nodes, "MULTIPLY")