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:

  1. Load O-Voxel data using o_voxel.io.read
  2. Decode occupancy bits from packed binary channels
  3. Generate mesh geometry via flexible_dual_grid_to_mesh
  4. Pack PBR attributes into a unified volume tensor
  5. Post-process and bake textures through to_glb
  6. 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.read to load O-Voxel data and decode the packed intersected occupancy mask into three boolean channels
  • Match grid_size and aabb parameters exactly to the original voxel grid configuration to prevent texture misalignment
  • Pack attributes with attr_layout mapping slices to material channels, following the glTF PBR specification
  • Call o_voxel.postprocess.to_glb for GPU-accelerated mesh cleaning, UV unwrapping, and differentiable texture baking via nvdiffrast
  • Export with mesh.export() to produce a GLB file with embedded PBRMaterial containing 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →