Refactor geometry generation to standardize Blender object coordinates

- Unified the canonical local frame for generated geometries (Plane, Depth Plane, Relief Plane, Cutout, Depth Cutout) to align with Blender's object coordinate system, setting X as horizontal, Z as up, and +Y as depth.
- Removed the `Depth Axis` parameter from `O Image Cutout` and `O Image Depth Cutout`, simplifying the geometry generation process.
- Updated the depth direction defaults to use +Y instead of -Z for `O Image Relief Plane` and Depth Symmetry.
- Adjusted the geometry nodes to ensure thickness, projection, and shell displacements operate along the Y-axis.
- Implemented new capabilities for handling object space normals to align with the new canonical frame.
- Enhanced the Depth Symmetry functionality to mirror across the canonical Y-axis and perform a final rotation to maintain the correct orientation.
- Added tests to validate the new coordinate system and ensure compatibility with existing assets.
This commit is contained in:
2026-09-15 13:33:37 +08:00
parent 591e62085a
commit 4035f5c9e4
47 changed files with 892 additions and 570 deletions

View File

@@ -6,6 +6,7 @@ from mathutils import Matrix, Vector
from anyimage.common import image as image_data
from anyimage.common import object as object_data
from anyimage.common.coordinate import canonical_direction_to_legacy
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
@@ -101,8 +102,8 @@ def test_converted_planes_center_the_origin_without_moving_world_corners():
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)
assert np.allclose(local[:, [0, 2]].min(axis=0), (-1.0, -0.5), atol=1e-6)
assert np.allclose(local[:, [0, 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)
@@ -199,9 +200,12 @@ def test_relief_plane_builds_a_fixed_base_terrain_block():
assert result.modifiers[0].node_group.name == "O Image Relief Plane"
modifier = result.modifiers[0]
group = modifier.node_group
for name in ("Thickness", "Depth Scale"):
identifier = object_data.modifier_input_identifier(group, name)
assert modifier[identifier] == 0.5
assert modifier[
object_data.modifier_input_identifier(group, "Thickness")
] == 0.0
assert modifier[
object_data.modifier_input_identifier(group, "Depth Scale")
] == 0.5
assert modifier[
object_data.modifier_input_identifier(group, "Depth Offset")
] == 1.0
@@ -219,12 +223,12 @@ def test_relief_plane_builds_a_fixed_base_terrain_block():
set_input("Thickness", 0.1)
set_input("Depth Offset", 0.0)
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)
assert np.isclose(baseline[:, 1].min(), 0.0, atol=1e-6)
assert np.isclose(baseline[:, 1].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)
if np.isclose(y, 0.0, atol=1e-6)
}
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
@@ -240,38 +244,38 @@ def test_relief_plane_builds_a_fixed_base_terrain_block():
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)
assert np.isclose(outward[:, 1].min(), -0.2, atol=1e-6)
assert np.isclose(outward[:, 1].max(), 0.1, 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)
if np.isclose(y, 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)
y_levels = np.unique(np.round(outward[:, 1], 6))
assert len(y_levels) >= 4
assert np.allclose(np.diff(y_levels), np.diff(y_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.1, atol=1e-5)
assert np.isclose(inward[:, 1].min(), 0.0, atol=1e-6)
assert np.isclose(inward[:, 1].max(), 0.1, 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 np.isclose(limited[:, 2].max(), 0.1, atol=1e-5)
assert np.isclose(limited[:, 1].min(), 0.0, atol=1e-6)
assert np.isclose(limited[:, 1].max(), 0.1, atol=1e-5)
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)
assert np.isclose(outward_beyond_thickness[:, 1].max(), 0.1, 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)
assert np.isfinite(sheet).all()
set_input("Reference Depth", 0.8)
assert np.allclose(_evaluated_positions(result)[:, 2], 0.0, atol=1e-5)
assert np.allclose(_evaluated_positions(result)[:, 1], 0.0, atol=1e-5)
def test_relief_plane_initializes_a_fitted_depth_direction():
@@ -313,6 +317,7 @@ def test_relief_plane_initializes_a_fitted_depth_direction():
uniform_scale = modifier[
object_data.modifier_input_identifier(modifier.node_group, "Uniform Scale")
]
expected = np.asarray((0.2 * uniform_scale, 0.1 * uniform_scale, 1.0))
canonical = np.asarray((0.2 * uniform_scale, 1.0, -0.1 * uniform_scale))
expected = np.asarray(canonical_direction_to_legacy(canonical))
expected /= np.linalg.norm(expected)
np.testing.assert_allclose(direction, expected, atol=1e-6)