KHR_draco_mesh_compression (Google's Draco 7,495 ) entropy-codes geometry into the smallest files, at the cost of a WebAssembly decode; EXT_meshopt_compression (meshoptimizer 8,458 ) stores quantized streams that decode far faster and shrink further under gzip. glTF Transform applies either to Poly Haven 82,537 's CC0 Mantel Clock 01 (Image Generation APIs's stand-in model, 15,236 vertices):
npm install --global @gltf-transform/cli # 4.5.0
cp cc0-mantel-clock.glb clock.glb
gltf-transform draco clock.glb clock-draco.glb
gltf-transform meshopt clock.glb clock-meshopt.glbinfo: clock.glb (3.13 MB) → clock-draco.glb (2.56 MB) info: clock.glb (3.13 MB) → clock-meshopt.glb (2.64 MB)
Geometry was only 642 KB; four JPEG textures are the rest (KTX2 and Basis). Decoders come from Babylon 89,603 's CDN on first use and run in web workers:
for (const file of ['clock.glb', 'clock-draco.glb', 'clock-meshopt.glb']) {
const started = performance.now();
const container = await BABYLON.LoadAssetContainerAsync(file, scene);
const vertices = container.meshes.reduce((sum, m) => sum + m.getTotalVertices(), 0);
console.log(`${file}: ${vertices} vertices in ${Math.round(performance.now() - started)} ms`);
}clock.glb: 15236 vertices in 142 ms clock-draco.glb: 15236 vertices in 747 ms clock-meshopt.glb: 15236 vertices in 121 ms
Draco's time includes fetching its 59 KB wrapper and 192 KB WebAssembly decoder (meshopt's is one 25 KB script; timings are from a shared machine). In production host the decoders yourself: set BABYLON.DracoDecoder.DefaultConfiguration (wasmUrl, wasmBinaryUrl, fallbackUrl) and BABYLON.MeshoptCompression.Configuration = { decoder: { url } }. Prefer meshopt for fast loading, Draco for the smallest download.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
/*
The book compresses Poly Haven's Mantel Clock 01 with glTF Transform and loads each file with
LoadAssetContainerAsync; the Draco and meshopt decoders then come from Babylon's CDN. This preview
cannot download files, so it builds a clock stand-in in code and charts the sizes the book measured.
In production, host the decoders yourself:
BABYLON.DracoDecoder.DefaultConfiguration = { wasmUrl, wasmBinaryUrl, fallbackUrl };
BABYLON.MeshoptCompression.Configuration = { decoder: { url } };
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.3, 7.8, new BABYLON.Vector3(1.5, 0.9, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const mat = (hex) => {
const m = new BABYLON.StandardMaterial(hex, scene);
m.diffuseColor = BABYLON.Color3.FromHexString(hex);
return m;
};
// The clock: a lathed wooden case, a dial and two hands
const profile = [[0.9, 0], [0.9, 0.12], [0.75, 0.2], [0.7, 0.9], [0.62, 1.25], [0.35, 1.55], [0, 1.62]]
.map(([r, y]) => new BABYLON.Vector3(r, y, 0));
const body = BABYLON.MeshBuilder.CreateLathe('case', { shape: profile, tessellation: 96, cap: BABYLON.Mesh.CAP_END }, scene);
body.scaling.z = 0.35; // flatten into a mantel clock
body.material = mat('#6d4c33');
const dial = BABYLON.MeshBuilder.CreateCylinder('dial', { diameter: 0.9, height: 0.05, tessellation: 64 }, scene);
dial.rotation.x = Math.PI / 2;
dial.position.set(0, 0.95, -0.27);
dial.material = mat('#fff8e1');
const hands = ['#222', '#b5452f'].map((hex, i) => {
const hand = BABYLON.MeshBuilder.CreateBox('hand' + i, { width: 0.03, height: i ? 0.4 : 0.28, depth: 0.02 }, scene);
hand.setPivotPoint(new BABYLON.Vector3(0, -(i ? 0.2 : 0.14), 0));
hand.position.set(0, 0.95 + (i ? 0.2 : 0.14), -0.31);
hand.material = mat(hex);
return hand;
});
scene.onBeforeRenderObservable.add(() => {
const t = performance.now() / 1000;
hands[0].rotation.z = -t / 12;
hands[1].rotation.z = -t;
});
const vertices = scene.meshes.reduce((sum, m) => sum + m.getTotalVertices(), 0);
// What each option adds to the first load (KB), as measured in the book
const BARS = [['raw geometry', 642, '#9e9e9e'], ['Draco decoder', 251, '#b5452f'], // 59 KB JS + 192 KB wasm
['meshopt decoder', 25, '#3f7d3a']]; // one script
BARS.forEach(([name, kb, hex], i) => {
const height = kb / 250;
const bar = BABYLON.MeshBuilder.CreateBox(name, { width: 0.6, depth: 0.6, height }, scene);
bar.position.set(1.7 + i * 1.6, height / 2, 0);
bar.material = mat(hex);
const anchor = new BABYLON.TransformNode('a' + i, scene);
anchor.position.set(bar.position.x, 0, 0);
const label = new BABYLON.GUI.TextBlock('', `${kb} KB\n${name}`);
Object.assign(label, { color: '#333', fontSize: 11, resizeToFit: true });
ui.addControl(label);
label.linkWithMesh(anchor);
label.linkOffsetY = 34;
});
const title = new BABYLON.GUI.TextBlock('', `clock stand-in: ${vertices} vertices · meshopt decodes fastest, Draco downloads smallest`);
Object.assign(title, { color: '#555', fontSize: 12, height: '22px', top: '8px',
verticalAlignment: BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP });
ui.addControl(title);
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>