This commit is contained in:
180
tests/nodes/test_depth_cutout.py
Normal file
180
tests/nodes/test_depth_cutout.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Behavior checks for the Depth Cutout balloon and shell shapes."""
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
def test_edge_turn_reshapes_thickness_without_moving_zero_thickness(mode):
|
||||
"""Edge Turn swings the wall direction and leaves zero thickness untouched."""
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = .3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Mode", mode)
|
||||
set_value("Front Inflation", .2)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
|
||||
turn = modifier_input_identifier(modifier.node_group, "Edge Turn")
|
||||
|
||||
def result(amount, edge_turn):
|
||||
set_modifier_input(modifier, thickness, amount)
|
||||
set_modifier_input(modifier, turn, edge_turn)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
return evaluated(obj)
|
||||
|
||||
plain, faces = result(0, 0)
|
||||
turned, turned_faces = result(0, 2)
|
||||
np.testing.assert_array_equal(plain, turned)
|
||||
assert faces == turned_faces
|
||||
plain, faces = result(1, 0)
|
||||
turned, turned_faces = result(1, 2)
|
||||
assert faces == turned_faces
|
||||
assert np.isfinite(turned).all()
|
||||
assert np.max(np.abs(plain - turned)) > 1e-5
|
||||
|
||||
|
||||
def test_shell_offset_scales_with_thickness_and_ignores_reference():
|
||||
"""A thin Shell moves a distance proportional to its thickness."""
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=0, resolution=12)
|
||||
image = bpy.data.images['Camera']
|
||||
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
|
||||
pixels[..., 2] = 1 + .15 * np.sin(pixels[..., 0] * 6) * np.cos(pixels[..., 1] * 5)
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value('Reference Depth', 10.)
|
||||
set_value('Thickness', 0.)
|
||||
front, _ = evaluated(obj)
|
||||
|
||||
def offsets(thickness, reference=10.):
|
||||
set_value('Thickness', thickness)
|
||||
set_value('Reference Depth', reference)
|
||||
points, faces = evaluated(obj)
|
||||
adjusted_front = front + (0, 10 - reference, 0)
|
||||
from scipy.spatial import cKDTree
|
||||
distances, indices = cKDTree(adjusted_front).query(points)
|
||||
np.testing.assert_allclose(np.sort(distances)[:len(front)], 0, atol=2e-6)
|
||||
rear = distances > thickness * .5
|
||||
assert rear.sum() == len(front)
|
||||
order = np.argsort(indices[rear])
|
||||
return ((points[rear] - adjusted_front[indices[rear]]) / thickness)[order], faces
|
||||
|
||||
smooth, faces = offsets(.02)
|
||||
thinner, thinner_faces = offsets(.01)
|
||||
distant, _ = offsets(.02, 20.)
|
||||
np.testing.assert_allclose(thinner, smooth, atol=2e-4)
|
||||
np.testing.assert_allclose(distant, smooth, atol=2e-4)
|
||||
assert thinner_faces == faces
|
||||
|
||||
|
||||
def test_shell_thickness_extends_behind_the_tilted_front():
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=0, resolution=4)
|
||||
image = bpy.data.images["Camera"]
|
||||
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
|
||||
pixels[..., 2] = 1 + 0.4 * pixels[..., 0]
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
front, _ = evaluated(obj)
|
||||
set_value("Thickness", 0.2)
|
||||
set_value("Front Inflation", 0.5)
|
||||
points, _ = evaluated(obj)
|
||||
front_set = {tuple(point) for point in front}
|
||||
assert front_set <= {tuple(point) for point in points}
|
||||
assert not np.allclose(points[len(front) :], front, atol=1e-6)
|
||||
|
||||
|
||||
def test_balloon_shell_converges_to_front_with_thickness():
|
||||
from scipy.spatial import cKDTree
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Mode", 0)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")
|
||||
|
||||
def amount(value):
|
||||
set_modifier_input(modifier, thickness, value)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
return evaluated(obj)
|
||||
|
||||
front, _ = amount(0)
|
||||
tree = cKDTree(front)
|
||||
gaps = []
|
||||
for value in (0.01, 0.001, 0.0001):
|
||||
points, _ = amount(value)
|
||||
gaps.append(tree.query(points)[0].max())
|
||||
assert gaps[0] > gaps[1] > gaps[2]
|
||||
np.testing.assert_allclose(gaps[1] / gaps[0], 0.1, rtol=0.02)
|
||||
|
||||
|
||||
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)
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = evaluated.to_mesh()
|
||||
try:
|
||||
attribute = mesh.attributes["o_normal_reduction"]
|
||||
assert attribute.domain == "POINT"
|
||||
values = [item.value for item in attribute.data]
|
||||
assert 0.0 in values and 1.0 in values
|
||||
assert all(np.isfinite(values))
|
||||
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())
|
||||
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.5), (0.1, 1.0),
|
||||
])
|
||||
def test_shell_ignores_front_inflation(thickness, inflation):
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Front Inflation", 0.0)
|
||||
baseline, baseline_faces = evaluated(obj)
|
||||
set_value("Front Inflation", inflation)
|
||||
inflated, inflated_faces = evaluated(obj)
|
||||
assert baseline_faces == inflated_faces
|
||||
np.testing.assert_array_equal(inflated, baseline)
|
||||
Reference in New Issue
Block a user