Course outline
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.
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
.wasmfile) that the browser runs at close to native speed; decoders ship as.wasmfiles. - 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.
02Optimise, keeping the parts
Run the pipeline with Draco and WebP, but without joining or flattening, so every named part survives.
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
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 asdown-tube,mesh_3,mesh_4,mesh_1_instance_0,mesh_1_instance_1,stemandgrip: each joined mesh kept one part's name or got a made-up one, and the other parts' names are gone. - 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 falseand compare the twoinfo:lines and aninspectof each: fewer meshes, a similar size, lost names.
Code & diagrams
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).
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 6Console only. The site's address is cut from the 404 message.
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 FoundBreak 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.
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
extensionsRequiredin the compressed bike:KHR_draco_mesh_compression,EXT_texture_webpandEXT_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
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
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 webpruns 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
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
.wasmfile 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
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
KTX2Loaderwith 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
The trap: joining. By default
optimizejoins 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
Compare Draco and Meshopt for the showroom: size, speed and what each needs.
Why did the course turn off joining when it compressed the bike?
Practice
Write the GLTFLoader setup that can load Draco, Meshopt and KTX2 files from this course's decoder folders.
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