本文 AI 產出,尚未審核

Three.js 動畫系統:深入了解 AnimationMixer 與 AnimationClip

簡介

在 3D 網頁開發中,動畫是讓場景活起來、提升使用者沉浸感的關鍵元素。Three.js 提供了一套完整且彈性的動畫系統,核心概念圍繞著 AnimationClip(動畫片段)與 AnimationMixer(動畫混音器)。掌握這兩者的使用方式,才能在 Three.js 中實作角色走路、機械臂運動、甚至是 UI 元件的過場動畫。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領 初學者到中級開發者 建立對 AnimationMixerAnimationClip 的完整認知,並提供可直接套用於專案的程式碼範例。


核心概念

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 只接受 Float32ArrayArray,每三個數字代表一個 3D 向量。

2. AnimationMixer:動畫的「執行器」

AnimationMixer 負責 播放、暫停、混合 以及 控制權重(weight)等操作。一個 Mixer 可以同時管理多個 ClipAction(單一 Clip 的播放實例),因此可以在同一個物件上同時播放走路、揮手等多段動畫,並透過權重混合產生自然過渡。

2.1 基本流程

  1. 建立 Mixerconst mixer = new THREE.AnimationMixer(rootObject);
  2. 取得 Actionconst action = mixer.clipAction(clip);
  3. 設定播放參數(loop、timeScale、weight…)
  4. 呼叫 action.play() 開始播放
  5. 在渲染迴圈中更新 Mixermixer.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)

最佳實踐

  1. 統一管理 Mixer:將所有 Mixer 放入一個陣列,於渲染迴圈一次性更新,避免遺漏。
  2. 使用 AnimationUtils 產生簡易 Clip:對於單純的旋轉、縮放,可直接呼叫 THREE.AnimationUtils.makeKeyframeTrack,減少手寫程式碼。
  3. 把 Clip 與 Action 以名稱作為索引mixer.clipAction('Walk') 讓程式更具可讀性。
  4. 預先計算 duration:載入外部模型後,遍歷 gltf.animations,呼叫 clip.optimize()clip.resetDuration(),確保時間正確。
  5. 使用 crossFadeTo 而非手動調整權重:Three.js 已內建平滑過渡演算法,減少錯誤。

實際應用場景

場景 需求 主要使用技巧
角色控制器(第三人稱/第一人稱) 行走、跑步、跳躍、攻擊等多段動畫的即時切換 crossFadeTotimeScale、多個 Action 同時播放(如跑步 + 揮劍)
機械臂或機器人動畫 精確控制每個關節的關鍵幀,且需要同步多條軸向運動 手寫 KeyframeTrackAnimationMixer單一 Action(不需要混合)
環境過場動畫(門開、燈光變化) 只需要一次性播放,且不與角色動畫衝突 為場景節點各自建立 Mixer,播放完後自動 stop(),使用 LoopOnce
大量 NPC 同步走路 數十至數百個模型共享同一走路動畫 Clip 共用 + 多 Mixer,減少記憶體占用
UI 動態效果(按鈕彈跳、滑動) 小型、短暫的動畫,常與 WebGL 互動結合 使用 THREE.AnimationClipAnimationMixer,或直接使用 gsap 但若在 3D 內部則仍建議 AnimationMixer

總結

  • AnimationClip 是動畫的 資料容器,負責描述「時間 vs 屬性」的關係。
  • AnimationMixer 則是 執行器,負責播放、暫停、混合以及控制時間尺度。
  • 透過 Actionmixer.clipAction())我們可以對單一 Clip 進行 loop、weight、timeScale 等細部調整,進而實作 交叉淡入多段同時播放快慢速 等常見需求。
  • 常見陷阱大多與 忘記更新 Mixer、權重衝突或 Clip 長度未設定 有關,遵守 每幀更新、統一管理 Mixer、正確設定 Loop 的最佳實踐即可避免大多數問題。
  • 在實務開發中,從 角色控制器大量 NPC 同步走路,AnimationMixer 與 AnimationClip 都提供了彈性且效能友好的解決方案。

掌握了這套動畫系統後,你就能在 Three.js 中創造出 流暢、自然且具互動性的 3D 動畫體驗,讓你的 Web 作品更具競爭力。祝你玩得開心,創作出令人驚豔的動畫效果! 🚀