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

AttributeBytes/vertex
Position (vec3 f32)12
Normal (vec3 f32)12
UV (vec2 f32)8
Tangent (vec4 f32)16
Total per vertex48
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.