Three.js 實戰專案:3D 互動網站
主題:載入模型與動畫
簡介
在 3D 網站或 WebGL 遊戲中,模型與動畫是最能提升使用者沉浸感的要素。單純的幾何圖形雖然可以快速示範 Three.js 的基礎操作,但要打造商業級或藝術感十足的作品,離不開外部 3D 模型(如 .glb/.gltf、.fbx、.obj)以及由藝術家製作好的動畫序列。
本篇文章將從 載入模型、解析動畫、控制播放 三個核心流程出發,結合實作範例,說明如何在 Three.js 中把外部模型與動畫完整呈現在瀏覽器上。文章適合已具備基礎 Three.js 知識(Scene、Camera、Renderer)的讀者,並提供從新手到中階開發者都能直接套用的技巧與最佳實踐。
核心概念
1. 為什麼選擇 glTF / GLB?
- 跨平台、輕量:glTF 是被稱為「JPEG of 3D」的開放格式,支援 PBR(Physically Based Rendering)材質,檔案大小相對較小。
- 原生支援動畫:GLTF/GLB 可直接內嵌骨骼動畫(skinning)或變形動畫(morph target),Three.js 的
GLTFLoader能自動解析。 - 瀏覽器友好:使用
ArrayBuffer讀取,無需額外的網路請求或轉檔步驟。
小技巧:若模型檔案較大(> 5 MB),建議使用 Draco 壓縮或 Meshopt,Three.js 已提供相應的解碼器。
2. 載入模型的基本流程
- 建立 Loader:
GLTFLoader(或FBXLoader、OBJLoader) - 設定解碼器(可選):若使用 Draco 壓縮,需要先載入
DRACOLoader。 - 呼叫
load方法:提供模型路徑、成功回呼、進度回呼與錯誤回呼。 - 將模型加入 Scene:
scene.add( gltf.scene )。
以下是一個最簡單的 GLTFLoader 範例:
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 1. 建立場景、相機、渲染器(略)
const loader = new GLTFLoader();
// 2. 載入模型
loader.load(
'models/robot.glb', // 模型路徑
(gltf) => {
// 成功回呼
const model = gltf.scene;
model.position.set(0, 0, 0);
scene.add(model);
console.log('模型載入完成!');
},
(xhr) => {
// 進度回呼(可顯示 loading bar)
console.log(`${(xhr.loaded / xhr.total) * 100}% loaded`);
},
(error) => {
// 錯誤回呼
console.error('載入失敗', error);
}
);
重點:
gltf.scene只是一個Group,裡面可能還包含animations、cameras、lights等子物件,視需求自行取用。
3. 解析與播放動畫
GLTFLoader 會在 gltf.animations 陣列中返回 THREE.AnimationClip 物件。使用 THREE.AnimationMixer 可以控制播放、暫停、混合等操作。
3.1 建立 AnimationMixer
let mixer; // 全域變數,方便在 animate loop 中使用
loader.load('models/character.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
// 1. 建立 Mixer,綁定到模型
mixer = new THREE.AnimationMixer(model);
// 2. 取得第一段動畫 Clip(可自行挑選)
const clip = gltf.animations[0];
// 3. 建立 Action,並立即播放
const action = mixer.clipAction(clip);
action.play();
});
3.2 在渲染循環中更新 Mixer
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // 每幀時間差(秒)
if (mixer) mixer.update(delta); // 更新所有動畫
renderer.render(scene, camera);
}
animate();
3.3 多動畫切換(走路、跑步、跳躍)
// 假設 gltf.animations 包含 walk、run、jump 三段
const actions = {};
gltf.animations.forEach((clip) => {
actions[clip.name] = mixer.clipAction(clip);
});
// 預設播放走路動畫
actions['Walk'].play();
// 切換到跑步動畫
function switchToRun() {
actions['Walk'].fadeOut(0.5); // 淡出走路
actions['Run'].reset().fadeIn(0.5).play(); // 淡入跑步
}
技巧:使用
fadeIn/fadeOut可以實現平滑過渡,避免動畫突變。
4. 使用 Draco 壓縮的 GLB
若模型檔案已使用 Draco 壓縮,需要先載入 DRACOLoader 並設定給 GLTFLoader:
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/libs/draco/'); // Draco 解碼器所在路徑
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('models/compressed_house.glb', (gltf) => {
scene.add(gltf.scene);
});
注意:如果使用 CDN,記得在
setDecoderPath時指向正確的.js、.wasm檔案,否則會出現「Failed to load Draco decoder」錯誤。
5. 變形動畫(Morph Targets)
除了骨骼動畫,glTF 也支援 Morph Targets(頂點變形),常用於臉部表情或形狀變化。Three.js 會自動將 morph 資料掛在 mesh.morphTargetInfluences 陣列中。
loader.load('models/face_expression.glb', (gltf) => {
const mesh = gltf.scene.getObjectByName('Head'); // 取得目標 Mesh
// 預設全部 Influence 為 0
mesh.morphTargetInfluences[0] = 0; // 例如「笑容」
mesh.morphTargetInfluences[1] = 0; // 例如「皺眉」
// 動畫:在 2 秒內從無表情過渡到微笑
const duration = 2; // 秒
const start = performance.now();
function morphAnimate(now) {
const elapsed = (now - start) / 1000;
const t = Math.min(elapsed / duration, 1);
mesh.morphTargetInfluences[0] = t; // 線性插值
if (t < 1) requestAnimationFrame(morphAnimate);
}
requestAnimationFrame(morphAnimate);
});
小提醒:若模型使用
MeshStandardMaterial,記得把morphTargets: true設為true,否則變形不會生效。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 模型過大 | 大於 5 MB 時載入時間過長,影響使用者體驗。 | 使用 Draco 或 Meshopt 壓縮;搭配 LOD(Level of Detail) 方案。 |
| 動畫播放不同步 | 多個 AnimationMixer 同時更新,導致時間基準不同。 |
共用同一個 Mixer,或在 clock.getDelta() 前統一時間來源。 |
| 材質失效 | GLTF 中的 PBR 材質在舊版 Three.js 會變成白色。 | 確保使用 r150+ 版本;若仍有問題,手動設定 material.metalness、roughness。 |
| Skeleton 失真 | 載入 FBX 時骨骼軸向不一致,導致動畫扭曲。 | 盡量使用 GLTF;若必須使用 FBX,先在 3D 軟體內 Apply Transform。 |
| 記憶體泄漏 | 每次切換模型卻未移除舊的 Mixer、Geometry、Texture。 | 在切換前呼叫 dispose():geometry.dispose(); material.dispose(); texture.dispose();,並將 mixer 設為 null。 |
其他最佳實踐
- 預載(Preload)資源:利用
LoadingManager統一管理進度條,提升使用者感知。 - 分離模型與動畫:將模型檔與動畫檔分開(如
character.glb+walk.glb),可在同一模型上重用不同動畫。 - 使用
SkeletonHelper:開發階段可視化骨骼結構,快速定位權重問題。 - 保持相同的時間基準:在
animate迴圈中只呼叫一次clock.getDelta(),再分別傳給所有 Mixer。 - 優化渲染:對於靜態模型使用
mesh.frustumCulled = false或 Bake lighting,降低 GPU 負擔。
實際應用場景
| 場景 | 需求 | 實作要點 |
|---|---|---|
| 產品展示網站 | 需要載入高品質的產品模型,提供旋轉、拆解動畫。 | 使用 GLTFLoader 搭配 KTX2 壓縮貼圖;利用 AnimationMixer 播放拆解序列。 |
| 互動式教育平台 | 角色模型須根據使用者選擇切換走路、跑步、跳躍等動畫。 | 為每個動作建立 clipAction,使用 fadeIn/fadeOut 實現平滑切換;利用 UI 按鈕觸發 switchToX()。 |
| AR/VR 體驗 | 大量模型需即時載入,且動畫必須保持低延遲。 | 採用 WebXR 結合 GLTFLoader,搭配 LOD 與 Draco 壓縮;預載關鍵模型與動畫。 |
| 遊戲化網站 | 角色表情變化(Morph Targets)與動作混合。 | 使用 morphTargetInfluences 控制臉部表情,配合 AnimationMixer 的 Weight blending 同時播放走路與說話。 |
| 數據可視化 | 需要將 3D 數據點映射成動畫粒子。 | 先載入簡易的 Points 模型,再透過 ShaderMaterial 動態改變位置與顏色;不需要骨骼動畫。 |
總結
載入模型與動畫是 Three.js 開發中最常見也最具挑戰性的環節。透過本篇文章,我們了解了:
- 為何 glTF/GLB 是首選格式,並掌握 DRACO、Meshopt 壓縮的使用方式。
- GLTFLoader、AnimationMixer、AnimationClip 的完整工作流程,從模型載入到動畫播放、混合與過渡。
- Morph Targets 與 Skeleton 動畫的差異與實作技巧。
- 常見的陷阱(模型過大、動畫同步、記憶體泄漏)以及對應的 最佳實踐。
- 多種 實務應用場景,幫助你在產品展示、教育平台、AR/VR 以及遊戲化網站中快速落地。
只要遵循「先壓縮、再載入、最後釋放」的流程,並善用 LoadingManager、AnimationMixer 的混合功能,你就能在瀏覽器裡呈現流暢且具表現力的 3D 互動體驗。祝你在 Three.js 的實戰專案中玩得開心,創造出令人驚豔的 3D 網站!