在 Day 13 中我們討論了 Tutting 的四大中階技術之一——Tracing(軌道)。
在本文開始前觀賞一部影片:The Art of Tracing | Dance Freestyle by Kevin Paradox
影片中的舞者名為 Kevin Paradox,是來自荷蘭的頂尖舞者、編舞家與舞蹈導師,在全球 Hip-Hop 與 Experimental(實驗舞蹈)領域享有極高聲譽。他在工作坊(Workshop)中示範了 Tracing 的技巧,在音樂的搭配下流暢的將身體作為軌道,毫無死角的保持滑動!!!
在真實舞蹈中,Tutting 舞者會讓手掌或指尖貼著另一條手臂或空間軸線滑動,產生像「貼在想像軌道上」的幾何錯覺,這個技巧任何舞風(例如:tutting、popping、hiphop...)都適用。
今天 DAY 18,我們要探討如何在手腳 Two-Bone IK 的基礎上,正式實作 Tracing 軌跡輔助系統!

加入Tracing 軌跡輔助系統後在能夠模擬真實舞者進行 Tracing 的技巧,以下為前後機制對照:
| 比較維度 | 傳統關節角度 Slerp 插值 | Tracing 軌跡即時求解 |
|---|---|---|
| 路徑平滑度 | 需高密度拍點逼近,否則有明顯稜角感 | 毫秒級無限平滑(Catmull-Rom 即時取樣) |
| 軀幹連動性 | 絕對座標固定,軀幹一動手就漂浮 | 相對 Root 綁定,軀幹位移軌跡自動跟走 |
| 時間軸維護 | 塞滿數十個拍點,極難微調修改 | 少量拍點 即可完整保留幾何曲線資料 |
| UI 動線規劃 | 擠在手腳 IK 分頁,操作混亂 | 獨立至 「軌跡」分頁,動線清晰 |
實現了 Tracing 軌跡功能後,XBOT 終於能做出順暢貼合手臂、繪製空間圓弧的流暢動態!透過將「編輯期生成」與「播放期即時 IK 取樣」相結合,我們在不破壞既有時間軸架構的前提下,完美解決了幾何曲線的平滑度痛點。
# 本次新增:軌跡輔助功能
## 目的
在 IK 模式下,讓末端骨骼(手掌/腳掌)可以沿著使用者畫的路徑(直線或曲線)移動,而不只是手動單點擺位。
---
## 階段一:編輯輔助工具(把軌跡取樣成一串拍點)
### 操作邏輯
- 場景中新增**紫色控制點球**(`trajPointMeshes[limb]`,陣列,順序=路徑順序)。
- 操作流程:
1. 先拖橘色 IK target 球到想要的位置
2. 按「+ 新增控制點」
3. 新控制點永遠對齊 target 球目前座標
> ⚠️ 這是使用者明確要求的行為,**不要改回自動偏移/延伸算法**,之前試過會造成疊點難選。
### 軌跡模式
- `TRAJ_MODE[limb]`:
- `'line'`:含 ≥3 點但仍逐點連折線
- `'curve'`:≥3 點時用 `THREE.CatmullRomCurve3`
### 核心函式
| 函式 | 說明 |
|---|---|
| `sampleTrajectoryFromPoints(mode, relPoints, t)` | **純數學函式**,不依賴場景 mesh。輸入相對座標陣列,回傳 0~1 進度對應的座標。**這個純函式版本是階段二能運作的關鍵**,播放時不會依賴場景中是否還留著控制點球。 |
| `sampleTrajectory(limb, t)` | 上述函式的即時預覽包裝(讀 `trajPointMeshes` 的即時 world 座標)。 |
| `generateKeyframesFromTrajectory()` | 沿路徑等間隔取樣 N 點(UI 可調,2~20),每點依序執行:套用進度 → `solveRootFollowForLimb` → `solveTwoBoneIK` → `syncTargetFromBone` → `addKeyframe()`,等同使用者手動逐點擺拍點。 |
---
## 階段二:曲線播放時即時平滑
### 使用者需求
曲線軌跡在播放時要真的平滑沿著曲線走,不能因為取樣點數不夠就有稜角。
### 做法
`generateKeyframesFromTrajectory()` 額外把軌跡資料烘焙進每個拍點:
``js
kf.traj[limb] = {
id: trajId, // 同一次生成共用一個id,用來判斷播放時兩個相鄰拍點是否屬於同一段連續軌跡
mode, // 'line' | 'curve'
points: relPoints, // 控制點,相對 root 骨骼「生成當下」世界座標的偏移量(不是絕對世界座標)
pole: poleSnapshot, // pole球,同樣相對root的偏移
t // 這個拍點在整條軌跡上的參數位置 0~1
}
``
控制點/pole 存**相對 root 骨骼偏移**而非絕對世界座標,這樣角色因為軀幹 FK 動畫而移動時,軌跡會自動跟著走。
### `updateKeyframePlayback(now)` 播放邏輯改動
1. **`collectTrajOverrideKeys(frameA, frameB)`**
檢查前後兩個拍點是否對同一個 limb 記錄了相同 `trajId` 的 `traj`,若是,回傳該 limb 的 `{chain.root, chain.mid}` 這兩個骨骼 key。
2. **跳過覆蓋 key**
一般的 `ALL_JOINT_KEYS` 逐骨骼角度 slerp 迴圈**跳過**這些 key。
3. **⚠️ 關鍵坑:world matrix 更新時機**
改完其他骨骼(例如軀幹)的本地四元數後,`bone.getWorldPosition()` 讀到的還是**上一幀**的 `matrixWorld`(three.js 只在 `renderer.render()` 才會重算)。
因此在算 rootBone 世界座標之前**必須**先手動呼叫一次:
```js
model.updateMatrixWorld(true)
否則肩膀/髖部世界座標會跟這一幀剛套用的軀幹動作對不上,軌跡跟身體會有一幀的滯後感。
(這跟原架構文件記錄的「IK 節流忘記比對 root 座標」是同一種坑的不同變形,都是「world 座標什麼時候才是最新的」沒搞清楚。)
applyTrajOverridesDuringPlayback(frameA, frameB, et)
對每個觸發覆蓋的 limb:
et(緩動後的轉場進度)在 frameA.traj[limb].t 與 frameB.traj[limb].t 之間內插出 curT
sampleTrajectoryFromPoints 取樣出當下曲線位置solveTwoBoneIK
末端骨骼(手掌/腳掌)本身旋轉仍照舊 slerp,不受影響(呼應「IK 只驅動 root+mid,末端保留 FK」的既有設計哲學)。
trajId 不匹配,維持原本的角度 slerp 行為,不受影響