Three.js From Zero · Article s6-02
S6-02 Draco Compression
Season 6 · Article 02
Draco Geometry Compression
Triangle meshes are enormous — 32 bytes per vertex, 12 per triangle. Draco crushes them 10×. Google-made, Khronos-extension, Three.js-integrated. One extra loader, 90% smaller downloads.
1. Where the size goes
| Attribute | Bytes/vertex |
|---|---|
| Position (vec3 f32) | 12 |
| Normal (vec3 f32) | 12 |
| UV (vec2 f32) | 8 |
| Tangent (vec4 f32) | 16 |
| Total per vertex | 48 |
| Index (u16) | 2 per index × 3 = 6/triangle |
100k-triangle mesh with normals + UVs ≈ 4.8 MB uncompressed.
2. How Draco compresses
- Quantization: truncate position/normal/UV precision (14 bits position, 10 normal, 12 uv). Lossless to the eye.
- Edgebreaker encoding: topology stored as a traversal of triangles with 5-symbol clers language. Tiny.
- Entropy coding: arithmetic coding on residuals. The final squeeze.
3. Ratios
- Position-only: ~30× compression.
- With normals + UVs: ~10-15×.
- With everything (tangents, colors): ~8-10×.
Decoder cost: ~1-5ms per mesh in WASM.
4. Author pipeline
# Option A: draco's CLI
draco_encoder -i mesh.obj -o mesh.drc
# Option B: gltf-transform (recommended)
gltf-transform draco model.glb model.draco.glb
# Option C: glTF-Pipeline (Cesium)
gltf-pipeline -i model.glb -o model.draco.glb -d
5. Three.js consumer
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/'); // or self-host
draco.setDecoderConfig({ type: 'js' }); // or 'wasm' for better perf
const loader = new GLTFLoader().setDRACOLoader(draco);
loader.load('model.draco.glb', gltf => scene.add(gltf.scene));
6. Live demo — same model, two versions
Load the same DamagedHelmet uncompressed vs the Draco-compressed variant. Compare file sizes + decode times.
–
7. Knobs
// When encoding
quantizePositionBits: 14 // default, raise for jewelry precision
quantizeNormalBits: 10 // drop to 8 for flat shapes
quantizeTexcoordBits: 12
quantizeColorBits: 8
quantizeGenericBits: 12
encodeSpeed: 7 // 0 (slow, best) - 10 (fast)
decodeSpeed: 7
Tune quantize* for quality. Tune encodeSpeed for authoring time.
8. Gotchas
- Decoder path: Google's
gstatic.com/draco/v1/decoders/works but is a CDN dependency. Self-host for reliability. - WASM vs JS decoder: WASM is faster but slightly bigger. Prefer WASM.
- Hard edges: aggressive normal quantization breaks flat shading on octahedral-encoded normals. Test.
- Tangents: included in Draco but some exporters strip. Recompute in Three.js if your normal maps look wrong.
- Can't encode morphs well: morph-target geometries grow modestly.
9. When to pick Draco vs Meshopt
- Draco: smaller ratio (~10× typical). Slower decode. Best for one-time loads.
- Meshopt (S6-04): modest ratio (~3×) but super fast decode (SIMD). Better for streaming, many small meshes.
Mix both in one glTF — position via Meshopt, unusual attributes via Draco. gltf-transform supports per-attribute compression.
10. Takeaways
- Draco compresses mesh data 10×. Quantization + edgebreaker + entropy coding.
- Encode with
gltf-transform draco. Decode with Three's DRACOLoader. - Self-host the decoder. Prefer WASM over JS.
- Tune quantize bits to balance quality vs size.
- Combine with Meshopt for modern best-of-both.