看著畫面上的 Xbot 靈巧地擺出各種姿勢,我本以為這套工具已經很厲害了。
直到昨天深夜,我隨便放了一首音樂準備試著編舞時,我才恍然發現:這套軟體目前只是一個「動作編輯器」,根本沒有產生動畫的功能耶 QQ!
不管姿勢調得再美,它依然只是死板板的靜態 3D 公仔。
為了驗證「動態編舞」的可行性,我決定貫徹 MVP 導向,用最小的程式碼改動與小跑步研究,讓 Xbot 真正跳起來!

要讓角色動起來,我們不需要做一個像 Premiere 或 Blender 那樣複雜的時間軸。
最 MVP 的做法,就是用一個簡單的 JavaScript 陣列:
有了 Frame 1 與 Frame 2,中間的動作該怎麼變過去?
如果直接「瞬間跳變」,看起來會像畫面卡頓;如果計算得不好,手臂可能會穿過自己的身體。
這時候,我們用最簡潔的 3D 旋轉過渡演算法——四元數球面線性插值(Slerp):
Slerp(Spherical Linear Interpolation,球面線性內插)是用來處理 3D 旋轉過渡 的技術。
簡單來說:讓一個物體從「旋轉角度 A」平滑、勻速地轉到「旋轉角度 B」。常見用途於 3D 遊戲相機追隨、角色骨骼動畫過渡、遙控無人機姿態修正。
圖片來源:https://starrykss.tistory.com/1806
按下 Play 鍵的那一刻,Xbot 的手臂順暢、平滑且乾淨地從前一個方塊滑向下一個方塊,動作順暢得讓人驚嘆!

這一步以 MVP 為導向的小跑步研究,對這套模擬器來說至關重要。
我們沒有花幾天時間去刻複雜的 UI 軌道,而是用最精簡的程式碼(陣列 + Slerp)快速驗證了核心體驗。
現在,這套工具真正擁有了「時間」的維度,舞者可以真正坐在電腦前,把腦海中的舞步序列一拍一拍寫下來,並即時預覽流暢的動態成果!
## Keyframe 拍點時間軸(核心新需求)
- 維護一個 `keyframes` 陣列,每個元素是 `{ angles: { 關節key: [x,y,z], ... } }`,涵蓋全部 19 個關節的當前 `target` 快照
- 「+ 新增拍點」按鈕:把目前 `target` 深拷貝 `push()` 進 `keyframes`,畫面上以拍點方塊(chip)列表呈現,標示 `F1`、`F2`……
- 每個拍點方塊要能:
- 點擊主體:立即套用該拍點角度到目前姿勢(方便預覽或接著用控制環微調)
- 點擊上面的刪除小按鈕:從陣列移除該拍點
- 標示目前選取中的拍點(供「更新選取拍點」「刪除選取拍點」按鈕使用)
- 額外按鈕:「更新選取拍點」(用目前姿勢覆寫選取中的拍點)、「清空全部」、「循環播放」核取方塊
- 「▶ 播放拍點」按鈕:
- 少於 2 個拍點時要提示使用者、不執行播放
- 開始播放時要停用「預設循環播放」與關節控制環(`transformControls.enabled = false`,並先 `detach()`),播放中畫面上的關節球不可再被點擊選取
- 播放邏輯:以 `requestAnimationFrame` 取得的時間戳計算每一拍的經過時間,一拍時長 = `60000 / bpm`(與預設循環共用同一顆 BPM 滑桿)
- **核心:每根骨骼在 `keyframes[i]` 與 `keyframes[i+1]` 之間,用四元數 Slerp 而非 Euler 線性插值**:
``js
const qa = eulerToQuat(anglesA);
const qb = eulerToQuat(anglesB);
const q = qa.clone().slerp(qb, t); // t: 0→1,本拍進度
bone.quaternion.copy(restQuat[key]).multiply(q);
``
這樣才能保證每個關節走最短旋轉路徑、角速度均勻,不會有 Euler 插值常見的抖動或繞遠路問題
- 每拍播放完(t 到達 1)要把 `target`/`current` 同步成該拍的角度(避免下一幀開始時跳動),再前進到下一個拍點區間;播放到最後一個拍點時,依「循環播放」是否勾選決定回到 F1 重播,或停止並停留在最後一個姿勢
- 播放中,目前正在播放的拍點方塊要有視覺提示(例如亮框),跟著播放進度移動
- 停止播放後要把控制環權限還回來(`transformControls.enabled = true`),可以繼續點關節球微調