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
50 KiB
Metin2 跨平台渲染引擎 PoC — 完整方案
内部研究方案,不对外公开。 基于对
m2dev-client-src-main、MobileSource及m2dev-client-main/assets中真实.gr2资源的实测编写。 所有版本号、格式偏移、API 数量均来自源码与文件抽查。
| 状态 | PoC 规划 |
| 技术栈 | C++20 · bgfx(RHI) · sokol_app(仅窗口/输入) |
| 目标平台 | macOS · iOS · Android(并行) |
| 判定周期 | ≈ 2–3 个月 → go / no-go |
| 复用 | EterBase · EterPack · EterImageLib · MobileSource 的 .sc 着色器草稿 |
| 自研 | libgr2(gr2 v6 读取器) |
选型变更记录:RHI 从 sokol_gfx 改为 bgfx。原因见 §03——
MobileSource里已存在一套面向 bgfx 的 Metin2 着色器草稿,bgfx 更 shipping 级、Metal/GLES/Vulkan 更成熟,且这不是一次性 PoC 而是最终移植的同一条代码路径。窗口/主循环/输入计划用sokol_app.h并把原生 handle 经bgfx::PlatformData交给 bgfx —— 这个交接不是 sokol_app 常规用法,是一处真风险,M1 独立子门禁验证,啃不下退回"桌面 SDL2 + 移动端各写最小原生壳"(§03 窗口层)。
01 · 目标与判定标准
PoC 不是"移植一小部分客户端",而是把跨平台方案里所有真正未知的环节压到一条最短路径上,跑通即视为整体可行。
必须证明的四件事
- 脱离 Granny 读 gr2 — 自研加载器能从 Metin2 真实的
.gr2中读出骨架、蒙皮网格、骨骼权重与动画轨道。 - 动画与蒙皮正确 — 骨骼动画采样 + 线性混合蒙皮的结果,与 Windows 客户端逐帧数值一致。
- 跨平台 RHI 成立 — 同一套渲染代码经 bgfx 在 macOS + iOS 真机 + Android 真机上出画面,且三端一致。
- 移动端性能达标 — 基准场景在中端机上达到目标帧率、显存与启动时间。
PoC 明确排除
地形与户外场景、特效与粒子、UI 与 Python 脚本层、网络、阴影、水面、天空盒、LOD 切换策略、多角色 AI。 这些都不含跨平台的新风险,属于后续正式移植的工作量而非可行性问题。
判定
PoC 的价值是降低不确定性,go 和 no-go 都是有效产出。
进度(2026-08-29):M0 完成(9166/9166 解析零崩溃)。M1/M2 代码完成, 数值保真已对拍真 Granny 2.9.12(骨骼世界矩阵 + 蒙皮顶点 ≤ 6.5e-5,见
oracle/)。 M3 iOS 端已交叉编译 + 模拟器运行 + 渲染与桌面一致;Android 脚手架待 NDK + 真机。 PoC 的核心技术问题(能否脱离 Granny 正确读 + 播 Metin2 gr2)= 已用数值证据回答「能」。 剩余是真机验证(性能 / 生命周期 / mediump)+ Android 端。 画质改进 pass(档 1 + 档 2)已完成:MSAA x4 / mip 链 / sRGB 线性着色 / 半球环境 + 3 方向光 / 双面; libgr2 加granny_material链解析 → 逐 tri_group 正确贴图(原来整模型套一张瞎猜的 dds)+ alpha-test 镂空。 无回归(gr2fuzz 9166 · render_fuzz 9166 · oracle 23/23)。细节见docs/steps/M1-static-render.md「画质改进 pass」。
- 里程碑 M3 全部门禁通过 = "C++ + bgfx、复用 EterGrnLib 逻辑" 的跨平台方案可行,进入正式移植规划。
- 门禁失败要区分两类:
- 工程性延期 —— 数值差 5%、某个曲线子类型没实现、某平台一个 API 用错。这不是"方案不行",是一个有明确修法、几天到两周能解的 bug。记下来继续。
- 方案性死路 —— 例如资产大量用 BitKnit 且烘焙退路也不可行、或 EterGrnLib 的动作混合逻辑无法脱离 D3D 复刻。这才触发"转 ozz-animation / Rust 重写 / 放弃"的评估。
- 最终交付一页纸结论,明说是哪类、卡在哪层。
定位澄清:这不是"跨平台版的 JTX Graphics"
两者是正交的两个轴,不要混淆:
| JTX Graphics(及同类视觉 mod) | 本方案 | |
|---|---|---|
| 改的是 | 画质:程序化天气/昼夜/天空、后处理 | 平台:Granny→libgr2、D3D9→bgfx、Win32→跨平台壳 |
| 平台 | 仍是 Windows / D3D9 / HLSL,焊死 | macOS / iOS / Android |
| 画面目标 | 比原版更好看 | 和原版一样(先求跑对,不追求提升) |
| 一句话 | 同平台,换皮升级 | 同画质,换平台 |
关系:本方案的核心动作就是把 Metin2 的固定管线换成可编程着色器(bgfx .sc)。做完之后,再叠一层类 JTX 的视觉增强会容易得多,而且因为着色器经 shaderc 编到 Metal/GLES/Vulkan,任何视觉增强天生就是三端的。正确顺序是"先重构后端(本方案)→ 再叠视觉层";反过来(先在 D3D9 上堆画质再想跨平台)是死路。JTX Graphics 本身闭源付费、无公开源码,不纳入技术选型。
02 · 架构总览
分层清晰、复用边界明确:底层文件与资产解码搬用 MobileSource 的对应代码(注意那套代码写了但从未链接成功,M1 是它第一次真正跑),中间的 gr2 解析全部自研,上层渲染走单一 RHI(bgfx)。
分层
| 层 | 内容 |
|---|---|
| 平台壳 | sokol_app.h(窗口 / 主循环 / 输入,四平台一份) → 原生 handle 经 bgfx::PlatformData 交给 bgfx(交接是风险,见 §03) |
| RHI | bgfx(经 bgfx.cmake,Metal / GLES3 / Vulkan / D3D11) · shaderc(.sc → 多 profile) · RHI 薄封装 ≈ StateManager |
| 渲染器 | 场景·相机 / 动画采样 / 蒙皮 CPU→GPU / 材质·贴图绑定 |
| 资产 | libgr2(自研) / EterImageLib(复用) / msm·msa 解析 / EterPack(复用,M3 前或 M4 接入) |
| 基础 | EterBase:CFileBase / CMappedFile(复用) / 数学:glm |
数据流
.gr2/.dds/.msm/.msa → libgr2 · EterImageLib · msm/msa → 中间表示(骨架·网格·曲线·材质)
→ 动画采样 → 世界姿势 → 蒙皮 → RHI(bgfx) → Metal / GLES3 / Vulkan
复用 / 自研 / 替换
| 模块 | 处理 | 说明 |
|---|---|---|
| EterBase | 复用(M1 首次验证) | CFileBase / CMappedFile 的 AAsset 分支 MobileSource 写了但没链接过;CRC32、lzo、内存映射搬过来,M1 第一次真跑,可能有坑。 |
| EterPack | 复用 | eterpack 读取 + LZO 解压 + 解密。PoC 从散装文件起步,M3 前或 M4 再接(打真机时可能提前,见 §08 平台行)。 |
| EterImageLib | 复用(M1 首次验证) | CDXTCImage(DXT1/3/5 软解到 RGBA8)、CTGAImage,Android 解压分支同样没跑过。移动端无 S3TC 硬件支持,运行时解压是必经路径。 |
MobileSource .sc 着色器 |
复用为基线 | shaders.rar/shaders/ 里那套面向 bgfx 的草稿(见 §04)。lit / textured / 2D 着色器 + texture_stage.sh 可直接改;蒙皮 / fog / 顶点色 / 跨平台编译步骤要补。 |
| EterGrnLib | 按源码复刻 | 代码焊死在 granny_* 与 D3DXMATRIX 上,不能直接编。当权威规格:网格→VB/IB 布局、材质调色板、LOD 控制、动作混合、挂点,照它的调用序列在 libgr2 + glm 上重写。 |
| granny2 / Granny SDK | 替换 | 自研 libgr2。泄露的 Granny 源码仅作格式参照,不链接、不进仓库产物。 |
| D3D9 / d3dx9 | 替换 | PoC 不碰完整 EterLib 渲染层。着色器从 .sc 草稿改,配一层 bgfx 封装。 |
| MSWindow / DirectInput | 替换 | sokol_app.h 提供窗口 / 主循环 / 输入,四平台统一;渲染交给 bgfx。 |
MobileSource里的两条渲染路径:一条是手写的GrpOpenGLD3D8→GLES shim(已进 CMake,~40% 文件参与编译,从未链接成功);另一条是 bgfx 路径(未接线,但有更完整的.sc着色器集)。本方案接续 bgfx 那条路径的着色器成果,不用 shim。
reuse/是冻结的 vendored 快照 + 本项目的修复提交在其之上,不是对../MobileSource的引用 —— 那套代码从没链接过,M1 一定会改它,必须版本可控。同理.sc着色器也拷进app/shaders/冻结。
03 · 技术选型
为什么是 bgfx(而不是 sokol_gfx / wgpu)
- 最难的活已有人用 bgfx 干了一半 —
shaders.rar里那套vs/fs_pnt、pdt、pt、terrain+texture_stage.sh+varying.def.sc就是冲 bgfx 写的。选 bgfx = 用shaderc编一下几乎直接能用;选 sokol = 得把.sc逐个翻成 sokol-shdc 的 GLSL,还要重写 helper。 - 这不是一次性 PoC — 目标是真做跨平台客户端,PoC 就是最终移植的同一条代码路径。RHI 选型要背几年,选一个 shipping 级、社区大、Metal/GLES/Vulkan 都成熟的更划算。
- 引擎结构对口 — Metin2 整套是 FVF + texture-stage + 多光源 + alpha-test 的固定管线思维,bgfx 的 vertex layout /
varying.def.sc/ state flags 跟这套心智模型贴得近。 - 调试设施 — 内置 stats HUD、
dbgTextPrintf、RenderDoc 钩子、BGFX_DEBUG_*,正是验证阶段要用的;bimg/texturec量产转 ASTC/ETC2 也用得上。 - 构建摩擦已消除 — 用
bgfx.cmake(社区维护的 CMake 封装),集成就是add_subdirectory的事,bx/bimg/bgfx三个仓库一起拉。
窗口层:sokol_app 与 bgfx 的交接是一处真风险
bgfx 自己不管窗口。计划做法:sokol_app 拿到原生 window handle → 填 bgfx::PlatformData → bgfx::init,让 bgfx 自己建设备。
但这不是 sokol_app 的常规用法:sokol_app 和 bgfx 都是"整个 app 框架",都想拥有 swapchain / context。sokol_app 在 iOS/Android 会自建 EAGL/EGL context 和 MTKView / GLSurfaceView,要让 bgfx 接管得专门压制这些(可能要 SOKOL_NO_ENTRY + 只用 sokol_app 的窗口/输入部分)。桌面上更稳的组合其实是 bgfx + SDL2 / GLFW。
因此:M1 给这个交接一个独立子门禁(macOS 上 bgfx 经 sokol_app 的 handle 出画面 + 输入可用);若两周内啃不下,退回"桌面 SDL2 + iOS/Android 各写最小原生壳"。
着色器
以 MobileSource/Cross Platform/shaders.rar/shaders/*.sc 为基线,用 bgfx 的 shaderc 编译。
不再手写 sokol-shdc GLSL。详见 §04 着色器来源。
- 目标 profile:
--platform osx -p metal(macOS)、--platform ios -p metal(iOS)、--platform android -p 300_es(Android GLES3)、--platform windows -p s_5_0(Windows 对照)。 - 产物
.bin用bin2c嵌成 C 数组,bgfx::createShader加载。 bgfx_shader.sh随 bgfx submodule 自动就位。.sc草稿写于 2025-12,针对某个 bgfx 版本 ——bgfx.cmake的 submodule 就 pin 到那个或相近版本,否则宏 / uniform 命名可能对不上。MobileSource里的compile_shaders.bat路径写死、只出 Vulkan 一个 profile,要换成 CMake 的可移植 custom command。
构建
- 单一
CMakeLists.txt+ 三份 toolchain:iOS(ios.toolchain.cmake)、Android NDK、macOS 原生。 third_party/放bgfx.cmake(含bx/bimg/bgfxsubmodule)、sokol(仅sokol_app.h)、glm。- C++20,
-fno-exceptions视复用代码情况而定(EterBase 用了少量异常,先保留)。 - 数学库用 glm:列主序,与 GL/Metal/bgfx 一致;替换
D3DXMATRIX时注意 D3DX 是行主序,上传前转置或全程列主序。 - CI 三端并行:macOS 原生、iOS 模拟器、Android 模拟器。
04 · 关键模块
libgr2 — gr2 v6 读取器(自研,核心)
只读,覆盖:
- 文件头 + section 表 — 定位各段;实现 section 解压(预期未压缩或 Oodle0/1)。
- 自描述类型树遍历 — 不硬编码结构,按文件内嵌的
data_type_definition递归解析,指针/引用重定位(relocation/marshalling 表)。 FileInfo根对象 — 取出Textures[]、Materials[]、Skeletons[]、VertexDatas[]、TriTopologies[]、Meshes[]、Models[]、Animations[]。- 骨架 — 骨骼数组(名称、父索引、
LocalTransform、InverseWorld4x4)。 - 网格 — 顶点(
PNT332/PNT3322及带骨骼索引+权重的蒙皮变体)、索引、三角组(按材质分段)、BoneBindings。 - 动画 —
TrackGroups→ 每骨的 position / orientation / scaleshear 曲线;曲线子类型解码(关键帧数组、B 样条拟合等)。
对照实现:泄露 Granny 源码里的 granny_data_type_definition.*、granny_file_info.*、granny_curve*.cpp 当算法参照读;不复制代码。
资源加载
- 贴图 —
.dds经CDXTCImage解到 RGBA8,用bgfx::createTexture2D上传(移动端显存 ×4,PoC 可接受)。量产阶段换离线转 ASTC / ETC2 或 Basis Universal(bgfx 的bimg/texturec就能做)。 - 文件访问 — 直接用
CMappedFile,桌面走 mmap,Android 走AAsset_getBuffer(MobileSource 已实现)。M0–M3 用散装文件,M4 切 eterpack。
动画与蒙皮
- 采样 — 按局部时钟对每骨曲线插值 → 局部变换 → 沿父链累积得世界矩阵(对应
GrannyBuildWorldPose/GrannyGetWorldPoseComposite4x4Array)。 - 过渡 — ease-in/out 曲线、loop count、raw local clock 的语义,照
EterGrnLib/Motion*与ModelInstanceMotion.cpp的调用序列复刻。 - 蒙皮 — M2 先 CPU 线性混合蒙皮(
skinMatrix = world[bone] * invBind[bone],逐顶点 4 权重加权);M4 移到顶点着色器(骨骼矩阵走 bgfx uniform 数组或 texture)。 - rigid mesh —
GrannyMeshIsRigid为真的网格不做蒙皮,只按挂载骨骼刚体变换 —— 分支照搬。
着色器来源:MobileSource 的 .sc 草稿
shaders.rar/shaders/ 是一份没写完的草稿,能用的部分省事,缺的部分正好是 PoC 核心。
| 文件 | 是什么 | 对我们 |
|---|---|---|
varying.def.sc |
bgfx attribute/varying 声明:a_position/a_color0/a_texcoord0/a_normal,v_normal/v_texcoord0/v_worldPos/v_texcoord1 |
结构直接用,但没有骨骼索引/权重,要加 a_indices / a_weight |
vs_pnt.sc / fs_pnt.sc |
position-normal-texcoord。FS 是重头:8 光源前向光照 + 全套 D3DCMP_* alpha-test + applyTextureStage |
lit mesh 着色器基线,M1–M2 直接改 |
vs_pdt.sc / fs_pdt.sc |
position-diffuse-texcoord,Metin2 最常用 FVF | 带顶点色的网格 |
vs_pc.sc / fs_pc.sc |
position-color,无纹理 | UI 图元 |
vs_pt.sc / pt2、fs_pt.sc / pt2 |
position-texcoord,2D 图像/精灵 | UI 贴图 |
vs_terrain.sc / fs_terrain.sc |
地形 splatting,很小 | 基本占位,PoC 不用 |
texture_stage.sh |
把 D3D 固定功能 SetTextureStageState 完整翻译成 shader:COLOROP/ALPHAOP(MODULATE、MODULATE2X/4X、ADD、ADDSIGNED、SUBTRACT、BLEND…)、arg 选择(TEXTURE/DIFFUSE/CURRENT/TFACTOR)、COMPLEMENT/ALPHAREPLICATE 标志,uniform 驱动 |
最值钱的一块,但它是一份未经校验的重实现 —— 见下方验证要求 |
texture_stage.sh必须专门验证:它是别人写的 D3D 固定功能重实现,MODULATE2X 的 clamp、arg complement、多 stage 串联都可能有细微错。M1/M2 要拿一个已知的多 stage 材质(从测试资产集里挑一个用了D3DTOP_MODULATE2X或ADDSIGNED的)单独和 oracle 对拍,不能只靠整场景 SSIM 兜。
状态 / 要补的:
.bin输出大多 31–34 字节,是空/失败产物 —— 忽略,从.sc重编。compile_shaders.bat路径写死、只出 Vulkan 一个 profile —— 换成 CMake custom command,出 metal / 300_es / s_5_0 三套。- 没有蒙皮 —— 没有骨骼矩阵、没有
a_indices/a_weight。新写vs_pnt_skinned.sc(骨骼矩阵走 uniform 数组或 texture,VS 里做 LBS)。这是 M2 的核心。 vs_pnt丢了a_color0(Metin2 网格常带顶点色),要补。v_texcoord1(第二 UV,lightmap/detail)声明了没用。- 没有 fog —— Metin2 大量用 D3D 雾,要加。
一句话:这套 .sc 把"固定管线光照 + alpha-test + texture-stage → shader"做掉了大半,蒙皮、fog、顶点色、跨平台编译步骤要自己补。
msm / msa — Metin2 包装格式
.msm 描述一个模型:引用的 .gr2、贴图、材质类型、挂点。.msa 描述动作:引用的动画 gr2、混合参数、事件。
格式简单,解析器逻辑就在 RaceManager.cpp / EterGrnLib/Util.cpp 里,照抄。
要有验证门:.msm / .msa 是人可读的文本文件。M2 加一条子检查——dump 解析结果(挂点名、动作列表、混合参数),和源文本逐项目视核对。否则 .msa 混合参数读错 → 动画错 → M2 矩阵 diff 失败,你会去 debug libgr2 / 采样,真 bug 却在这里。
RHI 封装
一层 ≈ 200 行的薄封装,把"设置纹理 / 设置顶点缓冲 / 设置变换 / 画一批索引三角形"映射到
bgfx::setVertexBuffer / setTexture / setUniform / setState / submit,语义对齐 StateManager。
目的是让从 EterGrnLib 复刻过来的渲染逻辑几乎逐行对应。
平台壳
sokol_app.h的单回调主循环,四平台一份main;原生 handle 经bgfx::PlatformData交给bgfx::init。这个交接本身是风险,见 §03 窗口层,M1 独立子门禁。- Android 上下文丢失 — 切后台 GL context 连同 GPU 资源可能失效。在 sokol_app 的
SUSPENDED/RESUMED事件里按 bgfx 的重置流程处理(bgfx::reset+ 必要时重建资源)—— M3 就处理,不要拖到最后。 - iOS — 全静态链接,无
dlopen;Metal drawable 生命周期照 sokol_app 处理;资源打进.bundle。 - 资产上真机的方式要在 M3 前定:9166 个散装
.gr2+ 一堆.dds不能直接堆进 APK /.bundle(体积、iOS 限制)。要么 M3 前提前接 eterpack(现排 M4),要么只把测试资产集打进去。M3 交付里"原样交叉编译"没算这一步。
05 · gr2 格式要点
抽查 warrior_cheongrin_lod_01.gr2 的文件头:
偏移 字节 含义
0x00 B8 67 B0 CA F8 6D B1 0F 84 72 8C 7E … 16 字节 magic:32 位小端 · 文件格式版本 6
0x10 B8 01 00 00 头部长度 = 0x1B8
0x20 06 00 00 00 格式版本字段 = 6
0x24 AC 09 01 00 TotalSize = 68012 = 文件实际大小 ✓ 解析正确
0x28 65 7A 20 53 CRC32
0x2C 38 00 00 00 section 表偏移 = 0x38
已确认(M0 T1–T9 全部实现并全量实测:9166/9166 解析成功、0 崩溃、0 非退化谓词失败)
libgr2= header/section/fixup + 自描述类型树遍历器 + FileInfo + 骨架(含 bind-pose 自洽检查)
- 蒙皮网格 + 动画曲线解码 + 采样。
gr2dump/gr2fuzz见tools/。报告见test/fuzz-report.json。
- magic =
GRNFileMV_Old(不是GRNFileMV_32Bit_LittleEndian),32 位小端。9155 个是格式 v6,11 个是 v7(容器兼容,都能读)。 - 每文件固定 8 个标准 section(Main / RigidVertex / RigidIndex / DeformableVertex / DeformableIndex / Texture / Discardable / Unloaded)。
- section 用 Oodle1 压缩(每段
Format==2;Texture section 例外,空)。之前"未压缩"是把HeaderFormat(=0)错当成 section 压缩。没有 BitKnit(全样本 0 个)。Oodle1 解码器已从泄露 SDK 端口进libgr2(oodle1.c),全量验证通过。 - 资产由 Granny Standard Exporter SDK 2.4.0.7 导出 —— oracle 的
granny2.dll应锁 2.4.x 线(见steps/00-oracle.mdT1)。 total_size字段 == 文件实际大小;解压后每个 section 恰好ExpandedDataSize。内嵌路径串(D:\Ymir Work\...)解压后干净可读 —— 内容正确性的 oracle-free 锚点。- 客户端共 9166 个 .gr2,散装在磁盘,PoC 阶段无需解包。
- 引擎实际用到的 Granny API 约 90 个,全部是"读文件 / 采样动画 / 变形顶点",无任何建模或导出。
- 顶点类型实测:
PNT332(rigid,4016 mesh)、PNT3322(rigid 双 UV,309)、PNT332_Skinned(3118)。无未知类型。蒙皮变体带BoneWeights(NormUInt8×4 或 Real32×4)+BoneIndices(UInt8×4 或 packed UInt32)。 - 曲线全部是
OldCurveType(Granny 2.4:{Int32 Degree; RefToArray Knots; RefToArray Controls},无压缩变体、无curve2/CurveDatavariant)。degree ∈ {0,1,2}(无 3)、dim ∈ {0,3,4,9}。gr2_anim.cpp全覆盖(常量 / 线性 / 二次 B 样条 + 四元数归一)→ §08 的"曲线子类型超出范围"风险消除,无需烘焙退路。 - 非单位 scaleshear 普遍:2267 个 skeleton 至少 1 骨、全语料 13298 骨带非单位 scale-shear。→ 组合公式走完整
R3·SS3(已实现);M2/M3 蒙皮按完整仿射,不可用world·invBind的正交近似。 - 多 skeleton / 多 model:一个文件可含本体 + 武器挂点各自的 skeleton(如
redthief2_soldier2= 3 skeleton / 3 model)。meshBoneBindings对每个 skeleton 试解析取悬空最少者。 - root 偏移两种放法:多数 skeleton 把 model-space 偏移放在
model.InitialPlacement(root 骨 local 为单位);少数烘进 root 骨 local。世界姿势 =Composite(local[root]) · InitialPlacement。 EterGrnLib已用#if GrannyProductMinorVersion == 4 / 7 / 8 / 9 / 11兼容多版本 —— 实测资产就是 2.4,与 libgr2 的选择一致。
M0 抽样统计结果(test/fuzz-report.json,全量 9166)
- 格式版本:v6 × 9155、v7 × 11。section 压缩:非空段 100% Oodle1,0 Oodle0 / 0 BitKnit → §08 BitKnit 退路项不触发。
- bind-pose 自洽(不依赖 oracle):多骨 skeleton 2013/2017
max|Δ| < 1e-3(warrior_cheongrin6.1e-5);4 个>=1e-1已定位到辅助骨子树的InverseWorld4x4未随InitialPlacement更新(含 1 个文件名带_backup),非阻塞。 - 曲线子类型直方图(degree·dim):pos
d0·d3(322k)/d2·d3(41k);rotd0·d4(79k)/d2·d4(210k);scaled0·d9(42k)/d1·d9(3k);大量d0·dim0(恒等轨道)。
M0 第一件事:锁定 oracle 用的 Granny 版本
资产是文件格式 v6(约 2.6–2.9 era),但 m2dev-client-src 头文件写 2.11.8。oracle 必须用实际能正确加载这批资产的那个 granny2.dll 版本去 dump,否则是拿错误的参考去校 libgr2。M0 起手要确认:这批 gr2 在哪个 Granny 版本下 GrannyGetFileInfo 返回完全正常(骨骼数、顶点数、动画时长都合理),把那个 DLL 固定进 oracle/。
06 · 里程碑与验证门
五个里程碑,每个都有明确的交付物和一道 go/no-go 门禁。单人全职到 M3 结束约 2–3 个月。 里程碑的验证逻辑与 RHI 无关;换 bgfx 只在 M1 多了一道"sokol_app + bgfx 交接"的子门禁。
分册:本节是索引,每个里程碑的文件级施工文档在
steps/:00-oracle(关键路径,M0 起)·M0-gr2-reader·M1-static-render·M2-anim-skinning·M3-mobile·M4-realistic-load。 M0+M1 的可演示 demo 施工图另见DEMO-PLAN.md。
M0 · gr2 解析器(独立 CLI) — 1–2 周
- 交付 — ① 锁定 oracle 的 Granny 版本(见 §05);②
gr2dump读warrior_*系列 + 若干 zone 静态件,产出 libgr2 的结构化 dump(骨架 / 网格 / 动画曲线)和 glTF;③ 对全量 9166 个.gr2跑 parse-fuzz,产出格式变体直方图;④ 从 fuzz 元数据里筛出测试资产集(见 §07)并写进test/assets.list。 - 门禁 — libgr2 结构化 dump 与 oracle 的
GrannyGetFileInfodump 逐字段一致(见验证);fuzz 零崩溃且产物通过非退化谓词;变体分布落在可实现范围(无 BitKnit,或有明确离线转换退路);测试资产集已产出。 - 验证 —
- 自洽检查(不依赖 oracle,先跑):从
LocalTransform链重建 bind pose 世界矩阵,验证每骨world_bind[i] · InverseWorld4x4[i] ≈ I。这一条抓"读矩阵时带了转置 / 手系错、且同样作用于正向读取"这类 oracle 对拍也发现不了的 bug。 - 主:libgr2 结构化 dump vs oracle dump 逐字段对拍 —— 骨骼数 / 父索引 / 名称 /
LocalTransform/InverseWorld4x4/ 顶点数 / 索引数 / 每骨曲线的关键帧数 / 动画时长,全部相等或 < 噪声地板。 - 辅:导出 glTF 在 Blender 目视骨架拓扑 + 绑定姿势网格。注意 glTF 只能证拓扑和 bind pose,证不了动画曲线解码对(glTF 动画表达不了 Granny 的 ease 曲线 / scale-shear / 常量轨道压缩)—— 动画正确性只认上面那条主验证。
- fuzz 非退化谓词:骨骼数 ∈ [1, 512];每骨父索引 < 自身索引或 = -1;变换无 NaN/Inf;顶点数 > 0;所有索引 < 顶点数;每顶点权重和 ∈ [0.99, 1.01];骨骼索引在范围内。不满足即计一次 fail。
- bootstrap 关系:M0 自身验证用硬编码引导集(
warrior_*+ 若干 zone);test/assets.list一旦产出,M1 及之后一律用它。
- 自洽检查(不依赖 oracle,先跑):从
M1 · macOS 静态渲染 — 2–3 周
- 交付 — sokol_app 窗口 + bgfx(Metal),渲染 gr2 静态网格(绑定姿势)+ 贴图,轨道相机;着色器从
vs/fs_pnt草稿改;texture_stage.sh接一个已知多 stage 材质。 - 子门禁 1(交接) — bgfx 经 sokol_app 的原生 handle 在 macOS 出画面 + 输入可用(见 §03 窗口层)。啃不下则退回 SDL2 方案。
- 子门禁 2(几何) — 网格拓扑、UV 正确;无背面剔除错误 / 法线翻转;一个多 stage 材质的着色结果与 oracle 对上。
- 验证 —
- 对比场景两侧都喂同一张预解码 RGBA(绕开 DXT),让截图 diff 只反映几何 / 光照 / texture-stage,不被软解 vs 硬件 S3TC 的 bit 级差异污染。
- oracle 侧要能进入确定态:固定相机矩阵注入、固定光、绑定姿势、无程序化摇摆 —— 这需要 oracle build 的少量改造,M1 一并做。
- 与 oracle 截图像素 diff + SSIM;开法线可视化模式自检。
M2 · 骨骼动画 + CPU 蒙皮 — 2–3 周
- 交付 — 解析
.msm/.msa,构建世界姿势,线性混合蒙皮,播放 idle / walk,支持 loop 与 ease;补vs_pnt_skinned.sc;装配一个多部件角色(身体 + 至少一个额外 gr2,如头发/时装)+ 一个挂点武器。 - 门禁(分级,按序) —
0.
.msm/.msa解析子检查:dump 解析结果和源文本逐项目视核对(挂点名、动作列表、混合参数)。- 骨骼世界矩阵与 oracle 的**层 ①(裸 Granny,
GrannyGetWorldPoseComposite4x4Array直出)**逐帧对拍,max|Δ| < ε_mat —— 隔离"曲线采样 + 世界姿势累积",先过。M2 只对层 ①;层 ②(过完EterGrnLib的ActorInstanceBlend+ LOD 骨骼裁剪)不在 PoC 范围,留到正式移植。 - 蒙皮顶点坐标与 oracle 逐帧对拍,‖Δ‖ < ε_vtx —— 隔离"蒙皮 + 顶点格式"。
- 多部件装配:挂点武器的世界变换与 oracle 一致(
GrannyFindBoneByName+ 挂点矩阵链复刻对)。 - LOD 一致性:同一模型 LOD 0–3 共享骨骼绑定,切换无跳变(顶点在切换帧的位移 < 阈值)。
- 骨骼世界矩阵与 oracle 的**层 ①(裸 Granny,
- 验证 — 数值层对拍(N 帧 × M 顶点、全部测试资产集,不是单文件;tie-break 见 §07 数值层)+ 视觉层(3 个确定姿势,差异分类见 §07 视觉层)。
- 动画曲线退路 — 若 M0 统计发现曲线子类型超出可实现范围,改走"oracle 离线烘焙成密集关键帧"(见 §08 动画行),libgr2 只读烘焙格式,M2 照常验证。
M3 · iOS + Android 真机 — 1–2 周(与 M2 尾段并行)
- 交付 — M2 工程交叉编译(含资产上真机的方案,见 §04 平台壳 —— 提前接 eterpack 或只打测试资产集,不是"原样交叉编译"就完事),两端真机运行基准场景;CI 三端冒烟。
- 门禁 —
- 验证 — 三端快照互拍 + 真机性能采集(帧时间 p99、显存、RSS、加载耗时)。
M4 · 逼近真实负载(可选) — 2 周
- 交付 — GPU 蒙皮;改走 eterpack 加载;多角色 + 一块地面 + 简单光照;LOD 切换。
- 门禁 — GPU 蒙皮结果与 CPU 一致;50 角色可交互帧率;eterpack 路径与散文件结果一致。
- 验证 — CPU / GPU 蒙皮对拍;扩展性能曲线(1 / 8 / 20 / 50 角色)。
07 · 验证方法论
核心思路:把 Windows 客户端当作"真值预言机"(oracle),新实现的每一层输出都要能和它对拍。分数值、视觉、性能三个层次,配自动化回归。
预言机:插桩的 Windows 客户端
在 Windows 侧构建一个精简 harness,链接锁定版本的 granny2.dll + EterGrnLib(版本确定见 §05),能对"给定 .gr2 + .msa + 时刻 t"导出:
- 两层骨骼矩阵:① 裸 Granny 层(
GrannyGetWorldPoseComposite4x4Array直出);② 过完EterGrnLib的 LOD 骨骼裁剪 +ActorInstanceBlend混合后的最终矩阵。libgr2 + 我们的采样先对①,装到引擎后对②。 - mesh 0 蒙皮后的顶点坐标;
- 确定态截图(固定相机矩阵注入、固定光、精确 model clock、单动作无混合、关掉待机摇摆 —— 需 oracle build 少量改造)。
坐标空间必须两侧都写死:oracle dump 和 libgr2 输出都声明用同一约定(左手 Y-up、单位、根变换是否已 apply)。§08 的 basis 转换高危项,就靠这条数值锚点来抓 —— 目视抓不住 90° 轴交换或 ×100 缩放。
oracle 自身要先过自检(在被信任之前):加载一个 gr2、dump t=0 的骨骼矩阵,应与该 gr2 存的 bind pose(InverseWorld4x4 求逆)一致。过不了这条,下游全建在沙子上 —— 这是 M0 的隐性前置门禁。
这套 dump 是 Windows-only,永远留在 Windows。oracle/ 的搭建(把 EterGrnLib 从完整客户端里单独抽出来编、两层矩阵 dump、确定态改造)从 M0 起,是关键路径,需要一个会 Windows / D3D 构建的人,§09 工作量表里单列。
数值层
阈值不是拍的,要先测噪声地板:把 oracle 跑两遍(或 oracle vs 另一个已知正确的 Granny 实现),看同一输入的输出抖动有多大,ε 设在地板之上一个安全余量。否则正确代码也会 fail —— 60 骨链上 float32 累积、Granny 内部用 float、我们用 double 再转,1e-4 / 1e-3 这种数很可能比噪声还紧。
| 对象 | 隔离的是 | 比对方式 | 阈值 |
|---|---|---|---|
gr2 结构(骨骼数 / 父索引 / 名称 / LocalTransform / 顶点数 / 索引数 / 每骨关键帧数 / 动画时长) |
解析正确性 | libgr2 vs oracle dump vs Blender 导入,三方一致 | 相等(浮点字段 < 噪声地板) |
| 骨骼世界矩阵(先对裸 Granny 层,装进引擎后对 EterGrnLib 层) | 动画采样 + 世界姿势累积 | 逐元素与 oracle 相减,取 max|Δ| | < ε_mat(噪声地板 × 余量) |
| 蒙皮顶点坐标 | 蒙皮 + 顶点格式(在矩阵已对上的前提下) | N 帧 × M 顶点,‖v_ours − v_oracle‖ |
< ε_vtx |
必须按序:先让骨骼矩阵对上,再看顶点。否则动画错 + 蒙皮错可能相互抵消、最终顶点却"对",掩盖两个 bug。 在整个测试资产集上跑,不是单文件。
tie-break:矩阵 / 顶点对拍默认 oracle = 真值。但如果 libgr2 与 oracle 差超过 ε、结果又都合理,需要第三个独立参照来判 —— 在测试资产集上,用 Blender io_scene_gr2 计算的世界矩阵(或第二个开源 gr2 库)做仲裁。这也能抓出 oracle 自己的 bug(DLL 版本选错、确定态改造引入坐标错)。
视觉层
- 确定性场景 — 固定相机矩阵、固定光、固定姿势(idle 第 0 帧、走路中段、旋转量大的姿势)。让 oracle 进入这个确定态需要改 oracle build(注入相机矩阵、精确 model clock、关程序化摇摆、单动作),这块工作在 M1。
- 贴图从对比里剔除 — 对比场景两侧都喂同一张预解码 RGBA。客户端用 GPU 硬件 S3TC、PoC 用
CDXTCImage软解,两者 bit 级有差,会让整张图偏移、SSIM 掉分,原因跟要验证的几何/蒙皮无关。 - 单一 SSIM 阈值不够,要差异分类 — 同一帧 Metal vs GLES 会有"正确但可见"的差别(各向异性过滤强度、mip 选择、gamma)。流程:
SSIM ≥ 0.98直接过;0.95–0.98之间进人工/规则分类,判定差异是否全落在"渲染器合理差异"集合里,是则过、否则 fail;< 0.95直接 fail。 - 三端互拍 — 三个端之间应比各自与 oracle 更接近(同一 shaderc 源、同一逻辑)。
- 失效模式清单 — 法线 / 背面翻转、UV 接缝、绑错骨骼(肢体飞出)、顶点塌陷(权重错)、z-fighting、alpha 排序、gamma / 色彩空间、左右手系镜像、整体缩放错(basis 转换,数值层才是主抓手)。
性能层(真机)
- 两种角色都要测 — ① 简化角色(≈ 5k 三角、≈ 60 骨、单 draw call);② 满配角色(多部件装配 + 武器 + 时装 + 挂点特效,1–2 万三角、多材质多 draw call)。只用简化角色外推真实负载会乐观。每种从 1 个放大到 8 / 20 / 50 个。
- 基准硬件写死具体机型(不是"级别")—— 例如 iPhone 11(A13)+ Pixel 6(Mali-G78)+ 一台 Adreno 机(如 Redmi Note 系列),三种主流移动 GPU 架构各一。
- 指标 — 帧时间 p50 / p99(ms)、FPS、draw call、GPU 显存(Xcode GPU report / Android GPU Inspector /
adb dumpsys meminfo)、冷启动到首帧、单个.gr2加载耗时、峰值 RSS。 - PoC 通过线:简化角色 1 个 ≥ 60fps、20 个 ≥ 30fps;满配角色 1 个 ≥ 60fps、8 个 ≥ 30fps;单角色加载 < 30ms;冷启动 < 3s;基准 RSS < 300MB。
- M4 加做 CPU vs GPU 蒙皮对比。
自动化 / CI
gr2-fuzz— 解析全部 9166 个.gr2,不许崩溃,产物通过非退化谓词(骨骼数 ∈ [1,512]、父索引 < 自身或 -1、无 NaN/Inf、索引 < 顶点数、权重和 ∈ [0.99,1.01]、骨骼索引在范围内);输出格式变体直方图。这一步最先抓出"某批文件是另一种变体"。- 快照回归 — 在 macOS CI runner 上用 bgfx 离屏渲染确定性场景,PNG 与提交的 golden 带容差比对。
- 三端冒烟 — CI 里 iOS 模拟器 + Android 模拟器构建并启动,断言"到达首帧 + 连续 N 帧无 bgfx / GL / Metal 验证层报错"(打开
BGFX_DEBUG_*、Metal API validation、GLESKHR_debug)。
测试资产集(精选约 15 个)
由 M0 的 fuzz 元数据 + 人工挑选产出,写进 test/assets.list,后续所有里程碑都跑这一套:
- 刚体静态件(zone 建筑)
- 单材质蒙皮角色(
warrior_novice) - 多材质 + 带 alpha 的蒙皮角色(时装)
- 多部件装配角色 + 挂点武器(考验
.msm多 gr2 组装 +GrannyFindBoneByName+ 挂点矩阵) - 高骨骼数 / 长动画
- 同一模型的全部 4 级 LOD(考验 LOD 间骨骼绑定一致性)
- 一个用了
D3DTOP_MODULATE2X或ADDSIGNED的多 stage 材质(专验texture_stage.sh) - 特效 / 挂点网格
- fuzz 阶段标出的异常个例(曲线子类型、压缩变体、超大骨骼数等的代表)
08 · 风险登记册
严重度:高 可能否决方案或大幅拖延 · 中 需专门投入 · 低 有成熟解法。
| 领域 | 风险 | 影响 / 缓解 | 严重度 |
|---|---|---|---|
| oracle | granny2.dll 版本与资产格式不匹配(资产 v6,src 头写 2.11.8) | 拿错误参考校 libgr2,全盘失真。M0 起手锁定"能正确加载这批资产的 DLL 版本",固定进 oracle/(见 §05)。 |
高 |
| gr2 | section 全部 Oodle1 压缩(M0 T1 实测确认,非可选);个别文件可能混 Oodle0 / BitKnit | Oodle0/1 有公开重实现(~200 行 LZ 变体),M0 T2 必做;BitKnit 无公开实现 —— 9166 全样本 0 个,若真遇到 → Windows 侧 Granny SDK 离线转未压缩。 | 中 |
| gr2 | 自描述类型树递归 / 未知类型 / 重定位表 | 写通用类型树遍历器(不假设布局)+ 与 SDK dump 逐字段对拍。 | 中 |
| 动画 | Granny 曲线压缩格式多样(关键帧数组、D3/D4nK 量化、B 样条拟合) | 三层退路:① M0 统计实际用到的子类型,多数只需实现关键帧 + 简单量化;② granny_curve*.cpp 当算法参照;③ 子类型超范围就走"oracle 离线烘焙成密集关键帧"(每骨每帧一个 TRS),libgr2 只读烘焙格式,PoC 完全绕开曲线解码,代价是动画数据变大 + 一个烘焙工具。加了 ③ 后此项可视为"中"。 |
高 → 中(有 ③ 兜底) |
| 动画 | ease-in/out 曲线、loop、局部时钟语义 | 照 EterGrnLib/Motion* 与 ModelInstanceMotion.cpp 的调用序列逐行复刻。 |
中 |
| 蒙皮 | 骨骼绑定顺序 / mesh binding 到骨架的重映射 | 复刻 GrannyNewMeshBinding 的按名匹配逻辑;M2 门禁先过骨骼矩阵、再过顶点,逐级隔离。 |
中 |
| 坐标系 | Granny 轴向 / 单位 → 运行时约定的 basis 转换(一个 90° 轴交换或 ×100 缩放目视看不出) | 主抓手是数值锚点:oracle dump 与 libgr2 输出都声明同一坐标约定(手系、单位、根变换是否 apply),骨骼矩阵逐元素对拍即可暴露;复刻 GrannyConvertSingleObject;朝向明确的资产做辅助目视。 |
高 |
| 集成 | sokol_app + bgfx 都想拥有 swapchain/context,非 sokol_app 常规用法 | M1 独立子门禁验证交接;退路是桌面 SDL2 + iOS/Android 各写最小原生壳(见 §03 窗口层)。 | 中 |
| 验证 | texture_stage.sh 是未校验的 D3D 固定功能重实现(MODULATE2X clamp、arg complement、多 stage 串联) |
用一个已知多 stage 材质在 M1/M2 单独和 oracle 对拍,不靠整场景 SSIM 兜。 | 中 |
| 验证 | 数值阈值 ε 拍脑袋,正确代码也可能 fail | 先跑 oracle 两遍测噪声地板,ε 设在地板 + 余量之上(见 §07 数值层)。 | 中 |
| 贴图 | 移动端无 S3TC / DXT 硬件支持(确定项) | PoC 运行时解压 DXT→RGBA8(复用 EterImageLib,显存 ×4);量产改离线转 ASTC / ETC2 或 Basis Universal(bgfx bimg/texturec)。 |
高 |
| 贴图 | NPOT / mipmap 链 / sRGB | GLES3 / Metal 均支持 NPOT;统一线性工作流 + sRGB 纹理视图。 | 低 |
| RHI | 深度范围 0–1 vs −1–1、裁剪空间 Y 翻转、行 / 列主序矩阵 | bgfx 有 bgfx::getCaps()->homogeneousDepth / originBottomLeft 抹平后端差异;矩阵全程列主序(glm);离屏 RT 用 bgfx 的约定。 |
中 |
| 着色器 | shaderc 跨编译到 metal / 300_es / s_5_0 行为差异 |
单一 .sc 源;开 bgfx / Metal / GLES 验证层;三端快照对拍。比手写多份 GLSL 更省心。 |
低 |
| 着色器 | .sc 草稿缺蒙皮 / fog / 顶点色,且 .bin 是废产物 |
从 .sc 重编;新写 vs_pnt_skinned.sc,补 fog 与 a_color0;compile_shaders.bat 换成 CMake custom command。属已知工作量,非未知风险。 |
低 |
| 精度 | 移动 GPU mediump 存不下骨骼矩阵 / 大坐标 | 顶点着色器里骨骼矩阵与位置用 highp;必要时把模型原点归一。 | 中 |
| 平台 | Android GL context 丢失后 GPU 资源需重建(确定项) | 在 sokol_app 挂起 / 恢复事件里按 bgfx 的 reset 流程处理;M3 就做,别拖到最后。 |
高 |
| 平台 | iOS 全静态链接、无 dlopen、后台丢 Metal drawable | 全静态;Metal 生命周期照 sokol_app 处理。 | 中 |
| 平台 | 9166 个散装资产上真机的方式没定(APK/bundle 体积、iOS 限制) | M3 前定:提前接 eterpack,或只打测试资产集。M3 交付要显式包含这一步。 | 中 |
| 构建 | bgfx 依赖 bx / bimg / bgfx 三仓 + 单一 CMake 出三端 |
用 bgfx.cmake 封装,submodule 固定版本;一开始就配好 toolchain + CI 三端并行。 |
中 |
| 授权 | libgr2 部分模块是从泄露 Granny SDK 直接端口(oodle1.c = radlz.c/radarith.c/arithbit.c 的解码路径逐行移植,非"看规格重写");其余(文件头 / 类型树 / 骨架)是照 struct 定义写的 |
比 clean-room 弱。对"内部自用、不公开、非商业"可接受;oodle1.c 顶部注明来源。一旦要公开 / 商用:oodle1.c 必须换成真正的 clean-room 实现或第三方 LZ(gr2 里的 Oodle1 只是个简单 LZ+算术编码,可重写),Granny 运行时逻辑评估 ozz-animation 替换或采购授权。 |
中 |
| 范围 | PoC 蔓延到地形 / 特效 / UI | 严守"骨骼蒙皮 + 三端跑通"边界;地形 / 特效 / Python 明确排除在 M4 之外。 | 中 |
| 范围 | Python/UI 层是仅次于 gr2 的第二大未知,被本 PoC 排除 | CPython 3 在 iOS(无 JIT / 全静态 / 脚本预编译)跑 Metin2 那 2000+ 个 .py + EterPythonLib C 扩展 + 依赖 D3D 图元的 ui.py 窗口系统 —— 这是 PoC 之后要立刻做的"第二个 PoC",不是"已知可行的工作量"。 |
高(对完整移植) |
09 · 工作量 · 人力 · 目录
工作量
| 工作项 | 单人全职 | 2 人小队 |
|---|---|---|
| oracle 工具(Windows:抽出 EterGrnLib 单独编、锁 Granny 版本、两层矩阵 + 顶点 dump、确定态改造、自检) | 1–2 周(与 M0 并行,含在关键路径) | 由会 Windows/D3D 的人专责 |
| M0 gr2 解析器 + fuzz + 结构化对拍 + 筛测试资产集 | 1–2 周 | 合计 ≈ 6–8 周(libgr2 / oracle+验证台 / RHI+壳 三线并行) |
| M1 macOS 静态渲染(含 bgfx↔sokol_app 交接子门禁 2–4 天) | 2–3 周 | |
| M2 动画 + CPU 蒙皮(曲线解码是关键路径) | 2–3 周 | |
| M3 iOS + Android 真机 | 1–2 周 | |
| 到 go/no-go 结论 | ≈ 2–3 个月 | ≈ 6–8 周 |
| M4 逼近真实负载(可选) | +2 周 | +1–2 周 |
人力硬约束:oracle 那条线需要一个能在 Windows 上搭 D3D9 客户端构建、把 EterGrnLib 拆出来单独链的人 —— 这和写 libgr2 / RHI 的技能不同,单人做要来回切换,小队要专人。
关键路径是 libgr2 曲线解码(M0 → M2)+ oracle 工具(M0) 两条并行。
建议目录结构
xrender-poc/
third_party/
bgfx.cmake/ bx/ bimg/ bgfx/ (submodule,含 shaderc / bin2c)
sokol/ 仅 sokol_app.h
glm/
reuse/ EterBase/ EterPack/ EterImageLib/ ← 冻结快照 + 本项目修复提交(非引用)
libgr2/
include/ src/ gr2_file.* gr2_types.* gr2_skeleton.*
gr2_mesh.* gr2_anim.* gr2_decompress.*
engine/ rhi.*(包 bgfx::) skinning.* animation.* material.* scene.* camera.*
formats/ msm.* msa.* race.* ← 照 EterGrnLib/RaceManager 复刻
app/
main.c 窗口壳:默认 sokol_app → bgfx::PlatformData → bgfx::init
交接过不了则切 SDL2(桌面)+ 各平台最小原生壳(见 §03)
shaders/ *.sc + texture_stage.sh + varying.def.sc
← 从 ../MobileSource/Cross Platform/shaders.rar/shaders 种子
+ vs_pnt_skinned.sc(新写)
shaders.cmake 可移植编译步骤:shaderc → metal / 300_es / s_5_0 → bin2c
platform/ macos/ ios/ android/ (壳工程 + toolchain)
oracle/ Windows-only:锁定版 granny2.dll + EterGrnLib
dump 两层骨骼矩阵 + 蒙皮顶点 + 确定态截图;坐标约定写死
tools/ gr2dump/ (M0 CLI) gr2fuzz/ snapshot_compare/ anim_bake/(曲线退路)
test/ assets.list(M0 产出) golden/ oracle_dumps/ noise_floor.json
cmake/ ios.toolchain.cmake android helpers
CMakeLists.txt
下一步
从 M0 起,两件事并行:
- 锁定 oracle 的 Granny 版本 —— 确认这批 v6 资产在哪个
granny2.dll下GrannyGetFileInfo完全正常,固定进oracle/。 - 写
gr2dump—— 输出warrior_cheongrin*.gr2的结构化 dump(骨架 / 网格 / 曲线)+ glTF;结构化 dump 与 oracle 逐字段对拍(这是主验证),glTF 仅供 Blender 目视骨架和 bind pose。
这是单块价值最高、且不依赖任何 RHI / 平台决策的第一块砖。
10 · 最终验收与结论证据包
"最终成果如何验证"分两层,PoC 与完整移植的验收方式不同。
| PoC 的最终成果 | 完整移植的最终成果 | |
|---|---|---|
| 是什么 | 一份 go/no-go 结论 + 证据包(不是能玩的客户端) | 能上架的三端 Metin2 客户端 |
| 验证目标 | "骨骼网格这条链在三端成立" 站得住、可复现 | 功能对等 + 三端一致 + 性能 / 稳定性达标 |
10.1 PoC 结论证据包
PoC "通过"的判据不是"看着对了",而是能交给别人独立重跑并核对的一个包。仓库 test/ 下产出:
① 可复现验证套件(CI 一条绿灯 = 全部过)
gr2fuzz报告:9166 / 9166 解析成功 + 格式变体直方图 —— 证明"能读全部资产",不是挑了几个好文件。numeric_diff报告:测试资产集 × N 帧 × M 顶点,骨骼矩阵max|Δ|与蒙皮顶点‖Δ‖的分布(直方图 + p99 + max),全部 < 阈值。snapshot对比:确定性场景在 Windows-oracle / macOS / iOS / Android 四组截图 + 两两 SSIM 矩阵。perf报告:每台真机 1 / 8 / 20 / 50 角色的帧时间 p50/p99、FPS、显存、RSS、冷启动、gr2 加载耗时,逐项标注是否越线。- 三端冒烟:
macos / ios-sim / android-sim三个 CI job 全过,无 bgfx / Metal / GLES 验证层报错。
② 门禁汇总表 —— M0–M3 每道门禁一个明确 pass/fail(见 §06),最终成果 = 全绿的汇总。
③ 反证清单(negative evidence) —— 把当初担心会 block 的点逐条列出实测结果。下表是待填模板,结论由 M0–M3 产出,不是现在已知的事实:
| 担心点 | 实测结果(TBD) |
|---|---|
| oracle 用的 Granny 版本 + oracle 自检 | 待 M0 |
libgr2 InverseWorld4x4 自洽(world_bind · invBind ≈ I) |
待 M0 |
| Granny 曲线子类型分布 / 是否需烘焙退路 | 待 M0 fuzz |
| 非单位 scaleshear 用量(影响蒙皮公式 + mediump) | 待 M0 fuzz |
| section 压缩类型分布 | 待 M0 fuzz |
.msm / .msa 解析对拍源文本 |
待 M2 |
| 坐标系 basis 转换(数值锚点 + Blender tie-break) | 待 M1–M2 |
| sokol_app + bgfx 交接 | 待 M1 子门禁 |
texture_stage.sh 多 stage 材质对拍 |
待 M1 |
| ε 噪声地板实测值 | 待 M2 |
| Android context loss 恢复(×100) | 待 M3 |
| iOS 全静态 + 后台 / 锁屏 / 来电恢复 | 待 M3 |
| 移动 GPU mediump 精度专项 | 待 M3 |
④ 一页纸结论 —— go / no-go:
- go → 附"正式移植还缺什么"清单(地形 / 特效 / UI / Python 的工作量估计)。
- no-go → 具体死在哪层 + 备选路径(ozz-animation 替换 / Rust 重写 / 动画烘焙退路)。
10.2 完整移植后的验收(属 PoC 之后另立项目,不在本方案范围)
以下 8 条是 PoC 通过、决定正式移植后要另写的验收纲要,本方案不覆盖其计划与工作量。PoC 只证了骨骼网格这一条。
- 资产全覆盖回归 —— 不是 15 个测试资产,而是全部 gr2 / 全部地图 / 全部特效 / 全部 UI 脚本跑一遍,自动截图 + 与 Windows 客户端逐场景对拍(把 oracle 从"单帧 dump"扩成"整机录制回放 + 全场景截图")。
- 功能对等清单 —— 登录 → 选人 → 进游戏 → 战斗 → 交易 → 公会 → 商城…每个 Python phase 在三端都能走通(手动 + 录制回放)。
- 三端一致性 —— 同一操作序列,三端渲染 + UI 布局 + 逻辑结果一致。
- 性能预算 —— 目标机型上完整场景(城市、BOSS 战、大量玩家)达到帧率 / 内存 / 发热 / 耗电 / 包体 / 流量预算。
- 稳定性 —— 三端各跑 N 小时 monkey / soak;崩溃率 < 阈值;context loss、来电、切后台、锁屏、热重启全过。
- 兼容性矩阵 —— iOS 最低版本 × 机型;Android GPU(Adreno / Mali / PowerVR)× API level × 厂商 ROM。
- 上架前检查 —— iOS 无私有 API、隐私清单、包大小;Android 64 位、target SDK、权限。
- 回归防线 —— CI 每次改动跑快照回归 + 性能基准,防"某次改动让 Mali 上花屏"这类回归。
10.3 贯穿始终的一条原则
Windows 客户端是唯一真值源。 任何"三端自己看着对"都不算数,必须能和 Windows 逐帧 / 逐场景对拍。 这套 oracle 基础设施从 M0 就开始建(先只 dump 骨骼 + 顶点 + 单帧),移植阶段扩成整机录制回放 —— 是整个项目最值的一笔投资。