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

@@ -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