本文 AI 產出,尚未審核

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 壓縮。

  • 壓縮率:一般情況可將 1020 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)。

程式碼範例

以下範例均假設已在專案根目錄安裝 threethree-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.readydracoLoader.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 快取。

最佳實踐

  1. 開發階段即壓縮:在導出 GLTF 時就啟用 DRACO (--draco) 與 Meshopt (--meshopt) 參數,避免事後再轉換。
  2. 分層載入:先載入低解析度模型與貼圖,等使用者接近時再載入高階版本(Progressive Loading)。
  3. 檢測硬體支援:使用 renderer.capabilitiesKTX2Loader.detectSupport 判斷是否支援 ASTC/BC7,必要時回退到較通用的 ETC1。
  4. 使用 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。 使用 GLTFLoadersetDRACOLoader + 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 網頁在 載入速度、記憶體占用與渲染效能 上都達到更佳的表現。祝開發順利,玩得開心!