How to Export GLB Files with Full PBR Material Support from TRELLIS.2 O-Voxel Outputs
TRELLIS.2's o_voxel.postprocess.to_glb pipeline converts O-Voxel outputs into glTF-standard GLB files with physically-based rendering materials by mesh extraction, UV unwrapping, and texture baking of per-voxel PBR attributes.
Exporting GLB files with PBR material support from TRELLIS.2 requires understanding how the O-Voxel representation stores geometry and material data. This guide walks through the complete workflow based on the official TRELLIS.2 source code, from loading voxel data to producing a standards-compliant GLB file.
Overview of the O-Voxel to GLB Pipeline
The microsoft/TRELLIS.2 repository provides a specialized pipeline in o_voxel/postprocess.py that handles the entire conversion. The process involves six distinct stages:
- Load O-Voxel data using
o_voxel.io.read - Decode occupancy bits from packed binary channels
- Generate mesh geometry via
flexible_dual_grid_to_mesh - Pack PBR attributes into a unified volume tensor
- Post-process and bake textures through
to_glb - Export the final GLB with
trimesh
Each stage requires specific parameter configurations to preserve material fidelity.
Loading and Decoding O-Voxel Data
The O-Voxel format (.vxz files) stores dual-grid vertices and per-voxel PBR attributes. The o_voxel.io.read function returns coordinates and a data dictionary containing all material channels.
import torch
import o_voxel
# Load the O-Voxel representation
coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
dual_vertices = data["dual_vertices"]
intersected = data["intersected"]
base_color = data["base_color"]
metallic = data["metallic"]
roughness = data["roughness"]
alpha = data["alpha"]
The intersected field requires bit-unpacking to reconstruct the boolean occupancy mask. As implemented in the reference example ovox2glb.py, this field packs three binary channels into a single integer value per voxel:
# Normalize vertices and decode 3-bit occupancy mask
dual_vertices = dual_vertices / 255
intersected = torch.cat([
intersected % 2,
intersected // 2 % 2,
intersected // 4 % 2,
], dim=-1).bool()
Mesh Generation from the Dual Grid
The flexible_dual_grid_to_mesh function in o_voxel/convert/flexible_dual_grid.py reconstructs surface geometry from the voxel grid. This dual-contouring approach produces watertight meshes with adaptive quality based on the split_weight parameter.
RES = 512 # Must match original grid resolution
rec_verts, rec_faces = o_voxel.convert.flexible_dual_grid_to_mesh(
coords.cuda(),
dual_vertices.cuda(),
intersected.cuda(),
split_weight=None, # Automatic splitting based on minimum angle
grid_size=RES,
aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
)
The aabb and grid_size parameters must match the values used during O-Voxel generation. Mismatches cause texture sampling errors in the final GLB output.
Packing PBR Attributes for Texture Baking
PBR material support requires organizing four channels—base color (RGB), metallic, roughness, and alpha—into a single attribute volume with an explicit layout mapping:
# Concatenate and normalize attributes to [0, 1]
attr_volume = torch.cat([
base_color.cuda(),
metallic.cuda(),
roughness.cuda(),
alpha.cuda(),
], dim=-1) / 255.0
# Define which tensor slices correspond to each material property
attr_layout = {
"base_color": slice(0, 3), # RGB channels
"metallic": slice(3, 4), # Single channel
"roughness": slice(4, 5), # Single channel
"alpha": slice(5, 6), # Single channel (stored with base color)
}
This layout follows the glTF PBR specification: base color and alpha share one texture, while metallic and roughness pack into another texture's blue and green channels respectively.
GLB Export with o_voxel.postprocess.to_glb
The to_glb function in o_voxel/postprocess.py (lines 60-331) orchestrates the complete post-processing pipeline:
| Stage | Functionality | Source Lines |
|---|---|---|
| Input normalization | Coerces AABB, voxel size, and grid size to tensors | 60-88 |
| Mesh initialization | Loads geometry into GPU-accelerated cumesh.CuMesh, fills holes, builds BVH |
100-158 |
| UV unwrapping | Cone-based chart clustering for texture parameterization | 195-215 |
| Texture baking | Differentiable rasterization via nvdiffrast with BVH queries and trilinear sampling |
225-267 |
| PBR material construction | Builds trimesh.visual.material.PBRMaterial with inpainted, clipped textures |
95-104, 268-331 |
Call to_glb with quality parameters tuned to your use case:
mesh = o_voxel.postprocess.to_glb(
vertices=rec_verts,
faces=rec_faces,
attr_volume=attr_volume,
coords=coords.cuda(),
attr_layout=attr_layout,
grid_size=RES,
aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
decimation_target=100_000, # Target vertex count after simplification
texture_size=2048, # Baked texture resolution
remesh=False, # Skip dual-contouring remesh for most inputs
verbose=True,
)
# Write final GLB with embedded PBR textures
mesh.export("helmet_with_pbr.glb")
Critical Parameters for Quality Control
| Parameter | Recommended Range | Impact |
|---|---|---|
decimation_target |
50,000–500,000 vertices | Lower values reduce file size but may lose geometric detail |
texture_size |
1024, 2048, or 4096 | Must be power-of-two; higher values improve texture fidelity |
remesh |
False (default), True for noisy inputs |
When enabled, replaces original mesh with remeshed version |
use_tqdm |
True for interactive use |
Progress bars for long-running operations |
The decimation_target parameter controls mesh simplification in the cleanup stage. Setting this too low causes visible polygon artifacts; too high produces unnecessarily large files.
Complete Working Example
This consolidated script mirrors the official ovox2glb.py example:
#!/usr/bin/env python3
"""Export GLB with PBR from TRELLIS.2 O-Voxel file."""
import torch
import o_voxel
def export_glb_from_ovoxel(
input_path: str,
output_path: str,
grid_resolution: int = 512,
texture_size: int = 2048,
decimation: int = 100_000,
) -> None:
"""Convert O-Voxel file to PBR-enabled GLB."""
# Load data
coords, data = o_voxel.io.read(input_path)
# Decode
dual_verts = data["dual_vertices"] / 255.0
intersected = torch.cat([
data["intersected"] % 2,
data["intersected"] // 2 % 2,
data["intersected"] // 4 % 2,
], dim=-1).bool()
# Build mesh
verts, faces = o_voxel.convert.flexible_dual_grid_to_mesh(
coords.cuda(),
dual_verts.cuda(),
intersected.cuda(),
split_weight=None,
grid_size=grid_resolution,
aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
)
# Pack attributes
attr_vol = torch.cat([
data["base_color"],
data["metallic"],
data["roughness"],
data["alpha"],
], dim=-1).cuda() / 255.0
layout = {
"base_color": slice(0, 3),
"metallic": slice(3, 4),
"roughness": slice(4, 5),
"alpha": slice(5, 6),
}
# Export
mesh = o_voxel.postprocess.to_glb(
vertices=verts,
faces=faces,
attr_volume=attr_vol,
coords=coords.cuda(),
attr_layout=layout,
grid_size=grid_resolution,
aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
decimation_target=decimation,
texture_size=texture_size,
verbose=True,
)
mesh.export(output_path)
print(f"Exported PBR GLB: {output_path}")
if __name__ == "__main__":
export_glb_from_ovoxel(
"input.vxz",
"output_pbr.glb",
grid_resolution=512,
texture_size=2048,
)
Key Source Files for Reference
| File Path | Purpose |
|---|---|
o_voxel/postprocess.py |
Core GLB exporter with PBR material construction (lines 60-331) |
o_voxel/examples/ovox2glb.py |
Official reference implementation |
o_voxel/io/vxz.py |
.vxz file format reader |
o_voxel/convert/flexible_dual_grid.py |
Dual-grid to triangle mesh conversion |
trellis2/renderers/pbr_mesh_renderer.py |
Additional PBR rendering utilities |
Summary
- Use
o_voxel.io.readto load O-Voxel data and decode the packedintersectedoccupancy mask into three boolean channels - Match
grid_sizeandaabbparameters exactly to the original voxel grid configuration to prevent texture misalignment - Pack attributes with
attr_layoutmapping slices to material channels, following the glTF PBR specification - Call
o_voxel.postprocess.to_glbfor GPU-accelerated mesh cleaning, UV unwrapping, and differentiable texture baking vianvdiffrast - Export with
mesh.export()to produce a GLB file with embeddedPBRMaterialcontaining base-color+alpha and metallic-roughness textures
Frequently Asked Questions
Why does my exported GLB have missing or distorted textures?
Texture distortion typically results from mismatched grid_size or aabb parameters between O-Voxel generation and GLB export. Verify these values match exactly. Additionally, ensure your attr_layout slices correctly map to the concatenated attribute volume channels.
How do I reduce file size without losing PBR quality?
Increase the decimation_target value to allow more vertices, then downsample the baked textures by reducing texture_size to 1024. Alternatively, enable aggressive simplification by setting decimation_target to 50,000 and keeping texture_size at 2048 for crisp materials on a coarser mesh.
What is the difference between remesh=True and remesh=False?
When remesh=False (recommended), to_glb cleans and simplifies the original dual-grid mesh. When remesh=True, the function performs an additional dual-contouring remeshing step that replaces the input mesh entirely—useful for extremely noisy voxel data but generally unnecessary for TRELLIS.2 outputs.
Can I export GLB without PBR materials for faster preview generation?
The to_glb function requires an attr_volume and attr_layout by design. For material-free exports, use trimesh directly with the mesh from flexible_dual_grid_to_mesh, or pass empty/constant attribute volumes with alpha=1.0 to produce visually uniform surfaces.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →