Three.js from Zero to Senior · 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.
Giờ bạn tạo được asset (Part 15) và nạp được chúng (Part 14). Vấn đề: bản xuất thô rất nặng. 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. Nén thường cắt nó 5–10× mà không mất chất lượng thấy được.
Có ba đòn bẩy độc lập, và một .glb thường béo ở nhiều hơn một chỗ:
- Hình học — vị trí/normal/UV của đỉnh (nén bằng Draco hoặc Meshopt).
- Texture — thường 80–90% dung lượng (nén bằng KTX2/Basis).
- Cấu trúc — accessor trùng, node thừa, keyframe dư (dọn bằng dedup/prune/resample).
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.
Loader ở Part 14 minh hoạ model glTF thường. File Draco, Meshopt hoặc KTX2 chỉ nạp được sau khi bạn nối decoder tương ứng như phần bootstrap bên dưới; demo hiện tại không giả vờ kiểm chứng các biến thể nén.
Cài và soi trước
Cài CLI toàn cục:
npm install --global @gltf-transform/cli
Đừng tối ưu mù. 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
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. Nếu texture chiếm phần lớn (thường vậy), bắt đầu từ đó.
Thắng nhanh bằng một lệnh
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
Lệnh này dedup, prune, nén hình học và nén lại texture trong một lượt. Đây là 80/20. 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.
Hình học: Draco vs Meshopt vs Quantize
gltf-transform draco helmet.glb helmet.draco.glb # geometry compression
gltf-transform meshopt helmet.glb helmet.meshopt.glb # geometry + morph/keyframe buffers
gltf-transform quantize helmet.glb helmet.quant.glb # quantization baseline
| Method | Best for | Pros | Cons |
|---|---|---|---|
| Draco | primitive geometry cần tỉ lệ nén cao | thường giảm byte geometry mạnh | wasm decode + quantization; không nén animation track |
| Meshopt | geometry, morph target và keyframe animation | decode nhanh, coverage buffer rộng | cần decoder; kích thước/kết quả phụ thuộc asset |
| Quantize | baseline không cần decoder riêng | dùng KHR_mesh_quantization | tỉ lệ nén thấp hơn khi đứng một mình |
Quy tắc: chọn bằng benchmark trên cùng asset. Draco chỉ nén primitive geometry, nên skinned mesh vẫn dùng được nhưng animation track không được lợi. Meshopt bao phủ cả morph/keyframe buffer. Cả hai dựa vào quantization ở một mức nào đó; hãy đo byte, decode, first render và visual error thay vì gắn nhãn “animation-safe” hay “nhỏ nhất” cho mọi model.
Texture: phần thật sự quan trọng
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 |
Đây là bài học hầu hết bỏ lỡ: WebP/AVIF chủ yếu thu nhỏ bản tải rồi thường được upload ở dạng texture GPU không nén. Một RGBA8 4K là khoảng 64 MiB cho level gốc, khoảng 85 MiB nếu có đủ mip chain; format thực tế có thể khác. KTX2/Basis transcode sang format GPU hỗ trợ, giảm memory/bandwidth. Dùng UASTC cho normal và data map cần fidelity; ETC1S phù hợp hơn với color map chấp nhận compression artifact — policy phải theo slot, không theo cả file.
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
Cấu trúc: tách thao tác an toàn khỏi thay đổi trade-off
dedup, prune và resample thường là bước dọn đầu tiên, nhưng vẫn phải chạy validator và visual test. Các lệnh còn lại thay topology, extension hoặc culling behavior nên không được gom vào nhãn “miễn phí”:
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 # adds EXT_mesh_gpu_instancing
gltf-transform join in.glb out.glb # fewer draws, possibly worse culling
gltf-transform simplify in.glb out.glb --ratio 0.5 --error 0.001 # fewer tris
join có thể tăng vertex count và biến nhiều bounding volume nhỏ thành một vùng culling lớn. instance đổi contract runtime sang EXT_mesh_gpu_instancing. weld và simplify thay topology; simplify còn lossy. Chỉ bật khi asset profile và renderer target chứng minh trade-off đúng.
Một công thức sẵn-sàng-cho-web thực tế
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 uastc s4.glb s5.glb \
--slots "{normalTexture,occlusionTexture,metallicRoughnessTexture}" \
--level 4 --rdo --rdo-lambda 4 --zstd 18
gltf-transform etc1s s5.glb s6.glb --quality 255 # remaining color slots
gltf-transform meshopt s6.glb hero.web.glb # geometry + animation
gltf-transform inspect hero.web.glb # confirm the win
Phía loader (nhắc lại)
File nén cần decoder — đúng thứ bootstrap ở Part 14 đã nối. 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.185.0/examples/jsm/libs/basis/')
.detectSupport(renderer); // MUST pass the renderer
loader.setDRACOLoader(draco)
.setKTX2Loader(ktx2)
.setMeshoptDecoder(MeshoptDecoder);
KTX2Loader.detectSupport(renderer) là bắt buộc — nó dò GPU để chọn đích transcode (ASTC, BC7, ETC2…). Bỏ qua thì texture KTX2 lặng lẽ fail.
Trong pipeline build (Node)
Cho build lặp lại được, dùng SDK thay vì CLI:
import { NodeIO } from '@gltf-transform/core';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import { dedup, prune, weld, resample, draco } from '@gltf-transform/functions';
import draco3d from 'draco3dgltf';
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({
'draco3d.decoder': await draco3d.createDecoderModule(),
'draco3d.encoder': await draco3d.createEncoderModule(),
});
const document = await io.read('hero.glb');
await document.transform(
dedup(), prune(), weld(), resample(), draco(),
);
await io.write('hero.web.glb', document);
Cắm vào build asset để designer thả .glb thô và CI xuất ra bản tối ưu.
Đo kết quả
Đừng tin cảm giác — đọc số từ inspect trước/sau, và theo dõi ba thứ lúc chạy:
- Kích thước truyền — số byte
.glbtrên đường truyền. - VRAM — chỉ KTX2 giảm; WebP/AVIF thì không.
- Thời gian decode + first render — cả decoder, texture transcode và shader compile đều phải đo trên device tier mục tiêu.
Lời cảnh báo của bậc thầy
- Nén mà thiếu decoder = trang hỏng. Ship loader Draco/KTX2/Meshopt cùng file đã tối ưu.
- WebP không phải nén VRAM. Cho scene mobile giới hạn bộ nhớ, bạn cần KTX2.
- 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.
- 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. - Đừ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.
Thực hành
inspectmột model, rồi chạyoptimizevà so kích thước.- 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.
- Lấy một mesh scan/AI high-poly và
simplify --ratio 0.25; tìm chỗ nó hỏng rõ.
Tiếp theo
Asset của bạn giờ vừa nhỏ vừa nạp được. 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.