Three.js 效能最佳化:貼圖與模型壓縮(DRACO / Meshopt)
簡介
在 WebGL 應用中,模型資料與貼圖往往是佔用帶寬與記憶體的最大來源。即使 Three.js 已經提供了許多渲染優化技巧,未經壓縮的幾何體與高解析度貼圖仍會導致載入時間過長、GPU 記憶體吃緊,甚至在行動裝置上出現掉幀或崩潰的情況。
本單元將說明兩種在 Three.js 生態系中廣受好評的壓縮技術:DRACO(幾何體壓縮)與 Meshopt(頂點索引與屬性最佳化)。同時,我們也會探討貼圖的壓縮(KTX2 / Basis)以及如何在程式碼層面正確使用這些工具,讓你的 3D 網頁在 載入速度、記憶體佔用 與 渲染效能 上都有實質提升。
核心概念
1. 為什麼要壓縮貼圖
- 檔案大小:未壓縮的 PNG/JPG 在傳輸時往往是 MB 級別,使用 Basis / KTX2 可將同等畫質的檔案縮小 70% 以上。
- GPU 記憶體:貼圖會直接佔用顯示卡的 VRAM,過大的貼圖會迫使 GPU 必須做「貼圖分割」(texture tiling) 或「MIPMAP 重新生成」,增加渲染成本。
- MIPMAP & Power‑of‑Two:KTX2 自動產生完整的 MIPMAP 並保證寬高為 2 的次方,這對 WebGL 的取樣與過濾非常友好。
2. DRACO 壓縮概念
DRACO 是 Google 開發的幾何體壓縮演算法,主要針對 頂點座標、法線、UV 進行三角形網格的 lossless 或 lossy 壓縮。
- 壓縮率:一般情況可將 10
20 MB 的 OBJ/GLTF 縮小至 12 MB。 - 解碼成本:DRACO 需要在瀏覽器端執行 JavaScript / WebAssembly 解碼,解碼時間通常在 10~30 ms 之間,對於大多數互動式應用來說是可接受的。
- Three.js 整合:
DRACOLoader已內建於 Three.js,只要載入相應的 decoder 檔案,即可透明解壓縮.glb/.gltf。
3. Meshopt 壓縮概念
Meshopt(Mesh Optimizer)是另一套針對 頂點索引、頂點屬性排序、量化 的優化工具,特別適合在 GPU 端減少 draw call 與 提升緩衝區存取效率。
- 索引重排:將相鄰三角形的頂點排在一起,提升 vertex cache 的命中率。
- 屬性量化:把 float 位置、法線、UV 壓縮為 16‑bit 或 8‑bit,減少傳輸與儲存空間。
- 解碼方式:Meshopt 需要
MeshoptDecoder(WebAssembly)在客戶端解碼,使用方式與 DRACO 類似,但支援的壓縮類型更廣(例如glb中的EXT_meshopt_compression)。
程式碼範例
以下範例均假設已在專案根目錄安裝 three、three-stdlib(包含 DRACOLoader)以及 meshoptimizer(npm 套件):
npm install three three-stdlib meshoptimizer
範例 1️⃣:使用 DRACOLoader 載入 DRACO 壓縮的 GLB
import * as THREE from 'three';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
// 建立場景、相機、渲染器(略)
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild(renderer.domElement);
// 設定 DRACO decoder 路徑,這裡使用官方提供的 CDN
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
// 載入 .glb(已經在建模工具中啟用 DRACO 壓縮)
const loader = new THREE.GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load('models/compressedModel.glb', gltf => {
scene.add(gltf.scene);
animate();
});
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
重點:只要
setDRACOLoader正確指向 decoder 檔案,Three.js 會在載入階段自動解壓縮,開發者不需要再手動處理幾何體。
範例 2️⃣:使用 MeshoptDecoder 載入 Meshopt 壓縮的 GLB
import * as THREE from 'three';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 先載入 Meshopt decoder(WebAssembly)
// 此檔案同樣可以放在 CDN 或自行編譯
await MeshoptDecoder.ready; // 確保 decoder 已載入完成
const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load('models/meshoptCompressed.glb', gltf => {
scene.add(gltf.scene);
// 若模型包含動畫,亦可直接播放
const mixer = new THREE.AnimationMixer(gltf.scene);
gltf.animations.forEach(clip => mixer.clipAction(clip).play());
animate();
});
function animate() {
requestAnimationFrame(animate);
mixer?.update(0.016); // 60 FPS
renderer.render(scene, camera);
}
小技巧:
MeshoptDecoder.ready會回傳一個 Promise,確保在真正載入模型前已完成 WASM 初始化,避免首次載入卡頓。
範例 3️⃣:KTX2 / Basis 壓縮貼圖的載入
import * as THREE from 'three';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
// 建立渲染器,並啟用 WebGL2(KTX2 需要)
// 若瀏覽器不支援 WebGL2,KTX2Loader 會自動回退到 BasisU
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.context.getExtension('WEBGL_compressed_texture_astc');
renderer.context.getExtension('WEBGL_compressed_texture_etc1');
renderer.context.getExtension('WEBGL_compressed_texture_s3tc');
const ktx2Loader = new KTX2Loader()
.setTranscoderPath('https://cdn.jsdelivr.net/npm/@loaders.gl/basis@2.3.13/dist/') // BasisU transcoder
.detectSupport(renderer);
const texture = await ktx2Loader.loadAsync('textures/brick_albedo.ktx2');
// 套用到材質
const material = new THREE.MeshStandardMaterial({ map: texture });
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
提示:使用
detectSupport後,KTX2Loader 會根據 GPU 支援自動選擇 ASTC、BC7、ETC2 等最佳壓縮格式,無需手動切換。
範例 4️⃣:動態切換貼圖解析度(LOD)
// 事先載入不同解析度的 KTX2 貼圖
const lowRes = await ktx2Loader.loadAsync('textures/hero_low.ktx2');
const highRes = await ktx2Loader.loadAsync('textures/hero_high.kt2');
// 建立材質,先使用低解析度貼圖
const material = new THREE.MeshStandardMaterial({ map: lowRes });
const mesh = new THREE.Mesh(heroGeometry, material);
scene.add(mesh);
// 監聽相機距離,適時升級貼圖
function updateTextureLOD() {
const distance = camera.position.distanceTo(mesh.position);
if (distance < 5 && material.map !== highRes) {
material.map = highRes; // 近距離切換高解析度
material.needsUpdate = true;
} else if (distance >= 5 && material.map !== lowRes) {
material.map = lowRes; // 遠距離切回低解析度
material.needsUpdate = true;
}
}
renderer.setAnimationLoop(() => {
updateTextureLOD();
renderer.render(scene, camera);
});
實務意義:在大型開放世界或 AR/VR 應用中,僅在玩家視野內使用高解析度貼圖,可大幅降低 GPU 記憶體佔用 與 帶寬需求。
範例 5️⃣:結合 DRACO 與 Meshopt(同時使用)
// 先準備 DRACO 與 Meshopt decoder
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
await MeshoptDecoder.ready;
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load('models/combinedCompressed.glb', gltf => {
scene.add(gltf.scene);
animate();
});
注意:
combinedCompressed.glb必須在建模階段同時啟用 DRACO 與 Meshopt(如使用 Blender + gltf-pipeline),否則載入器會忽略未啟用的擴展。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| Decoder 未載入完成就呼叫 load | 直接呼叫 loader.load 可能在 decoder 尚未初始化時發生錯誤。 |
使用 await MeshoptDecoder.ready 或 dracoLoader.preload(),確保 decoder 已就緒。 |
| 貼圖尺寸非 2 的次方 | 非 Power‑of‑Two (POT) 會導致瀏覽器自動調整 MIPMAP,增加 CPU 負擔。 | 使用 KTX2Loader.detectSupport 產生的貼圖,或在工具中手動將寬高調整為 256、512、1024… |
| 混用壓縮與未壓縮的同一模型 | 同一場景中同時使用 DRACO、Meshopt 與原始 GLTF 會讓資源管理變得複雜。 | 統一 壓縮流程:在建模階段一次性套用 DRACO+Meshopt,避免混用。 |
| 過度量化導致視覺失真 | 量化到 8‑bit 可能在大範圍模型上出現明顯的凹凸不平。 | 先測試 16‑bit 量化(meshoptCompressionLevel: 2),只在不顯眼的物件上使用更激進的設定。 |
| WASM 下載阻塞 UI | 大型 decoder (≈ 1 MB) 若在首次載入時直接下載,會造成畫面卡頓。 | 預載 decoder(例如在首頁 <head> 中使用 <script type="module" src=".../meshopt_decoder.module.js"></script>),或使用 Service Worker 快取。 |
最佳實踐
- 開發階段即壓縮:在導出 GLTF 時就啟用 DRACO (
--draco) 與 Meshopt (--meshopt) 參數,避免事後再轉換。 - 分層載入:先載入低解析度模型與貼圖,等使用者接近時再載入高階版本(Progressive Loading)。
- 檢測硬體支援:使用
renderer.capabilities或KTX2Loader.detectSupport判斷是否支援 ASTC/BC7,必要時回退到較通用的 ETC1。 - 使用 CDN:將 decoder、KTX2 transcoder 放在 CDN(如 jsDelivr、Google Hosted Libraries)可減少 DNS 查詢與 latency。
實際應用場景
| 場景 | 為何需要貼圖/模型壓縮 | 推薦組合 |
|---|---|---|
| 行動端 AR 應用 | 行動網路頻寬有限、GPU 記憶體僅 256‑512 MB。 | DRACO + Meshopt + KTX2 (ASTC) |
| 大型多人線上 (WebGL MMO) | 同時呈現千上萬個角色,必須最小化每個實例的記憶體占用。 | Meshopt (索引重排) + 低解析度貼圖 + 動態 LOD |
| 產品展示(電商) | 高品質貼圖能提升商品感受,但不希望影響頁面載入速度。 | 只壓縮貼圖(KTX2),模型保持原始(若模型小) |
| VR 觀展 | VR 需要 90 FPS,任何額外的 CPU 解碼都可能造成掉幀。 | 事先在服務端完成 DRACO+Meshopt 壓縮,客端只解碼一次 |
| 教育平台(大量示範模型) | 多模型同時載入,若不壓縮會導致瀏覽器 OOM。 | 使用 GLTFLoader 的 setDRACOLoader + setMeshoptDecoder,配合 Cache 機制重複使用已解碼的緩衝區 |
總結
- 貼圖與模型的壓縮是提升 Three.js 應用效能的關鍵,尤其在行動裝置與大規模場景中更是不可或缺。*
- DRACO 讓幾何資料在傳輸階段大幅縮小,Meshopt 則在 GPU 端優化緩衝區存取與 draw call 效率。兩者可以同時使用,互補彼此的優勢。
- KTX2 / Basis 為貼圖提供了跨平台的高壓縮率與自動 MIPMAP,配合
detectSupport可自動選擇最佳 GPU 壓縮格式。 - 在實作時,務必先載入 decoder、確保 WebAssembly 已就緒,並遵守 Power‑of‑Two、統一壓縮流程 的原則,才能避免常見的卡頓與記憶體泄漏。
透過本文的概念說明與實作範例,你現在可以在自己的 Three.js 專案中快速導入 DRACO、Meshopt 與 KTX2,讓 3D 網頁在 載入速度、記憶體占用與渲染效能 上都達到更佳的表現。祝開發順利,玩得開心!