ClaudeDocs/HANDOFF.md
交接:現況、卡在哪、下一步
這份每輪重寫,不追加。 歷史過程一律在 RE_NOTES_*.md,
本檔只描述「現在是什麼狀態」。狀態聲明只出現在這份文件裡。
最後一次核對:2026-08-10(GLB 內容、檔案存在性、行數都是本輪實際量的)。
0. 一句話狀態
資產、動畫、臉部、頭髮、相機、Live2D 全部可交付;
彈簧骨物理是唯一還沒解完的東西(誤差 110%,仍略差於「完全不做」的 100%),
症狀收斂到單一項:過擺 1.50 倍;
卡通渲染解到一半(_VCMASK / ramp 已解,描邊 pass 與 stencil 未解)。
1. 交付物的界線
<輸出>/hololive_549_final.glb(229 MB,本輪實際開檔核對:
569 個 animation、1 個 camera 節點):
可信 549 個角色動作(逐格烘焙)
可信 20 個相機動作(§44,手算對照組驗證,視線誤差 5.55e-17)
可信 眼/眉/虹膜 morph(誤差 1.6e-06)
可信 頭髮網格與掛點
不可信 彈簧骨動作(裙擺/頭髮/緞帶)—— 看起來合理,實測 110%
沒有 嘴型(glTF 核心不支援可動畫的 UV 位移;資料另出 _avatar/facial_decal.json)
沒有 卡通著色(材質是 PBR 近似)
Live2D 側:motion3.json 已改用 Cubism 三次 Bezier 恆等轉換,
最大誤差 1.04e-05(§43,改動前是 5.773)。
產出狀態的區分
| 狀態 | |
|---|---|
| 寫出來 | 全部 |
| 已 commit | 全部(工作區只剩未追蹤的 _avatar/*.json 中間產物與 skill 目錄改名) |
| 已推遠端 | 未確認 —— 本輪沒查 remote |
| GLB 已更新 | 是(本輪開檔確認 569 animations / 1 camera) |
工作區未追蹤的 _avatar/baked*.json、driven_*.json 是中間產物(最大 1.5 GB),
不進版控;needs_rebake.txt 同理。
2. 卡點:彈簧骨過擺 1.50 倍
2.1 現況數字(預設參數)
110% 誤差 / 真值動作幅度 (100% = 彈簧骨完全不動)
55.9° 軸夾角中位數 (§38 之前是 88.6°,幾乎正交)
1.50x 過擺倍率 <- 唯一還沒被任何機制動過的症狀
k=0 對齊掃描排名 2/90 <- 模擬帶有正確的時間資訊
這組數字目前重現不出來(§47)。 沒有任何地方記錄它是用哪個 base GLB + 哪套服裝 swing JSON 跑出來的,而手上的模型與
swing_truth.json對不上 (真值偏離靜止 29.04°,文件寫的是 6~16°;裙擺骨出現 155° 的不可能姿勢)。在票 09 找回輸入組合之前,不要拿新的量測結果跟這組數字比。 §36~§43 的結論都建立在它上面,那些結論本身沒有被推翻, 但「再量一次來驗證」這條路目前是斷的。
量測請用
scripts/measure_swing.py(§47.1),它會把輸入來源印在數字上面。
不要只看誤差百分比。 §37 證明舊的 142% 落在「時間亂序帶」內, 分辨不出模擬與它自己的隨機平移版。四支工具要一起看:
scripts/diag_swing_trace.py 單根骨逐尤拉分量 + 平移對照組 <- 解析度最高
scripts/diag_swing_align.py k=0 是否為最佳位移 <- 有沒有時間資訊
scripts/diag_swing_error.py 軸夾角(雜訊底線 ±13°)
compare_swing.py 誤差% + 過擺倍率
一個機制若讓 k=0 掉出最佳,誤差百分比再好看都是退步。
2.2 ProcessDynamicBoneLayer 已經沒有未實作的子系統
積分器、limitInfo、骨長約束、spring、根部修正、碰撞、
QuartzDriverSkirtBone(§41)、環狀鏈骨平滑(§42)—— 全部實作完了。
預設含 §42 的鏈平滑(--no-smoothing 可關)。
2.3 已排除的假設(都量過,不是推的)
| 假設 | 章節 | 結果 |
|---|---|---|
[x20+0x54] 角速度累加器 |
§36/§38 | 過擺修到 1.00x 但破壞時間資訊,不是這樣接的 |
| 重力主導 | §37/§38 | 夾過限制後重力沒有影響 |
| 綁定 local 旋轉 | §37 | 軸錯最兇的骨綁定旋轉是 0.0° |
| 逐格對齊錯位 | §37/§41 | 無 k 大幅勝出 |
| 座標約定(四元數鏡射) | §36 | mirrorX 正確 |
swingPowerWeight / bakeAnimationWeight |
§35 | 前者實測 1.0,後者 44 條曲線全 0 |
| 碰撞 | §34/§38 | 實作了,誤差變差 |
_ast / QuartzDriver 驅動 |
§41 | 解完且雙路驗證,但不是剩餘誤差的原因 |
| 鏈骨平滑 | §42 | 實作完成、拓撲確實在作用,不是那個缺失的阻尼 |
| 子步數 | §43 | 1/2/3/4 都試過,不是單調關係 |
2.4 只剩兩個候選
1. damping 的第二個用途
目前只進 Verlet 慣性項 (1-damping)²。重讀 0x02792f20 附近,
確認 damping 有沒有被用在別的地方。
--vel 能把過擺壓到 1.00x 但破壞時間資訊(§38)=> 阻尼不是「力進速度」的形式。
2. 風(目前最有希望)—— **機制已於 §48 解出**
ActorAnimationWindUtility::CalcWindPower @ 0x0279DEF0
(0x02793194 是呼叫點,不是函式本體)
wind = base
+ 振幅 × sin(2π × (頻率 × time + 相位))
+ windPower × (noise(pos × 空間頻率 × time) × 0.5 + 0.5)
兩個 Single 是 time 與 windPowerScale。**time 同時是 noise 的座標縮放
與 sin 的相位** —— 所以風是帶相位的週期驅動源,不是常力。
這是唯一在形狀上能同時解釋兩個症狀的機制:過擺 1.50x(能量太多)
卻 k=0 仍是最佳(時間資訊正確)—— 純阻尼解釋不了後者。
尚未確認:time 的來源(Time.time 還是動畫本地時間,loop clip 上差很多)、
0x40 之後第二組欄位、以及 run00 當時場景到底有沒有風(T1)。
2.5 兩個「忠於遊戲但讓數字變差」的旗標
--delta-current(§39)與 --quartz(§41)都忠於二進位、都讓百分比變差,
原因相同:下游缺阻尼,所以正確的輸入反而被懲罰。兩者目前預設關閉。
阻尼補上之後應該一起翻成預設,判準用 §39 的分組表與 §37 的對齊掃描,
不是總體百分比。
3. 需要跑遊戲才能做的事(累積中,下次進遊戲一起做)
進園區那兩下是人工點的(§19 自動化擱置);進去之後全部可自動跑。
- 把大腿與
_ast錄進同一個 run(agent.bakeSwing走 Transform 階層找骨, 加幾個骨名即可)。用途:釘死 QuartzDriver 參考骨的延遲 —— 直接比對說 lag=1、下游看說 lag=0,兩個量測不一致(§41)。 - 若要做風:確認場景的風向/風強在 run00 錄製當時是什麼值。
4. 現行資料版本
_extracted_motions_3d_v2/ 重新解過的 clip 曲線(含三次係數)← 用這份,不要用舊的
_avatar/swing/ 279 套服裝的身體彈簧骨參數(本輪核對:279 個檔)
_avatar/swing_hair/ 頭髮彈簧骨參數(3 個檔)
_avatar/swing_truth.json 遊戲真值(run00,90 格 × 117 骨,789 KB)
_avatar/facial_decal.json 嘴型切換清單
_avatar/shader/ shader 產物(gitignore,版權素材)
<輸出>/hololive_549_final.glb 229 MB 成品
5. 這個專案反覆踩到的坑
| 坑 | 教訓 |
|---|---|
| 「資料缺失」其實是查錯表 | 同一批 PathID 有三種指涉(Transform / GameObject / MonoBehaviour),查錯只會安靜回 "?"。§40 與 §42 各絆一次 |
| 用彙總中位數判斷機制有沒有效 | 局部 76° 的改變會被吃掉。要比就比兩次輸出本身 |
| 數字沒動就編解釋 | 「鏈太短所以沒作用」是編出來的,不是量出來的 |
| 用統計反推資產已宣告的事實 | alphaMode 靠貼圖直方圖猜 → 三個材質共用貼圖全判錯 |
| 效應小於雜訊還挑最小值 | 有雜訊時「N 選 1」必然回傳贏家,而贏家不是證據 |
| 筆記寫對不代表實作正確 | boneAxis 用了骨自己的位移;尤拉反函數符號寫錯 |
貫穿全部:「二進位讀對」不等於「模型正確」。 逆向可以逐條驗證,整合不行 —— 整合只能靠 ground truth 量。
6. 借別人筆記的方式(mos9527 的 PJSK 筆記,§24/§25)
來源:https://mos9527.com/posts/pjsk/archive-20240105/
借方法,不借結論。 他把貼圖每個通道當獨立訊號去問語意 —— 這個方法讓我們
回頭解出 _VCMASK(原本 README 寫「那是 shader 遮罩,Blender 會忽略」=承認沒解)。
但他那邊頂點色 R = 描邊遮罩,我們這邊是每通道兩個 4-bit、共 8 個參數,
其中一個是寫進 render target alpha 的材質 ID —— 完全不同的約定。
最有價值的不是他解出的東西,而是讀他的筆記時注意到自己缺什麼:
他的驗收是目視比對,我們在數值層面嚴格得多,卻在渲染那層完全沒有驗收 ——
alphaMode 的 bug 能活到使用者截圖來問就是這個缺口。
這個觀察直接促成 render_sheet.py,以及後來的
bake_swing_truth.py + compare_swing.py。