本文 AI 產出,尚未審核

Three.js – 載入外部模型

模型優化與預載(DRACO、Meshopt)


簡介

在 WebGL 與 Three.js 的專案中,模型檔案往往是最佔用資源的部分。未經壓縮的 OBJ、FBX、GLTF 等格式,從檔案大小到 GPU 記憶體佔用,都會直接影響頁面的載入時間與渲染效能。
因此,模型優化預載 (pre‑loading) 成為提升使用者體驗的關鍵技巧。本文將聚焦於兩套業界常用的壓縮與優化方案——DRACOMeshopt,說明它們的原理、在 Three.js 中的使用方式,以及實務上避免踩雷的最佳實踐。


核心概念

1. 為什麼需要模型壓縮?

  • 檔案體積:未壓縮的 GLTF/GLB 可能達到數十 MB,下載速度受限於使用者的網路環境。
  • GPU 記憶體:每個頂點、法線、UV 都會佔用顯示卡記憶體,過多的資料會導致 draw call 增多、幀率下降。
  • 傳輸效能:瀏覽器在解析 JSON、二進位時會產生額外的 CPU 開銷,壓縮格式可以在解碼階段直接得到緊湊的緩衝區。

2. DRACO 壓縮

DRACO 是 Google 開發的幾何壓縮演算法,專門針對 頂點位置、法線、顏色、UV 進行高效編碼。

  • 壓縮率:常見模型可減少 70%~90% 的檔案大小。
  • 解碼方式:Three.js 內建 DRACOLoader,在瀏覽器端即時解碼為原始的 BufferGeometry。
  • 支援情況:GLTF/GLB 官方支援 DRACO,其他格式則需自行轉換。

3. Meshopt 壓縮

Meshopt(Mesh Optimizer)是一套針對 索引、頂點緩衝區、骨骼動畫 的優化與壓縮工具,核心目標是 減少 draw call、提升 GPU 訪問效率

  • 壓縮類型meshopt_encoder 可產生 .meshopt 壓縮檔,MeshoptDecoder 在瀏覽器端解碼。
  • 優化功能:重新排序頂點、合併相同屬性、移除未使用的頂點,對於大型場景尤為有效。
  • 結合 GLTF:GLTF 2.0 允許在 meshopt_compression 擴充中嵌入 Meshopt 壓縮資料,Three.js 的 GLTFLoader 可直接讀取。

4. 預載 (Pre‑loading) 與資源管理

在單頁應用 (SPA) 中,一次性載入所有模型 會導致長時間的白屏。常見做法是:

  1. 使用 LoadingManager 追蹤多個資源的載入進度。
  2. 分段載入(lazy‑load):根據視角或使用者互動,只在需要時才載入模型。
  3. 快取 (Cache):利用瀏覽器快取或 Service Worker 把已下載的壓縮檔案存起來,避免重複下載。

程式碼範例

下面的範例示範了 DRACOMeshopt 在 Three.js 中的完整流程,並結合 LoadingManager 進行預載。

範例 1:設定 LoadingManager 與進度條

// 建立 LoadingManager,所有 loader 都會共用此 manager
const manager = new THREE.LoadingManager();
manager.onStart = (url, itemsLoaded, itemsTotal) => {
  console.log(`開始載入 ${url} (${itemsLoaded}/${itemsTotal})`);
};
manager.onProgress = (url, itemsLoaded, itemsTotal) => {
  const percent = (itemsLoaded / itemsTotal) * 100;
  document.getElementById('progress').style.width = `${percent}%`;
};
manager.onLoad = () => console.log('全部資源載入完成!');
manager.onError = (url) => console.error(`載入失敗: ${url}`);

提示:把 <div id="progress"></div> 放在 HTML 中作為簡易的載入條。

範例 2:使用 DRACOLoader 載入 DRACO 壓縮的 GLTF

// 設定 DRACOLoader 的路徑 (必須指向 draco_decoder.js、draco_wasm_wrapper.js)
const dracoLoader = new THREE.DRACOLoader(manager);
dracoLoader.setDecoderPath('/libs/draco/');   // 你的 draco 檔案所在資料夾
dracoLoader.setDecoderConfig({ type: 'js' }); // 或 'wasm',建議使用 wasm 版較快

// GLTFLoader 內建支援 DRACO,只要把 dracoLoader 注入即可
const gltfLoader = new THREE.GLTFLoader(manager);
gltfLoader.setDRACOLoader(dracoLoader);

// 載入模型
gltfLoader.load('models/character_draco.glb', (gltf) => {
  const model = gltf.scene;
  scene.add(model);
  console.log('DRACO 模型載入完成');
});

範例 3:結合 Meshopt 壓縮的 GLTF

// 先載入 MeshoptDecoder(一次即可)
await MeshoptDecoder.ready;

// 自訂一個 GLTFLoader,啟用 meshopt 解碼
const gltfLoaderMeshopt = new THREE.GLTFLoader(manager);
gltfLoaderMeshopt.setMeshoptDecoder(MeshoptDecoder);

