Fork of dgrauet/Hunyuan3D-2.1-mlx + our generate_e2e.py CLI, env-tunable remesh (HY3D_REMESH_FACES), and HARDWARE.md. Upstream training data (mini_trainset) and demo images stripped — inference needs none of it. Full upstream history: github.com/dgrauet/Hunyuan3D-2.1-mlx
120 lines
3.1 KiB
Python
120 lines
3.1 KiB
Python
"""Camera utilities for MLX mesh rendering.
|
|
|
|
Direct port of camera_utils.py from PyTorch to MLX.
|
|
Projection matrices stay as numpy (computed once, small).
|
|
transform_pos operates on MLX arrays.
|
|
"""
|
|
|
|
import math
|
|
|
|
import mlx.core as mx
|
|
import numpy as np
|
|
|
|
|
|
def transform_pos(mtx, pos, keepdim=False):
|
|
"""Transform positions by a 4x4 matrix.
|
|
|
|
Args:
|
|
mtx: (4, 4) numpy or mx.array transformation matrix.
|
|
pos: (N, 3) or (N, 4) mx.array positions.
|
|
keepdim: if False, prepend batch dim -> (1, N, 4).
|
|
|
|
Returns:
|
|
(1, N, 4) or (N, 4) mx.array of transformed positions.
|
|
"""
|
|
if isinstance(mtx, np.ndarray):
|
|
t_mtx = mx.array(mtx)
|
|
else:
|
|
t_mtx = mtx
|
|
|
|
if pos.shape[-1] == 3:
|
|
ones = mx.ones((pos.shape[0], 1), dtype=pos.dtype)
|
|
posw = mx.concatenate([pos, ones], axis=1)
|
|
else:
|
|
posw = pos
|
|
|
|
result = posw @ t_mtx.T
|
|
if keepdim:
|
|
return result
|
|
return mx.expand_dims(result, axis=0)
|
|
|
|
|
|
def get_mv_matrix(elev, azim, camera_distance, center=None):
|
|
"""Build world-to-camera matrix from spherical coordinates.
|
|
|
|
Returns:
|
|
(4, 4) numpy float32 array.
|
|
"""
|
|
elev = -elev
|
|
azim += 90
|
|
|
|
elev_rad = math.radians(elev)
|
|
azim_rad = math.radians(azim)
|
|
|
|
camera_position = np.array([
|
|
camera_distance * math.cos(elev_rad) * math.cos(azim_rad),
|
|
camera_distance * math.cos(elev_rad) * math.sin(azim_rad),
|
|
camera_distance * math.sin(elev_rad),
|
|
])
|
|
|
|
if center is None:
|
|
center = np.array([0, 0, 0])
|
|
else:
|
|
center = np.array(center)
|
|
|
|
lookat = center - camera_position
|
|
lookat = lookat / np.linalg.norm(lookat)
|
|
|
|
up = np.array([0, 0, 1.0])
|
|
right = np.cross(lookat, up)
|
|
right = right / np.linalg.norm(right)
|
|
up = np.cross(right, lookat)
|
|
up = up / np.linalg.norm(up)
|
|
|
|
c2w = np.concatenate(
|
|
[np.stack([right, up, -lookat], axis=-1), camera_position[:, None]],
|
|
axis=-1,
|
|
)
|
|
|
|
w2c = np.zeros((4, 4), dtype=np.float32)
|
|
w2c[:3, :3] = c2w[:3, :3].T
|
|
w2c[:3, 3:] = -c2w[:3, :3].T @ c2w[:3, 3:]
|
|
w2c[3, 3] = 1.0
|
|
return w2c
|
|
|
|
|
|
def get_orthographic_projection_matrix(
|
|
left=-1, right=1, bottom=-1, top=1, near=0, far=2
|
|
):
|
|
"""Orthographic projection matrix.
|
|
|
|
Returns:
|
|
(4, 4) numpy float32 array.
|
|
"""
|
|
m = np.eye(4, dtype=np.float32)
|
|
m[0, 0] = 2 / (right - left)
|
|
m[1, 1] = 2 / (top - bottom)
|
|
m[2, 2] = -2 / (far - near)
|
|
m[0, 3] = -(right + left) / (right - left)
|
|
m[1, 3] = -(top + bottom) / (top - bottom)
|
|
m[2, 3] = -(far + near) / (far - near)
|
|
return m
|
|
|
|
|
|
def get_perspective_projection_matrix(fovy, aspect_wh, near, far):
|
|
"""Perspective projection matrix.
|
|
|
|
Returns:
|
|
(4, 4) numpy float32 array.
|
|
"""
|
|
fovy_rad = math.radians(fovy)
|
|
return np.array(
|
|
[
|
|
[1.0 / (math.tan(fovy_rad / 2.0) * aspect_wh), 0, 0, 0],
|
|
[0, 1.0 / math.tan(fovy_rad / 2.0), 0, 0],
|
|
[0, 0, -(far + near) / (far - near), -2.0 * far * near / (far - near)],
|
|
[0, 0, -1, 0],
|
|
],
|
|
dtype=np.float32,
|
|
)
|