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

@@ -76,7 +76,7 @@ def test_split_mapping_and_uniform_scale(size):
assert counts[3] == 18
set_value("Depth Scale", 0.0)
coords, faces = evaluated(obj)
assert len(coords) == 18 and np.abs(coords[:, 2]).max() < 1e-6
assert len(coords) == 18 and np.abs(coords[:, 1]).max() < 1e-6
def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
@@ -84,7 +84,7 @@ def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
set_value("Depth Split", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == 18 and len(faces) == 8
assert set(np.round(coords[:, 2], 4)) == {2.0, -2.0}
assert set(np.round(coords[:, 1], 4)) == {2.0, -2.0}
def test_cleanup_keeps_split_parts_of_a_large_source_island():
@@ -108,7 +108,7 @@ def test_split_topology_uses_depth_before_display_scaling(depth_scale):
coords, faces = evaluated(obj)
assert len(coords) == len(original) == 18
assert faces == original_faces
np.testing.assert_allclose(coords[:, 2], original[:, 2] * depth_scale, atol=1e-6)
np.testing.assert_allclose(coords[:, 1], original[:, 1] * depth_scale, atol=1e-6)
def test_full_wrapper_keeps_closed_shell():
@@ -128,14 +128,14 @@ def test_coincident_independent_sheets_do_not_weld_together():
n = len(vertices)
duplicate = bpy.data.meshes.new("Close sheets")
duplicate.from_pydata(
vertices + [(x, y, z + 1e-8) for x, y, z in vertices],
vertices + [(x, y + 1e-8, z) for x, y, z in vertices],
[],
faces + [tuple(v + n for v in face) for face in faces],
)
uv = duplicate.uv_layers.new(name="UVMap")
for loop in duplicate.loops:
x, y, z = duplicate.vertices[loop.vertex_index].co
uv.data[loop.index].uv = (x / 2, y)
x, _y, z = duplicate.vertices[loop.vertex_index].co
uv.data[loop.index].uv = (x / 2, z)
obj.data = duplicate
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
@@ -143,31 +143,6 @@ def test_coincident_independent_sheets_do_not_weld_together():
assert component_count(faces) == 2
def test_shell_source_indices_cross_identity_grid_rows():
from scipy.spatial import cKDTree
from tools.nodes.common.depth_surface import thicken_depth_surface
bpy.ops.wm.read_factory_settings(use_empty=True)
group = bpy.data.node_groups.new("Indexed Shell", "GeometryNodeTree")
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
grid = group.nodes.new("GeometryNodeMeshGrid")
grid.inputs["Vertices X"].default_value = 65
grid.inputs["Vertices Y"].default_value = 65
amount = group.nodes.new("ShaderNodeValue")
amount.outputs[0].default_value = 0.1
shell = thicken_depth_surface(group, grid.outputs["Mesh"], amount.outputs[0])
group.links.new(shell["geometry"], group.nodes.new("NodeGroupOutput").inputs["Geometry"])
obj = bpy.data.objects.new("Indexed Shell", bpy.data.meshes.new("Empty"))
bpy.context.collection.objects.link(obj)
obj.modifiers.new("Shell", "NODES").node_group = group
points, faces = evaluated(obj)
expected = np.array([(x / 64 - .5, y / 64 - .5, z)
for z in (0, -.1) for y in range(65) for x in range(65)])
assert len(points) == len(expected)
assert cKDTree(points).query(expected)[0].max() < 1e-6
assert_closed(faces)
@pytest.mark.parametrize("resolution", [1, 4])
def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
@@ -201,10 +176,8 @@ def test_cut_vertices_preserve_camera_rays_and_other_vertices():
before, after = original[source_index], coords[result_index]
if obj.data.vertices[source_index].co.x != 1.0:
np.testing.assert_allclose(after, before, atol=1e-6)
before_depth, after_depth = 3.0 - before[2], 3.0 - after[2]
np.testing.assert_allclose(
after[:2] / after_depth, before[:2] / before_depth, atol=1e-6
)
else:
assert not np.allclose(after, before, atol=1e-6)
def test_source_surface_sampling_preserves_depth_on_side_walls():