Add cutout shell and volume node assets for depth volume processing
- Implemented `cutout_shell.py` to build front normals and shell positions for balloon geometry. - Created `cutout_volume.py` to establish smooth center fields and manage balloon surface attributes. - Introduced `pisa_sr_export.py` for exporting PiSA-SR components with dependencies and asset finalization.
This commit is contained in:
@@ -12,16 +12,12 @@ import pytest
|
||||
ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "tools" / "node_assets"))
|
||||
from build_cutout import build_node_group, build_cutout_modifier_group
|
||||
from build_cutout_modifiers import build_cutout_group
|
||||
|
||||
|
||||
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
groups = {
|
||||
shape: build_node_group(shape)
|
||||
for shape in ("SURFACE", "BALLOON", "DEPTH_BALLOON", "DEPTH_SURFACE")
|
||||
}
|
||||
group = build_cutout_modifier_group(groups) if wrapper else groups["DEPTH_SURFACE"]
|
||||
group = build_cutout_group("DEPTH_SOLID")
|
||||
# Two columns with a complete depth jump along their common boundary.
|
||||
points = [
|
||||
(x * size / resolution, y * size / resolution, 0)
|
||||
@@ -75,17 +71,47 @@ def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||
("Reference Depth", size),
|
||||
("Depth Scale", 1.0),
|
||||
("Thickness", 0.0),
|
||||
("Cleanup", 0.0),
|
||||
("Split", 0.0),
|
||||
("Front Inflation", 0.0),
|
||||
("Split Threshold", 0.0),
|
||||
("Mode", 1),
|
||||
("Smooth", 0),
|
||||
):
|
||||
set_value(name, value)
|
||||
if wrapper:
|
||||
set_value("Shape", 3)
|
||||
set_value("Cleanup Threshold", 0.0)
|
||||
set_value("Inward Axis", 0)
|
||||
set_value("Smooth Iterations", 0)
|
||||
set_value("Smooth", 0)
|
||||
return obj, set_value
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
def evaluated(obj):
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
@@ -108,12 +134,16 @@ def assert_closed(faces):
|
||||
@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", split)
|
||||
set_value("Split Threshold", split)
|
||||
set_value("Thickness", thickness)
|
||||
coords, faces = evaluated(obj)
|
||||
assert np.isfinite(coords).all()
|
||||
assert_closed(faces)
|
||||
assert coords[:, 2].min() == pytest.approx(-6 - thickness, abs=1e-5)
|
||||
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])
|
||||
@@ -122,7 +152,7 @@ def test_split_mapping_and_uniform_scale(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", split)
|
||||
set_value("Split Threshold", split)
|
||||
coords, faces = evaluated(obj)
|
||||
counts.append(len(coords))
|
||||
assert len(faces) == 8
|
||||
@@ -130,32 +160,55 @@ def test_split_mapping_and_uniform_scale(size):
|
||||
assert counts[3] == 18
|
||||
set_value("Depth Scale", 0.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == 15 and np.abs(coords[:, 2]).max() < 1e-6
|
||||
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", 0.5)
|
||||
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_removes_new_islands_before_thickness():
|
||||
obj, set_value = surface()
|
||||
set_value("Cleanup", 1.1)
|
||||
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("Split", 0.5)
|
||||
assert len(evaluated(obj)[1]) == 0
|
||||
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", 0.5)
|
||||
set_value("Split Threshold", 0.5)
|
||||
set_value("Thickness", 0.2)
|
||||
set_value("Smooth Iterations", 2)
|
||||
set_value("Smooth", 2)
|
||||
coords, faces = evaluated(obj)
|
||||
assert np.isfinite(coords).all()
|
||||
assert_closed(faces)
|
||||
@@ -206,7 +259,7 @@ def test_coincident_independent_sheets_do_not_weld_together():
|
||||
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", 1.0)
|
||||
set_value("Split Threshold", 1.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == len(obj.data.vertices)
|
||||
assert len(faces) == len(obj.data.polygons)
|
||||
@@ -216,7 +269,7 @@ def test_planar_depth_has_no_split_at_any_density(resolution):
|
||||
def test_small_positive_split_preserves_unsplit_projection():
|
||||
obj, set_value = surface(step=2.0)
|
||||
original, original_faces = evaluated(obj)
|
||||
set_value("Split", 0.01)
|
||||
set_value("Split Threshold", 0.01)
|
||||
coords, faces = evaluated(obj)
|
||||
np.testing.assert_allclose(coords, original, atol=1e-6)
|
||||
assert faces == original_faces
|
||||
@@ -225,7 +278,7 @@ def test_small_positive_split_preserves_unsplit_projection():
|
||||
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", 0.5)
|
||||
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):
|
||||
@@ -240,11 +293,15 @@ def test_cut_vertices_preserve_camera_rays_and_other_vertices():
|
||||
|
||||
def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
|
||||
obj, set_value = surface(step=3.0)
|
||||
set_value("Split", 0.5)
|
||||
set_value("Split Threshold", 0.5)
|
||||
set_value("Thickness", 0.1)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
assert set(np.round(coords[:, 2], 4)) == {0.0, -0.1, -3.0, -3.1}
|
||||
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])
|
||||
@@ -255,9 +312,9 @@ def test_temporary_attributes_are_removed_and_user_attributes_survive(thickness,
|
||||
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
|
||||
for value in attribute.data:
|
||||
value.value = 0.75
|
||||
set_value("Split", split)
|
||||
set_value("Split Threshold", split)
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Smooth Iterations", 2)
|
||||
set_value("Smooth", 2)
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
@@ -282,7 +339,7 @@ def test_zero_center_distance_does_not_split_or_produce_nan():
|
||||
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", 1.0)
|
||||
set_value("Split Threshold", 1.0)
|
||||
coords, faces = evaluated(obj)
|
||||
assert len(coords) == 4 and len(faces) == 2
|
||||
assert np.isfinite(coords).all()
|
||||
@@ -300,7 +357,7 @@ def test_triangle_corner_weights_do_not_open_sloped_side_walls():
|
||||
pixels[..., 2] += np.arange(32)[:, None] * 0.02
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Split", 0.5)
|
||||
set_value("Split Threshold", 0.5)
|
||||
set_value("Thickness", 0.8)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
@@ -315,7 +372,7 @@ def test_linear_depth_slope_has_density_independent_selection(resolution):
|
||||
pixels[..., 2] = 1 + 4 * pixels[..., 0]
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Split", 0.5)
|
||||
set_value("Split Threshold", 0.5)
|
||||
assert len(evaluated(obj)[0]) == len(obj.data.vertices)
|
||||
set_value("Split", 1.0)
|
||||
set_value("Split Threshold", 1.0)
|
||||
assert len(evaluated(obj)[0]) > len(obj.data.vertices)
|
||||
|
||||
Reference in New Issue
Block a user