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:
@@ -170,7 +170,7 @@ def test_smoothing_without_split_moves_outline_only_within_boundary_band():
|
||||
def test_smoothed_cut_shells_stay_closed_and_separate(mode):
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
obj, set_value = diagonal_surface()
|
||||
obj, set_value = diagonal_surface(triangles=True)
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
|
||||
"value", np.full(len(obj.data.vertices), 0.3, dtype=np.float32)
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Evaluate opposite-boundary cleanup on explicit mesh topology."""
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -98,31 +99,39 @@ def test_strips_are_removed_before_projection_and_closed_thickness(kind):
|
||||
image.update()
|
||||
set_value("Depth Scale", 1.0)
|
||||
set_value("Reference Depth", 11.0)
|
||||
assert len(evaluated(obj)[1]) == 8
|
||||
if kind == "cutout":
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
assert len(evaluated(obj)[1]) == 14
|
||||
else:
|
||||
assert len(evaluated(obj)[1]) == 8
|
||||
set_value("Depth Split", 0.5)
|
||||
vertices, faces = evaluated(obj)
|
||||
assert len(faces) == 4 and len(vertices) == 9
|
||||
if kind == "cutout":
|
||||
assert len(faces) == 6 and len(vertices) == 7
|
||||
else:
|
||||
assert len(faces) == 4 and len(vertices) == 9
|
||||
assert component_count(faces) == 1
|
||||
set_value("Thickness", 0.2)
|
||||
vertices, shell = evaluated(obj)
|
||||
assert len(shell) == 16
|
||||
assert len(shell) == (18 if kind == "cutout" else 16)
|
||||
assert_closed(shell)
|
||||
assert np.isfinite(vertices).all()
|
||||
assert component_count(shell) == 1
|
||||
set_value("Thickness", 0.0)
|
||||
assert len(evaluated(obj)[1]) == 4
|
||||
assert len(evaluated(obj)[1]) == (6 if kind == "cutout" else 4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["plane", "cutout"])
|
||||
@pytest.mark.parametrize("kind", ["plane"])
|
||||
def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(kind):
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
from tests.support.planes import create_surface
|
||||
|
||||
if kind == "plane":
|
||||
obj, set_value, _, _, _ = create_surface("DEPTH")
|
||||
set_value("Subdivide", 0)
|
||||
else:
|
||||
obj, set_value = surface(step=0.0, resolution=1)
|
||||
obj, set_value, _, _, _ = create_surface("DEPTH")
|
||||
set_value("Subdivide", 0)
|
||||
original, faces = evaluated(obj)
|
||||
assert len(faces) == 2
|
||||
set_value("Depth Split", 0.001)
|
||||
|
||||
@@ -134,15 +134,19 @@ def test_coincident_independent_sheets_do_not_weld_together():
|
||||
@pytest.mark.parametrize("resolution", [1, 4])
|
||||
def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
|
||||
obj, set_value = surface(step=0.0, resolution=resolution)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
original, original_faces = evaluated(obj)
|
||||
set_value("Depth Split", 1.0)
|
||||
coords, faces = evaluated(obj)
|
||||
if resolution == 1:
|
||||
assert len(coords) == 0 and not faces
|
||||
return
|
||||
assert len(coords) == len(obj.data.vertices)
|
||||
assert len(faces) == len(obj.data.polygons)
|
||||
np.testing.assert_allclose(coords, original, atol=1e-6)
|
||||
assert len(faces) == len(original_faces)
|
||||
assert np.isfinite(coords).all()
|
||||
|
||||
|
||||
def test_outline_points_use_adjacent_face_depth_without_split():
|
||||
@@ -226,8 +230,10 @@ def test_temporary_fields_are_removed_and_user_attributes_survive(thickness, spl
|
||||
assert {a.name for a in mesh.attributes if a.name.startswith(("o_depth_", "o_balloon_surface_"))} == {"o_depth_probe"}
|
||||
assert "UVMap" in mesh.attributes
|
||||
assert ".o_anyimage_user_weight" in mesh.attributes
|
||||
assert "_o_anyimage_depth_cut" not in mesh.attributes
|
||||
assert ".o_anyimage_original_boundary" not in mesh.attributes
|
||||
assert ".o_anyimage_depth_cut" not in mesh.attributes
|
||||
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
||||
assert all(
|
||||
v.value == pytest.approx(0.75)
|
||||
for v in mesh.attributes["o_user_weight"].data
|
||||
|
||||
@@ -195,7 +195,7 @@ def test_geometry_attribute_contract_includes_nested_groups(source):
|
||||
allowed = {
|
||||
"UVMap": ("FLOAT2", "CORNER"),
|
||||
"o_balloon": ("FLOAT", "POINT"),
|
||||
".o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
||||
"_o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
||||
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_face": ("FLOAT", "FACE"),
|
||||
"o_depth_axis": ("FLOAT", "FACE"),
|
||||
|
||||
@@ -27,7 +27,9 @@ def test_depth_attribute_cleanup_has_no_warnings(kind):
|
||||
assert mesh.vertices
|
||||
assert not {a.name for a in mesh.attributes} & {
|
||||
".o_anyimage_depth_cut", ".o_anyimage_original_boundary",
|
||||
"_o_anyimage_depth_cut",
|
||||
}
|
||||
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
||||
assert not list(modifier.node_warnings), [
|
||||
warning.message for warning in modifier.node_warnings
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user