jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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
MethodBest forProsCons
Dracostatic meshes, smallest downloadhighest geometry ratiowasm decode cost on load; lossy (quantization)
Meshoptanything, incl. animation & morphsfast decode, tiny decoder, GPU-friendlyslightly larger than Draco
Quantizea universal baselineneeds 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
FormatDecoded whereBest forThe catch
KTX2 ETC1Sstays compressed on GPUcolor/AO maps, smallest VRAM + filelower quality
KTX2 UASTCstays compressed on GPUnormal maps, fine detailbigger than ETC1S, still GPU-compressed
WebP / AVIFdecoded to full RGBA in VRAMquick file-size winsno 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ớ 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 .glb bytes over the wire. {Kích thước truyền — số byte .glb trê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/.blend and treat optimized files as build artifacts. {Giữ bản gốc. Nén là lossy và một chiều; lưu .glb/.blend gố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}

  1. inspect a model, then run optimize --compress draco --texture-compress webp and compare sizes. {inspect một model, rồi chạy optimize và so kích thước.}
  2. 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.}
  3. 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.}