本文 AI 產出,尚未審核
Three.js – 載入外部模型
模型優化與預載(DRACO、Meshopt)
簡介
在 WebGL 與 Three.js 的專案中,模型檔案往往是最佔用資源的部分。未經壓縮的 OBJ、FBX、GLTF 等格式,從檔案大小到 GPU 記憶體佔用,都會直接影響頁面的載入時間與渲染效能。
因此,模型優化與預載 (pre‑loading) 成為提升使用者體驗的關鍵技巧。本文將聚焦於兩套業界常用的壓縮與優化方案——DRACO 與 Meshopt,說明它們的原理、在 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) 中,一次性載入所有模型 會導致長時間的白屏。常見做法是:
- 使用
LoadingManager追蹤多個資源的載入進度。 - 分段載入(lazy‑load):根據視角或使用者互動,只在需要時才載入模型。
- 快取 (Cache):利用瀏覽器快取或 Service Worker 把已下載的壓縮檔案存起來,避免重複下載。
程式碼範例
下面的範例示範了 DRACO 與 Meshopt 在 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 需要 extensionsUsed 與 extensionsRequired 正確設定。 |
使用官方 meshopt_encoder 產生檔案,並在 GLTFLoader 注入 MeshoptDecoder。 |
| 資源重複下載 | 多個 loader 各自載入相同的 draco 或 meshopt 解碼器,造成浪費。 | 透過 LoadingManager 讓所有 loader 共用同一個 decoder 實例。 |
| 快取失效 | Service Worker 未正確回傳 Cache-Control 標頭,導致每次重新下載。 |
在伺服器端設定 Cache-Control: max-age=31536000,或在 Service Worker 中手動管理快取。 |
| 模型過度壓縮 | 壓縮率過高會導致頂點精度下降,出現可見的幾何失真。 | 在 draco_encoder 或 meshopt_encoder 中調整品質參數 (compressionLevel、quantizationBits)。 |
最佳實踐:
- 先測試原始模型,確定渲染正確後再進行壓縮。
- 使用 WASM 解碼器(DRACO、Meshopt)以取得最佳效能。
- 結合 LoadingManager,統一管理所有資源的載入與錯誤處理。
- 分段懶載:遠距離使用低 poly,近距離再載入高精度模型。
- 快取策略:結合瀏覽器快取與 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 專案在任何裝置上都能保持高速、穩定的渲染表現!