Command Palette

Search for a command to run...

Hectal
Course outline
Topic 10.5Intermediate ~19 min · step 5 of 7

Compression: Draco, Meshopt and KTX2

In one line

glTF Transform shrinks the exported bike from 1.3 MB to 34 KB: Draco or Meshopt compress the geometry, WebP or KTX2 the textures. Compressed files need a decoder, and the default settings can merge your named parts.

0/7 · 0%

Think of it like this

Moving house, you take the bed apart and vacuum-pack the duvets. The truck carries far less volume; at the other end you spend a few minutes unpacking. Compression is the vacuum bag: a smaller download, then a moment of decoding in the browser.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Draco
A geometry compression method from Google, used in glTF through the KHR_draco_mesh_compression extension.
Meshopt
A geometry and animation compression method (EXT_meshopt_compression) designed to decode very fast.
KTX2 / Basis Universal
A texture container and a compression format that stays compressed in GPU memory (KHR_texture_basisu).
decoder
The code that turns compressed data back into usable data in the browser; for Draco and Basis it is WebAssembly.
WebAssembly
Compact, pre-compiled code (a .wasm file) that the browser runs at close to native speed; decoders ship as .wasm files.
draw call
One drawing command that three.js sends to the GPU. Roughly one per mesh, so fewer meshes means fewer draw calls (Topic 17.2).
glTF Transform
A free command-line tool and library (@gltf-transform/cli) for inspecting, optimising and converting glTF files.

Step by step

01Inspect first

Before changing anything, look at what is in the file. inspect lists meshes, materials and textures with sizes, and estimates GPU memory.

terminal
$ npx @gltf-transform/cli@4.5.1 inspect lumen-bike.glb
── expected output ──
... TEXTURES
│ # │ ... │ slots │ mimeType │ resolution │ size │ gpuSize │
│ 0 │ ... │ baseColorTexture │ image/png │ 1024x1024 │ 1.08 MB │ 5.59 MB │
Shortened. The texture is 1.08 MB of the 1.29 MB file.

02Optimise, keeping the parts

Run the pipeline with Draco and WebP, but without joining or flattening, so every named part survives.

terminal
$ npx @gltf-transform/cli@4.5.1 optimize lumen-bike.glb lumen-bike-draco.glb --compress draco --texture-compress webp --flatten false --join false
── expected output ──
...
info: lumen-bike.glb (1.33 MB) → lumen-bike-draco.glb (33.75 KB)
The tool writes sizes in decimal KB; the example below prints KiB (1 KiB = 1,024 bytes).

03Compare the files

The first example loads four versions of the same bike and prints size, draw calls, triangles and how many named parts it can still find. Draco is the smallest; the joined file has the fewest draw calls and has lost five of six names.

04Give the loader its decoders

The second example loads compressed files without a decoder, and with a decoder path that does not exist. Each fails with its own message. Copy the decoder files into public/data/3d/draco/ (from node_modules/three/examples/jsm/libs/draco/gltf/) so the path never depends on someone else's server.

Try it yourself

  1. 1

    Read the joined file's parts

    In "Four files, one bike", add if (file.includes("joined")) gltf.scene.traverse((o) => o.isMesh && console.log(" ", o.name)); inside the loop. The joined file's meshes print as down-tube, mesh_3, mesh_4, mesh_1_instance_0, mesh_1_instance_1, stem and grip: each joined mesh kept one part's name or got a made-up one, and the other parts' names are gone.

  2. 2

    Run the pipeline yourself

    In your project, run the optimise command above on your own exported lumen-bike.glb. Then run it again without --flatten false --join false and compare the two info: lines and an inspect of each: fewer meshes, a similar size, lost names.

Code & diagrams

Four files, one bike New tab

All four files are on this site, made from lumen-bike.glb with glTF Transform 4.5.1. The last one used optimize's defaults (joining on).

idle

Console

Run the scene to see its console.

Expected console output

lumen-bike.glb                1294.5 KiB | draw calls 25 | triangles 8168 | parts found by name 6 of 6
lumen-bike-meshopt.glb          46.5 KiB | draw calls 25 | triangles 8168 | parts found by name 6 of 6
lumen-bike-draco.glb            33.0 KiB | draw calls 25 | triangles 8168 | parts found by name 6 of 6
lumen-bike-draco-joined.glb     31.5 KiB | draw calls  7 | triangles 8136 | parts found by name 1 of 6
Compressed files need their decoder New tab

Console only. The site's address is cut from the 404 message.

idle

Console

Run the scene to see its console.

