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:
Binary file not shown.
13
src/anyimage/common/coordinate.py
Normal file
13
src/anyimage/common/coordinate.py
Normal 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)
|
||||
@@ -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(),
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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),),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user