DRACOLoader

Compressing Geometry with DRACOLoader

Draco 7,495 (github.com/google/draco (https://github.com/google/draco 7,495 ), Apache 2.0) quantizes and entropy-codes mesh data; glTF carries it as KHR_draco_mesh_compression. glTF Transform 4.5.0 compresses the fox (Fox model by PixelMannen (CC0), rigging and animation by tomkranis (CC BY 4.0), glTF conversion by @AsoboStudio and @scurest (CC BY 4.0), via the Khronos glTF Sample Assets) in one command:

Draco-compressing a glTF fileJavaScript
gltf-transform draco fox.glb fox-draco.glb
Output
info: fox.glb (162.85 KB) → fox-draco.glb (91.92 KB)

A DRACOLoader given to GLTFLoader decodes in Web Workers with WebAssembly. In r186 its decoder defaults to the copy beside DRACOLoader.js; call setDecoderPath() only to self-host examples/jsm/libs/draco/:

Loading the original and the Draco-compressed fox with one loaderJavaScript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const loader = new GLTFLoader().setDRACOLoader(new DRACOLoader());
for (const file of ['fox.glb', 'fox-draco.glb']) {
  const { scene, animations } = await loader.loadAsync(file);
  const { geometry } = scene.getObjectByProperty('isSkinnedMesh', true);
  console.log(file, geometry.attributes.position.count, 'vertices,',
    geometry.index ? geometry.index.count : 0, 'indices,', animations.length, 'clips');
}
Output
fox.glb 1728 vertices, 0 indices, 3 clips
fox-draco.glb 434 vertices, 1728 indices, 3 clips

Draco also welded duplicate vertices into an indexed mesh. It compresses geometry only, and most of this file is animation: gltf-transform meshopt (EXT_meshopt_compression, decoded after loader.setMeshoptDecoder() with the add-ons' meshopt_decoder.module.js) packs animation too and produced 78.27 KB. Draco shines on dense scans.

What Draco does to positions: the same mesh quantized to 14, 8 and 5 bits, with estimated sizesHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
  canvas { display: block; max-width: 100%; }
  .labels { display: flex; width: 600px; max-width: 100%; text-align: center; }
  .labels div { flex: 1; }
</style>
<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="labels" id="labels"></div>
<script type="module">
import * as THREE from 'three';
import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 230);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(30, 600 / 230, 0.1, 50);
camera.position.set(0, 0.4, 5);
camera.lookAt(0, 0, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);

// Draco snaps each position to a grid of 2^bits steps across the bounding box,
// then entropy-codes the small integers. Fewer bits: smaller file, coarser shape.
function quantize(source, bits) {
  const geometry = source.clone();
  geometry.computeBoundingBox();
  const { min, max } = geometry.boundingBox, steps = 2 ** bits - 1;
  const pos = geometry.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    for (let axis = 0; axis < 3; axis++) {
      const lo = min.getComponent(axis), range = max.getComponent(axis) - lo || 1;
      const q = Math.round((pos.getComponent(i, axis) - lo) / range * steps);
      pos.setComponent(i, axis, lo + q / steps * range);
    }
  }
  geometry.computeVertexNormals();
  return geometry;
}

// A dense, rounded "sculpture": the kind of mesh (a scan) where Draco shines
const source = mergeVertices(new THREE.TorusKnotGeometry(0.42, 0.15, 200, 32));
const vertices = source.attributes.position.count, indices = source.index.count;
const rawBytes = vertices * 12 + indices * 4;                   // float32 xyz + uint32 index

[['original float32', 32], ['Draco 14 bits', 14], ['8 bits', 8], ['5 bits', 5]].forEach(([name, bits], i) => {
  const geometry = bits === 32 ? source : quantize(source, bits);
  const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: '#e09a10', roughness: 0.4, metalness: 0.2 }));
  mesh.position.x = (i - 1.5) * 1.35;
  scene.add(mesh);
  // Rough estimate: bits per coordinate plus ~1.5 bytes per triangle after edgebreaker coding
  const bytes = bits === 32 ? rawBytes : Math.round(vertices * 3 * bits / 8 + indices / 3 * 1.5);
  const label = document.getElementById('labels').appendChild(document.createElement('div'));
  label.innerHTML = `<b>${name}</b><br>≈ ${(bytes / 1024).toFixed(0)} KB`;
});

renderer.setAnimationLoop((time) => {
  scene.children.forEach((o) => { if (o.isMesh) o.rotation.y = time / 3000; });
  renderer.render(scene, camera);
});
</script>