Files
AnyImage/tests/test_depth_surface_split.py
nan 4c902ed7db feat(node_assets): add image plane geometry nodes and validation scripts
- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness.
- Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment.
- Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling.
- Authored `plane_layouts.py` to define node arrangements for image geometry assets.
- Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
2026-09-08 23:24:27 +08:00

277 lines
10 KiB
Python

"""Behavior checks for face separation and connected Depth Surface shells."""
import sys
from pathlib import Path
from collections import Counter
import bpy
import bmesh
import numpy as np
import pytest
ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "tools" / "node_assets"))
from cutout import build_cutout_group
from depth_surface_test_support import assert_closed, component_count, evaluated, surface
@pytest.mark.parametrize("mode", [0, 1])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_shell_walls_directly_connect_caps(mode, split):
from anyimage.common.object import modifier_input_identifier, set_modifier_input
obj, set_value = surface(resolution=4)
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
"value", np.full(len(obj.data.vertices), 0.3)
)
set_value("Split Threshold", split)
front, faces = evaluated(obj)
edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0])))
boundary_count = sum(count == 1 for count in edges.values())
set_value("Mode", mode)
modifier = obj.modifiers[0]
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE" if mode == 0 else "DISTANCE")
for amount in (0.01, 2.0):
set_modifier_input(modifier, thickness, amount)
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
points, shell_faces = evaluated(obj)
assert len(points) == 2 * len(front)
assert len(shell_faces) == 2 * len(faces) + boundary_count
assert all(len(face) == 4 for face in shell_faces)
assert np.isfinite(points).all()
assert_closed(shell_faces)
@pytest.mark.parametrize("thickness", [0.1, 1.4])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_surface_caps_and_segmented_walls_are_connected(thickness, split):
obj, set_value = surface()
set_value("Split Threshold", split)
set_value("Thickness", thickness)
coords, faces = evaluated(obj)
assert np.isfinite(coords).all()
assert_closed(faces)
set_value("Thickness", 0.0)
front, _ = evaluated(obj)
from scipy.spatial import cKDTree
assert cKDTree(front).query(coords)[0].max() <= thickness + 1e-5
assert cKDTree(coords).query(front)[0].max() < 1e-5
@pytest.mark.parametrize("size", [0.01, 1.0, 100.0])
def test_split_mapping_and_uniform_scale(size):
obj, set_value = surface(step=2.0, size=size)
# Face centers are size/2 apart: gradient is 4.
counts = []
for split in (0.0, 0.25, 0.5, 1.0):
set_value("Split Threshold", split)
coords, faces = evaluated(obj)
counts.append(len(coords))
assert len(faces) == 8
assert counts[:3] == [15] * 3
assert counts[3] == 18
set_value("Depth Scale", 0.0)
coords, faces = evaluated(obj)
assert len(coords) == 18 and np.abs(coords[:, 2]).max() < 1e-6
def test_midpoint_selects_gradient_six_and_preserves_faces():
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == 18 and len(faces) == 8
assert set(np.round(coords[:, 2], 4)) == {0.0, -3.0}
def test_cleanup_keeps_split_parts_of_a_large_source_island():
obj, set_value = surface(wrapper=True)
set_value("Cleanup Threshold", 110.0)
assert len(evaluated(obj)[1]) == 8
set_value("Split Threshold", 0.5)
assert len(evaluated(obj)[1]) == 8
set_value("Thickness", 0.2)
assert_closed(evaluated(obj)[1])
set_value("Cleanup Threshold", 210.0)
assert len(evaluated(obj)[1]) == 0
@pytest.mark.parametrize("depth_scale", [0.0, 0.25, 1.0, 2.0, 4.0])
def test_split_topology_uses_depth_before_display_scaling(depth_scale):
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
original, original_faces = evaluated(obj)
set_value("Depth Scale", depth_scale)
coords, faces = evaluated(obj)
assert len(coords) == len(original) == 18
assert faces == original_faces
np.testing.assert_allclose(coords[:, 2], original[:, 2] * depth_scale, atol=1e-6)
@pytest.mark.parametrize("shape", ["SOLID", "DEPTH_SOLID", "DEPTH_BALLOON"])
def test_each_asset_cleans_the_original_geometry_before_shaping(shape):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = build_cutout_group(shape)
links = [link for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
assert len(links) == 1
assert links[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
def test_full_wrapper_smooth_keeps_closed_shell():
obj, set_value = surface(wrapper=True)
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.2)
set_value("Smooth", 2)
coords, faces = evaluated(obj)
assert np.isfinite(coords).all()
assert_closed(faces)
def test_coincident_independent_sheets_do_not_weld_together():
obj, set_value = surface(step=0.0)
mesh = obj.data
vertices = [tuple(v.co) for v in mesh.vertices]
faces = [tuple(f.vertices) for f in mesh.polygons]
n = len(vertices)
duplicate = bpy.data.meshes.new("Close sheets")
duplicate.from_pydata(
vertices + [(x, y, z + 1e-8) for x, y, z in vertices],
[],
faces + [tuple(v + n for v in face) for face in faces],
)
uv = duplicate.uv_layers.new(name="UVMap")
for loop in duplicate.loops:
x, y, z = duplicate.vertices[loop.vertex_index].co
uv.data[loop.index].uv = (x / 2, y)
obj.data = duplicate
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
assert_closed(faces)
assert component_count(faces) == 2
@pytest.mark.parametrize("resolution", [1, 2, 4])
def test_planar_depth_has_no_split_at_any_density(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 1.0)
coords, faces = evaluated(obj)
assert len(coords) == len(obj.data.vertices)
assert len(faces) == len(obj.data.polygons)
np.testing.assert_allclose(coords, original, atol=1e-6)
def test_small_positive_split_preserves_unsplit_projection():
obj, set_value = surface(step=2.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.01)
coords, faces = evaluated(obj)
np.testing.assert_allclose(coords, original, atol=1e-6)
assert faces == original_faces
def test_cut_vertices_preserve_camera_rays_and_other_vertices():
obj, set_value = surface(step=3.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.5)
coords, faces = evaluated(obj)
for source_face, result_face in zip(original_faces, faces):
for source_index, result_index in zip(source_face, result_face):
before, after = original[source_index], coords[result_index]
if obj.data.vertices[source_index].co.x != 1.0:
np.testing.assert_allclose(after, before, atol=1e-6)
before_depth, after_depth = 1.0 - before[2], 1.0 - after[2]
np.testing.assert_allclose(
after[:2] / after_depth, before[:2] / before_depth, atol=1e-6
)
def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
obj, set_value = surface(step=3.0)
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
assert_closed(faces)
set_value("Thickness", 0.0)
front, _ = evaluated(obj)
from scipy.spatial import cKDTree
assert cKDTree(front).query(coords)[0].max() <= 0.1 + 1e-5
assert cKDTree(coords).query(front)[0].max() < 1e-5
@pytest.mark.parametrize("thickness", [0.0, 0.2])
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_temporary_attributes_are_removed_and_user_attributes_survive(thickness, split):
obj, set_value = surface(wrapper=True)
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
for value in attribute.data:
value.value = 0.75
set_value("Split Threshold", split)
set_value("Thickness", thickness)
set_value("Smooth", 2)
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
mesh = result.to_mesh()
try:
assert not any(a.name.startswith("o_depth_") for a in mesh.attributes)
assert "UVMap" in mesh.attributes
assert all(
v.value == pytest.approx(0.75)
for v in mesh.attributes["o_user_weight"].data
)
finally:
result.to_mesh_clear()
def test_zero_center_distance_does_not_split_or_produce_nan():
obj, set_value = surface(step=6.0)
mesh = bpy.data.meshes.new("Coincident face centers")
mesh.from_pydata(
[(0, 0, 0), (1, 0, 0), (0.5, 1, 0), (0.5, 1, 0)], [], [(0, 1, 2), (1, 0, 3)]
)
uv = mesh.uv_layers.new(name="UVMap")
for face in mesh.polygons:
for li in face.loop_indices:
uv.data[li].uv = (0.25 if face.index == 0 else 0.75, 0.5)
obj.data = mesh
set_value("Split Threshold", 1.0)
coords, faces = evaluated(obj)
assert len(coords) == 4 and len(faces) == 2
assert np.isfinite(coords).all()
def test_triangle_corner_weights_do_not_open_sloped_side_walls():
obj, set_value = surface(step=3.0)
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.triangulate(bm, faces=list(bm.faces))
bm.to_mesh(obj.data)
bm.free()
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
pixels[..., 2] += np.arange(32)[:, None] * 0.02
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
set_value("Thickness", 0.8)
coords, faces = evaluated(obj)
assert_closed(faces)
assert component_count(faces) == 2
@pytest.mark.parametrize("resolution", [1, 2, 4])
def test_linear_depth_slope_has_density_independent_selection(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
pixels[..., 2] = 1 + 4 * pixels[..., 0]
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
assert len(evaluated(obj)[0]) == len(obj.data.vertices)
set_value("Split Threshold", 1.0)
assert len(evaluated(obj)[0]) > len(obj.data.vertices)