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

Binary file not shown.

View File

@@ -0,0 +1,13 @@
"""Direction conversion between legacy UI parameters and canonical geometry."""
def canonical_direction_to_legacy(direction):
"""Encode a canonical direction as the legacy Direction parameter."""
x, y, z = direction
return (-x, -z, y)
def canonical_direction_to_symmetry_legacy(direction):
"""Encode a canonical direction as the symmetry legacy Direction parameter."""
x, y, z = direction
return (-x, -z, y)

View File

@@ -90,7 +90,7 @@ def _weighted_depth_plane(coordinates, values, weights):
def fit_depth_direction(coordinates, depth, valid, uniform_scale):
"""Fit a robust metric depth plane and return its normalized direction."""
"""Fit a robust metric depth plane on the XZ plane and return its direction."""
import numpy as np
coordinates = np.asarray(coordinates, dtype=np.float64)
@@ -121,22 +121,22 @@ def fit_depth_direction(coordinates, depth, valid, uniform_scale):
1.0,
(2.5 * scale) / np.maximum(np.abs(centered), 1e-12),
)
direction = np.asarray((plane[0], plane[1], 1.0), dtype=np.float64)
direction = np.asarray((plane[0], 1.0, plane[1]), dtype=np.float64)
direction /= np.linalg.norm(direction)
return tuple(float(value) for value in direction)
def fit_image_depth_direction(image, bounds, uniform_scale):
"""Fit the depth direction across a complete image plane."""
"""Fit the depth direction across a complete XZ image plane."""
import numpy as np
depth, valid = camera_depth_values(image)
height, width = depth.shape
x_min, x_max, y_min, y_max = bounds
x = x_min + (np.arange(width) + 0.5) / width * (x_max - x_min)
y = y_max - (np.arange(height) + 0.5) / height * (y_max - y_min)
xx, yy = np.meshgrid(x, y)
coordinates = np.column_stack((xx.ravel(), yy.ravel()))
z = y_min + (np.arange(height) + 0.5) / height * (y_max - y_min)
xx, zz = np.meshgrid(x, z)
coordinates = np.column_stack((xx.ravel(), zz.ravel()))
return fit_depth_direction(
coordinates,
depth.ravel(),

View File

@@ -3,6 +3,20 @@
import bpy
def blender_frame_rotation():
"""Map Image Empty local axes to the canonical Blender object frame."""
from mathutils import Matrix
return Matrix(
(
(1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, -1.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
)
def modifier_input_identifier(node_group, name, *, subtype=None):
for item in node_group.interface.items_tree:
if item.item_type != "SOCKET" or item.in_out != "INPUT":

View File

@@ -2,7 +2,7 @@
import bpy
from ...common.object import modifier_input_identifier, set_modifier_input
from ...common.object import blender_frame_rotation, modifier_input_identifier, set_modifier_input
from ...common.node import load_node_group
@@ -14,7 +14,7 @@ def build_image_plane_matrix(matrix_world, bounds):
x_min, x_max, y_min, y_max = bounds
center = ((x_min + x_max) * 0.5, (y_min + y_max) * 0.5, 0.0)
return matrix_world @ Matrix.Translation(center)
return matrix_world @ Matrix.Translation(center) @ blender_frame_rotation().inverted()
def create_image_plane_mesh(name, bounds):
@@ -28,10 +28,10 @@ def create_image_plane_mesh(name, bounds):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(
(
(-half_width, -half_height, 0.0),
(half_width, -half_height, 0.0),
(half_width, half_height, 0.0),
(-half_width, half_height, 0.0),
(-half_width, 0.0, -half_height),
(half_width, 0.0, -half_height),
(half_width, 0.0, half_height),
(-half_width, 0.0, half_height),
),
(),
((0, 1, 2, 3),),

View File

@@ -13,6 +13,7 @@ from ...common.depth import (
load_depth_metadata,
load_depth_result_image,
)
from ...common.coordinate import canonical_direction_to_legacy
from ...common.material import create_image_material
from ...common.color_image import material_color_image
from ...common.object import (
@@ -96,6 +97,7 @@ def create_depth_plane_object(
)
except ValueError:
depth_direction = (0.0, 0.0, 1.0)
depth_direction = canonical_direction_to_legacy(depth_direction)
inputs.append((
"Depth Direction",
depth_direction,

View File

@@ -1,6 +1,7 @@
"""Calibrate metric reference depth and the Depth Symmetry base plane."""
from ...common.depth import fit_depth_direction
from ...common.coordinate import canonical_direction_to_symmetry_legacy
DEPTH_REFERENCE_PERCENTILE = 95.0
@@ -115,7 +116,7 @@ def depth_symmetry_projection_parameters(
)
if not usable.any():
raise ValueError("MoGe-2 found no valid Depth Symmetry samples")
local_coordinates = vertices[:, :2]
local_coordinates = vertices[:, (0, 2)]
reference = metric_depth_reference(
camera_depth,
calibration_valid,
@@ -127,4 +128,4 @@ def depth_symmetry_projection_parameters(
usable,
uniform_scale,
)
return reference, direction
return reference, canonical_direction_to_symmetry_legacy(direction)

View File

@@ -512,7 +512,13 @@ def cutout_domain(mesh, bounds, world_metric, balloon_algorithm="POISSON"):
dtype=np.float64,
)
local -= crop_center
vertices = np.column_stack((local, np.zeros(len(local), dtype=np.float64)))
vertices = np.column_stack(
(
local[:, 0],
np.zeros(len(local), dtype=np.float64),
local[:, 1],
)
)
return vertices, faces, uv, boundary, planar, profile

View File

@@ -7,6 +7,7 @@ import bpy
from ...common.depth import camera_depth_values, depth_uniform_scale
from ...common.material import create_image_material
from ...common.object import (
blender_frame_rotation,
finalize_object_result,
modifier_input_identifier,
set_modifier_input,
@@ -43,7 +44,11 @@ def _cutout_world_matrix(source_matrix, crop_center):
from mathutils import Matrix
center_x, center_y = (float(value) for value in crop_center)
return source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
return (
source_matrix
@ Matrix.Translation((center_x, center_y, 0.0))
@ blender_frame_rotation().inverted()
)
def _assign_uv(mesh, uv):
@@ -89,6 +94,7 @@ def create_shape_object(
gesture="LASSO",
):
import numpy as np
from mathutils import Matrix
normal_mode = NormalMode.OBJECT if shape in DEPTH_CUTOUT_SHAPES else NormalMode(normal_mode)
bounds = crop_plane_bounds(
@@ -179,6 +185,11 @@ def create_shape_object(
source_object.matrix_world,
crop_center,
)
if shape == "DEPTH_SYMMETRY":
mesh_object.matrix_world = (
mesh_object.matrix_world
@ Matrix.Rotation(-1.5707963267948966, 4, "Z")
)
mesh_object["anyimage_mesh_shape"] = shape
node_group = load_cutout_node_group("DEPTH_SOLID" if shape == "DEPTH_SYMMETRY" else shape)
modifier = mesh_object.modifiers.new(name=node_group.name, type="NODES")

View File

@@ -2,12 +2,6 @@
BALLOON_ATTRIBUTE_NAME = "o_balloon"
NORMAL_REDUCTION_ATTRIBUTE_NAME = "o_normal_reduction"
CUTOUT_Z_TO_X = (
(0.0, 0.0, -1.0, 0.0),
(-1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
CUTOUT_NODE_GROUP_NAMES = {
"FLAT": "O Image Cutout",
"SOLID": "O Image Cutout",