How to Export Generated 3D Assets to GLB with PBR Materials Using o_voxel.postprocess.to_glb()

The o_voxel.postprocess.to_glb() function converts raw voxel-derived meshes into production-ready GLB files by performing GPU-accelerated mesh cleaning, UV unwrapping, differentiable texture baking, and PBR material assembly in a single end-to-end pipeline.

The Microsoft TRELLIS.2 repository provides a complete pipeline for transforming neural voxel representations into industry-standard 3D assets. When you need to export generated 3D assets to GLB with PBR materials using o_voxel.postprocess.to_glb(), the function encapsulates the entire post-processing workflow—from mesh decimation to texture baking—leveraging CUDA-accelerated libraries to handle high-resolution geometry efficiently.

Understanding the GLB Export Pipeline

The implementation in o-voxel/o_voxel/postprocess.py orchestrates a seven-stage pipeline that runs entirely on the GPU. According to the source code, each stage prepares the geometry and materials for standard glTF 2.0 compliance.

Input Normalization and Grid Setup

The function begins by accepting vertex tensors, face indices, a sparse attribute volume, voxel coordinates, and axis-aligned bounding box (AABB) data. As implemented in lines 60–88 of postprocess.py, the routine converts any list, tuple, or NumPy inputs to torch.Tensor objects and derives the voxel grid size when only one of voxel_size or grid_size is explicitly supplied.

GPU-Accelerated Mesh Processing

All geometry is immediately moved to the GPU and wrapped in a cumesh.CuMesh object (lines 99–152). This CUDA-based structure enables fast mesh cleaning, hole-filling, and optional remeshing via dual contouring. The pipeline supports aggressive simplification through the decimation_target parameter, which reduces vertex counts to meet performance budgets for web delivery or real-time rendering.

UV Parameterization with Cone-Angle Clustering

Before texture baking, the cleaned mesh undergoes UV unwrapping using cone-angle clustering (mesh.uv_unwrap). Lines 95–112 of the source keep the resulting UV atlas, vertex normals, and vertex-to-face maps resident on GPU memory. This ensures seamless handoff to the rasterization stage without costly CPU-GPU transfers.

Differentiable Texture Baking

The core sampling logic (lines 124–166) utilizes nvdiffrast to rasterize the UV-mapped mesh into a 2D texture of size texture_size. For each texel, the renderer recovers the corresponding 3D position, queries a BVH-accelerated lookup against the original high-resolution mesh, and trilinearly samples the sparse attribute volume (grid_sample_3d). The sampled channels are then split according to attr_layout specifications for Base-Color, Metallic, Roughness, and Alpha.

PBR Material Assembly

Following the baking stage, the function constructs a trimesh.visual.material.PBRMaterial object (lines 87–103). The channel mapping follows standard glTF conventions:

  • Base-Color and Alpha are combined into a single RGBA texture.
  • Metallic and Roughness are packed into a single texture where the Red channel = 0, Green channel = Roughness, and Blue channel = Metallic.

The textures undergo in-painting to fill UV seam artifacts before final assembly.

Coordinate System Conversion

GLTF expects a right-handed Y-up coordinate system. Lines 112–115 of postprocess.py handle the necessary transforms by swapping the Y and Z axes and flipping the V-coordinate of the UV map to ensure correct orientation in standard 3D viewers.

Complete Usage Example

The repository provides a reference implementation in o-voxel/examples/ovox2glb.py that demonstrates the full workflow from voxel file to GLB export.

Basic Export Workflow

import torch
import o_voxel

# Load voxel-encoded asset

coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
dual_vertices = data["dual_vertices"] / 255
intersected = torch.stack([
    data["intersected"] % 2,
    data["intersected"] // 2 % 2,
    data["intersected"] // 4 % 2,
], dim=-1).bool()

# Convert dual-grid to raw mesh

rec_verts, rec_faces = o_voxel.convert.flexible_dual_grid_to_mesh(
    coords.cuda(),
    dual_vertices.cuda(),
    intersected.cuda(),
    grid_size=512,
    aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
)

# Pack PBR attributes into attribute volume

attr_volume = torch.cat([
    data["base_color"].cuda(),
    data["metallic"].cuda(),
    data["roughness"].cuda(),
    data["alpha"].cuda(),
], dim=-1) / 255

