jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

6 MIN READ Updated JUL 12, 2026

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.

Mở demo toàn màn hình

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
MethodBest forProsCons
Dracoprimitive geometry cần tỉ lệ nén caothường giảm byte geometry mạnhwasm decode + quantization; không nén animation track
Meshoptgeometry, morph target và keyframe animationdecode nhanh, coverage buffer rộngcần decoder; kích thước/kết quả phụ thuộc asset
Quantizebaseline không cần decoder riêngdùng KHR_mesh_quantizationtỉ 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
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

Đâ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, pruneresample 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. weldsimplify 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 .glb trê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/.blend gố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

  1. inspect một model, rồi chạy optimize và so kích thước.
  2. 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. 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.