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
13 KiB
Phase 1 中期评审 — mtgodot-poc
日期:2026-08-29(评审后又做了一轮收尾,见 §4/§5 的「已做」标注)· 平台:macOS(Apple M2)· Godot 4.7.1 · 对标
GODOT-POC-PLAN.md§00 Phase 1 结论先行:桥接层工程量小、风险已基本出清,建议进入 Phase 2(一加 13 / iPhone 16 三设备 bring-up,自用)。M2.5 材质门禁已闭(贴图走 gr2 material 绑定 + 逐 tri_group 拆 surface);仅剩逐材质 oracle 纸面对拍这一可选项。
1. 做到了什么(M0'–M2.5)
| 里程碑 | 状态 | 证据 |
|---|---|---|
| M0' 脚手架 | ✅ | GDExtension 加载、Metin2Model/Metin2AnimPlayer 注册、libgr2 静态链接、Godot 4.7 工程 Metal Forward+ 出画 |
| M1 静态渲染 | ✅ | warrior_cheongrin:75 骨 / 5 表面 / 3324 顶点,正立、朝 +Z、unit_scale=0.01、flip_z/flip_winding=false、贴图正确(test/golden/m1-bindpose.png)。多骨架:warrior_lord(v6,74)、assassin(v6,95,17 表面)、shaman_lord(v7,93) 均加载渲染正确 |
| M2 骨骼动画 | ✅ | gr2::sample_pose(跨文件)→ 蒙皮,两条路径都用 libgr2 的 skin 矩阵完整仿射(shear 保留):默认 CPU LBS(每帧重建 mesh);MTGODOT_GPUSKIN=1 走自写蒙皮顶点着色器(m2_material SRC_SKIN,逐骨 4×3 矩阵存 RGBAF 纹理,绕过 Skeleton3D)。dance_1 两条路径头/护甲/躯干都正确、与 xrender-poc bgfx demo 一致。selfcheck 0 NaN。Skeleton3D::set_bone_pose 路径 |
| M2.5 材质与观感 | ✅ 门禁已闭(材质精确映射);观感打磨留 Phase 2 | 4 个 ShaderMaterial shader:SRC_MIX(opaque / alpha / alpha-test)· SRC_SKIN(同 + GPU 蒙皮 vertex)· SRC_ADD(additive)· SRC_ADD_SKIN。贴图 = gr2 material 绑定(Mesh::material_textures[matidx],mesh-local)→ 找不到才 _resolve_texture 文件名启发式;build_parts() 按 tri_group.material_index 把一个 gr2 mesh 拆成 N 个 surface(shaman 1→2、assassin 17→18、warrior 5→5,实测各面贴图正确)。blend/cull/alpha 靠 guess_blend() 按表面名启发式(effect→add、hair→alpha-test、cloak/cape/skirt→alpha)。方向光阴影 + procedural sky + MTGODOT_FOG=1 引擎雾(test/golden/m25-*.png)。 |
2. 桥接层工程量(实测)
extension/src 现 ≈ 1997 行(不含 godot-cpp;含端口自 xrender-poc 的 DXT 解码 ~180 行)。
中期评审时约 1100 行,评审后收尾又加了 ~700(build_parts 拆 surface、SRC_SKIN GPU 蒙皮着色器、
.msa/.msm 解析、路径解析、逐 surface 材质缓存)。
| 文件 | 行 | 职责 |
|---|---|---|
metin2_model.{h,cpp} |
~870 | Metin2Model 节点、.gr2/.msm 加载、build_parts 拆 surface、贴图解析、材质、CPU/GPU 蒙皮、属性 |
metin2_anim.{h,cpp} |
~420 | Metin2AnimPlayer、每帧 sample_pose → CPU/GPU 蒙皮、.msa 解析 + motion event 派发、selfcheck |
gr2_bridge.{h,cpp} |
~290 | gr2 Mat4→Transform3D(4×4 转置)、build_skeleton/build_skin、build_parts+build_mesh、Z-up→Y-up make_conv |
m2_material.{h,cpp} |
~235 | 4 个 ShaderMaterial shader(SRC_MIX / SRC_SKIN / SRC_ADD / SRC_ADD_SKIN)+ 退出前 cleanup_material_shaders |
dxt.{h,cpp} |
~180 | DDS DXT1/3/5 软解(端口,level 0) |
register_types.{h,cpp} |
~40 | GDExtension 入口 |
libgr2(vendored)新增 gr2_material.cpp + Mesh::material_textures |
~150 | dump_materials() + 逐 mesh MaterialBindings→贴图名 |
耗时:M0'–M2.5 一次会话跑通,评审后收尾一次会话。与计划 §05 估的「3.5–5 周单人」相比 核心桥接远比预估轻 ——
libgr2 已完成、Godot 的 Skeleton3D/ArrayMesh/Skin 直接可用;蒙皮因 Godot 内置路径丢 shear(见 §4)
改为自己做(CPU LBS 默认 + 自写顶点着色器),也只是 ~200 行。
3. 关键技术结论
- 坐标/单位:gr2 行主序 Mat4 → Godot
Transform3D就是一次 4×4 转置(gr2 平移在第 4 行 = Godot.origin)。Z-up cm → Y-up m(rotate(-90°,X)+scale(0.01))放在Metin2Model节点自身 transform。四个测试骨架全部flip_z=false、flip_winding=false正确 —— Metin2/Granny 内容对 Godot 右手系不需要额外镜像。 - 动画走 B 方案 + 自做蒙皮:
_process里sample_pose出world[]/skin[](skin[i] = inverse_world[i]·world[i])。Godot 内置蒙皮(Skeleton3D+set_bone_pose)不能用 —— 它把每骨 transform 分解成 position + 单位四元数 + 逐轴 scale,丢掉 Granny 烘在 rig 骨上的 shear(§4)。改为:默认 CPU LBS(Metin2Model::cpu_skin,每帧把skin[]逐顶点作用、重建ArrayMesh);MTGODOT_GPUSKIN=1走自写蒙皮顶点着色器(SRC_SKIN,逐骨完整 4×3 矩阵存 RGBAF 纹理)。set_bone_global_pose/Skeleton3D蒙皮路径已删。不烘Animation资源(A 方案走Skeleton3D骨骼、同样丢 shear,不用)。 - 贴图:Godot 4
Image.load()不支持.dds→ 运行时软解 DXT(dxt.cpp,level 0 +generate_mipmaps())。.gr2用.gdignore+ 绝对路径加载,绕开 Godot import。 - 移动端预判:目标设备定为 Mac(Metal)+ 一加 13(Vulkan)+ iPhone 16(Metal),均现代硬件,不涉及 Compatibility(GLES3)。CPU LBS 路径(默认)+
ShaderMaterial+ArrayMesh无平台特性风险。自写蒙皮着色器用了texelFetch+ RGBAF 纹理 +BONE_INDICES—— Vulkan / Metal 都支持,Phase 2 在一加 13 / iPhone 16 各跑一次确认(出问题则人群场景回落 CPU 或改 uniform 数组传骨骼)。
4. 剩余风险 / 门禁开口
| 项 | 影响 | 出路 |
|---|---|---|
tools/anim_probe 全时间轴扫 dance_1 240 帧,libgr2 的 world[] 全部良态(0 奇异 / 0 NaN,Head/Neck 是干净刚体),且 oracle/run-diff-suite.sh 已对拍真 Granny @ t=0/4.7/11.3/20 ≤4.1e-5(含 d=2 四元数曲线)。真因:Granny 给每个 Metin2 模型的 ~7 根骨(bone_front_01..04、Bone_shoulder_01..04 —— 前襟条 + 护肩片)烘了 shear + 非均匀缩放(world det 低到 0.065),而 Godot Skeleton3D::set_bone_pose 把 transform 分解成 position + 单位四元数 + 逐轴 scale,shear 被静默丢弃(TRS 往返误差 0.07–0.94)→ 那几根骨塌。wait 同样有这 7 根坏骨,只是动得少不明显(不是 dance_1 专属)。 |
已消除:① CPU 线性混合蒙皮转默认路径(Metin2Model::cpu_skin),直接把 libgr2 的 skin[] 逐顶点作用、绕过 Skeleton3D;② MTGODOT_GPUSKIN=1 走自写蒙皮顶点着色器(SRC_SKIN / SRC_ADD_SKIN,逐骨完整 4×3 矩阵存 RGBAF 纹理),不正交化、shear 保留。旧 set_bone_pose 路径已删。headless 对拍:dance_1 @ t=7/19 两条路径头/颈/躯干与 xrender-poc bgfx demo 一致。 |
已完成。代价:CPU 路径每帧变顶点 + 重建 mesh(人群切 GPU 着色器路径)。anim_probe 扫全 race:warrior 7 根、sura 16 根 shear 骨(test/shear-bones-survey.md),故自写着色器是必需、非优化。 |
tri_group.material_index 是 mesh-local 索引(进 Mesh::MaterialBindings),不是全局 Materials[] —— 原来用 dump_materials()[matidx] 才把 body/face 搞混。改用 libgr2 的 Mesh::material_textures[matidx](与 MaterialBindings 平行、xrender-poc session 里已按渲染结果验过 warrior/sura/assassin/shaman 全对)。use_gr2_materials 默认开、文件名启发式降为回退。 |
已消除。剩:跑一次 oracle/run-diff-suite.sh 级别的逐材质对拍做纸面确认(可选,观感已对);MODULATE2X/ADDSIGNED 等 stage op 仍只近似 |
|
gr2_bridge.cpp 加 build_parts(),一个 gr2 mesh 有 N>1 个 tri_groups 就拆成 N 个 Godot surface(各自 material + 索引子区间)。build_mesh 和 CPU 蒙皮路径都按 RenderPart 走。shaman(1 mesh→2 surface)、assassin(17→18)实测各面贴图正确。 |
已做 | |
test/compare.py(2026-08-29) |
oracle gate(读 test/m2-numeric.json,libgr2 vs Granny 23/23 ≤6.5e-5)+ 每 case headless 跑 mtgodot,判据:进程退出码 0、截图存在、surfaces>0、动画 selfcheck NaN/Inf=0(CPU 与 GPU 两条路径各一遍)。产出 .cpu.png/.gpu.png + test/compare-report.json。渲染器 / 相机不同,不做与 bgfx-reference 的像素比。缺:带容差的自动视觉回归门禁(BACKLOG H3)。 |
基本完成 |
| 性能 | 前台窗口 M2 重测(test/godot-macos-stress.json,默认 CPU LBS):1 角色 46fps、10 角色 36fps、30 角色 24fps。CPU 路径瓶颈是每帧 cpu_mesh 全量重建(不是 LBS 数学)。构建仍 ~170ms/角色(单线程无缓存)。GPU 蒙皮着色器已实现(MTGODOT_GPUSKIN=1,每帧只更新一张骨骼纹理),人群场景应切它 —— stress harness 未用 GPU 路径重跑。 |
Phase 2:GPU 路径跑人群 bench + 多线程构建 + eterpack 共享资产 + 一加 13 / iPhone 16 实测。 |
| 剩余小项 | cleanup_material_shaders,register_types 卸载时调)· alpha 排序 corner case(render_priority 粗排)· 背面剔除写死 cull_back(Metin2 有 CULL_NONE 面)· _frame_model 对披风类框选偏 |
均不阻塞,见 BACKLOG.md(A3 A6 B10 E5)。法线用混合矩阵上 3×3 不逆转置 —— 与 Granny runtime 一致,是正确行为不是风险 |
5. 建议
进入 Phase 2(一加 13 / iPhone 16 三设备 bring-up,自用)。
理由:Phase 1 要证明的三件事里,①gr2→Godot 场景对象、②动画运行时移植,已用可运行代码 + 截图 + 0-NaN 自检证明可行且工程量小;③观感还原:自定义 shader、逐 tri_group 材质、阴影、sky、fog、多骨架已就位,M2.5 门禁(材质精确映射)已闭,剩余是观感打磨(多 stage op / cull / 阴影调优),不构成方案性风险。
Phase 2 前的收尾清单 —— 大部分已在 2026-08-29 做完:
- ✅
test/compare.py—— oracle gate + 每 case 退出码 / 截图 /surfaces>0/NaN=0判据(CPU + GPU 两路)。compare-report.json。 - ✅ M2.5 材质门禁 ——
build_parts()按tri_group.material_index拆 surface + 用Mesh::material_textures(mesh-local)取贴图。shaman/assassin 多材质面正确。逐材质 oracle 纸面对拍可选。 - ✅
修 libgr2 曲线误判 —— CPU 蒙皮转默认(见 §4)。 - ✅ 前台性能重测(
test/godot-macos-stress.json):30 角色 24fps;瓶颈是每帧 mesh 全量重建。 - ✅
anim_probe全 race 扫(test/shear-bones-survey.md):warrior 7 根、sura 16 根 shear 骨;assassin/shaman 干净。据此已实现自写 GPU 蒙皮顶点着色器(SRC_SKIN,逐骨完整 4×3 矩阵)——MTGODOT_GPUSKIN=1现在走它、不再走坏的Skeleton3D。warriordance_1GPU 路径与 CPU 一致(test/compare-shots/*.gpu.png)。 - ✅
.msa接线:anim_path收.msa→MotionFileName+Accumulation(get_accumulation)+MotionEventData(get_events()+motion_event信号,带循环回绕)。throw.msa事件 t≈0.83 触发。 - 🟡
.msm接线:gr2_path收.msm→ 解析 + 解析BaseModelFileName(含 eterpack 散包目录扫描)自动加载 base model +get_hair_options()发型目录(75 项)。warrior_w.msm实测。发型 mesh 实际挂接仍 ⬜ —— 需 base 骨架的逐帧矩阵(与 anim player 共享),属 Phase 2 模型组合。
Godot route 桥接层已验证正确:数值真值 =
oracle_diff(libgr2 vs 真 Granny 2.9.12,23 用例 ≤6.5e-5), 两条蒙皮路径都逐顶点作用这批skin[]矩阵;CPU 与 GPU 渲染wait/dance_1与 xrender bgfx demo 抽帧一致。 原 #3「libgr2 曲线 bug」是误判,真因是 Godot 内置蒙皮(set_bone_pose/Skeleton3D)正交化丢 shear,已用自做蒙皮规避。 (旧文里的「get_bone_global_posevsconv(world)≤2.6e-5」是已删的Skeleton3D校验路径的数字,不再适用。)
不建议现在做的:地形、.mse 特效、eterpack、UI/Python —— 均按计划留到正式移植。
「离一个可用客户端还差什么」的完整分层清单见
BACKLOG.md(A Phase-1 收尾 / B 材质保真 / C 动画运行时 / D 模型组合 / E 渲染功能 / F 平台 / G 资产管线 / H 测试工程 / I libgr2 覆盖面),含条目 ID、粗估规模、排期。