How to Decode Meshoptimizer-Compressed glTF in Three.js or Babylon.js

To decode meshoptimizer-compressed glTF files, import the meshopt_decoder.mjs WebAssembly module and register it with your loader—Three.js requires explicit registration via loader.setMeshoptDecoder(MeshoptDecoder) while Babylon.js automatically detects the global MeshoptDecoder object to decompress EXT_meshopt_compression buffers at runtime.

The zeux/meshoptimizer repository provides the reference implementation for the EXT_meshopt_compression glTF extension, which reduces geometry size through quantization and reordering. Assets compressed using the gltfpack CLI tool with the -c flag require the project's JavaScript WebAssembly decoder to restore vertex and index buffers before they can be used by WebGL engines. Both Three.js and Babylon.js integrate this decoder, exposing it through slightly different APIs that affect how you initialize your scene loader.

What the Meshopt Decoder Does

The decoder module at js/meshopt_decoder.mjs ships two WebAssembly builds—scalar and SIMD—and automatically selects the fastest implementation supported by the browser. It implements the decompression algorithms defined in the EXT_meshopt_compression specification by exposing functions that mirror the C API declared in src/meshoptimizer.h.

The decoder exposes four key functions that the glTF loaders call internally:

  • meshopt_decodeVertexBuffer: Decompresses attribute buffers such as positions, normals, and UVs.
  • meshopt_decodeIndexBuffer: Reconstructs triangle index buffers.
  • meshopt_decodeIndexSequence: Handles arbitrary index sequences for non-triangle data.
  • meshopt_decodeGltfBuffer: A wrapper that selects the appropriate routine and applies filters (octahedral, quaternion, etc.).

Three.js specifically uses the asynchronous variant decodeGltfBufferAsync, which allocates temporary memory in the WebAssembly heap, copies the compressed data, runs the native C implementation, and returns the uncompressed result as a Uint8Array.

Three.js Integration (Version r122+)

Three.js added native support for the extension in r122 and later. You must explicitly provide the decoder instance to the GLTFLoader before calling load().

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { MeshoptDecoder } from './meshopt_decoder.mjs';

const loader = new GLTFLoader();

// Register the decoder
loader.setMeshoptDecoder(MeshoptDecoder);

loader.load(
    'models/compressed.glb',
    (gltf) => {
        scene.add(gltf.scene);
    },
    undefined,
    (error) => console.error('Failed to load glTF:', error)
);

When the loader encounters a bufferView containing the EXT_meshopt_compression extension, it internally calls MeshoptDecoder.decodeGltfBufferAsync to decompress the data before uploading it to the GPU.

Babylon.js Integration

Babylon.js ships the decoder integration as part of its loaders package. The engine checks for a global MeshoptDecoder object automatically, so you only need to import the module before invoking SceneLoader.

import { MeshoptDecoder } from './meshopt_decoder.mjs';

const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);

// Babylon automatically detects MeshoptDecoder
BABYLON.SceneLoader.Append(
    '',
    'models/compressed.glb',
    scene,
    () => {
        engine.runRenderLoop(() => scene.render());
    }
);

The loader implementation in packages/loaders/src/glTF/2.0/extensions/extensionMeshoptCompression.ts checks for the existence of MeshoptDecoder and calls decodeGltfBuffer to decompress vertex and index data as the glTF file is parsed.

Complete Working Example

The following HTML module demonstrates how to load a compressed glTF file in either engine. Uncomment the block for the engine you wish to use.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Meshopt-Compressed glTF Demo</title>
    <script type="module">
        // ------------------------------------------------
        // Three.js Implementation
        // ------------------------------------------------
        import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
        import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js';
        import { MeshoptDecoder } from './js/meshopt_decoder.mjs';

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        document.body.appendChild(renderer.domElement);
        
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
        camera.position.set(0, 1, 3);

        const loader = new GLTFLoader();
        loader.setMeshoptDecoder(MeshoptDecoder);
        loader.load('model.glb', (gltf) => {
            scene.add(gltf.scene);
            animate();
        });

        function animate() {
            requestAnimationFrame(animate);
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.render(scene, camera);
        }

        // ------------------------------------------------
        // Babylon.js Implementation (comment out Three.js block above to use)
        // ------------------------------------------------
        /*
        import { MeshoptDecoder } from './js/meshopt_decoder.mjs';
        
        const canvas = document.getElementById('renderCanvas');
        const engine = new BABYLON.Engine(canvas, true);
        const scene = new BABYLON.Scene(engine);
        
        // Camera and light setup omitted for brevity
        
        BABYLON.SceneLoader.Append('', 'model.glb', scene, () => {
            engine.runRenderLoop(() => scene.render());
        });
        */
    </script>
</head>
<body style="margin: 0; overflow: hidden;"></body>
</html>

Key Source Files in the Repository

Understanding where the implementation lives helps debug integration issues:

  • js/meshopt_decoder.mjs: The public JavaScript entry point that loads the WebAssembly module and exposes the decode* functions.
  • js/meshopt_decoder.cjs: CommonJS wrapper for Node.js or bundler environments.
  • src/meshoptimizer.h: C API header used by the WebAssembly build; defines the native functions that perform the actual decompression.
  • gltf/README.md: Official documentation for generating compressed glTF files using gltfpack -c and decoding them in browsers.
  • gltf/gltfpack.cpp: Source of the command-line tool where the KHR_meshopt_compression extension data is written into the glTF file.
  • packages/loaders/src/glTF/2.0/extensions/extensionMeshoptCompression.ts (Babylon.js): Shows how the Babylon loader detects the global decoder and invokes decodeGltfBuffer.

Summary

  • Generate compressed assets using gltfpack -c (or -cc for higher compression) to create EXT_meshopt_compression glTF files.
  • Load the meshopt_decoder.mjs module before loading any compressed scenes; it requires a modern browser with WebAssembly support.
  • Three.js: Call loader.setMeshoptDecoder(MeshoptDecoder) on your GLTFLoader instance before calling load().
  • Babylon.js: Ensure MeshoptDecoder is available as a global import or script; the engine automatically uses it when parsing compressed buffer views.
  • The decoder allocates temporary WASM memory during decompression, but the resulting buffers are standard Uint8Array objects suitable for WebGL upload.

Frequently Asked Questions

How do I know if a glTF file uses meshoptimizer compression?

Check the extensionsUsed array in the glTF JSON for EXT_meshopt_compression. If present, the file contains compressed buffer views that require the decoder. Standard glTF loaders will fail to load these files without the decoder registered.

Can I use the decoder without a module bundler?

Yes. You can load the decoder via a standard <script type="module"> tag that imports meshopt_decoder.mjs, or use a UMD build if available from a CDN. The key requirement is that the MeshoptDecoder object must be available in the scope where the glTF loader executes, either as a global variable or imported module.

What is the performance cost of decoding at runtime?

The WebAssembly decoder is highly optimized and typically decompresses geometry faster than the network time saved by downloading smaller files. The operation runs synchronously (or asynchronously via decodeGltfBufferAsync in Three.js) on the main thread, but for most assets the time is negligible compared to shader compilation and texture upload.

Does this work with legacy browsers?

No. The decoder requires WebAssembly support, which is not available in older browsers like Internet Explorer. To support legacy environments, generate uncompressed glTF files using gltfpack without the -c flag, or provide fallback assets that exclude the EXT_meshopt_compression extension.

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 →