// 載入使用 meshopt_compression 的 GLTF
gltfLoaderMeshopt.load('models/scene_meshopt.glb', (gltf) => {
  const sceneModel = gltf.scene;
  scene.add(sceneModel);
  console.log('Meshopt 模型載入完成');
});

範例 4:分段懶載 (Lazy‑load) 近距離模型

// 假設有兩個模型:遠距離低模 low.glb、近距離高模 high_draco.glb
let lowModel, highModel;

// 先載入遠距離模型
gltfLoader.load('models/low.glb', (gltf) => {
  lowModel = gltf.scene;
  scene.add(lowModel);
});

// 監聽相機距離,當距離 < 20 時再載入高模
function checkDistance() {
  if (!highModel && camera.position.distanceTo(lowModel.position) < 20) {
    gltfLoader.load('models/high_draco.glb', (gltf) => {
      highModel = gltf.scene;
      // 移除低模,改為高模
      scene.remove(lowModel);
      scene.add(highModel);
    });
  }
}
renderer.setAnimationLoop(() => {
  checkDistance();
  renderer.render(scene, camera);
});

範例 5:使用 Service Worker 快取壓縮檔案

// 在 service-worker.js 中
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  // 只快取 .glb、.meshopt、.draco 檔案
  if (url.pathname.endsWith('.glb') ||
      url.pathname.endsWith('.meshopt') ||
      url.pathname.endsWith('.draco')) {
    event.respondWith(
      caches.open('threejs-assets').then((cache) => {
        return cache.match(event.request).then((response) => {
          return response || fetch(event.request).then((networkRes) => {
            cache.put(event.request, networkRes.clone());
            return networkRes;
          });
        });
      })
    );
  }
});

:在主程式中註冊 navigator.serviceWorker.register('/service-worker.js') 即可。


常見陷阱與最佳實踐

陷阱 說明 解決方案
DRACO 解碼速度慢 若使用 type: 'js'(純 JavaScript)解碼器,解碼會較慢。 改用 WebAssembly (type: 'wasm') 並確保 draco_wasm_wrapper.js 正確載入。
Meshopt 壓縮檔未正確載入 GLTF 中 meshopt_compression 需要 extensionsUsedextensionsRequired 正確設定。 使用官方 meshopt_encoder 產生檔案,並在 GLTFLoader 注入 MeshoptDecoder
資源重複下載 多個 loader 各自載入相同的 draco 或 meshopt 解碼器,造成浪費。 透過 LoadingManager 讓所有 loader 共用同一個 decoder 實例。
快取失效 Service Worker 未正確回傳 Cache-Control 標頭,導致每次重新下載。 在伺服器端設定 Cache-Control: max-age=31536000,或在 Service Worker 中手動管理快取。
模型過度壓縮 壓縮率過高會導致頂點精度下降,出現可見的幾何失真。 draco_encodermeshopt_encoder 中調整品質參數 (compressionLevelquantizationBits)。

最佳實踐

  1. 先測試原始模型,確定渲染正確後再進行壓縮。
  2. 使用 WASM 解碼器(DRACO、Meshopt)以取得最佳效能。
  3. 結合 LoadingManager,統一管理所有資源的載入與錯誤處理。
  4. 分段懶載:遠距離使用低 poly,近距離再載入高精度模型。
  5. 快取策略:結合瀏覽器快取與 Service Worker,減少重複下載。

實際應用場景

場景 為何需要 DRACO / Meshopt 實作重點
線上 3D 產品展示 商品模型往往細節豐富,檔案大小可能超過 10 MB。 使用 DRACO 壓縮模型,配合 LoadingManager 與進度條,提升首次載入速度。
大型多人在線遊戲 (MMO) 場景包含成千上萬的靜態建築與動態角色。 針對靜態建築使用 Meshopt 重新排序頂點,減少 draw call;角色使用 DRACO 壓縮,降低網路頻寬。
AR/VR Web 體驗 移動裝置算力與記憶體有限。 結合 DRACO 壓縮與 Meshopt 優化,並在 AR 入口處先載入低模,待使用者靠近時才切換高模。
教育平台的 3D 模型教學 多個模型同時顯示,易造成瀏覽器卡頓。 使用 GLTFLoader.setMeshoptDecoder,一次載入多個 Meshopt 壓縮的模型,並透過 Service Worker 快取。

總結

  • 模型優化是提升 Three.js 應用效能的關鍵,尤其在網路環境與裝置多樣化的今天。
  • DRACO 以高壓縮比縮減幾何資料體積,Meshopt 則在 GPU 訪問層面優化索引與頂點排列,兩者可互補使用。
  • 預載與資源管理(LoadingManager、懶載、快取)讓使用者不會因為一次性下載過大檔案而產生長時間白屏。
  • 實務上,先在開發環境驗證模型正確性,再逐步加入 DRACO、Meshopt 壓縮,最後配合 Service Worker 做離線快取,便能打造流暢且具備良好使用者體驗的 Web 3D 應用。

掌握模型壓縮與預載技巧,讓你的 Three.js 專案在任何裝置上都能保持高速、穩定的渲染表現!