- Introduced new test files for image layer, image plane, normal scale, object normal rotation, shadeless, and temporary attributes. - Implemented tests to validate the behavior of alpha curves, depth layers, and normal scales in various rendering engines. - Refactored import paths for node groups to align with the new directory structure. - Added checks for temporary attribute lifetimes and ensured proper cleanup without warnings. - Enhanced CLI commands to require the Blender group and validate the presence of generated node assets before packaging. - Updated packaging scripts to enforce the existence of necessary assets, preventing build failures.
181 lines
7.0 KiB
Python
181 lines
7.0 KiB
Python
"""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)
|