attr_layout = {
    "base_color": slice(0, 3),
    "metallic":   slice(3, 4),
    "roughness":  slice(4, 5),
    "alpha":      slice(5, 6),
}

# Export to GLB with PBR materials

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=512,
    aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
    decimation_target=100_000,
    texture_size=2048,
    verbose=True,
)

mesh.export("rec_helmet.glb")

Customizing Pipeline Parameters

The to_glb() function exposes several parameters to control quality and performance:

  • remesh=True – Enables dual-contouring remeshing before simplification. Use this when extracting non-manifold geometry from complex voxel grids.
  • decimation_target – Target vertex count after simplification. Set to 30_000 for web-optimized assets or 100_000+ for high-detail cinematic models.
  • texture_size – Resolution of baked PBR textures. Typical values are 1024 for fast previews and 4096 for photorealistic production assets.
  • mesh_cluster_refine_iterations – Controls UV chart clustering quality. Increase to 2 or 3 for cleaner UV seams on complex topology.
  • use_tqdm – Displays a progress bar for monitoring long-running exports in Jupyter notebooks or CLI environments.
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=512,
    aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
    decimation_target=50_000,
    texture_size=4096,
    remesh=True,
    mesh_cluster_refine_iterations=2,
    verbose=False,
    use_tqdm=True,
)

Key Implementation Files

Understanding the internal architecture helps when debugging or extending the pipeline:

  • o-voxel/o_voxel/postprocess.py – Contains the core to_glb() routine implementing input normalization, mesh cleaning, UV unwrapping, texture baking, and PBR material creation.
  • o-voxel/examples/ovox2glb.py – Reference implementation showing how to load .vxz assets, convert dual-grid representations to meshes, and invoke the export pipeline.
  • trellis2/renderers/pbr_mesh_renderer.py – Provides helper utilities for rendering PBR meshes and handling texture material definitions used internally by to_glb().
  • trellis2/utils/mesh_utils.py – Supplies additional mesh manipulation helpers for cleaning and simplification operations.

Summary

  • o_voxel.postprocess.to_glb() handles the complete pipeline from raw voxel mesh to GLB export with physically-based rendering materials.
  • The function operates entirely on GPU using CUDA-accelerated libraries (cumesh, nvdiffrast) to process hundreds of thousands of faces and high-resolution textures efficiently.
  • Input normalization automatically converts various data formats to tensors and derives grid parameters.
  • Texture baking uses differentiable rasterization to sample sparse voxel attributes onto UV-mapped textures following glTF PBR conventions.
  • Coordinate conversion automatically transforms geometry to the Y-up, right-handed system required by GLTF standards.

Frequently Asked Questions

What input formats does to_glb() accept?

The function accepts torch.Tensor objects for vertices, faces, and attribute volumes, though it automatically converts lists, tuples, and NumPy arrays to tensors during input normalization. The attr_layout dictionary must define slices for base_color, metallic, roughness, and alpha channels that match the last dimension of your attr_volume tensor.

How does the texture baking work internally?

According to the source code in lines 124–166 of postprocess.py, the pipeline uses nvdiffrast to rasterize the UV-unwrapped mesh into a texture atlas. It then employs a BVH accelerator to map each texel back to 3D space and performs trilinear sampling (grid_sample_3d) from the sparse attribute volume. This differentiable approach ensures accurate color transfer from the original high-resolution voxel data to the final texture.

Can I control the mesh simplification level?

Yes, the decimation_target parameter specifies the desired number of vertices after simplification. The cumesh.CuMesh implementation performs quadric error decimation on the GPU. For web applications, set this to 30_000 or lower; for high-fidelity assets used in offline rendering, you can maintain original density by setting it higher than your input vertex count or omitting the parameter.

What coordinate system does the exported GLB use?

The exported GLB conforms to the glTF 2.0 specification, which requires a right-handed coordinate system with Y-up. Lines 112–115 of postprocess.py automatically swap the Y and Z axes from the voxel grid's native orientation and flip the UV V-coordinates to ensure correct texture mapping in standard 3D viewers like Blender, Three.js, or Unity.

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 →