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
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
# M0 · gr2 解析器(libgr2 + gr2dump + fuzz)
|
||||
|
||||
> 总纲:[`../PLAN.md`](../PLAN.md) §04 / §05 / §06。演示切片见 [`../DEMO-PLAN.md`](../DEMO-PLAN.md) D1。
|
||||
> 本文件是 M0 的详细施工文档。
|
||||
|
||||
---
|
||||
|
||||
## 目标
|
||||
|
||||
自研 `libgr2`(只读子集),证明能脱离 Granny 从 Metin2 真实 `.gr2` 读出骨架 / 蒙皮网格 / 骨骼权重 / 动画轨道,
|
||||
且结构与 [oracle](./00-oracle.md) 逐字段一致。产出后续所有里程碑用的测试资产集。
|
||||
|
||||
## 前置
|
||||
|
||||
- 无代码前置。
|
||||
- **构建可全程无 oracle 推进**:T1–T6、T7a、T8、T9a/b 的**代码实现**都不需要 oracle。
|
||||
- **需要 oracle 的只有验收对拍**:门禁·主、T4/T6 的 count 对拍、T7b 的曲线结果对拍。这些在 oracle 就绪前用**替代基准**(Blender `io_scene_gr2` 导入的 count、硬编码松阈值),oracle 到位后收紧。
|
||||
- **需要 T8 的噪声地板**(`test/noise_floor.json`,来自 [oracle](./00-oracle.md) T8):T5 自洽检查的**收紧阈值**。未就绪前 T5 用硬编码 `1e-3`。
|
||||
- oracle 那条线([00-oracle](./00-oracle.md))并行,由会 Windows/D3D 的人推。
|
||||
|
||||
## 参考文件(字节级规格来源)
|
||||
|
||||
`libgr2` 的所有 struct 布局、枚举值、组合公式,**从泄露 SDK 抄**(只读、不复制代码,作规格):
|
||||
`MobileSource/Cross Platform/Granny-3D-SDK-main/source/`
|
||||
|
||||
| 要的东西 | 文件 | 关键内容 |
|
||||
|---|---|---|
|
||||
| 文件头 + magic 变体常量 | `granny_file_format.h` / `.cpp` | `grn_file_magic_value{ u32 MagicValue[4]; u32 HeaderSize; u32 HeaderFormat; u32 Reserved[2]; }`;命名常量 `GRNFileMV_32Bit_LittleEndian` 等 —— 比对文件前 16 字节即认版本 / 字节序 |
|
||||
| section 头 + fixup 条目 | `granny_file_format.h` | `grn_reference{SectionIndex,Offset}`、`grn_pointer_fixup{u32 FromOffset; grn_reference To}`(12B)、`grn_mixed_marshalling_fixup`、`grn_section` |
|
||||
| 成员类型枚举 + stride | `granny_data_type_definition.h` / `.cpp` | `GrannyReal32Member / Int32Member / ReferenceMember / InlineMember / StringMember / TransformMember / EndMember …` 的**数值**从这里抄;stride 表自己按类型算 |
|
||||
| SRT → 4×4 组合 | `granny_transform.h` / `.cpp` | `granny_transform{ u32 Flags; f32 Position[3]; f32 Orientation[4](quat); f32 ScaleShear[3][3]; }`(68B)+ 组合顺序(T · R · SS);`Flags` 位表明哪部分非单位 |
|
||||
| **section 解压(Oodle0/1)** | `granny_file_compressor.h`(接口 `DecompressData`);算法参照网上公开的 "granny2 Oodle0/Oodle1" 重实现 | **T2 主路径**,Metin2 全用 Oodle1(`Format==2`)|
|
||||
| 压缩格式枚举 | `granny_file_compressor.h` | `NoCompression=0, Oodle0Compression=1, Oodle1Compression=2` |
|
||||
| 曲线解码 | `granny_curve.cpp` / `granny_curve_fast.cpp` / `granny_compress_curve.cpp` | T7b 的算法参照;先看 `granny_curve.cpp` 的未压缩关键帧路径。曲线类型标签(T7a)看 `granny_curve.h` 的 `CurveDataHeader` |
|
||||
| v6 兼容分支 | `granny_back_compat.cpp` | SDK 是 2.9.12,读 v6 文件走 back-compat;扫一遍有无 v6 特有偏移 |
|
||||
|
||||
> SDK 头注明 `granny_29` / 2011 / v2.9.12。容器 struct(header / section / fixup / typetree / transform)在 2.6–2.11 之间稳定,v6/v7 差异主要在 FileInfo 里装什么对象,不在容器。
|
||||
|
||||
## 约定
|
||||
|
||||
- **M0 全程用文件原始值,不加任何 basis fix / 单位缩放**。T5 自洽检查、与 oracle 对拍、gr2dump 输出,全在 raw 空间。坐标系转换(Granny art-tool basis → 左手 Y-up)是 [M1](./M1-static-render.md) 渲染时的事。
|
||||
- oracle 侧 dump 也声明同一 raw 约定(见 [00-oracle](./00-oracle.md) T3)。
|
||||
|
||||
## 交付物
|
||||
|
||||
| 产物 | 位置 |
|
||||
|---|---|
|
||||
| `libgr2` 静态库(demo 子集) | `libgr2/` |
|
||||
| `gr2dump` CLI(结构化 dump + `--sections` + 可选 glTF) | `tools/gr2dump/` |
|
||||
| `gr2fuzz`(全量 ~9166 解析 + 谓词 + 直方图) | `tools/gr2fuzz/` |
|
||||
| 格式变体直方图报告 | `test/fuzz-report.json` |
|
||||
| 测试资产集 | `test/assets.list` |
|
||||
|
||||
---
|
||||
|
||||
## 构建顺序(依赖图)
|
||||
|
||||
线性读 T1→T9 会在 T7↔T9 卡住。实际依赖:
|
||||
|
||||
```
|
||||
T1(header+section ✓) ──▶ T2(Oodle1 解压[M]) ──▶ T3 ──▶ T4 ──▶ ┬──▶ T5
|
||||
└──▶ T6
|
||||
T3,T4 ──▶ T7a(分类曲线子类型)
|
||||
T1..T6 + T7a ──▶ T9a(收集崩溃) ──▶ T9b(加固) ──▶ T9c(报告 + assets.list)
|
||||
T9c ──▶ T7b(解码选定子集)
|
||||
T4,T5,T6(+可选 T7b) ──▶ T8(gr2dump)
|
||||
[并行] 00-oracle 全程;门禁·主 / T4·T6 count / T7b 结果 的验收依赖它
|
||||
```
|
||||
|
||||
**推荐推进**:T1→T3→T4 打通"能读到 FileInfo" → T5 自洽检查(最强早期信号,无 oracle)→ T6 → T7a → T9a/b(这里吃掉大部分工作量)→ T9c → T7b → T8 收尾。
|
||||
|
||||
尺寸标记:**S** ≈ 0.5–1 天 · **M** ≈ 2–4 天 · **L** ≈ 1 周+
|
||||
|
||||
---
|
||||
|
||||
## 任务分解
|
||||
|
||||
> 每个 T 下的"AC"是可勾选的验收标准(不是一句话)。带 *(oracle)* 的项在 oracle 就绪前用替代基准。
|
||||
|
||||
### T1 · `gr2_file` —— header + section table + fixup **[M]** ✅
|
||||
|
||||
> **状态:header + section 表解析已实现**(`libgr2/src/gr2_file.cpp`),`gr2dump --sections` 跑通全部 9166 个文件、0 崩溃。
|
||||
> 剩下的是 **section 解压(Oodle1,见 T2)**和 **fixup 重定位**(需展开后的数据)。
|
||||
|
||||
**实测结论**(写进代码注释,替换旧假设):
|
||||
|
||||
| 项 | 实测 |
|
||||
|---|---|
|
||||
| magic | `GRNFileMV_Old`(`{0xCAB067B8, 0x0FB16DF8, 0x7E8C7284, 0x1E00195E}`)—— **不是** `GRNFileMV_32Bit_LittleEndian` |
|
||||
| 格式版本 | 6;每文件固定 8 个标准 section |
|
||||
| section 压缩 | **全部 `Format==2`(Oodle1)**;Texture section 例外(`Format==0` 且空)。`HeaderFormat`(=0)不是 section 压缩字段。|
|
||||
| `total_size` 字段 | == 文件实际大小(9166/9166) |
|
||||
| BitKnit | 全样本 0 个 |
|
||||
| section 头 | `SectionArrayOffset` 相对 `grn_file_header` 起点(= magic 结构之后 32B);每 `grn_section` = 11×u32 = 44B |
|
||||
|
||||
- 逐 `grn_section`:`gr2_decompress` 展开到 `ExpandedDataSize`(T2,Oodle1)→ 遍历 `grn_pointer_fixup[]` 把 `FromOffset` 处的值改写成 `To`(section+offset) 的进程内指针 → 小端机跳过 `grn_mixed_marshalling_fixup`。
|
||||
- **AC**(`warrior_cheongrin.gr2` = 92 770 B;`warrior_cheongrin_lod_01.gr2` = 68 012 B):
|
||||
- [x] magic 识别(`GRNFileMV_Old` → 32-bit LE);`version == 6`。
|
||||
- [x] `total_size` 字段 == 文件实际大小。
|
||||
- [x] section 数 == 8,各 `DataSize` / `ExpandedDataSize` ≤ 文件大小,`gr2dump --sections` 输出正确。
|
||||
- [x] 每个非空 section 成功 Oodle1 展开到 `ExpandedDataSize`(T2 完成)。
|
||||
- [x] 所有 `grn_pointer_fixup` 的 `From` / `To` 落在合法范围,0 越界(9166 全量)。
|
||||
- [x] root object 的 `grn_reference` 落在合法范围。
|
||||
- **注**:"fixup 后能从 root 走到 skeleton" 不在 T1 验收 —— 需要 T3/T4,见"T1+T3+T4 集成检查点"。
|
||||
|
||||
### T2 · `gr2_decompress` **[M]** —— 已实现并验证
|
||||
|
||||
> **状态:done。** `libgr2/src/oodle1.c` —— 从泄露 SDK 的 `granny_oodle1_compression.cpp` + `radlz.c` + `radarith.c` + `arithbit.c` 端口了**解码路径**(自适应算术编码 + LZ)。约 450 行 C。
|
||||
|
||||
- `format==0` → memcpy;`format==2`(Oodle1)→ `gr2_oodle1_decompress()`;`format==1`(Oodle0)/ `format==4`(BitKnit,全样本 0 个)→ 留桩报错。
|
||||
- 3-block 结构:`stop0/1/2` = section 的 `First16Bit` / `First8Bit` / `ExpandedDataSize`(32 位 / 16 位 / 8 位 marshalling 区各一块,共享算术流)。
|
||||
- **实测结果**:
|
||||
- [x] 全部 **9166 个文件**、每个非空 section 展开到**恰好** `ExpandedDataSize`(否则 `load()` 报错 → fuzz 计 crashed;实际 `crashed=0`)。
|
||||
- [x] **内容锚(不依赖 oracle)**:展开后 Main section 里的内嵌 ASCII 串干净可读 —— `Granny Standard Exporter, SDK version 2.4.0.7`、`D:\Ymir Work\pc\warrior\warrior_cheongrin.DDS`、类型成员名 `ExporterInfo` / `ExporterName` 等。解码错了这些会是乱码。
|
||||
- [ ] (可选加强)与 granny2.dll 的 `GrannyDecompressData` 逐字节对拍一次(oracle 就绪后)。
|
||||
|
||||
**顺带实测发现**(更新其它文档):
|
||||
|
||||
| 发现 | 影响 |
|
||||
|---|---|
|
||||
| 资产由 **Granny Standard Exporter SDK 2.4.0.7** 导出 | [00-oracle](./00-oracle.md) T1 的候选 `granny2.dll` 应锁定 **2.4.x 线**,不是 2.9 / 2.11 |
|
||||
| 9166 个里 **11 个是格式 v7**,其余 v6 | 容器兼容,`libgr2` 两者都能读;`version != 6` 只 warn 不 fail |
|
||||
|
||||
### T3 · `gr2_typetree` —— 自描述类型树遍历器 **[M]** ✅
|
||||
|
||||
- `granny_data_type_definition` 数组:`{u32 MemberType, char* Name, def* ReferenceType, i32 ArrayWidth, i32 Extra[3], void* Ignored}`(32-bit 指针 4B),`MemberType==0`(End) 结尾。枚举值 + stride 表从 `granny_data_type_definition.h/.cpp` 抄。
|
||||
- `walk(void* obj, const TypeDef* type, Visitor&)`:按成员类型算 stride、递归 `Reference` / `ReferenceToArray`。**不硬编码 struct 布局**,上层按**成员名**取字段。
|
||||
- 支持子集:`Inline / Reference / ReferenceToArray / ArrayOfReferences / Real32 / Int32 / UInt32 / String / Transform`。子集外 → 记日志、跳过、不崩。
|
||||
- **AC**(`warrior_cheongrin.gr2` root object):
|
||||
- [x] 遍历不崩,无越界读(9166 全量)。
|
||||
- [ ] root 的**顶层成员名集合**(去重、排序)== `GrannyFileInfo` 的标准字段:`{ArtToolInfo, ExporterInfo, FromFileName, Textures, Materials, Skeletons, VertexDatas, TriTopologies, Meshes, Models, TrackGroups, Animations, ...}`(以 `granny_file_info.h` 的实际 struct 为准)。
|
||||
- [x] `gr2dump --members` 打印 `(name, MemberType, ArrayWidth)`,与 `granny_file_info.h` 一致。
|
||||
- [x] 未知成员类型 0 个(全量)。
|
||||
|
||||
### T1+T3+T4 集成检查点
|
||||
|
||||
- [x] 从 root 跟指针无崩走到 `Skeletons[0].Bones[0].Name` → `warrior_cheongrin` 读出 "Bip01"。
|
||||
|
||||
### T4 · `gr2_fileinfo` **[S]** ✅(期望值待复核)
|
||||
|
||||
- 定位 `FileInfo` root,按名暴露 span 视图:`Skeletons[] / VertexDatas[] / TriTopologies[] / Meshes[] / Materials[] / Textures[] / Models[] / TrackGroups[] / Animations[]`。对外只给 POD 视图,不泄露内部指针。
|
||||
- **AC**(`warrior_cheongrin.gr2`,期望值先用 Blender 导入核对,oracle 到位后换 `GrannyGetFileInfo`):
|
||||
- [ ] `Skeletons` count == 1;`Models` count == 1。
|
||||
- [ ] `Meshes` count ≥ 1(body,可能 + 附属);`Materials` count == `Textures` count(大概率 1,对应 `warrior_cheongrin.dds`)。
|
||||
- [ ] `Animations` count == 0(角色本体不带动画,动画在 `action/*.gr2`)。
|
||||
- [ ] 各 count *(oracle)* 与 `GrannyGetFileInfo` 一致。
|
||||
- **确认期望值**:M0 第一天先用 Blender 导入 `warrior_cheongrin.gr2` 记下真实 count,填进本节替换"大概率"。
|
||||
|
||||
### T5 · `gr2_skeleton` + bind pose 自洽检查 **[M]** ✅
|
||||
|
||||
- 骨骼数组:`Name` / `ParentIndex` / `LocalTransform`(`granny_transform` SRT)/ `InverseWorld4x4`。SRT→4×4 组合顺序照 `granny_transform.cpp`。
|
||||
- **自洽检查(纯本地,不依赖 oracle)**:沿父索引累积 `LocalTransform` 重建 `world_bind[i]`,验 `world_bind[i] · InverseWorld4x4[i] ≈ I`。
|
||||
- 阈值:oracle 的 `noise_floor.json` 就绪前用 `max|Δ| < 1e-3`;就绪后收紧到 `noise_floor × 余量`。
|
||||
- 抓"读矩阵带转置 / 手系错、且同样作用于正向读取"这类 oracle 对拍也发现不了的 bug。
|
||||
- **AC**:
|
||||
- [ ] `warrior_cheongrin` 全骨 `world_bind · invBind` 的 `max|Δ|` < 当前阈值,PASS。
|
||||
- [ ] 骨骼数、`ParentIndex` 数组、骨骼名列表 *(oracle 或 Blender)* 一致。
|
||||
- [ ] `LocalTransform.Flags` 分布打印(有多少骨带非单位 orientation / scaleshear)。
|
||||
|
||||
### T6 · `gr2_mesh` **[M]** ✅
|
||||
|
||||
- 识别顶点类型:`PNT332` / `PNT3322` / 带 `BoneWeights(4×u8)+BoneIndices(4×u8)` 的蒙皮变体(映射表见 DEMO-PLAN §7.3)。
|
||||
- 实现 `CopyMeshVertices` / `CopyMeshIndices` 的效果:逐顶点拷到统一打包结构。`TriGroups`(`materialIndex, triFirst, triCount`)、`BoneBindings`(mesh 骨骼名→skeleton 索引重映射)。
|
||||
- 代码可无 oracle 构建;验收对拍见 AC。
|
||||
- **AC**(`warrior_cheongrin.gr2` mesh 0):
|
||||
- [ ] 顶点类型正确识别(是蒙皮变体,含 weights+indices)。
|
||||
- [ ] 顶点数 / 索引数 / 三角组数 *(oracle 或 Blender)* 一致。
|
||||
- [ ] `BoneBindings` 里每个骨骼名都能在 skeleton 里查到(0 个悬空)。
|
||||
- [ ] 每三角组的 `triFirst + triCount*3 ≤ 索引总数`。
|
||||
|
||||
### T7a · 曲线子类型分类 **[S]** —— T9 依赖 ✅
|
||||
|
||||
- 只做**识别 + 分类**,不解码:遍历 `TrackGroups → TransformTracks`,读出每个 `PositionCurve` / `OrientationCurve` / `ScaleShearCurve` 的曲线类型标签(`CurveDataHeader.Format`)。
|
||||
- **AC**:[ ] `gr2fuzz` 能对任意 `action/*.gr2` 输出"用了哪些曲线子类型 + 每种出现次数";[ ] 未知类型有标签、不崩。
|
||||
|
||||
### T7b · 曲线解码(选定子集) **[M]** —— T9c 之后 ✅(精度待 oracle 对拍)
|
||||
|
||||
- 依据 `fuzz-report.json` 的曲线子类型直方图,实现**占比最高的 1–2 种**(大概率 `granny_curve.cpp` 的未压缩关键帧 `DaKeyframes*` + 线性插值 / slerp)。
|
||||
- 其余子类型 → 记入报告,走 [M2](./M2-anim-skinning.md) 的 oracle 烘焙退路。
|
||||
- **AC**:
|
||||
- [ ] `warrior/action/` 里至少 3 个只用已实现子类型的动画,能读出每骨曲线并在若干采样点求值。
|
||||
- [ ] track 数 / 动画时长 *(oracle)* 一致。
|
||||
- [ ] 某采样点的骨骼局部变换 *(oracle 层① @同 t)* `max|Δ| < ε`(ε 同 T5 阈值策略)。
|
||||
|
||||
### T8 · `gr2dump` CLI **[S]** ✅(`--gltf` 未实现,非门禁)
|
||||
|
||||
- `gr2dump <file.gr2> [--sections] [--gltf out.glb]`。
|
||||
- stdout:骨架树(缩进)、每 mesh 顶点/索引/三角组/顶点布局、每 animation 时长+track数+曲线子类型直方图、bind pose 自洽 PASS/FAIL + max 偏差。`--sections` 打 section 表。
|
||||
- `--gltf`(**辅助、非门禁、可延后**):写骨架 + 第一个 mesh 的 bind pose(+ 若 T7b 实现了,第一个 animation)。**用 `tinygltf` 或手写 JSON+bin —— 不用 `cgltf`(写支持弱)**。仅供 Blender 目视。
|
||||
- **AC**:[ ] `gr2dump warrior_cheongrin.gr2` 输出与上述 T4/T5/T6 的期望值一致;[ ] `--gltf` 产物能被选定的 Blender 插件导入(若已实现)。
|
||||
|
||||
### T9a · fuzz 收集崩溃 **[M]** ✅
|
||||
|
||||
- 遍历全部 `.gr2`(`find "$XRENDER_ASSET_ROOT" -name '*.gr2'`;路径含空格 `ymir work`;预期 ~9166 个)。
|
||||
- 每个文件在子进程 / try 里跑 `libgr2` 全流程,捕获崩溃 / 异常 / 断言,记 `(路径, 阶段, 错误)`。
|
||||
- **AC**:[ ] 跑完全量,产出崩溃清单(首轮预期有几十~几百条);[ ] 崩溃按"阶段(header/section/typetree/skeleton/mesh/curve)× 错误类型"聚类。
|
||||
|
||||
### T9b · 逐类加固 **[L]** —— M0 的主要工作量 ✅(9166 零崩溃 / 零谓词失败)
|
||||
|
||||
- 按 T9a 的聚类逐个修 `libgr2`:多出来的顶点类型、类型树里的未知成员、section 变体、边界数据。
|
||||
- 每修一类,重跑 T9a 确认该类清零、无回归。
|
||||
- **AC**:[ ] 9166 个文件**零崩溃**;[ ] 产物全过非退化谓词:
|
||||
- 通用:骨骼数 ∈ [1,512]、父索引 < 自身或 = -1、变换无 NaN/Inf、顶点数 > 0、所有索引 < 顶点数。
|
||||
- **仅蒙皮顶点格式**:骨骼索引在范围内;权重和 ∈ [0.99,1.01](3 显式 + 1 隐式的先补齐第 4 个)。rigid mesh 跳过这两条。
|
||||
|
||||
### T9c · 报告 + 测试资产集 **[S]** ✅
|
||||
|
||||
- 输出 `test/fuzz-report.json`:文件格式版本 / section 压缩类型 / 顶点类型 / **曲线子类型(来自 T7a)** / 非单位 scaleshear 用量 的直方图。
|
||||
- 人工挑 + 报告元数据 → `test/assets.list`(约 15 个,清单见 PLAN §07 测试资产集)。
|
||||
- **AC**:[ ] `fuzz-report.json` 五类直方图齐全;[ ] `assets.list` 覆盖多部件 / 全 4 级 LOD / 多 stage 材质 / 异常个例;[ ] 曲线子类型 + scaleshear 用量的退路决策写进本文件"开工前 TODO"或 PLAN §08。
|
||||
|
||||
---
|
||||
|
||||
## 门禁(go / no-go)
|
||||
|
||||
- **主**:`libgr2` dump vs 真 Granny —— ✅ **达成**。`oracle probe` 的 `GrannyGetFileInfo` 与 `gr2dump` 逐字段一致;`tools/oracle_diff` 对拍 Granny **2.9.12** 的 `GrannyGetWorldPose4x4Array` + `GrannyDeformVertices`:23 用例(13 模型 bind + 8 动画帧,含 v7 / 双 root / 刚体)骨骼矩阵 ≤ 4.6e-5、蒙皮顶点 ≤ 6.5e-5(`test/m2-numeric.json`)。oracle 在 macOS + Wine + MinGW 跑(`oracle/RUNBOOK.md`)。
|
||||
- **自洽**:T5 bind pose 自洽 —— ✅ `warrior_cheongrin` `6.1e-5` PASS。全量 2013/2017 < 1e-3;4 个 `>=1e-1` **经 oracle 证实 libgr2 正确**(assassin 对 Granny 3.1e-5),是自洽不变量本身对这几个双-root 资产不成立,非 libgr2 bug。
|
||||
- **fuzz**:T9b 全量 9166 —— ✅ **零崩溃 + 零谓词失败**。
|
||||
- **变体可实现**:✅ 无 BitKnit / 无 Oodle0;曲线全 `OldCurveType`,degree ≤ 2,T7b 全覆盖。不做的变体见 [`../../libgr2/README.md`](../../libgr2/README.md)「明确不做」(均 0 出现,坏输入干净报错不崩)。
|
||||
- **产出**:✅ `test/{fuzz-report,m2-numeric,noise_floor}.json` + `test/assets.list` + `test/oracle_dumps/`。
|
||||
- **noise_floor**:✅ `test/noise_floor.json`。同-DLL 确定性 = 0(byte-identical);跨实现地板 = mat 4.6e-5 / vtx 6.5e-5;跨版本一档待另一个 granny2.dll。
|
||||
|
||||
**M0 结论:全部门禁达成(含 vs 真 Granny 逐字段对拍)。**
|
||||
|
||||
## 验证
|
||||
|
||||
- **bootstrap 关系**:M0 自身验证用硬编码引导集(`warrior_*` + 若干 zone);`assets.list` 产出后,M1+ 一律用它。
|
||||
- **辅助**:`--gltf` 拖进 Blender 目视骨架拓扑 + bind pose 网格。glTF 证不了动画曲线解码(表达不了 ease / scale-shear / 常量轨道压缩)。
|
||||
|
||||
## 开工前要定的 TODO(跑起来才能定,非文档缺陷)
|
||||
|
||||
- **[T4/T5/T6 期望值] Blender 导入核对** —— M0 第一天用选定的 Blender 插件导入 `warrior_cheongrin.gr2`,记下真实的 skeleton / mesh / material / vertex / bone count,替换各 T 里 "大概率" 的占位期望值,写进 `test/README.md`。
|
||||
- **[T7b] 曲线子类型清单** —— 具体做哪 1–2 种,要 T7a + T9c 的 `fuzz-report.json` 才知道。T7b 开工前 `gr2_anim.cpp` 先只做 `granny_curve.cpp` 的未压缩关键帧 + 线性插值。
|
||||
- **[验证] Blender `io_scene_gr2` 插件** —— 有多个同名插件(SWTOR 版、Metin2 fork),轴向约定各异。M0 第一周:定一个、确认能导入 `warrior_cheongrin.gr2`、记录轴向约定到 `test/README.md`。定不下来 → 三方 tie-break 暂时只靠 oracle,或找第二个开源 gr2 库。
|
||||
|
||||
## 本步风险(从 PLAN §08 筛)
|
||||
|
||||
| 风险 | 状态 / 缓解 |
|
||||
|---|---|
|
||||
| **section 全是 Oodle1**(实测 9166),T2 从 [S] 变 [M] | **已解**:`oodle1.c` 端口自泄露 SDK,9166/9166 展开 == `ExpandedDataSize` |
|
||||
| **T9b 加固吃掉大部分工期** | **已解**:9166 零崩溃 + 零谓词失败。实际 T9b 几乎没触发额外加固(类型树通用遍历 + 懒解引用 + 全程边界检查一次到位)|
|
||||
| section 用 Oodle0 | **不适用**:全样本 0 个 |
|
||||
| section 用 BitKnit | **不适用**:全样本 0 个 |
|
||||
| 自描述类型树递归 / 未知类型 | **已解**:T3 通用遍历器不假设布局;全量 0 个未知成员类型 |
|
||||
| 曲线子类型多样(B 样条拟合等) | **已解**:全部 `OldCurveType`,degree ∈ {0,1,2}、dim ∈ {0,3,4,9}。T7b 全覆盖(degree 3 全样本 0 个)|
|
||||
| 非单位 scaleshear 普遍存在 | **确认普遍**(2267 skeleton / 13298 骨):组合公式已用完整 `R3·SS3`;通知 M2/M3 蒙皮走完整仿射 |
|
||||
| `InverseWorld4x4` 读错但一致 | **已兜住**:T5 自洽检查全量跑,2013/2017 < 1e-3,4 个异常定位到辅助骨子树(见上)|
|
||||
|
||||
## DoD 清单
|
||||
|
||||
- [x] T1 header + section 表:`gr2dump --sections` 跑通全部 9166 个文件、0 崩溃、`total_size` 全对
|
||||
- [x] T2 Oodle1 解压:9166/9166 全 section 展开 == `ExpandedDataSize`;内嵌路径串解压后干净可读(内容锚)
|
||||
- [ ] T2 加强:与 `GrannyDecompressData` 逐字节对拍一次(oracle 就绪后)
|
||||
- [x] T1 fixup:pointer fixup 索引建立(`(section<<32|from)->Ref`),懒解引用,0 越界(9166 全量)
|
||||
- [x] T3 类型树遍历器:root object 顶层成员名集合 == `granny_file_info` 标准字段(13 个),`gr2dump --members` 打印一致
|
||||
- [x] T1+T3+T4 集成检查点:从 root 跟指针无崩走到 `Skeletons[0].Bones[0].Name` 读出字符串(`warrior_cheongrin` → "Bip01")
|
||||
- [x] T4 FileInfo:`warrior_cheongrin.gr2` → skeletons=1 / models=1 / animations=0 / materials=4 / textures=2 / meshes=5(*期望值待 Blender/oracle 复核*)
|
||||
- [x] T5 骨架 + bind pose 自洽:`warrior_cheongrin` `max|Δ|=6.1e-5` PASS(阈值 1e-3);全量 2017 个多骨 skeleton 中 2013 个 < 1e-3(4 个异常见下)
|
||||
- [x] T6 网格:`warrior_cheongrin` 5 mesh 全 `PNT332_Skinned`,顶点/索引/三角组自洽(`triFirst+triCount*3==indices`),bone-binding 0 悬空
|
||||
- [x] T9a/b:9166 个 `.gr2` **零崩溃** + **零谓词失败**(骨骼数/父索引/NaN/索引越界/权重和/bone-index 范围)
|
||||
- [x] T9c:`test/fuzz-report.json`(6 类直方图)+ `test/assets.list`(18 个)产出
|
||||
- [x] T7a 曲线子类型统计完成(见下);T7b OldCurve 解码(d0 常量 / d1 线性 / d2 二次 B 样条 + 四元数归一)+ `Animation::sample_local`,`dance_1` 等 3+ 采样点无非有限数
|
||||
- [ ] 门禁·主:结构 dump vs oracle 逐字段一致(oracle 就绪后)
|
||||
- [ ] T4/T5/T6 期望值 Blender/oracle 复核(替换"待复核"占位)
|
||||
- [ ] T7b 精度:某采样点骨骼局部变换 vs oracle 层① `max|Δ| < ε`(oracle 就绪后)
|
||||
|
||||
## M0 实现结果(fuzz 全量 9166,`test/fuzz-report.json`)
|
||||
|
||||
| 维度 | 结果 |
|
||||
|---|---|
|
||||
| 解析成功 | **9166 / 9166**,0 崩溃,0 谓词失败 |
|
||||
| 格式版本 | v6 × 9155(magic `GRNFileMV_Old`)、**v7 × 11**(magic `GRNFileMV_32Bit_LittleEndian`,非 Old;含 `shaman_lord` / snow_dungeon zone / `warrior_rabbit1`)|
|
||||
| section 压缩 | 非空 section 全 **Oodle1**(64162 段);每文件 1 个空 section(compression=none)。**无 Oodle0,无 BitKnit** |
|
||||
| 顶点类型 | `PNT332` × 4016(rigid)、`PNT3322` × 309(rigid 双 UV)、`PNT332_Skinned` × 3118。**无 `PNT3322_Skinned`,无未知类型** |
|
||||
| 曲线格式 | **全部是 `OldCurveType`**(Granny 2.4:`{Int32 Degree; RefToArray Knots; RefToArray Controls}`,无压缩变体、无 `curve2`/`CurveData` variant)。子类型 = (degree, dim):<br>pos `d0·dim3`(322k) / `d0·dim0`(2.5k,恒等) / `d2·dim3`(41k);<br>rot `d0·dim0`(77k) / `d0·dim4`(79k,常量四元数) / `d2·dim4`(210k);<br>scale `d0·dim0`(321k) / `d0·dim9`(42k,常量 3×3) / **`d1·dim9`(3k,线性 3×3)**。**无 degree 3** |
|
||||
| ScaleShear | 2267 个 skeleton 至少 1 骨带非单位 scaleshear,全语料 13298 骨。**普遍存在** → 组合公式必须走完整 `R3·SS3`(已实现,warrior self-check 15 个 ss 骨仍 6.1e-5)|
|
||||
| bind pose 自洽 | 多骨 skeleton:`<1e-4` × 1835、`<1e-3` × 178、`>=1e-1` × **4** |
|
||||
|
||||
**bind pose 自洽 4 个异常**(`>=1e-1`):`assassin.gr2`(PC + season1,同)、`warrior_rabbit1_backup.gr2`、`hair_11_1.gr2`。
|
||||
这 4 个都是**多 root skeleton**(如 assassin:`assasin_low` + `Bip01` 两个 root),且 model 的 `InitialPlacement` 非单位。
|
||||
|
||||
> **后续更正(oracle 对拍后)**:这不是 libgr2 的 bug。`assassin` bind pose 的 **骨骼世界矩阵 + 蒙皮顶点与真 Granny 2.9.12 逐字段一致(3.1e-5 / 4.6e-5)**(见 [00-oracle](./00-oracle.md) / `oracle/RUNBOOK.md`)。
|
||||
> `world[i]·InverseWorld4x4[i] ≈ I` 这个**自洽不变量本身**对这几个资产不成立 —— 它们的 `InverseWorld4x4` 是相对「不含 InitialPlacement 的参考」烘的,而 `world = local链 · InitialPlacement`。是资产属性,不是读取错误。
|
||||
> 自洽检查因此是个**比 oracle 对拍弱的信号**:能抓转置 / 手系 bug,但对「InverseWorld 参考系不一致」会误报。`warrior_cheongrin` 等单 root 模型不受影响(IP 为单位,两者等价)。
|
||||
|
||||
## 关键格式发现(补 PLAN §05 / DEMO-PLAN §7)
|
||||
|
||||
1. **`ReferenceToVariantArray` 磁盘布局是 `{def* Type; int32 Count; void* Ptr}`(Type 在前)**,不是 `{Count; Type; Ptr}`。`VertexData.Vertices` 用它。踩过一次。
|
||||
2. **`transform_track`(Granny 2.4)= `{String Name; Inline PositionCurve; Inline OrientationCurve; Inline ScaleShearCurve}`** —— 无 `Flags` 成员,且 Position 在 Orientation 之前,与 2.9 SDK 的 `TransformTrackType` 不同。类型树遍历器按文件自带 typedef 解,不硬编码,所以不受影响;但硬编码偏移会错。
|
||||
3. **root 偏移的两种放法**:多数 Metin2 skeleton 把 model-space 偏移放在 `model.InitialPlacement`,root 骨 `LocalTransform` 是单位;少数(如 `warrior_cheongrin`)把偏移烘进 root 骨 local。self-check 必须 `world[root] = Composite(local[root]) · InitialPlacement`。
|
||||
4. **一个文件可有多个 skeleton**(本体 + 武器/挂点,各带自己的 1-骨 skeleton 和 model)。mesh 的 `BoneBindings` 要对每个 skeleton 试解析、取悬空最少的那个。
|
||||
5. 组合矩阵语义(`granny_transform.cpp BuildCompositeTransform4x4` + `granny_matrix_operations.cpp ColumnMatrixMultiply4x3Impl`):行主序、平移在 elem 12–14、`world[i] = Composite(local[i]) · world[parent]`、上 3×3 = `transpose(R3·SS3)`。self-check 用同一套。
|
||||
Reference in New Issue
Block a user