本文 AI 產出,尚未審核
Three.js 動畫系統(Animation System)
主題:GLB 模型動畫播放
簡介
在 3D 網頁應用中,模型動畫往往是最能提升沉浸感與互動性的元素。
Three.js 作為目前最流行的 WebGL 框架,已內建完整的動畫系統,能直接讀取包含骨架、關節與關鍵幀的 GLB(binary glTF)檔案並在瀏覽器中即時播放。
本篇文章將從 概念、實作、常見陷阱 以及 最佳實踐 四個面向,逐步說明如何使用 Three.js 讀取 GLB 模型、建立動畫混合器(AnimationMixer),以及控制動畫的播放、暫停、切換與混合。
適合 初學者 想快速上手,也能為 中階開發者 提供在大型專案中管理多段動畫的技巧與範例。
核心概念
1. GLB 與動畫的基本概念
| 名稱 | 說明 |
|---|---|
| glTF | 「JPEG of 3D」的開放格式,支援材質、貼圖、骨架與動畫。 |
| GLB | glTF 的二進位版,所有 JSON、二進位緩衝與貼圖打包成單一檔案,方便網路傳輸。 |
| AnimationClip | 一段完整的動畫資料,包含多條 KeyframeTrack(位置、旋轉、縮放等)。 |
| AnimationMixer | 負責在場景中播放、混合、暫停 AnimationClip 的核心物件。 |
| AnimationAction | AnimationMixer 為每個 AnimationClip 產生的控制介面,提供 play、stop、fadeIn、setLoop 等方法。 |
Tip:GLB 檔案內可以同時包含多個動畫(例如「走路」+「跳躍」),只要在載入後正確取得對應的
AnimationClip,就能自由切換或混合。
2. 使用 GLTFLoader 載入模型
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// 建立基礎場景
const scene = new THREE.Scene();
const clock = new THREE.Clock(); // 用於更新 mixer
// 載入 GLB
const loader = new GLTFLoader();
loader.load(
'models/character.glb',
(gltf) => {
const model = gltf.scene;
scene.add(model);
// 取得動畫剪輯陣列
const clips = gltf.animations; // Array<THREE.AnimationClip>
initAnimationMixer(model, clips);
},
(xhr) => console.log(`GLB loading: ${(xhr.loaded / xhr.total) * 100}%`),
(error) => console.error('GLB load error:', error)
);
GLTFLoader.load 的第三個參數是 進度回呼,在開發大型模型時可以即時顯示 loading bar;第四個參數則是錯誤處理。
3. 建立 AnimationMixer、AnimationClip、AnimationAction
let mixer; // 全域變數,供 render loop 使用
let actions = {}; // 用於儲存各動畫的 Action
function initAnimationMixer(model, clips) {
// 每個可動畫的物件(通常是根骨架)都需要一個 Mixer
mixer = new THREE.AnimationMixer(model);
// 依照 clips 名稱建立 Action,方便後續存取
clips.forEach((clip) => {
const action = mixer.clipAction(clip);
actions[clip.name] = action;
// 預設不自動播放,讓開發者自行決定時機
action.enabled = true;
action.setLoop(THREE.LoopRepeat);
action.clampWhenFinished = true; // 停止時保持最後幀
});
// 直接播放預設動畫(例如第一個)
const firstClipName = clips[0].name;
actions[firstClipName].play();
}
重點:
AnimationMixer必須在每一幀 (renderloop) 呼叫mixer.update(deltaTime),才能讓動畫前進。
4. 控制播放、暫停、快轉、循環
// 在 UI 按鈕或鍵盤事件中呼叫
function playAnimation(name, fadeTime = 0.5) {
const toPlay = actions[name];
if (!toPlay) {
console.warn(`Animation "${name}" not found`);
return;
}
// 先淡出目前正在播放的動畫
const currentlyPlaying = Object.values(actions).find(a => a.isRunning());
if (currentlyPlaying && currentlyPlaying !== toPlay) {
currentlyPlaying.fadeOut(fadeTime);
}
// 淡入新動畫
toPlay.reset().fadeIn(fadeTime).play();
}
// 暫停 / 繼續
function togglePause() {
const playing = Object.values(actions).find(a => a.isRunning());
if (playing) playing.paused = !playing.paused;
}
// 調整播放速度(1 = 正常、2 = 2 倍速、0.5 = 半速)
function setSpeed(name, speed) {
const action = actions[name];
if (action) action.timeScale = speed;
}
fadeIn/fadeOut讓動畫之間的過渡更自然,避免突兀的姿勢切換。reset()會把時間歸零,常在切換動畫前使用。
5. 多動畫混合與過渡(Cross‑Fade)
假設角色同時要跑步並揮手,我們可以把「跑步」設為基礎循環,然後把「揮手」加到上層,使用 加權 (weight) 來混合。
// 假設已經有兩段 Clip:run、wave
function playMixedAnimation(baseName, overlayName, overlayWeight = 0.8, fadeTime = 0.3) {
const base = actions[baseName];
const overlay = actions[overlayName];
// 基礎動畫永遠循環播放
base.reset().setEffectiveWeight(1.0).play();
// Overlay 動畫只在需要時播放
overlay.reset()
.setEffectiveWeight(overlayWeight) // 控制影響程度
.setLoop(THREE.LoopOnce, 1) // 只播放一次
.fadeIn(fadeTime)
.play()
.clampWhenFinished = true; // 完成後保持最後姿勢
}
實作要點
setEffectiveWeight:決定每條動畫對最終骨架姿勢的貢獻度。LoopOnce:Overlay 動畫常用一次性播放,避免持續干擾基礎動作。clampWhenFinished:保留最後幀,讓角色在揮手結束後自然回到跑步姿勢。
6. 渲染迴圈與時間管理
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // 取得自上一幀的秒數
if (mixer) mixer.update(delta); // 更新所有 Action
renderer.render(scene, camera);
}
animate();
使用 THREE.Clock 可以自動處理 變動的幀率,保證動畫速度不會因電腦效能不同而失真。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記呼叫 mixer.update(delta) |
動畫不會前進,或卡在第一幀。 | 必須在每一幀的渲染迴圈中更新 Mixer。 |
同時播放多段 LoopRepeat 動畫 |
會產生權重衝突,導致姿勢異常。 | 為每個動畫設定適當的 weight,或使用 fadeOut 先停掉舊動畫。 |
| GLB 中的動畫名稱不一致 | 直接使用硬編碼名稱會找不到 Clip。 | 在載入後列印 gltf.animations.map(c => c.name),確認名稱或使用索引。 |
| 動畫速度過快/過慢 | timeScale 設為 0 或負值會停住或倒放。 |
確認 timeScale 為正數,必要時限制最小值(如 Math.max(0.1, speed))。 |
重複建立 AnimationMixer |
每次載入模型都新建 Mixer,導致記憶體泄漏。 | 只為每個 可動畫的根物件 建立一次 Mixer,若要重新載入則先 mixer.uncacheRoot(model)。 |
最佳實踐
- 統一管理 Action:將所有
AnimationAction放入一個物件(如actions),便於查找與切換。 - 使用
fadeIn/fadeOut:即使是簡單的切換,也建議加上淡入淡出,提升視覺品質。 - 分離載入與播放:先完成模型與動畫的載入,之後再根據 UI/遊戲狀態觸發播放,避免在載入過程中即時執行
play()。 - 針對手機端優化:使用
GLTFLoader.setDRACOLoader壓縮幾何體,減少下載與解碼時間。 - 記憶體回收:場景切換或模型銷毀時,務必呼叫
mixer.uncacheRoot(model)與renderer.dispose(),避免 WebGL 記憶體泄漏。
實際應用場景
| 場景 | 需求 | 可能的實作方式 |
|---|---|---|
| 角色控制器(RPG、FPS) | 需要「走路、跑步、跳躍、攻擊」等多段動畫,且在切換時保持流暢。 | 使用 playAnimation() 搭配 fadeIn/fadeOut,把「跑步」設為基礎 LoopRepeat,其他動作以 LoopOnce 混合。 |
| 產品展示(機械、家具) | 需要展示「開門、旋轉、組裝」等序列動畫。 | 依序呼叫 playAnimation(),利用 setLoop(THREE.LoopOnce) 控制每段動畫只播放一次,並在完成後自動切換下一段。 |
| AR/VR 互動 | 動畫必須在不同裝置上保持同步且低延遲。 | 透過 THREE.Clock 取得精確時間,並在每個 frame 前先更新 Mixer,確保即使在低 FPS 情況下仍能保持正確時間進度。 |
| UI/介面動畫 | 小型圖示或按鈕的「跳動」效果。 | 可將 GLB 中的簡單動畫(如「彈跳」)作為 AnimationClip,使用 setLoop(THREE.LoopPingPong, 2) 產生往返效果。 |
| 多角色協同動畫 | 多個角色同時執行不同動畫,如「領隊走路」+「隊員揮手」。 | 為每個角色建立獨立的 AnimationMixer,但可透過同一套 UI 控制多個 Mixer 的 playAnimation(),保持同步。 |
總結
- GLB 為現代 Web 3D 所推薦的模型與動畫封裝格式,能一次傳遞幾何、材質與多段動畫。
- Three.js 的動畫系統核心是
AnimationMixer、AnimationClip與AnimationAction,只要正確取得 Clip、建立 Action,就能自由控制播放、暫停、淡入淡出與權重混合。 - 實務上,建議:
- 先載入再播放,避免在尚未取得 Clip 前就呼叫
play()。 - 使用
fadeIn/fadeOut,讓動畫切換更自然。 - 妥善管理 Mixer 與 Action,避免記憶體泄漏。
- 結合
THREE.Clock,確保在不同裝置與幀率下動畫速度一致。
- 先載入再播放,避免在尚未取得 Clip 前就呼叫
掌握以上概念與範例後,你即可在 角色遊戲、產品展示、AR/VR 互動 等各種 Web 3D 專案中,靈活且高效地播放 GLB 動畫,為使用者帶來更具沉浸感的體驗。祝開發順利,玩得開心!