終於來到整個系列中最讓我興奮、也最期待的重頭戲章節了!
延續 DAY 21 對手指舞(Finger Tutting)概念的解構,今天我們將正式跨越理論,深入 3D 引擎的核心內部,詳細探討 Finger Tutting 模式 在 3D Tutting 模擬器中的技術落地與開發實踐過程!
從 30 個微觀掌指關節的建構、CCD 迭代鏈的幾何演算,再到按鈕化卡片 UI 的互動設計,看看我們如何將極小空間內的高密度幾何變化,轉化為直覺、高效的數位編舞工具!

雙手各 5 指、每指 3 節,共計 30 個指節都能像身體關節一樣被選取,並調用 3D 旋轉環微調角度。

為了解決在 3D 場景中逐一抓取微小指節球的痛苦體驗,新增了專屬的「手指」分頁:左右手各一張卡片,每根手指為一列,設有「根 / 中 / 末」三顆水平按鈕,點擊即可直接選取指節並控制旋轉環。
此外,場景中仍保留直覺的 3D 指節球選取,但將球體半徑縮小,避免相鄰指節在畫面中過度擠壓、難以點選。

每根手指配置一顆青色的指尖 Target 目標球,開啟後只需拖曳目標球,整根手指骨骼即會自動收斂彎曲,無需逐節手動調校。
比照脊椎 IK 採用 3 節 CCD(Cyclic Coordinate Descent)迭代鏈,而非手臂/腿部的兩節解析解——手指跟脊椎同構:只有單一自然彎曲方向,不像手臂需要 pole 球指定彎曲側,因此可以直接複用同一套 CCD 求解器,不必另外設計 pole 球機制。

