# How to Export GLB Files with Full PBR Material Support from TRELLIS.2 O-Voxel Outputs

> Learn to export GLB files with PBR materials from TRELLIS.2 O-Voxel outputs. This guide details mesh extraction, UV unwrapping, and texture baking for high-fidelity rendering.

- Repository: [Microsoft/TRELLIS.2](https://github.com/microsoft/TRELLIS.2)
- Tags: how-to-guide
- Published: 2026-08-04

---

**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`](https://github.com/microsoft/TRELLIS.2/blob/main/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.

```python
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`](https://github.com/microsoft/TRELLIS.2/blob/main/ovox2glb.py), this field packs three binary channels into a single integer value per voxel:

```python

# 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`](https://github.com/microsoft/TRELLIS.2/blob/main/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.

```python
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:

```python

# 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`](https://github.com/microsoft/TRELLIS.2/blob/main/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:

```python
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`](https://github.com/microsoft/TRELLIS.2/blob/main/ovox2glb.py) example:

```python
#!/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`](https://github.com/microsoft/TRELLIS.2/blob/main/o_voxel/postprocess.py) | Core GLB exporter with PBR material construction (lines 60-331) |
| [`o_voxel/examples/ovox2glb.py`](https://github.com/microsoft/TRELLIS.2/blob/main/o_voxel/examples/ovox2glb.py) | Official reference implementation |
| [`o_voxel/io/vxz.py`](https://github.com/microsoft/TRELLIS.2/blob/main/o_voxel/io/vxz.py) | `.vxz` file format reader |
| [`o_voxel/convert/flexible_dual_grid.py`](https://github.com/microsoft/TRELLIS.2/blob/main/o_voxel/convert/flexible_dual_grid.py) | Dual-grid to triangle mesh conversion |
| [`trellis2/renderers/pbr_mesh_renderer.py`](https://github.com/microsoft/TRELLIS.2/blob/main/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.