Fix the UV baker's frame: o_voxel exports rotated

The everything-on job failed the gate at IoU 0.660 on geometry that measures 0.956.
Cause: o_voxel's to_glb applies _BLENDER_ROT on export, (x,y,z) -> (x, z, -y), which
is EXACTLY the inverse of to_camera_frame. So the vertex baker and the UV baker were
returning meshes in different frames and the gate double-rotated the UV one.

Verified numerically: OV == _BLENDER_ROT, and OV @ _BLENDER_ROT.T == I.

to_glb now un-rotates back into the voxel-grid frame and returns a Trimesh, so every
path in mesh.py speaks one frame. After the fix the baked mesh measures IoU 0.883 --
identical to its input -- with bounds matching to 3dp.

That is the THIRD frame bug this session (mesh vertices vs ProjGrid's rotated lattice;
marching_cubes' voxel-index space; now o_voxel's export rotation). None of them throw:
each produces a plausible object that renders fine and silhouettes wrong. The gate
caught all three, which is the argument for having it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-08-03 20:50:41 +10:00
parent bf8b1a35d5
commit 7624051dbf
2 changed files with 21 additions and 7 deletions

View File

@ -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],

View File

@ -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)),
# 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)
mesh = scene if isinstance(scene, trimesh.Trimesh) else scene.dump(concatenate=True)
print(f" bake UV {len(mesh.faces):,} faces, {a.texture_size}px "
f"{time.time() - t:6.1f}s")
info["baker"] = a.baker