Finger Tutting 模式的落地,讓 3D Tutting 模擬器正式打通了從大關節到指尖的全尺度控制鏈,讓 Finger Tutting 這種極致微觀的舞蹈風格擁有了被精準模擬的空間!
明天,就讓我們用這套 Finger Tutting 模式實際編排一套完整的排舞吧!
# Prompt:為 Xbot 3D 姿勢編輯器加入手指 FK / IK 控制
## 背景與前提
專案是純瀏覽器單頁 3D 人體姿勢關鍵影格動畫編輯器(three.js,無建置工具),模型固定讀
three.js 官方範例 Xbot.glb,骨骼命名遵循 Mixamo 慣例(前綴可能是 `mixamorig`,程式用
`findBone(root, suffix)` 以 `name.endsWith(suffix)` 比對,所以不用管前綴)。
現有架構已經有一套完整、可重用的關節控制系統,新增任何功能前**先摸清楚這套系統**,
盡量重用而不是另起爐灶:
- `BONE_SUFFIXES`:`{ 內部key: "Mixamo骨骼名後綴" }`,是全系統唯一的骨骼註冊表
- `ALL_JOINT_KEYS = Object.keys(BONE_SUFFIXES)`:所有可控關節的迴圈依據
- `bones{}` / `restQuat{}` / `current{}` / `target{}`:四個以 key 對應的字典,FK角度資料的核心
- `markerMeshes{}`:關節球,3D場景裡可點擊,跟著骨骼世界座標更新
- `LABEL_LOOKUP{}`:key → 中文顯示名稱
- `selectJoint(key)` / `deselectJoint()`:FK關節選取,attach/detach `transformControls`
- `IK_CHAINS`:手臂/腿的兩節解析解IK設定(root/mid/end + poleOffset)
- `SPINE_IK_CHAIN`:脊椎的CCD多節IK設定(`bones`陣列可轉的鏈 + `effector`只讀位置的骨頭)
- `solveTwoBoneIK()`:兩節解析解求解器(手臂/腿用,需要pole球指定彎曲方向)
- `solveCCDChain(boneChain, effectorBone, targetPos, iterations, damping)`:多節CCD迭代求解器
(脊椎用,**不需要pole球**,因為迭代時每次用 cross product 動態算旋轉軸,
不依賴任何預先假設的局部旋轉軸方向)
- `selectedIK = { limb, role }`:IK目標球/極向球的選取狀態,`limb`用字串前綴分流不同種類
(例:"lookAt_head"、"spine"、"body"),這是後續要擴充新IK類型時該遵循的命名慣例
- `isIKDrivenKey(key)`:判斷某關節現在是否被IK接管,被接管的關節FK彈簧插值(`updateBones`)要跳過
- `allPickableMeshes()`:raycaster用來決定「畫面上哪些球現在可以被點到」的清單,
**任何新增的可點擊3D球都必須加進這裡,否則點擊沒有反應**(這是實作時真的踩過的坑)
## 任務一:手指 FK(基礎關節控制)
**目標**:讓 Xbot 的手指骨骼可以像其他身體關節一樣被選取、旋轉、記錄進拍點/JSON。
**做法**:
1. 用雙層迴圈產生手指骨骼定義,Mixamo標準命名是 `{Left|Right}Hand{Thumb|Index|Middle|Ring|Pinky}{1|2|3}`
(第4節是指尖端點骨,通常沒有實質旋轉意義,先不納入FK控制,但保留給IK當effector用,見任務三)。
用迴圈把這30個 key(左右手×5指×3節)`Object.assign` 進 `BONE_SUFFIXES`,
不要手動一條條寫死。
2. 同樣用迴圈把中文標籤(例:"右手食指2")批次寫進 `LABEL_LOOKUP`。
3. 因為 `ALL_JOINT_KEYS`、`bones{}`、`markerMeshes{}`、selectJoint、拍點快照、JSON匯出全部
都是通用迴圈跑 `ALL_JOINT_KEYS`,指骨key加進 `BONE_SUFFIXES` 之後這些全自動生效,
**不需要另外寫手指專屬的選取/旋轉/存檔邏輯**。
4. 手指骨節間距很小,原本身體關節球的半徑(約0.03)會讓相�- 鄰指節重疊難點選,
手指的 marker 球要用更小的半徑(約0.012)並換一個顏色跟身體關節區分。
**已知限制(不要嘗試在這個環境裡解決,如實告知使用者)**:
哪個局部軸(X/Y/Z)是手指「彎曲」方向,沒辦法在開發環境裡實際渲染驗證,只能標註成
「合理猜測」,需要使用者實機打開瀏覽器拖旋轉環試出來。**不要**自己猜一個軸就寫死
「握拳/張開」之類的角度預設,猜錯了會誤導使用者、產生詭異的手指姿態。
## 任務二:手指 FK 面板排版(可掃描性優化)
**問題**:30顆指節按鈕用同一種視覺權重平鋪成一排(flex-wrap),使用者要逐字讀按鈕文字
才能定位「哪根手指第幾節」,可掃描性差,操作頻率高時很痛苦。
**做法**:改成「一指一列、三節橫排」的表格式排版:
- 左右手分兩張卡片(`grid-template-columns: 1fr 1fr`,背景色塊+邊框跟其他區塊視覺切開)
- 每張卡片內,每根手指一個 `flex` row:左側固定寬度的手指名稱 label,
右側三顆等寬按鈕(文字用短標籤「根/中/末」,完整名稱放 `title` 屬性做 hover 提示)
- 窄螢幕(<640px)用 media query 把兩欄改單欄
這個排版改動只是把「同一層迴圈全部append進同一個row」改成「外層迴圈建立row,
內層迴圈把3個按鈕塞進該row」,選取邏輯完全不用動。
## 任務三:手指 IK(每指獨立CCD鏈)
**目標**:讓使用者不用逐節手動轉FK,直接拖曳指尖的一顆target球,整根手指自動彎曲收斂過去。
**關鍵設計決策(照抄脊椎IK的模式,不要照抄手臂/腿的兩節解析解模式)**:
手指跟脊椎同構——只有一個自然彎曲方向(往掌心收 / 往前彎),不像手臂需要pole球指定
「手肘要彎去哪個方向」這種歧義。所以:
- 每指一條3節CCD鏈:`{ bones:[根,中,末], tipSuffix:"...Hand{Finger}4", label }`,
用跟 `SPINE_IK_CHAIN` 一樣的形狀,但因為有左右手×5指共10條鏈,
要做成一個以 finger-id 為 key 的字典(例:"rThumb"、"lPinky"),不要複製貼上10次。
- effector優先用**指尖第4節骨**(Mixamo標準骨架通常都有,純粹讀位置、不參與旋轉),
這根骨頭**不要**加進 `BONE_SUFFIXES`/`ALL_JOINT_KEYS`(不開放FK控制,只在IK求解時讀位置)。
模型萬一沒有這根骨頭,`findBone`找不到時要優雅退回用第3節自己當effector,
只是精準度變差、不會整個壞掉,並且 `console.warn` 提示。
- 直接複用 `solveCCDChain()`,**不要**另外寫新的求解函式。迭代次數/damping可以比脊椎
(8輪/0.5)略小略快(例:6輪/0.6),因為手指鏈短、彎曲幅度通常不大。
- **不做pole球**——這是刻意的設計取捨,不是漏做。
**這個方案的重要優勢,實作時要意識到**:CCD是每次迭代動態算旋轉軸收斂到世界座標,
**完全不依賴任何預先假設的局部軸方向**,所以手指IK可以在「彎曲軸還沒驗證」的狀態下
就先做、且保證正確,不像FK的握拳/張開角度預設那樣會卡在未驗證的軸向假設上。
**整合進既有IK選取系統時要覆蓋到的所有分流點**(用 `"fingerIK_" + fingerId` 當
`selectedIK.limb` 前綴,跟現有的 `"lookAt_" + name` 命名風格一致):
1. `buildXxxIKMarkers()`:建立target球(小尺寸、跟手腳IK target球顏色區分),
`visible=false`初始,`userData.pickType`/`userData.fingerId`
2. **`allPickableMeshes()`——一定要把新球加進這個清單,這是最容易漏掉、漏了點擊完全沒反應的地方**
3. raycaster的pickType分派(`setupPickRaycaster`裡的if/else鏈)
4. `selectIKMarker(limb, role)`裡解析mesh的if/else鏈,加一個 `limb.startsWith("fingerIK_")` 分支
5. `highlightIKMarkers()`:選取時的縮放/變色回饋
6. `updateSelectedBar()`:頂部選取資訊列的標籤文字
7. 「重置此球位置」按鈕的 onclick 分流
8. `isIKDrivenKey()`:該指IK開啟時,FK彈簧插值(`updateBones`)要跳過那三節
9. 主迴圈 `animate()` 裡呼叫求解函式的位置(放在其他IK求解之後)
10. 開關函式(`setFingerIKEnabled`):開啟時同步target球到目前姿勢位置(避免瞬間跳動)、
隱藏該指FK關節球、關閉時如果正選取著要 `deselectJoint()`
**UI**:在任務二的每根手指列尾端加一顆小的「IK」切換鈕,開啟後該指3顆FK球暫時隱藏,
換成一顆可拖曳的target球。