- 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.
215 lines
7.1 KiB
Python
215 lines
7.1 KiB
Python
from types import SimpleNamespace
|
|
from tests.support.blender 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_preview.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_preview.interactive_preview_polygon(
|
|
points, points[-1]
|
|
)
|
|
moving = self.rectify_preview.interactive_preview_polygon(
|
|
points, (20.0, 70.0)
|
|
)
|
|
|
|
self.assertEqual(len(stationary), 3)
|
|
self.assertEqual(len(moving), 4)
|
|
|
|
|
|
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_geometry.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_geometry.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_geometry.canonical_perspective_quad(corners)
|
|
for permutation in itertools.permutations(corners):
|
|
np.testing.assert_allclose(
|
|
self.rectify_geometry.canonical_perspective_quad(permutation),
|
|
ordered,
|
|
)
|
|
|
|
|
|
def test_rectify_detects_source_image_overlap(self):
|
|
overlaps = self.rectify_geometry.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_preview.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_geometry.MAX_INTERACTIVE_ASPECT,
|
|
)
|
|
self.assertEqual(
|
|
self.rectify_preview.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.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_geometry.perspective_quad_aspect(
|
|
((0, 0), (8, 0), (8, 4), (0, 4))
|
|
)
|
|
|
|
self.assertAlmostEqual(ratio, 2.0)
|
|
|
|
|
|
def test_rectify_maps_crop_to_source_plane_bounds(self):
|
|
bounds = self.rectify_geometry.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))
|