Three.js 動畫系統:深入了解 AnimationMixer 與 AnimationClip
簡介
在 3D 網頁開發中,動畫是讓場景活起來、提升使用者沉浸感的關鍵元素。Three.js 提供了一套完整且彈性的動畫系統,核心概念圍繞著 AnimationClip(動畫片段)與 AnimationMixer(動畫混音器)。掌握這兩者的使用方式,才能在 Three.js 中實作角色走路、機械臂運動、甚至是 UI 元件的過場動畫。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領 初學者到中級開發者 建立對 AnimationMixer 與 AnimationClip 的完整認知,並提供可直接套用於專案的程式碼範例。
核心概念
1. AnimationClip:動畫的「資料」
AnimationClip 代表一段 時間軸上的關鍵幀(Keyframe)資料,它本身不會直接驅動物件,而是描述「在什麼時間點、哪些屬性要變成什麼值」。常見的來源有:
| 來源 | 說明 |
|---|---|
手寫程式碼(THREE.KeyframeTrack) |
直接在程式中定義關鍵幀 |
| GLTF/FBX 匯入 | 3D 建模軟體導出的動畫自動轉成 AnimationClip |
AnimationUtils 產生 |
使用 THREE.AnimationUtils 產生簡易動畫(例如旋轉、縮放) |
1.1 主要屬性
| 屬性 | 型別 | 說明 |
|---|---|---|
name |
String | 片段名稱,後續可用 mixer.clipAction(name) 取得 |
duration |
Number | 影片長度(秒),若為 -1 會自動根據關鍵幀長度計算 |
tracks |
Array<KeyframeTrack> | 包含所有關鍵幀軌道(位置、旋轉、縮放、材質等) |
1.2 建立簡易 Clip(範例 1)
// 讓一個 Mesh 在 2 秒內沿 X 軸從 0 移動到 5
const times = [0, 2]; // 時間點(秒)
const values = [0, 0, 0, 5, 0, 0]; // 起點 (0,0,0) → 終點 (5,0,0)
const positionTrack = new THREE.VectorKeyframeTrack(
'.position', // 目標屬性路徑(相對於 Mesh 本身)
times,
values
);
const clip = new THREE.AnimationClip('moveX', 2, [positionTrack]);
註:
VectorKeyframeTrack只接受Float32Array或Array,每三個數字代表一個 3D 向量。
2. AnimationMixer:動畫的「執行器」
AnimationMixer 負責 播放、暫停、混合 以及 控制權重(weight)等操作。一個 Mixer 可以同時管理多個 ClipAction(單一 Clip 的播放實例),因此可以在同一個物件上同時播放走路、揮手等多段動畫,並透過權重混合產生自然過渡。
2.1 基本流程
- 建立 Mixer:
const mixer = new THREE.AnimationMixer(rootObject); - 取得 Action:
const action = mixer.clipAction(clip); - 設定播放參數(loop、timeScale、weight…)
- 呼叫
action.play()開始播放 - 在渲染迴圈中更新 Mixer:
mixer.update(deltaTime);
2.2 範例 2:載入 GLTF 並播放內建動畫
const loader = new THREE.GLTFLoader();
loader.load('model/character.glb', (gltf) => {
const model = gltf.scene;
scene.add(model);
// 建立 Mixer,根物件通常是模型的根節點
const mixer = new THREE.AnimationMixer(model);
// gltf.animations 內可能有多個 AnimationClip
const walkClip = THREE.AnimationClip.findByName(gltf.animations, 'Walk');
// 取得 Action,設定為循環播放
const walkAction = mixer.clipAction(walkClip);
walkAction.loop = THREE.LoopRepeat;
walkAction.clampWhenFinished = false;
walkAction.play();
// 在渲染迴圈中更新
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta); // <-- 必須每幀呼叫
renderer.render(scene, camera);
}
animate();
});
3. 混合多段動畫(Cross‑Fade)
實務上最常見的需求是 從一段動畫平滑過渡到另一段(例如從站立切換到跑步)。Three.js 提供 action.crossFadeTo 方法,內部會自動調整兩段 Action 的權重。
3.1 範例 3:站立 ↔ 跑步的交叉淡入
// 假設已經有 mixer、idleClip、runClip
const idleAction = mixer.clipAction(idleClip);
const runAction = mixer.clipAction(runClip);
// 先播放 idle,設定為持續循環
idleAction.play();
// 當玩家按下「前進」鍵時,從 idle 淡入 run
function onMoveForward() {
runAction.reset(); // 從頭開始
runAction.play();
idleAction.crossFadeTo(runAction, 0.5, false); // 0.5 秒淡入
}
// 當玩家停止移動時,跑步淡回站立
function onStopMoving() {
idleAction.reset();
idleAction.play();
runAction.crossFadeTo(idleAction, 0.5, false);
}
小技巧:
crossFadeTo的第三個參數warp若設為true,Three.js 會自動調整兩段動畫的播放速度,使過渡更自然。
4. 直接控制時間(Time‑Scale & Seek)
有時候需要 快放、慢放 或 跳到特定時間點(例如同步音效)。Action.timeScale 控制播放速度,Action.time 可直接設定當前時間。
4.1 範例 4:慢動作與同步音效
const jumpAction = mixer.clipAction(jumpClip);
jumpAction.play();
// 0.5 倍速播放(慢動作)
jumpAction.timeScale = 0.5;
// 同步音效:在 0.2 秒時播放「跳躍」聲音
const audio = new THREE.Audio(listener);
audio.setBuffer(jumpSoundBuffer);
jumpAction.enabled = true;
// 在渲染迴圈中檢查時間點
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
if (jumpAction.time > 0.2 && !audio.isPlaying) {
audio.play();
}
renderer.render(scene, camera);
}
5. 多物件共用同一 Clip(Instancing)
若場景中有多個相同類型的角色(例如大量怪物),重複載入同一 AnimationClip 會浪費記憶體。正確的做法是 共用 Clip,為每個物件建立獨立的 Mixer。
5.1 範例 5:大量怪物共用走路動畫
// 先載入一次 GLTF,取得走路 Clip
let walkClip;
loader.load('monster.glb', (gltf) => {
walkClip = THREE.AnimationClip.findByName(gltf.animations, 'Walk');
});
// 之後每次產生怪物
function createMonster() {
const monster = new THREE.Mesh(monsterGeometry, monsterMaterial);
scene.add(monster);
const mixer = new THREE.AnimationMixer(monster);
const walkAction = mixer.clipAction(walkClip);
walkAction.play();
// 把 mixer 存起來,渲染時一起更新
mixers.push(mixer);
}
// 渲染迴圈
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixers.forEach(m => m.update(delta));
renderer.render(scene, camera);
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記在每幀呼叫 mixer.update |
動畫不會前進或卡住 | 把 mixer.update(delta) 放在渲染迴圈最前面 |
| Clip Duration 為 -1 | 有時載入的 Clip 沒有自動計算長度,導致 action.play() 立即結束 |
使用 clip.resetDuration() 或手動設定 clip.duration |
| 多個 Mixer 同時更新同一物件 | 會產生權重衝突,導致動畫不穩定 | 每個物件僅使用 一個 Mixer,若需多段動畫則使用同一 Mixer 的多個 Action |
| KeyframeTrack 的路徑寫錯 | 例如 .position 寫成 .pos,會無法驅動屬性 |
參考 Object3D 的屬性名稱,或使用 THREE.PropertyBinding.parseTrackName 測試 |
| 權重(weight)未正確設定 | 混合動畫時會出現突兀或「卡住」的感覺 | 在交叉淡入前先把 action.enabled = true,並使用 action.setEffectiveWeight(value) 調整 |
忘記 loop 設定 |
預設是 LoopOnce,一次播放後自動停止 |
若需要持續播放,設定 action.setLoop(THREE.LoopRepeat, Infinity) |
最佳實踐
- 統一管理 Mixer:將所有 Mixer 放入一個陣列,於渲染迴圈一次性更新,避免遺漏。
- 使用
AnimationUtils產生簡易 Clip:對於單純的旋轉、縮放,可直接呼叫THREE.AnimationUtils.makeKeyframeTrack,減少手寫程式碼。 - 把 Clip 與 Action 以名稱作為索引:
mixer.clipAction('Walk')讓程式更具可讀性。 - 預先計算
duration:載入外部模型後,遍歷gltf.animations,呼叫clip.optimize()與clip.resetDuration(),確保時間正確。 - 使用
crossFadeTo而非手動調整權重:Three.js 已內建平滑過渡演算法,減少錯誤。
實際應用場景
| 場景 | 需求 | 主要使用技巧 |
|---|---|---|
| 角色控制器(第三人稱/第一人稱) | 行走、跑步、跳躍、攻擊等多段動畫的即時切換 | crossFadeTo、timeScale、多個 Action 同時播放(如跑步 + 揮劍) |
| 機械臂或機器人動畫 | 精確控制每個關節的關鍵幀,且需要同步多條軸向運動 | 手寫 KeyframeTrack、AnimationMixer 的 單一 Action(不需要混合) |
| 環境過場動畫(門開、燈光變化) | 只需要一次性播放,且不與角色動畫衝突 | 為場景節點各自建立 Mixer,播放完後自動 stop(),使用 LoopOnce |
| 大量 NPC 同步走路 | 數十至數百個模型共享同一走路動畫 | Clip 共用 + 多 Mixer,減少記憶體占用 |
| UI 動態效果(按鈕彈跳、滑動) | 小型、短暫的動畫,常與 WebGL 互動結合 | 使用 THREE.AnimationClip 與 AnimationMixer,或直接使用 gsap 但若在 3D 內部則仍建議 AnimationMixer |
總結
- AnimationClip 是動畫的 資料容器,負責描述「時間 vs 屬性」的關係。
- AnimationMixer 則是 執行器,負責播放、暫停、混合以及控制時間尺度。
- 透過 Action(
mixer.clipAction())我們可以對單一 Clip 進行 loop、weight、timeScale 等細部調整,進而實作 交叉淡入、多段同時播放、快慢速 等常見需求。 - 常見陷阱大多與 忘記更新 Mixer、權重衝突或 Clip 長度未設定 有關,遵守 每幀更新、統一管理 Mixer、正確設定 Loop 的最佳實踐即可避免大多數問題。
- 在實務開發中,從 角色控制器 到 大量 NPC 同步走路,AnimationMixer 與 AnimationClip 都提供了彈性且效能友好的解決方案。
掌握了這套動畫系統後,你就能在 Three.js 中創造出 流暢、自然且具互動性的 3D 動畫體驗,讓你的 Web 作品更具競爭力。祝你玩得開心,創作出令人驚豔的動畫效果! 🚀