- 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.
238 lines
7.8 KiB
Python
238 lines
7.8 KiB
Python
from types import SimpleNamespace
|
|
from blender_test_support import BlenderTestCase
|
|
|
|
|
|
class RectifyToolTest(BlenderTestCase):
|
|
def test_rectify_rectifies_identity_pixels(self):
|
|
import numpy as np
|
|
|
|
top_down = np.arange(3 * 4 * 4, dtype=np.float32).reshape((3, 4, 4))
|
|
top_down /= float(top_down.max())
|
|
top_down[:, :, 3] = 1.0
|
|
result = self.image_data.warp_projective_pixels(
|
|
np.flipud(top_down).ravel(),
|
|
(4, 3),
|
|
((0, 0), (4, 0), (4, 3), (0, 3)),
|
|
(4, 3),
|
|
)
|
|
|
|
np.testing.assert_allclose(
|
|
np.flipud(np.asarray(result).reshape((3, 4, 4))),
|
|
top_down,
|
|
atol=1e-6,
|
|
)
|
|
|
|
|
|
def test_projective_sampling_keeps_outside_pixels_transparent(self):
|
|
import numpy as np
|
|
|
|
source = np.ones((2, 2, 4), dtype=np.float32)
|
|
result = self.image_data.warp_projective_pixels(
|
|
np.flipud(source).ravel(),
|
|
(2, 2),
|
|
((-2, 0), (2, 0), (2, 2), (-2, 2)),
|
|
(4, 2),
|
|
)
|
|
top_down = np.flipud(np.asarray(result).reshape((2, 4, 4)))
|
|
|
|
np.testing.assert_array_equal(top_down[:, :2, 3], 0.0)
|
|
np.testing.assert_allclose(top_down[:, 1, :3], 1.0)
|
|
np.testing.assert_allclose(top_down[:, 2:, 3], 1.0)
|
|
|
|
|
|
def test_projective_sampling_uses_premultiplied_alpha(self):
|
|
import numpy as np
|
|
|
|
source = np.zeros((1, 2, 4), dtype=np.float32)
|
|
source[0, 0] = (1.0, 0.0, 0.0, 1.0)
|
|
source[0, 1] = (0.0, 1.0, 0.0, 0.0)
|
|
sampled = self.image_data.sample_rgba(
|
|
self.image_data.premultiplied_rgba(
|
|
np.flipud(source).ravel(),
|
|
(2, 1),
|
|
),
|
|
np.asarray([[[1.0, 0.5]]]),
|
|
)
|
|
straight = self.image_data.straight_rgba(sampled)
|
|
|
|
np.testing.assert_allclose(straight[0, 0], (1.0, 0.0, 0.0, 0.5))
|
|
|
|
|
|
def test_homography_rejects_degenerate_points(self):
|
|
with self.assertRaisesRegex(ValueError, "degenerate"):
|
|
self.image_data.homography_from_points(
|
|
((0, 0), (1, 0), (2, 0), (3, 0)),
|
|
((0, 0), (1, 0), (1, 1), (0, 1)),
|
|
)
|
|
|
|
|
|
def test_rectify_preview_uses_srgb_without_changing_pixels(self):
|
|
written = []
|
|
preview = SimpleNamespace(
|
|
colorspace_settings=SimpleNamespace(name="Non-Color"),
|
|
alpha_mode=None,
|
|
pixels=SimpleNamespace(foreach_set=written.extend),
|
|
update=lambda: None,
|
|
)
|
|
previous_data = self.fake_bpy.data
|
|
self.fake_bpy.data = SimpleNamespace(
|
|
images=SimpleNamespace(new=lambda *_args, **_options: preview)
|
|
)
|
|
pixels = (0.1, 0.2, 0.3, 0.4)
|
|
source = SimpleNamespace(
|
|
name="Source.png",
|
|
colorspace_settings=SimpleNamespace(name="Non-Color"),
|
|
)
|
|
try:
|
|
result = self.rectify.create_perspective_preview_image(
|
|
source, pixels
|
|
)
|
|
finally:
|
|
self.fake_bpy.data = previous_data
|
|
|
|
self.assertIs(result, preview)
|
|
self.assertEqual(preview.colorspace_settings.name, "sRGB")
|
|
self.assertEqual(preview.alpha_mode, "STRAIGHT")
|
|
self.assertEqual(written, list(pixels))
|
|
|
|
|
|
def test_rectify_preview_skips_cursor_at_last_clicked_point(self):
|
|
points = ((10.0, 10.0), (80.0, 10.0), (70.0, 60.0))
|
|
|
|
stationary = self.rectify.interactive_preview_polygon(
|
|
points, points[-1]
|
|
)
|
|
moving = self.rectify.interactive_preview_polygon(
|
|
points, (20.0, 70.0)
|
|
)
|
|
|
|
self.assertEqual(len(stationary), 3)
|
|
self.assertEqual(len(moving), 4)
|
|
|
|
|
|
def test_rectify_draws_scene_linear_texture_as_srgb_in_blender_5(self):
|
|
draw_options = self.rectify.preview_texture_draw_options
|
|
|
|
self.assertEqual(draw_options((4, 5, 3)), {})
|
|
self.assertEqual(
|
|
draw_options((5, 0, 0)),
|
|
{"is_scene_linear_with_rec709_srgb_target": True},
|
|
)
|
|
|
|
|
|
def test_rectify_tightly_crops_alpha(self):
|
|
import numpy as np
|
|
|
|
top_down = np.zeros((4, 4, 4), dtype=np.float32)
|
|
top_down[1:3, 1:3] = (0.2, 0.4, 0.6, 1.0)
|
|
pixels, output_size, placement_bounds = (
|
|
self.rectify.extract_perspective_pixels(
|
|
np.flipud(top_down).ravel(),
|
|
(4, 4),
|
|
((0, 0), (4, 0), (4, 4), (0, 4)),
|
|
1.0,
|
|
)
|
|
)
|
|
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
|
|
|
|
self.assertEqual(output_size, (2, 2))
|
|
self.assertEqual(placement_bounds, (1.0, 1.0, 3.0, 3.0))
|
|
self.assertTrue(np.all(result[:, :, 3] == 1.0))
|
|
|
|
|
|
def test_rectify_rejects_non_convex_corners(self):
|
|
with self.assertRaisesRegex(ValueError, "convex"):
|
|
self.rectify.validate_perspective_quad(
|
|
((0, 0), (10, 0), (4, 4), (0, 10))
|
|
)
|
|
|
|
|
|
def test_rectify_corner_order_is_click_order_independent(self):
|
|
import itertools
|
|
|
|
import numpy as np
|
|
|
|
corners = ((10, 20), (80, 15), (90, 70), (5, 75))
|
|
ordered = self.rectify.canonical_perspective_quad(corners)
|
|
for permutation in itertools.permutations(corners):
|
|
np.testing.assert_allclose(
|
|
self.rectify.canonical_perspective_quad(permutation),
|
|
ordered,
|
|
)
|
|
|
|
|
|
def test_rectify_detects_source_image_overlap(self):
|
|
overlaps = self.rectify.perspective_quad_overlaps_image
|
|
|
|
self.assertFalse(overlaps(((20, 20), (30, 20), (30, 30), (20, 30)), (10, 10)))
|
|
self.assertTrue(overlaps(((5, 5), (15, 5), (15, 15), (5, 15)), (10, 10)))
|
|
self.assertTrue(overlaps(((-5, -5), (15, -5), (15, 15), (-5, 15)), (10, 10)))
|
|
|
|
|
|
def test_rectify_limits_output_pixel_count(self):
|
|
width, height = self.image_data.projective_output_size(
|
|
((0, 0), (10000, 0), (10000, 10000), (0, 10000)),
|
|
1.0,
|
|
)
|
|
|
|
self.assertLessEqual(
|
|
width * height,
|
|
self.image_data.MAX_PROJECTIVE_OUTPUT_PIXELS,
|
|
)
|
|
|
|
|
|
def test_rectify_mouse_adjusts_preview_aspect_exponentially(self):
|
|
adjust = self.rectify.aspect_ratio_from_mouse
|
|
|
|
self.assertAlmostEqual(adjust(1.5, 240.0), 3.0)
|
|
self.assertAlmostEqual(adjust(1.5, -240.0), 0.75)
|
|
self.assertEqual(
|
|
adjust(1.0, 100000.0),
|
|
self.rectify.MAX_INTERACTIVE_ASPECT,
|
|
)
|
|
self.assertEqual(
|
|
self.rectify.snapped_aspect_ratio(1.7),
|
|
(16.0 / 9.0, "16 : 9"),
|
|
)
|
|
|
|
|
|
def test_rectify_preview_is_centered_at_requested_aspect(self):
|
|
left, bottom, width, height = self.rectify.preview_draw_bounds(
|
|
(1000, 800), 2.0
|
|
)
|
|
|
|
self.assertAlmostEqual(width / height, 2.0)
|
|
self.assertAlmostEqual(left + width * 0.5, 500.0)
|
|
self.assertAlmostEqual(bottom + height * 0.5, 400.0)
|
|
|
|
|
|
def test_rectify_uses_quad_edges_for_initial_aspect(self):
|
|
ratio = self.rectify.perspective_quad_aspect(
|
|
((0, 0), (8, 0), (8, 4), (0, 4))
|
|
)
|
|
|
|
self.assertAlmostEqual(ratio, 2.0)
|
|
|
|
|
|
def test_rectify_ignores_an_already_removed_preview(self):
|
|
operator = self.rectify.RectifyImagePerspective()
|
|
operator._preview_image = object()
|
|
|
|
def remove(*_args, **_options):
|
|
raise ReferenceError("StructRNA of type Image has been removed")
|
|
|
|
self.fake_bpy.data = SimpleNamespace(images=SimpleNamespace(remove=remove))
|
|
operator._remove_preview()
|
|
|
|
self.assertIsNone(operator._preview_image)
|
|
|
|
|
|
def test_rectify_maps_crop_to_source_plane_bounds(self):
|
|
bounds = self.rectify.perspective_placement_bounds(
|
|
((10, 20), (30, 20), (30, 40), (10, 40)),
|
|
(100, 50),
|
|
(-10, -5, 110, 55),
|
|
)
|
|
|
|
self.assertEqual(bounds, (8.0, 18.0, 32.0, 42.0))
|