- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
262 lines
9.3 KiB
Python
262 lines
9.3 KiB
Python
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
import bpy
|
|
import numpy as np
|
|
from mathutils import Matrix, Vector
|
|
|
|
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
|
|
|
sys.path.insert(0, str(SOURCE_ROOT))
|
|
|
|
|
|
from anyimage.common import image as image_data
|
|
from anyimage.common import object as object_data
|
|
from anyimage.operators.clipboard_image import actions as clipboard_actions
|
|
from anyimage.operators.convert_to_plane import object as plane_object
|
|
from anyimage.operators.cutout_tool import shape as cutout_shape
|
|
from image_object_test_support import _depth_image, _depth_metadata, _evaluated_positions, _inputs
|
|
|
|
|
|
def test_build_plane_keeps_one_quad_and_generates_topology_in_geometry_nodes():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
image = bpy.data.images.new("Source", width=4096, height=2048)
|
|
source = bpy.data.objects.new("Source", None)
|
|
source.empty_display_type = "IMAGE"
|
|
source.data = image
|
|
source.empty_display_size = 2.0
|
|
bpy.context.collection.objects.link(source)
|
|
material = bpy.data.materials.new("Source.material")
|
|
|
|
result = plane_object.create_plane_object(
|
|
bpy.context,
|
|
source,
|
|
2,
|
|
material,
|
|
)
|
|
|
|
assert result["anyimage_mesh_shape"] == "PLANE"
|
|
assert len(result.data.vertices) == 4
|
|
assert len(result.data.polygons) == 1
|
|
assert all(polygon.use_smooth for polygon in result.data.polygons)
|
|
assert cutout_shape.BALLOON_ATTRIBUTE_NAME not in result.data.attributes
|
|
assert result.modifiers[0].node_group.name == "O Image Plane"
|
|
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
evaluated_mesh = evaluated.to_mesh()
|
|
try:
|
|
assert len(evaluated_mesh.polygons) > 0
|
|
assert result.data.materials[0] == material
|
|
assert evaluated_mesh.materials[0].original == material
|
|
assert all(polygon.material_index == 0 for polygon in evaluated_mesh.polygons)
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
|
|
def test_clipboard_plane_uses_the_mesh_material_slot():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
image = bpy.data.images.new("Clipboard Plane", width=128, height=64, alpha=True)
|
|
|
|
with patch.object(clipboard_actions, "_place_in_view"):
|
|
result = clipboard_actions.add_image_plane(bpy.context, image)
|
|
|
|
assert _inputs(result.modifiers[0].node_group) == [
|
|
"Geometry",
|
|
"Subdivide",
|
|
"Thickness",
|
|
]
|
|
assert len(result.data.materials) == 1
|
|
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
evaluated_mesh = evaluated.to_mesh()
|
|
try:
|
|
assert evaluated_mesh.materials[0].original == result.data.materials[0]
|
|
assert all(polygon.material_index == 0 for polygon in evaluated_mesh.polygons)
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
|
|
def test_converted_planes_center_the_origin_without_moving_world_corners():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
def create_source(name):
|
|
image = bpy.data.images.new(name, width=200, height=100)
|
|
source = bpy.data.objects.new(name, None)
|
|
source.empty_display_type = "IMAGE"
|
|
source.data = image
|
|
source.empty_display_size = 2.0
|
|
source.empty_image_offset = (0.25, -0.2)
|
|
source.matrix_world = (
|
|
Matrix.Translation((3.0, -2.0, 5.0))
|
|
@ Matrix.Rotation(0.4, 4, "Z")
|
|
@ Matrix.Diagonal((1.5, 0.75, 2.0, 1.0))
|
|
)
|
|
bpy.context.collection.objects.link(source)
|
|
return source
|
|
|
|
def assert_centered(result, bounds, source_matrix):
|
|
x_min, x_max, y_min, y_max = bounds
|
|
center = Vector(
|
|
((x_min + x_max) * 0.5, (y_min + y_max) * 0.5, 0.0)
|
|
)
|
|
assert np.allclose(
|
|
tuple(result.matrix_world.translation),
|
|
tuple(source_matrix @ center),
|
|
atol=1e-6,
|
|
)
|
|
local = np.asarray([tuple(vertex.co) for vertex in result.data.vertices])
|
|
assert np.allclose(local[:, :2].min(axis=0), (-1.0, -0.5), atol=1e-6)
|
|
assert np.allclose(local[:, :2].max(axis=0), (1.0, 0.5), atol=1e-6)
|
|
expected = sorted(
|
|
tuple(source_matrix @ Vector((x, y, 0.0)))
|
|
for x in (x_min, x_max)
|
|
for y in (y_min, y_max)
|
|
)
|
|
actual = sorted(
|
|
tuple(result.matrix_world @ vertex.co)
|
|
for vertex in result.data.vertices
|
|
)
|
|
assert np.allclose(actual, expected, atol=1e-6)
|
|
|
|
plane_source = create_source("Centered Plane")
|
|
plane_bounds = image_data.image_empty_bounds(plane_source)
|
|
plane_matrix = plane_source.matrix_world.copy()
|
|
plane_material = bpy.data.materials.new("Centered Plane Material")
|
|
plane = plane_object.create_plane_object(
|
|
bpy.context,
|
|
plane_source,
|
|
0,
|
|
plane_material,
|
|
)
|
|
assert_centered(plane, plane_bounds, plane_matrix)
|
|
assert plane["anyimage_mesh_shape"] == "PLANE"
|
|
|
|
depth_source = create_source("Centered Depth Plane")
|
|
depth_bounds = image_data.image_empty_bounds(depth_source)
|
|
depth_matrix = depth_source.matrix_world.copy()
|
|
depth_values = np.ones((2, 2, 3), dtype=np.float32)
|
|
depth_image = _depth_image(depth_values, np.ones((2, 2), dtype=bool))
|
|
depth_material = bpy.data.materials.new("Centered Depth Material")
|
|
depth_plane = plane_object.create_depth_plane_object(
|
|
bpy.context,
|
|
depth_source,
|
|
0,
|
|
depth_material,
|
|
depth_image,
|
|
_depth_metadata(
|
|
((0.5, 0.0, 0.5), (0.0, 0.5, 0.5), (0.0, 0.0, 1.0)),
|
|
(2, 2),
|
|
1.0,
|
|
),
|
|
)
|
|
assert_centered(depth_plane, depth_bounds, depth_matrix)
|
|
assert depth_plane["anyimage_mesh_shape"] == "DEPTH_PLANE"
|
|
depth_modifier = depth_plane.modifiers[0]
|
|
uniform_scale = depth_modifier[
|
|
object_data.modifier_input_identifier(
|
|
depth_modifier.node_group,
|
|
"Uniform Scale",
|
|
)
|
|
]
|
|
assert np.isclose(
|
|
depth_modifier[
|
|
object_data.modifier_input_identifier(
|
|
depth_modifier.node_group,
|
|
"Reference Depth",
|
|
)
|
|
],
|
|
uniform_scale,
|
|
)
|
|
|
|
|
|
def test_depth_plane_builds_a_fixed_base_terrain_block():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
image = bpy.data.images.new("Terrain", width=4, height=4)
|
|
source = bpy.data.objects.new("Terrain", None)
|
|
source.empty_display_type = "IMAGE"
|
|
source.data = image
|
|
source.empty_display_size = 2.0
|
|
bpy.context.collection.objects.link(source)
|
|
material = bpy.data.materials.new("Terrain Material")
|
|
depth_image = _depth_image(
|
|
np.ones((4, 4, 3), dtype=np.float32),
|
|
np.ones((4, 4), dtype=bool),
|
|
)
|
|
result = plane_object.create_depth_plane_object(
|
|
bpy.context,
|
|
source,
|
|
2,
|
|
material,
|
|
depth_image,
|
|
_depth_metadata(
|
|
((1.5, 0.0, 1.5), (0.0, 1.5, 1.5), (0.0, 0.0, 1.0)),
|
|
(4, 4),
|
|
1.0,
|
|
),
|
|
)
|
|
modifier = result.modifiers[0]
|
|
group = modifier.node_group
|
|
|
|
def set_input(name, value):
|
|
object_data.set_modifier_input(
|
|
modifier,
|
|
object_data.modifier_input_identifier(group, name),
|
|
value,
|
|
)
|
|
result.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
|
|
set_input("Depth Scale", 0.0)
|
|
set_input("Thickness", 0.1)
|
|
baseline = _evaluated_positions(result)
|
|
assert np.isclose(baseline[:, 2].min(), 0.0, atol=1e-6)
|
|
assert np.isclose(baseline[:, 2].max(), 0.1, atol=1e-6)
|
|
baseline_base = {
|
|
(round(float(x), 6), round(float(y), 6))
|
|
for x, y, z in baseline
|
|
if np.isclose(z, 0.0, atol=1e-6)
|
|
}
|
|
|
|
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
evaluated_mesh = evaluated.to_mesh()
|
|
try:
|
|
assert result.data.materials[0] == material
|
|
assert evaluated_mesh.materials[0].original == material
|
|
assert all(polygon.material_index == 0 for polygon in evaluated_mesh.polygons)
|
|
assert "UVMap" in evaluated_mesh.uv_layers
|
|
finally:
|
|
evaluated.to_mesh_clear()
|
|
|
|
set_input("Depth Scale", 1.0)
|
|
set_input("Reference Depth", 1.2)
|
|
outward = _evaluated_positions(result)
|
|
assert np.isclose(outward[:, 2].min(), 0.0, atol=1e-6)
|
|
assert np.isclose(outward[:, 2].max(), 0.3, atol=1e-5)
|
|
outward_base = {
|
|
(round(float(x), 6), round(float(y), 6))
|
|
for x, y, z in outward
|
|
if np.isclose(z, 0.0, atol=1e-6)
|
|
}
|
|
assert outward_base == baseline_base
|
|
z_levels = np.unique(np.round(outward[:, 2], 6))
|
|
assert len(z_levels) >= 4
|
|
assert np.allclose(np.diff(z_levels), np.diff(z_levels)[0], atol=1e-5)
|
|
|
|
set_input("Reference Depth", 0.95)
|
|
inward = _evaluated_positions(result)
|
|
assert np.isclose(inward[:, 2].min(), 0.0, atol=1e-6)
|
|
assert np.isclose(inward[:, 2].max(), 0.05, atol=1e-5)
|
|
|
|
set_input("Reference Depth", 0.0)
|
|
limited = _evaluated_positions(result)
|
|
assert np.isclose(limited[:, 2].min(), 0.0, atol=1e-6)
|
|
assert limited[:, 2].max() > 0.0
|
|
assert limited[:, 2].max() < 0.001
|
|
|
|
set_input("Reference Depth", 1.3)
|
|
outward_beyond_thickness = _evaluated_positions(result)
|
|
assert np.isclose(outward_beyond_thickness[:, 2].max(), 0.4, atol=1e-5)
|
|
|
|
set_input("Thickness", 0.0)
|
|
set_input("Reference Depth", 1.2)
|
|
sheet = _evaluated_positions(result)
|
|
assert np.allclose(sheet[:, 2], 0.2, atol=1e-5)
|