Files
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

136 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M2 · 骨骼动画 + CPU 蒙皮
> 总纲:[`../PLAN.md`](../PLAN.md) §04 / §06 / §07。演示切片见 [`../DEMO-PLAN.md`](../DEMO-PLAN.md) D5(拉伸)。
> 本文件是 M2 的详细施工文档。
---
## 目标
解析 `.msm` / `.msa`,把 [M0](./M0-gr2-reader.md) 读出的动画曲线采样成世界姿势,CPU 线性混合蒙皮,
让 warrior 播 idle / walk / dance,结果与 [oracle](./00-oracle.md) 的**层①(裸 Granny)**逐帧数值一致。
装配一个多部件角色 + 挂点武器,验 LOD 一致性。
## 前置
- [M1](./M1-static-render.md) 完成:静态网格 + 贴图上屏。
- [M0](./M0-gr2-reader.md) T7`gr2_anim` 子集)+ scaleshear 用量已知。
- [oracle](./00-oracle.md) T3(层① dump+ T5(蒙皮顶点 dump)+ T8(噪声地板 → ε)。
## 交付物
| 产物 | 位置 |
|---|---|
| `engine/animation.{h,cpp}` 曲线采样 + 世界姿势累积 | `engine/` |
| `engine/skinning.{h,cpp}` CPU LBS | `engine/` |
| `formats/msa.cpp`+ 视需要 `msm.cpp` | `formats/` |
| `app/shaders/vs_pnt_skinned.sc` | `app/shaders/` |
| `tools/anim_bake/`(曲线退路,视 M0 结论决定是否要) | `tools/` |
| 数值对拍报告 | `test/m2-numeric.json` |
---
## 任务分解
### T1 · `formats/msa` + `msm` 解析 + 验证门
- `.msa`(文本):引用的动画 `.gr2`、混合参数(blend time / ease)、事件。逻辑照 `RaceManager.cpp` / `EterGrnLib/Util.cpp`
- `.msm`(文本,仅多部件时需要):base model gr2、挂点、材质类型。
- **验证门(PLAN §04 要求)**:dump 解析结果(挂点名、动作列表、混合参数),和源文本逐项目视核对。
- **完成判据**`dance_1.msa` 解析出的动画路径 + blend 参数与文本一致。
### T2 · `engine/animation` —— 曲线采样 + 世界姿势累积
- 按局部时钟 `t` 对每骨 position / orientation / scaleshear 曲线插值 → 局部 `granny_transform` → 沿父链累积 → `world[bone]`
- 对应 `GrannyBuildWorldPose` / `GrannyGetWorldPoseComposite4x4Array`
- ease-in/out 曲线、loop count、raw local clock 语义:照 `EterGrnLib/Motion*` + `ModelInstanceMotion.cpp` 调用序列复刻。
- **非单位 scaleshear**M0 若报告普遍存在,`world[bone]` 要保留完整仿射(4×4 或 4×3),不能退化成刚体。
- **完成判据**`gr2_anim` 子集覆盖的动画能采样出每帧世界矩阵。
### T3 · `engine/skinning` —— CPU LBS
- `skinMatrix[bone] = world[bone] · InverseWorld4x4[bone]`
- 逐顶点:`v' = Σ weight[i] · skinMatrix[boneIndex[i]] · v`4 权重),法线用 `skinMatrix` 的 3×3scaleshear 时要用逆转置)。
- `GrannyMeshIsRigid` 为真的网格不蒙皮,只按挂载骨骼刚体变换 —— 分支照搬。
- **完成判据**bind poseidentity 动画)下 CPU 蒙皮结果 == 原始顶点。
### T4 · `vs_pnt_skinned.sc`GPU 版,D5 演示用)
- `mat4 skin = u_bones[a_indices.x]*a_weight.x + u_bones[a_indices.y]*a_weight.y + ...`
- CPU 端 `rhi::setBones(skinMatrix[], boneCount)` 上传 uniform 数组。
- **注意**M2 的**数值门禁走 CPU 蒙皮**(可 dump 顶点对拍);GPU shader 只是演示。CPU/GPU 一致性放 [M4](./M4-realistic-load.md)。
### T5 · 多部件装配 + 挂点武器
- `.msm` → base gr2 + 额外 gr2(头发 / 时装);各自 mesh binding 到**同一骨架**。
- 武器:`GrannyFindBoneByName("Bip01 R Hand"(或对应名))` → 取该骨世界矩阵 × 武器局部挂点矩阵 → 武器 gr2 的 model 变换。
- **完成判据**:挂点武器的世界变换与 oracle 一致。
### T6 · LOD 一致性
- 加载同模型 LOD 03`warrior_cheongrin_lod_01/02/03.gr2`),确认它们绑**同一骨架**、骨骼索引一致。
- 在某帧切 LOD,测顶点位移(对应骨骼的顶点,切换前后位置差)。
- **完成判据**:切换帧顶点位移 < 阈值(无肉眼可见跳变)。
### T7 · 曲线退路(`tools/anim_bake`,条件性)
- **仅当** M0 fuzz 报告曲线子类型超出"1–2 种可实现"范围时启用。
- `anim_bake`Windows 侧用 oracle 把动画按固定帧率(如 60fps)烘焙成密集关键帧(每骨每帧一个 TRS),存自有格式。
- `libgr2` / `animation` 增加"读烘焙格式"分支,PoC 完全绕开曲线解码。
- **完成判据**:烘焙动画在 M2 数值对拍中照常通过。
---
## 门禁(分级,按序)
0. `.msm` / `.msa` 解析子检查通过(T1)。
1. **骨骼世界矩阵**与 oracle **层①(裸 Granny`GrannyGetWorldPoseComposite4x4Array` 直出)** 逐帧对拍,`max|Δ| < ε_mat`noise_floor × 余量)—— 隔离曲线采样 + 姿势累积,**先过**。
- **M2 只对层①**。层②(`ActorInstanceBlend` + LOD 骨骼裁剪)不在 PoC 范围,留正式移植。
2. **蒙皮顶点坐标**与 oracle 逐帧对拍,`‖Δ‖ < ε_vtx`(在矩阵已对上的前提下)—— 隔离蒙皮 + 顶点格式。
3. 挂点武器世界变换与 oracle 一致(T5)。
4. LOD 0–3 共享骨骼绑定,切换无跳变(T6)。
## 验证
- 数值层对拍:N 帧 × M 顶点、**全部测试资产集**,不是单文件。
- **必须按序**:先骨骼矩阵,再顶点。否则动画错 + 蒙皮错相互抵消、最终顶点却"对",掩盖两个 bug。
- **tie-break**libgr2 与 oracle 分歧又都合理时,用 Blender `io_scene_gr2` 在同 `t` 算的世界矩阵仲裁(也能抓 oracle 自己的 bug)。
- 视觉层:3 个确定姿势(idle 第 0 帧、走路中段、旋转量大的姿势),差异分类见 PLAN §07。
## 本步风险(从 PLAN §08 筛)
| 风险 | 状态 / 缓解 |
|---|---|
| Granny 曲线压缩格式多样 | **消除**M0 实测全 `OldCurveType`degree ≤ 2`gr2_anim` 全覆盖,无烘焙退路依赖 |
| ease/loop/局部时钟语义 | demo 用简单 loop`fmod(t, duration)`);ease / blend / accumulation 是正式移植照 `EterGrnLib/Motion*` 复刻,PoC 不需要 |
| 骨骼绑定顺序 / mesh binding 重映射 | `gr2::sample_pose` 按骨骼名 retarget181 warrior 动画 NaN=0);skin 矩阵按 `mesh.bone_bindings` → skeleton 索引 |
| rigid + deformable mesh 混合 | `engine/skinning.cpp``mesh.rigid` 分支(单骨刚体 vs 4 权重混合)|
| 非单位 scaleshear 让 `world·invBind` 近似失效 | 全程完整仿射 4x4`mul4x3` 保 3x3 + 平移);法线用 skin 3x3scaleshear 严格应逆转置,M2 先近似,记 M4 收紧)|
| ε 拍脑袋 | 待 oracle T8 的 noise_floor × 10 |
| ~~数值门禁未跑~~ | **已跑**macOS + Wine + MinGW 交叉编译的 `oracle.exe`Granny 2.9.12vs libgr2,骨骼矩阵 + 蒙皮顶点全 ≤ 6.5e-5(float 累积误差量级)。`oracle/RUNBOOK.md` / `tools/oracle_diff` |
## DoD 清单
- [x] `.msm`/`.msa` 解析 dump 与源文本核对通过(`formats/textscript` token 树 + `msa`/`msm`
- [x] **骨骼世界矩阵 vs oracle 层① `max|Δ| ≤ 4.6e-5`** —— `oracle/run-diff-suite.sh`**23 用例**warrior_cheongrin/lord ×4 LOD、assassin 双 root、shaman_lord **v7**、redthief2、snakeman、rabbit_backup、ox_01dance_1/attack/run/wait @ 多个 t)。`test/m2-numeric.json`
- [x] **蒙皮顶点 vs oracle `‖Δ‖ ≤ 6.5e-5`**(同上,vs `GrannyDeformVertices`;法线用同 3x3,Granny 也不做逆转置 —— `engine/skinning.cpp`
- [x] `test/noise_floor.json`:同-DLL 确定性 0,跨实现地板 mat 4.6e-5 / vtx 6.5e-5,ε=1e-3
- [x] **`app/render_fuzz` 全量渲染烟测**9166 个 `.gr2` 全过真管线(bgfx + `Scene::build` + DXT 解码 + CPU 蒙皮 + `submit`skinned 4055 / anim-only 5109 / rigid 2)——**0 崩溃 / 0 空场景 / 0 NaN 姿势**`test/render-fuzz.json`)。
- **顺带修的 libgr2 bug**`redthief_general/{back,front}_damage.gr2` 的 finger track 控制点在文件里就是 `NaN``read_old_curve` 现在遇到非有限 knot/control 整条曲线弃(回退 bind),不把 NaN 灌进蒙皮链。
- 抽样截图里「模型扭曲」的都是 **render_fuzz 把某物种的动画 retarget 到别的 rig**(如怪物动画播到 warrior 身上,或 warrior dance 播到怪物身上)—— 按名 retarget 到 bind pose 比例不同的骨架,肢体会拉长。**不是 libgr2/engine bug**(同 rig 的 warrior + dance_1/attack/run/wait 已对拍 Granny 1e-5)。render_fuzz 已改成只在 rig 匹配时才应用动画,其余画 bind pose。
- [~] 挂点武器变换 —— `gr2::sample_pose` 出每骨世界矩阵,武器 = `world[handBone]·mount`;未接进 demo(无 .msm 武器数据),逻辑就绪
- [x] LOD 03 共享骨架:`warrior_cheongrin` lod_01/02/03 与 base 均 75 骨、骨骼名 + ParentIndex 逐项一致 → 切 LOD 骨骼索引不变、无跳变
- [x] `xrender-demo``Space``dance_1`CPU LBS 每帧更新 dynamic VB,姿势连贯(`test/golden/m2-*.png`
- [ ] (条件)曲线退路 `anim_bake` —— **不需要**M0 实测曲线全 `OldCurveType` degree ≤ 2`gr2_anim` 全覆盖
## M2 实现结果
| 项 | 结果 |
|---|---|
| T1 `.msa`/`.msm` | `formats/textscript.cpp` 通用 token 树 + `msa.cpp`/`msm.cpp``dance_1.msa`→duration 28.333334 / accum 0`throw.msa`→1 event(type10, t=0.824)`warrior_w.msm`→base + hair_path + 75 hairs。dump 与源文本逐项一致。 |
| T2 世界姿势 | `gr2::sample_pose(skeleton, animation, t, world, skin)`libgr2,跨文件:model gr2 的 skeleton + anim gr2 的 tracks,按骨骼名 retarget)。`world[i]=Composite(local[i])·(parent<0?InitialPlacement:world[parent])``skin[i]=InverseWorld4x4[i]·world[i]`。bind pose 时 `skin ≈ I`6.1e-5)。 |
| T3 CPU LBS | `engine/skinning.cpp``v'=Σ wᵢ·skin[bᵢ]·v`4 权重归一),rigid mesh 走单骨刚体分支。`skin_bind_pose_residual` 自检 = 0(skin 全单位 → 输出 == 输入)。 |
| T4 GPU 蒙皮 shader | 跳过:M2 数值门禁走 CPU(可 dump 对拍),demo 也用 CPU + dynamic VB。`vs_pnt_skinned.sc` 留到 M4 CPU/GPU 一致性。 |
| 无 oracle 抽检 | 全部 181 个 `pc/warrior/**.gr2`:139 个带动画,各在 t=0/⅓/⅔/1 采样 —— **NaN=0**56 个 track 全名匹配 skeleton(其余部分匹配,未匹配的骨退回 bind,安全)。 |
| demo 用法 | `XR_ANIM=<anim.gr2\|.msa>` `XR_ANIM_T=<0..1\|秒>` + 交互 `Space` 播放/暂停。 |