Expected console output

Draco file, no DRACOLoader:
  THREE.GLTFLoader: No DRACOLoader instance provided.
Meshopt file, no decoder:
  THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files
Draco file, decoder path wrong:
  fetch for "/data/3d/decoders/draco_wasm_wrapper.js" responded with 404: Not Found

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Ship a joined model to a page that picks parts

In your showroom, load /data/3d/models/lumen-bike-draco-joined.glb instead of lumen-bike-draco.glb and run scene.getObjectByName("front-wheel") in the console.

terminal
$ scene.getObjectByName("front-wheel")
── what you'll see ──
undefined
The bike looks exactly the same. Only the code that finds parts by name stops working.

Myth vs fact

Myth

A smaller file means less GPU memory.

Fact

Draco, Meshopt, JPEG and WebP shrink the download only: the GPU gets the full numbers and full RGBA pixels after decoding. Only KTX2 (Basis) textures stay compressed on the GPU.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Draco is lossy: it quantises positions (by default 14 bits in glTF Transform) and may reorder vertices. For CAD-precise parts, raise the quantisation bits or use Meshopt with KHR_mesh_quantization, which is exact to its stated precision.

  • ▸

    Decoders cost time on the main thread or in workers: DRACOLoader decodes in Web Workers (setWorkerLimit); the Meshopt decoder is a small WebAssembly module that decodes faster. For many small models, the decoder download can outweigh the savings: measure.

  • ▸

    Read extensionsRequired in the compressed bike: KHR_draco_mesh_compression, EXT_texture_webp and EXT_mesh_gpu_instancing. A loader or viewer that lacks any of them refuses the file. GLTFLoader supports all three, and current browsers decode WebP; an older tool or a partner's viewer may not.

Remember this

  1. 1

    The problem this solves: Lena's phone spends seconds downloading a 1.3 MB bike over mobile data. Almost all of it is padding: float numbers with more precision than a bike needs, and a texture saved as a lossless PNG.

  2. 2

    glTF Transform. A free command-line tool (MIT licence) that edits glTF files. npx @gltf-transform/cli optimize in.glb out.glb --compress draco --texture-compress webp runs a whole pipeline: remove unused data, weld duplicate vertices, simplify slightly, join meshes, compress geometry and textures. Run it in a terminal in your project; npx downloads it the first time.

  3. 3

    Geometry: Draco or Meshopt. Draco (by Google) is the smallest: the bike went to 33.0 KiB. It needs a WebAssembly decoder, about 250 KB (a 192 KB .wasm file and a 58 KB wrapper), loaded once: new DRACOLoader().setDecoderPath("/data/3d/draco/"). Meshopt (by Arseny Kapoulkine) was 46.5 KiB here, decodes faster and also compresses animations: loader.setMeshoptDecoder(MeshoptDecoder). Both need their decoder, or loading fails with a clear message.

  4. 4

    Textures: WebP or KTX2. WebP made the 1,057 KiB PNG a few KiB (this tread is dark and simple). But a WebP or JPEG is decoded to full RGBA on the GPU: 1024 × 1024 × 4 bytes plus mipmaps is about 5.33 MiB whatever the file size (Topic 6.6). KTX2 with Basis Universal stays compressed on the GPU, often a quarter of that or less. It needs KTX2Loader with the Basis transcoder (/data/3d/basis/) and the KTX-Software tools to make the files, which this course does not run on the page.

  5. 5

    The trap: joining. By default optimize joins meshes that share a material and flattens the scene tree, to cut draw calls. The bike went from 25 draw calls to 7, but only 1 of the 6 named parts we looked for could still be found: picking a part (Phase 8) and the price list would break silently. The course's file was made with --flatten false --join false: 25 draw calls, every name kept, 33.0 KiB (the joined file is 31.5 KiB: joining saves draw calls, and a little size). Choose per model: join scenery, keep the parts people click.

Explain it without notes

01

Compare Draco and Meshopt for the showroom: size, speed and what each needs.

02

Why did the course turn off joining when it compressed the bike?

Practice

01

Write the GLTFLoader setup that can load Draco, Meshopt and KTX2 files from this course's decoder folders.

02

A teammate's model is 8 MB, mostly four 4096 × 4096 PNG textures. List the commands or steps to make it phone-friendly, and the GPU memory before and after.

Done when you can

  • Inspect a model's sizes with glTF Transform

  • Compress geometry with Draco or Meshopt and set the matching decoder

  • Explain why download size and GPU memory differ

  • Keep the parts your code needs when optimising