- Unified the canonical local frame for generated geometries (Plane, Depth Plane, Relief Plane, Cutout, Depth Cutout) to align with Blender's object coordinate system, setting X as horizontal, Z as up, and +Y as depth. - Removed the `Depth Axis` parameter from `O Image Cutout` and `O Image Depth Cutout`, simplifying the geometry generation process. - Updated the depth direction defaults to use +Y instead of -Z for `O Image Relief Plane` and Depth Symmetry. - Adjusted the geometry nodes to ensure thickness, projection, and shell displacements operate along the Y-axis. - Implemented new capabilities for handling object space normals to align with the new canonical frame. - Enhanced the Depth Symmetry functionality to mirror across the canonical Y-axis and perform a final rotation to maintain the correct orientation. - Added tests to validate the new coordinate system and ensure compatibility with existing assets.
172 lines
8.2 KiB
Python
172 lines
8.2 KiB
Python
"""Verify boundary depth filtering before camera and radial projection."""
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from tests.support.depth_surface import evaluated
|
|
from tests.tools.nodes.test_boundary_smoothing import boundary_neighbors, edge_band
|
|
from tools.nodes.common.boundary_smoothing import smooth_boundary_camera
|
|
|
|
|
|
def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surfaces():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
points, faces, cameras, protected = [], [], [], []
|
|
for offset, depth in ((0, 2), (81, 50)):
|
|
for y in range(9):
|
|
for x in range(9):
|
|
value = 20 if offset == 0 and (x, y) == (4, 0) else depth
|
|
if offset == 0 and (x, y) == (8, 8):
|
|
value = 0
|
|
points.append((x, y, 0))
|
|
cameras.append((x * value / 10, y * value / 10, value))
|
|
protected.append(x == 0)
|
|
faces.extend((offset + y * 9 + x, offset + y * 9 + x + 1,
|
|
offset + (y + 1) * 9 + x + 1, offset + (y + 1) * 9 + x)
|
|
for y in range(8) for x in range(8))
|
|
mesh = bpy.data.meshes.new("Disconnected camera samples")
|
|
mesh.from_pydata(points, [], faces)
|
|
camera = np.asarray(cameras, np.float32)
|
|
mesh.attributes.new("test_camera", "FLOAT_VECTOR", "POINT").data.foreach_set("vector", camera.ravel())
|
|
mesh.attributes.new("test_protected", "BOOLEAN", "POINT").data.foreach_set("value", protected)
|
|
obj = bpy.data.objects.new("Depth samples", mesh)
|
|
bpy.context.collection.objects.link(obj)
|
|
group = bpy.data.node_groups.new("Boundary depth test", "GeometryNodeTree")
|
|
for direction in ("INPUT", "OUTPUT"):
|
|
group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry")
|
|
control = group.interface.new_socket(name="Boundary Smooth", in_out="INPUT", socket_type="NodeSocketInt")
|
|
source = group.nodes.new("NodeGroupInput")
|
|
attributes = []
|
|
for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_protected", "BOOLEAN")):
|
|
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
|
node.data_type = kind
|
|
node.inputs["Name"].default_value = name
|
|
attributes.append(node.outputs["Attribute"])
|
|
position = group.nodes.new("GeometryNodeSetPosition")
|
|
group.links.new(source.outputs["Geometry"], position.inputs["Geometry"])
|
|
group.links.new(smooth_boundary_camera(group, *attributes), position.inputs["Position"])
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
group.links.new(position.outputs[0], output.inputs["Geometry"])
|
|
modifier = obj.modifiers.new("Depth", "NODES")
|
|
modifier.node_group = group
|
|
modifier[control.identifier] = 0
|
|
np.testing.assert_array_equal(evaluated(obj)[0], camera)
|
|
modifier[control.identifier] = 12
|
|
obj.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
result, result_faces = evaluated(obj)
|
|
assert result_faces == faces
|
|
assert 2 < result[4, 2] < 10
|
|
assert np.isfinite(result).all()
|
|
np.testing.assert_array_equal(result[protected], camera[protected])
|
|
np.testing.assert_array_equal(result[81:], camera[81:])
|
|
np.testing.assert_array_equal(result[80], camera[80])
|
|
band = edge_band(faces, set(boundary_neighbors(faces)) - set(np.flatnonzero(protected)))
|
|
interior = [i for i in range(len(camera)) if i not in band]
|
|
assert interior
|
|
np.testing.assert_array_equal(result[interior], camera[interior])
|
|
valid = camera[:, 2] > 0
|
|
np.testing.assert_allclose(result[valid, :2] / result[valid, 2:3],
|
|
camera[valid, :2] / camera[valid, 2:3], atol=1e-6)
|
|
|
|
|
|
|
|
def test_cutout_fine_outline_smooths_only_within_boundary_band():
|
|
from tests.support.depth_surface import surface
|
|
from anyimage.operators.cutout_tool.mesh import build_mesh
|
|
|
|
obj, set_value = surface(step=0, resolution=8)
|
|
yy, xx = np.mgrid[:32, :64]
|
|
alpha = ((xx - 32)**2 / 27**2 + (yy - 16)**2 / 13**2 < 1).astype(float)
|
|
points, faces, _, _, _ = build_mesh(alpha, 3, .9)
|
|
obj.data.clear_geometry()
|
|
obj.data.from_pydata([(x / 32, y / 32, 0) for x, y in points], [], faces.tolist())
|
|
uv = obj.data.uv_layers.get("UVMap") or obj.data.uv_layers.new(name="UVMap")
|
|
for loop in obj.data.loops:
|
|
uv.data[loop.index].uv = points[loop.vertex_index] / (64, 32)
|
|
image = bpy.data.images["Camera"]
|
|
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
|
|
pixels[..., 2] = 2 + 12 * ((xx > 25) & (xx < 39) & (yy > 25))
|
|
image.pixels.foreach_set(pixels.ravel())
|
|
image.update()
|
|
set_value("Boundary Smooth", 0)
|
|
set_value("Smooth Weight", 1)
|
|
before, original_faces = evaluated(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces = evaluated(obj)
|
|
assert result_faces == original_faces
|
|
band = edge_band(original_faces, boundary_neighbors(original_faces))
|
|
interior = [i for i in range(len(before)) if i not in band]
|
|
assert interior
|
|
np.testing.assert_array_equal(after[interior], before[interior])
|
|
assert np.max(np.linalg.norm(after - before, axis=1)) > 1e-4
|
|
set_value("Boundary Smooth", 0)
|
|
np.testing.assert_array_equal(evaluated(obj)[0], before)
|
|
|
|
|
|
def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
|
|
from tests.support.planes import create_surface, evaluated as plane_mesh
|
|
|
|
obj, set_value, _, _, image = create_surface("DEPTH")
|
|
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
|
|
pixels[..., 2] = 2
|
|
pixels[2:6, 6:10, 3] = 0
|
|
pixels[1:3, 7:9, 2] = 12
|
|
image.pixels.foreach_set(pixels.ravel())
|
|
image.update()
|
|
for name, value in (("Subdivide", 5), ("Depth Scale", 1), ("Smooth Weight", 0)):
|
|
set_value(name, value)
|
|
before, faces, uv, *_ = plane_mesh(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces, result_uv, *_ = plane_mesh(obj)
|
|
assert faces == result_faces
|
|
np.testing.assert_array_equal(result_uv, uv)
|
|
assert after[:, 1].max() < before[:, 1].max() - .1
|
|
protected = np.any(np.isclose(uv, 0) | np.isclose(uv, 1), axis=1)
|
|
corners = np.concatenate(faces)
|
|
outer = np.unique(corners[protected])
|
|
np.testing.assert_array_equal(after[outer], before[outer])
|
|
np.testing.assert_allclose(after[:, [0, 2]] / (1.2 + after[:, 1:2]),
|
|
before[:, [0, 2]] / (1.2 + before[:, 1:2]), atol=1e-6)
|
|
set_value("Depth Mask", False)
|
|
intact = plane_mesh(obj)[0]
|
|
set_value("Boundary Smooth", 0)
|
|
np.testing.assert_array_equal(plane_mesh(obj)[0], intact)
|
|
|
|
|
|
@pytest.mark.parametrize("region", ["seam", "pole"])
|
|
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
|
|
from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh
|
|
|
|
yy, xx = np.mgrid[:64, :128]
|
|
distance = np.minimum(xx, 128 - xx)
|
|
hole = (distance**2 + (yy - 32)**2 < 9**2) if region == "seam" else (yy > 54)
|
|
spike = ((distance < 4) & (yy > 40) & (yy < 44)) if region == "seam" else ((yy > 50) & (xx < 8))
|
|
depth = np.where(spike, 10., 3.)
|
|
obj, set_value = panorama(depth, (~hole).astype(float), subdivide=5)
|
|
set_value("Smooth Weight", 0)
|
|
before, faces, uv = panorama_mesh(obj)
|
|
set_value("Boundary Smooth", 12)
|
|
after, result_faces, result_uv = panorama_mesh(obj)
|
|
assert result_faces == faces
|
|
np.testing.assert_array_equal(result_uv, uv)
|
|
radii = np.linalg.norm(before, axis=1)
|
|
smoothed = np.linalg.norm(after, axis=1)
|
|
rim = list(boundary_neighbors(faces))
|
|
assert smoothed[rim].max() < radii[rim].max() - .1
|
|
np.testing.assert_allclose(after / smoothed[:, None], before / radii[:, None], atol=1e-6)
|
|
band = edge_band(faces, boundary_neighbors(faces))
|
|
interior = [i for i in range(len(before)) if i not in band]
|
|
assert interior
|
|
np.testing.assert_array_equal(after[interior], before[interior])
|
|
set_value("Depth Mask", False)
|
|
near = panorama_mesh(obj)[0]
|
|
set_value("Invalid Distance", 250)
|
|
far = panorama_mesh(obj)[0]
|
|
valid = np.linalg.norm(near, axis=1) < 50
|
|
assert valid.any() and (~valid).any()
|
|
np.testing.assert_array_equal(far[valid], near[valid])
|
|
np.testing.assert_allclose(np.linalg.norm(far[~valid], axis=1), 250, atol=5e-5)
|
|
set_value("Depth Scale", 0)
|
|
np.testing.assert_allclose(np.linalg.norm(panorama_mesh(obj)[0], axis=1), 1, atol=1e-6)
|