diff --git a/pixal3d_mlx/mesh.py b/pixal3d_mlx/mesh.py index 5682c76..f2aca14 100644 --- a/pixal3d_mlx/mesh.py +++ b/pixal3d_mlx/mesh.py @@ -24,6 +24,7 @@ from typing import Tuple import mlx.core as mx import numpy as np +import trimesh # Upstream fixes both: the model always works in a unit cube centred on the origin. AABB = [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]] @@ -151,7 +152,7 @@ def to_glb(vertices, faces, tex_voxels, attr_layout: dict, resolution: int, except ImportError: from o_voxel import postprocess_cpu as pp - return pp.to_glb( + scene = pp.to_glb( vertices=vertices, faces=faces, attr_volume=_torch(tex_voxels.feats).float(), @@ -168,6 +169,18 @@ def to_glb(vertices, faces, tex_voxels, attr_layout: dict, resolution: int, remesh=remesh, remesh_band=1, remesh_project=0, ) + # o_voxel EXPORTS ROTATED: it applies _BLENDER_ROT, i.e. (x,y,z) -> (x, z, -y), + # which is exactly the inverse of `to_camera_frame`. Left alone, the vertex baker + # and the UV baker return meshes in DIFFERENT frames, and the silhouette gate + # double-rotates the UV one — it scored 0.660 on geometry that measures 0.956. + # Rotate back so every path in this module speaks the voxel-grid frame; the export + # rotation is then applied once, deliberately, at the very end if wanted. + from .proj import _BLENDER_ROT + + mesh = scene if isinstance(scene, trimesh.Trimesh) else scene.dump(concatenate=True) + mesh.vertices = np.asarray(mesh.vertices) @ _BLENDER_ROT.T + return mesh + # Upstream rotates the asset out of its internal frame on the way out (inference.py). EXPORT_ROTATION = np.array([[-1, 0, 0, 0], diff --git a/scripts/image_to_mesh.py b/scripts/image_to_mesh.py index 917d452..4801eb0 100644 --- a/scripts/image_to_mesh.py +++ b/scripts/image_to_mesh.py @@ -171,12 +171,13 @@ def main(): f" {time.time() - t:6.1f}s") else: import torch - scene = to_glb(torch.from_numpy(np.asarray(pre.vertices, np.float32)), - torch.from_numpy(np.asarray(pre.faces, np.int32)), - tex_voxels, PBR_ATTR_LAYOUT, info["output_resolution"], - texture_size=a.texture_size, - decimation_target=a.target_faces or 500_000) - mesh = scene if isinstance(scene, trimesh.Trimesh) else scene.dump(concatenate=True) + # to_glb now returns a Trimesh already un-rotated into the voxel-grid + # frame, so it is directly comparable with the vertex-baked path + mesh = to_glb(torch.from_numpy(np.asarray(pre.vertices, np.float32)), + torch.from_numpy(np.asarray(pre.faces, np.int32)), + tex_voxels, PBR_ATTR_LAYOUT, info["output_resolution"], + texture_size=a.texture_size, + decimation_target=a.target_faces or 500_000) print(f" bake UV {len(mesh.faces):,} faces, {a.texture_size}px " f"{time.time() - t:6.1f}s") info["baker"] = a.baker