Three.js from Zero to Senior · Bonus Part 16 — Shrinking glTF: Draco, Meshopt & KTX2 with gltf-transform
A 3.7 MB helmet is brutal on mobile. Compress geometry with Draco/Meshopt, textures with KTX2, and clean structure with gltf-transform — often 5–10× smaller, no visible loss. Commands, decision tables, the loader hookup and the traps.
You can now create assets (Part 15) and load them (Part 14). {Giờ bạn tạo được asset (Part 15) và nạp được chúng (Part 14).} The problem: a raw export is heavy. {Vấn đề: bản xuất thô rất nặng.} That Damaged Helmet is 3.7 MB — fine on Wi-Fi, brutal on a phone, and worse once you have ten of them. {Cái Damaged Helmet nặng 3.7 MB — ổn trên Wi-Fi, tàn nhẫn trên điện thoại, và tệ hơn khi bạn có mười cái.} Compression routinely cuts that 5–10× with no visible quality loss. {Nén thường cắt nó 5–10× mà không mất chất lượng thấy được.}
There are three independent levers, and a .glb is usually fat in more than one: {Có ba đòn bẩy độc lập, và một .glb thường béo ở nhiều hơn một chỗ:}
- Geometry — vertex positions/normals/UVs (compress with Draco or Meshopt). {Hình học — vị trí/normal/UV của đỉnh (nén bằng Draco hoặc Meshopt).}
- Textures — usually 80–90% of the bytes (compress with KTX2/Basis). {Texture — thường 80–90% dung lượng (nén bằng KTX2/Basis).}
- Structure — duplicate accessors, unused nodes, redundant keyframes (clean with dedup/prune/resample). {Cấu trúc — accessor trùng, node thừa, keyframe dư (dọn bằng dedup/prune/resample).}
The tool for all three is gltf-transform — a CLI and SDK by Don McCurdy that edits glTF losslessly and reproducibly. {Công cụ cho cả ba là gltf-transform — một CLI và SDK của Don McCurdy, chỉnh glTF không mất dữ liệu và tái lập được.}
The loader from Part 14 already has the Draco, KTX2 and Meshopt decoders wired — so everything you compress below drops straight into it. {Loader ở Part 14 đã nối sẵn decoder Draco, KTX2 và Meshopt — nên mọi thứ bạn nén bên dưới thả thẳng vào được.}
Install and inspect first {Cài và soi trước}
Install the CLI globally: {Cài CLI toàn cục:}
npm install --global @gltf-transform/cli
Never optimize blind. {Đừng tối ưu mù.} Run inspect to see whether a model is geometry-heavy or texture-heavy — it tells you which lever to pull: {Chạy inspect để biết model nặng hình học hay nặng texture — nó cho biết kéo đòn bẩy nào:}
gltf-transform inspect helmet.glb
The report lists meshes, vertex counts, materials, and every texture with its resolution and size. {Báo cáo liệt kê mesh, số đỉnh, material, và từng texture kèm độ phân giải và dung lượng.} If textures dominate (they usually do), start there. {Nếu texture chiếm phần lớn (thường vậy), bắt đầu từ đó.}
The one-command win {Thắng nhanh bằng một lệnh}
For most assets, the optimize umbrella does the right thing: {Với hầu hết asset, lệnh tổng optimize làm đúng việc:}
gltf-transform optimize helmet.glb helmet.opt.glb \
--compress draco \
--texture-compress webp
This dedups, prunes, compresses geometry and recompresses textures in one pass. {Lệnh này dedup, prune, nén hình học và nén lại texture trong một lượt.} It’s the 80/20. {Đây là 80/20.} But the defaults aren’t ideal for every scene — for the best results, inspect, then apply the individual commands below. {Nhưng mặc định không lý tưởng cho mọi scene — để tốt nhất, hãy inspect rồi áp các lệnh riêng bên dưới.}
Geometry: Draco vs Meshopt vs Quantize {Hình học: Draco vs Meshopt vs Quantize}
gltf-transform draco helmet.glb helmet.draco.glb # smallest file
gltf-transform meshopt helmet.glb helmet.meshopt.glb # fast decode, animation-safe
gltf-transform quantize helmet.glb helmet.quant.glb # baseline, tiny decoder cost
| Method | Best for | Pros | Cons |
|---|---|---|---|
| Draco | static meshes, smallest download | highest geometry ratio | wasm decode cost on load; lossy (quantization) |
| Meshopt | anything, incl. animation & morphs | fast decode, tiny decoder, GPU-friendly | slightly larger than Draco |
| Quantize | a universal baseline | needs only core (KHR_mesh_quantization) | least compression |
Rule of thumb: Draco when download size is king and meshes are static; Meshopt when you have animation or want the cheapest decode. {Quy tắc: Draco khi kích thước tải là tối thượng và mesh tĩnh; Meshopt khi có animation hoặc muốn decode rẻ nhất.} Both are lossy via quantization — push it too far and normals get faceted, so verify visually. {Cả hai đều lossy qua quantization — ép quá thì normal bị gãy mặt, nên hãy kiểm tra bằng mắt.}
Textures: the part that actually matters {Texture: phần thật sự quan trọng}
Textures are where the megabytes hide, and there’s a subtle but critical distinction: {Texture là nơi megabyte ẩn náu, và có một khác biệt tinh tế nhưng then chốt:}
gltf-transform etc1s helmet.glb helmet.etc1s.glb # KTX2, smallest, lossy
gltf-transform uastc helmet.glb helmet.uastc.glb # KTX2, high quality
gltf-transform webp helmet.glb helmet.webp.glb # file-size only
| Format | Decoded where | Best for | The catch |
|---|---|---|---|
| KTX2 ETC1S | stays compressed on GPU | color/AO maps, smallest VRAM + file | lower quality |
| KTX2 UASTC | stays compressed on GPU | normal maps, fine detail | bigger than ETC1S, still GPU-compressed |
| WebP / AVIF | decoded to full RGBA in VRAM | quick file-size wins | no VRAM savings |
This is the lesson most people miss: WebP/AVIF only shrink the download. {Đây là bài học hầu hết bỏ lỡ: WebP/AVIF chỉ thu nhỏ bản tải.} On the GPU they expand back to full uncompressed RGBA, so a 4K texture still eats ~64 MB of VRAM. {Trên GPU chúng nở lại thành RGBA đầy đủ, nên một texture 4K vẫn ngốn ~64 MB VRAM.} KTX2/Basis stays compressed in VRAM, cutting memory and bandwidth — which is why it’s the production choice for 3D, especially on mobile. {KTX2/Basis vẫn nén trong VRAM, cắt cả bộ nhớ và băng thông — nên nó là lựa chọn production cho 3D, nhất là mobile.} Use ETC1S for color/AO and UASTC for normal maps. {Dùng ETC1S cho màu/AO và UASTC cho normal map.}
And before you compress, resize: a 4K map on a prop seen at 200px is pure waste. {Và trước khi nén, hãy resize: một map 4K trên một prop nhìn ở 200px là lãng phí thuần tuý.}
gltf-transform resize helmet.glb helmet.r.glb --width 1024 --height 1024
Structure: free bytes from cleanup {Cấu trúc: byte miễn phí từ dọn dẹp}
These are lossless and almost always safe: {Các lệnh này lossless và gần như luôn an toàn:}
gltf-transform dedup in.glb out.glb # merge identical accessors/textures
gltf-transform prune in.glb out.glb # drop unused nodes/materials/textures
gltf-transform weld in.glb out.glb # merge equivalent vertices
gltf-transform resample in.glb out.glb # remove redundant animation keyframes
gltf-transform instance in.glb out.glb # GPU-instance shared meshes
gltf-transform join in.glb out.glb # merge meshes → fewer draw calls
gltf-transform simplify in.glb out.glb --ratio 0.5 --error 0.001 # fewer tris
simplify is the one to handle with care — it’s lossy mesh decimation (great for scans/sculpts/AI meshes, dangerous on hero assets). {simplify là lệnh cần cẩn thận — nó lossy (tuyệt cho scan/sculpt/AI, nguy hiểm với asset chủ đạo).}
A realistic web-ready recipe {Một công thức sẵn-sàng-cho-web thực tế}
Order matters: clean → resize → compress textures → compress geometry. {Thứ tự quan trọng: dọn → resize → nén texture → nén hình học.}
gltf-transform dedup hero.glb s1.glb
gltf-transform prune s1.glb s2.glb
gltf-transform weld s2.glb s3.glb
gltf-transform resize s3.glb s4.glb --width 2048 --height 2048
gltf-transform etc1s s4.glb s5.glb # color/AO → KTX2
gltf-transform meshopt s5.glb hero.web.glb # geometry + animation
gltf-transform inspect hero.web.glb # confirm the win
The loader side (recap) {Phía loader (nhắc lại)}
Compressed files need their decoders — exactly what Part 14’s bootstrap wired in. {File nén cần decoder — đúng thứ bootstrap ở Part 14 đã nối.} Without these, a compressed .glb throws on load: {Thiếu chúng, một .glb nén sẽ văng lỗi khi nạp:}
const draco = new DRACOLoader()
.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/');
const ktx2 = new KTX2Loader()
.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/')
.detectSupport(renderer); // MUST pass the renderer
loader.setDRACOLoader(draco)
.setKTX2Loader(ktx2)
.setMeshoptDecoder(MeshoptDecoder);
KTX2Loader.detectSupport(renderer) is mandatory — it inspects the GPU to pick a transcode target (ASTC, BC7, ETC2…). {KTX2Loader.detectSupport(renderer) là bắt buộc — nó dò GPU để chọn đích transcode (ASTC, BC7, ETC2…).} Skip it and KTX2 textures silently fail. {Bỏ qua thì texture KTX2 lặng lẽ fail.}
In a build pipeline (Node) {Trong pipeline build (Node)}
For repeatable builds, drive the SDK instead of the CLI: {Cho build lặp lại được, dùng SDK thay vì CLI:}
import { NodeIO } from '@gltf-transform/core';
import { dedup, prune, weld, resample, draco } from '@gltf-transform/functions';
const io = new NodeIO(); // register Draco/KTX2 deps per docs
const document = await io.read('hero.glb');
await document.transform(
dedup(), prune(), weld(), resample(), draco(),
);
await io.write('hero.web.glb', document);
Wire this into your asset build so designers drop a raw .glb and CI emits the optimized one. {Cắm vào build asset để designer thả .glb thô và CI xuất ra bản tối ưu.}
Measuring the win {Đo kết quả}
Don’t trust vibes — read the numbers from inspect before and after, and watch three things at runtime: {Đừng tin cảm giác — đọc số từ inspect trước/sau, và theo dõi ba thứ lúc chạy:}
- Transfer size — the
.glbbytes over the wire. {Kích thước truyền — số byte.glbtrên đường truyền.} - VRAM — only KTX2 reduces this; WebP/AVIF do not. {VRAM — chỉ KTX2 giảm; WebP/AVIF thì không.}
- Decode time — Draco adds a wasm decode spike on load; Meshopt is near-free. {Thời gian decode — Draco thêm một đỉnh decode wasm khi nạp; Meshopt gần như miễn phí.}
Master’s warnings {Lời cảnh báo của bậc thầy}
- Compression without decoders = broken page. Ship the Draco/KTX2/Meshopt loaders alongside the optimized file. {Nén mà thiếu decoder = trang hỏng. Ship loader Draco/KTX2/Meshopt cùng file đã tối ưu.}
- WebP is not VRAM compression. For memory-bound mobile scenes, you need KTX2. {WebP không phải nén VRAM. Cho scene mobile giới hạn bộ nhớ, bạn cần KTX2.}
- Resize before you transcode. The biggest texture win is fewer pixels, not a cleverer codec. {Resize trước khi transcode. Thắng lớn nhất về texture là ít pixel hơn, không phải codec khôn hơn.}
- Keep the source. Compression is lossy and one-way; archive the raw
.glb/.blendand treat optimized files as build artifacts. {Giữ bản gốc. Nén là lossy và một chiều; lưu.glb/.blendgốc và coi file tối ưu là build artifact.} - Don’t double-compress. Re-running Draco/Basis on already-compressed data degrades quality for no gain. {Đừng nén hai lần. Chạy lại Draco/Basis trên dữ liệu đã nén làm giảm chất lượng mà không lợi.}
Practice {Thực hành}
inspecta model, then runoptimize --compress draco --texture-compress webpand compare sizes. {inspectmột model, rồi chạyoptimizevà so kích thước.}- Compress the same model two ways — WebP and KTX2 ETC1S — and compare both file size and GPU memory in the browser’s dev tools. {Nén cùng model hai cách — WebP và KTX2 ETC1S — và so cả kích thước file lẫn bộ nhớ GPU.}
- Take a high-poly scan/AI mesh and
simplify --ratio 0.25; find where it visibly breaks. {Lấy một mesh scan/AI high-poly vàsimplify --ratio 0.25; tìm chỗ nó hỏng rõ.}
What’s next {Tiếp theo}
Your assets are now small and loadable. {Asset của bạn giờ vừa nhỏ vừa nạp được.} Next we make them move: in Part 17 we rig and animate — importing skinned characters and clips from Mixamo/Blender, driving them with AnimationMixer, and blending states with crossFadeTo. {Tiếp theo ta làm chúng chuyển động: ở Part 17 ta rig và animate — nhập nhân vật có xương và clip từ Mixamo/Blender, điều khiển bằng AnimationMixer, và trộn trạng thái bằng crossFadeTo.}