- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px. - Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth. - Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data. - Added tests to validate the new boundary padding functionality and its integration with existing cutout processes. feat(geometry): remove overall mesh smoothing from geometry nodes - Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing. - Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes. - Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent. - Updated tests and documentation to reflect the changes in node interfaces and behaviors. feat(normals): scale tangent normals with depth - Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`. - Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute. - Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
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[:, 2].min() > before[:, 2].min() + .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[:, :2] / (1.2 - after[:, 2:3]),
|
|
before[:, :2] / (1.2 - before[:, 2:3]), 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)
|