Optimize Cutout Geometry Nodes

- Refactor Depth Cutout to reuse the projected outline field, reducing redundant boundary calculations and improving performance.
- Derive the center field from shared low-frequency fields, eliminating an unnecessary blur operation.
- Implement triangle-only strip cleanup for Depth Cutout, as the input is confirmed to be triangulated.
- Short-circuit zero-thickness branches in both Cutout and Depth Cutout to avoid unnecessary geometry generation.
- Standardize internal attribute naming with a new `_o_` prefix and implement wildcard cleanup for temporary attributes.
- Update relevant tests to ensure functionality remains consistent and validate changes against existing behavior.
This commit is contained in:
2026-09-16 08:42:13 +08:00
parent cf512dc013
commit 7f878a2f3f
22 changed files with 584 additions and 135 deletions

View File

@@ -1,9 +1,10 @@
"""Contracts for smooth depth volume, editable presets and normal masks."""
from collections import Counter
import bpy
import numpy as np
import pytest
from tools.nodes.groups.image_depth_cutout import build_volume_fields
from tools.nodes.groups.image_depth_cutout import build_image_depth_cutout_group, build_volume_fields
from anyimage.common.object import modifier_input_identifier, set_modifier_input
@@ -191,7 +192,7 @@ def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
evaluated.to_mesh_clear()
def test_depth_cutout_normal_map_only_affects_front_faces():
def test_depth_cutout_normal_reduction_uses_point_domain():
from tests.support.depth_surface import surface
obj, set_value = surface()
set_value("Thickness", 0.3)
@@ -200,14 +201,77 @@ def test_depth_cutout_normal_map_only_affects_front_faces():
mesh = evaluated.to_mesh()
try:
attribute = mesh.attributes["o_normal_reduction"]
assert attribute.domain == "FACE"
weights = [1.0 - item.value for item in attribute.data]
assert weights.count(1) == len(obj.data.polygons)
assert weights.count(0) == len(mesh.polygons) - len(obj.data.polygons)
assert attribute.domain == "POINT"
values = [item.value for item in attribute.data]
assert values
assert set(values) <= {0.0, 0.5, 1.0}
assert 0.0 in values and 1.0 in values
finally:
evaluated.to_mesh_clear()
def test_depth_cutout_reuses_boundary_and_slow_field_nodes():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = build_image_depth_cutout_group()
counts = Counter(node.bl_idname for node in group.nodes)
assert counts["GeometryNodeInputMeshEdgeNeighbors"] <= 15
assert counts["GeometryNodeBlurAttribute"] <= 12
def test_center_derives_from_shared_slow_fields():
bpy.ops.wm.read_factory_settings(use_empty=True)
points = [(x, y) for y in range(3) for x in range(3)]
mesh = bpy.data.meshes.new("Center fields")
mesh.from_pydata(
[(x, y, 0) for x, y in points], [],
[(0, 1, 4, 3), (1, 2, 5, 4), (3, 4, 7, 6), (4, 5, 8, 7)],
)
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
for value in profile.data:
value.value = 0.2
obj = bpy.data.objects.new("Center fields", mesh)
bpy.context.collection.objects.link(obj)
group = bpy.data.node_groups.new("Center derivation test", "GeometryNodeTree")
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
group.interface.new_socket(name="Thickness", in_out="INPUT", socket_type="NodeSocketFloat").default_value = 1
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
source = group.nodes.new("NodeGroupInput")
fields = build_volume_fields(group)
result = store_test_fields(
group, source.outputs["Geometry"],
{"depth_slow": fields["depth_slow"], "profile_slow": fields["profile_slow"], "center": fields["center"]},
)
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
obj.modifiers.new("Fields", "NODES").node_group = group
bpy.context.view_layer.update()
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
try:
depth_slow = np.array([item.value for item in result.attributes["depth_slow"].data])
profile_slow = np.array([item.value for item in result.attributes["profile_slow"].data])
center = np.array([item.value for item in result.attributes["center"].data])
np.testing.assert_allclose(center, depth_slow - profile_slow, atol=1e-5)
finally:
evaluated.to_mesh_clear()
def test_zero_thickness_short_circuits_shell_without_warnings():
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface()
set_value("Thickness", 0.0)
points, faces = evaluated(obj)
assert len(faces) == len(obj.data.polygons)
modifier = obj.modifiers[0]
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
assert not list(modifier.node_warnings), [
warning.message for warning in modifier.node_warnings
]
finally:
result.to_mesh_clear()
@pytest.mark.parametrize("thickness,inflation", [
(0.0, 1.0), (0.02, 0.0), (0.02, 0.0001),
(0.02, 0.5), (0.1, 0.5), (0.1, 1.0),