交接:現況、卡在哪、下一步

這份每輪重寫,不追加。 歷史過程一律在 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*.jsondriven_*.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 自動化擱置);進去之後全部可自動跑。

  1. 大腿_ast 錄進同一個 run(agent.bakeSwing 走 Transform 階層找骨, 加幾個骨名即可)。用途:釘死 QuartzDriver 參考骨的延遲 —— 直接比對說 lag=1、下游看說 lag=0,兩個量測不一致(§41)。
  2. 若要做風:確認場景的風向/風強在 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

在 SiaoHub 檢視原始檔