Files
mtgodot-poc/docs/RENDERING-REPAIR-PLAN.md
T
shenleiandClaude Opus 5 e0c1853909 fix(net): 主角 VID 归参考端所有,GC_CHARACTER_DEL 不再把玩家「去主角化」
实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,2D UI 与小地图正常。

根因(已实证,非推测)。`EntityStore::mut_despawn()` 在 despawn 的 vid 等于
主角 vid 时把 `m_main_vid` 清零。40250 服务器在 spawn-in 头几秒会对玩家自己的
vid 反复下发 GC_CHARACTER_DEL + GC_CHARACTER_ADD(视野重算,实测约 10 次)。
第一次 DEL 就把玩家永久去主角化:`get_main_vid()` 返回 0 →
`_sync_main_character()` 直接 return → 建模代码从未跑过(main_attempts=0)→
占位胶囊留在 y=0 → 相机跟着它钻到 C1 地形底下 → 满屏灰紫。地图其实一直是好的
(chunks_built 20/20、objects_placed 565)。

参考端怎么做(40250 ClientVS22)。主角身份属于 `CNetworkActorManager::m_dwMainVID`,
只由 `SetMainActorVID()`(GC_MAIN_CHARACTER)写,只在管理器析构时清;
`RemoveActor()` 删 actor 行和角色实例,但绝不碰它;重新加入时
`__AppendCharacterManagerActor()` 用 `kCreateData.m_isMain = __IsMainActorVID(dwVID)`
重新推导,并再跑一次 `SetMainCharacterIndex(dwVID)`。

修法(entity_store.cpp):
- `mut_despawn()` 只删行、只发 Despawn,不动 m_main_vid;m2dev 分支的
  GC_CHARACTER_DEL 同规则。
- `mut_spawn_full()` 在 `e.vid == m_main_vid` 时把 `is_main` 置回 true,
  并重新广播 MainSet,对齐参考端的重新推导。

顺带落地的渲染修复(同一次实机排查中定位):
- libgr2 `sample_pose()` 增加 `root_offset` 形参;`Metin2AnimPlayer::apply_pose()`
  显式传单位阵而不是模型的 InitialPlacement。Metin2 动画 gr2 的根骨轨道自带骨盆
  高度,再乘一次 IP 会把整个人抬高一个骨盆高(武 99.4cm / 刺 92.6 / 巫 103.7 /
  萨 95.5,正好各自的 IP.z)——即此前的「人物悬空」。`bind_pose()` 语义不变。
- `Metin2Model::_load_hair()` 改用与身体/武器同一条 `make_material()`
  (specular_disabled)而不是 roughness=1.0 的 StandardMaterial3D,头发不再被
  PBR 高光洗成奶白色块。

取证与回归工具:
- `project/net_trace.gd`(新):主角 vid 生命周期取证。挂在 AppFlow 之后逐帧读
  `get_main_vid()`,归零时打印 `NETTRACE: MAIN_LOST was=.. now=0 reason=..` 加最近
  24 条环形事件;`reason` 区分 despawn 与 silent(reset_for_map_change 清空
  m_changes,是静默清零)。`MT_NETTRACE=1` 打开逐事件日志。
- `game_scene.gd`:诊断 tick 支持重复采样(分辨主角装配是「没跑」还是「没跑完」),
  新增 main_sync_ready / main_loading / main_view_key / main_map_vid /
  main_retry_in_ms 字段;`MT_DIAG_SHOT=<png>` 让游戏自己存一帧实机画面
  (screencapture 拍到的是终端窗口,没用)。
- `project/model_render_test.gd` + `test/rendering/model_baseline.json`(新):
  八个种族的模型渲染基线(--bless 重新落盘)。
- `project/char_bench_test.gd`(新):无网络无地图的单角色实验台,五变体 ×
  六机位出证据图,只产图不做阈值判定。
- `project/package_render_test.gd`(新):导出包内的渲染自检入口
  (导出模板不接受 --script,只能走 MT_TEST_MODE)。
- `/project/build/` 加入 .gitignore(渲染用例的落图输出)。

实机复验(真实服务器,非模拟):
- 修前 `LIVE_SMOKE FAIL: 主角色 VID 有效`;
  `NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn t=+7737ms`,
  环里紧跟 `spawn vid=25910 is_main=false`。
- 修后 `LIVE_SMOKE RESULT: PASS`,main_vid=25933、entities=19;
  40s 实机跑 16 次诊断采样全部 model_built=True / main_attempts=1 /
  main_sync_ready=True,player_pos 落在 C1 地形上而非 y=0;
  MT_DIAG_SHOT 存下的帧里地形、建筑、NPC、树木、真实角色模型、HUD、小地图俱全。

回归:gamescene_test / equip_model_test / gamescene_visual_test(feet_gap
0.001373)/ model_render_test(8 races)/ char_select_visual_test / 包内
MT_TEST_MODE=render 全绿;ctest 22/23,唯一失败 net.classic_session 已用
「还原本次改动 → 重跑 → diff 失败集合」确认与本次修改无关(失败集合完全相同)。

注:画面整体偏暗属于光照/色调差距,受 PARITY-GAP.md §0 门禁约束(对照帧未落盘前
不做相机/光照/色调/材质调参),本次不碰。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
2026-09-08 17:15:55 +09:00

35 KiB
Raw Blame History

新客户端渲染修复实施路线

更新日期:2026-09-08。目标客户端:本仓库 metin2-client,第一交付平台为 macOS。

本文是实施规格,2026-09-08 已开始第一批 R0/R1 修改;阶段结果见文末执行记录。未标明通过的工作仍待执行,不代表整体渲染或最终包完成。

1. 目标、依据与当前状态

第一交付目标:实际 macOS 客户端进入游戏后显示正确地图和真实人物,身体/发型/装备组合正常,人物在地面上,底部技能栏可用。之后再精细还原 40250 的材质、环境、树木、特效和性能。

画面真值优先使用用户当前 40250 Windows 客户端的同资源、同场景截图。EnriqueGF 的场景截图是可行性和场景完整性参考,不能直接当作 40250 像素真值;其主角背对镜头,也不能证明正面发型和所有动画正确。原始骨骼数值以固定版本 Granny oracle 的可复现实测为参照。

当前问题/证据 状态 实施要求
用户反馈进入游戏出现胶囊、背景异常 待真实客户端复验 检查地图、实体、相机与加载时序,不直接归因于模型格式
game_scene.gd 已加入 _sync_main_character()_main_model_attempted 工作区有补丁,尚未完成最终画面验收 检查晚到实体、失败重试、角色切换和重连的生命周期
本地 world_demo.gd 曾加载真实 A1 地图及模型 历史组件证据 只能证明这组资源链路可运行,不能证明联机 .app 正常
发型挡脸、人物悬空 用户反馈,根因仍需隔离 分别验证变换、蒙皮、深度、地形高度与动画接触点
底部技能未显示 用户反馈,已有 quickbar.gd 检查创建、布局、图标、服务端槽位恢复,不重新发明快捷栏
日志曾出现 unknown GC header 59 (last: 32,148) 历史协议阻塞,是否仍存在待复验 若重现,单列协议问题;不得跳过未知包继续解析来伪造联机通过
选角 flip_winding、材质及视觉测试已有未提交修改 实验状态 保留现有改动,记录基线,再逐项验证,不全局翻转或覆盖

历史 README / MIDREVIEW 的阶段完成描述不代表当前最终包通过。现有截图、日志在使用前检查是否存在、属于哪个构建和资源版本。

2. 两个上游项目的使用边界

本路线依据已经检查的固定提交;上游后续变化需要重新审查:

  • EnriqueGF/metin2-godot078d39a41a3ef17c9e09fb9807956cb1a2f16641
  • kcanakdag/metin2-godotime6a82d793c1d83e34f3e71200fdfc67a78bcb96d3
参考实现 本项目的应用方式 不应推导的结论
EnriqueGFtools/compare_visual_golden.py 引入区域、遮罩、误差阈值和差异图方法 截图存在或整体平均误差低不代表脸部正确
EnriqueGFtools/build_character_glb.pygodot/client/online/presentation/warrior_actor.gd 同一角色离线 GLB 对照;核对身体/发型/武器的组合和动作同步 转换包含 TRS 分解、舍弃 shear;不能假定动画无损
EnriqueGFgodot/client/online/presentation/avatar_registry.gd 借鉴有效实体数据驱动创建、切地图清理、重复数据处理 其快照协议不能直接替代我们的原版协议事件
EnriqueGFtools/prepare_legacy_ui_assets.pygodot/client/online/ui/ 核对 .sub 裁剪、原版布局和按钮状态 西班牙语素材、字体、窗口尺寸不能直接替代当前中文配置
EnriqueGFtools/audit_city_asset_dependencies.py 地图资源依赖追踪、缺失分类和报告 全部依赖可解析不等于全部已经正确渲染
godotimetools/export_playable.py 实际包资源清单、哈希与加载审计,适配 macOS Web/Linux 通过不代表 macOS 通过
godotimecontent/profiles/p0-warrior-dog.jsonclient/scripts/content/ 模型、动作、附件和依赖的统一验证清单 三个精选模型通过不代表四职业和全量换装通过
godotimetools/test_browser_accounts.py 借鉴双客户端、重连、换装及账户生命周期用例 不迁移其 SpacetimeDB 服务端来修复渲染

参考源码:

直接移植 EnriqueGF 原创代码时保留 MIT 声明和提交来源。godotime 在本次检查的提交没有项目级许可证,先借鉴方法;直接复制实现前明确授权。原始游戏资源的来源与使用范围沿用本仓库 THIRD-PARTY.md,不把上游许可证视为原版资产授权。

3. 技术路线与顺序

执行顺序:R0 基线 → R1 场景装配 → R2 模型/发型 → R3 材质/环境 → R4 UI → R5 最终包验收 → R6 性能与内容扩展。R4 可在 R1 后独立推进;R5 的资源清单工具可提前建设,最终验收必须等待 R1–R4 完成。

  1. 保留原版网络协议与当前 GR2 主链路,以最小改动修复实际缺陷。
  2. 引入独立 GLB 对照场景,不先全量转换资源或更换运行时。
  3. 对照决定具体资源采用哪条路径;结果需要记录,不能预设 GR2 或 GLB 必然更好。
  4. 角色与静态物件分别决策。静态物件可先试离线 Godot 资源缓存;人物保留完整仿射矩阵的需求单独评估。
  5. 每项修复必须在真实 GameScene 和导出包中复验。单模型预览只能作为中间证据。

4. R0:建立可重复基线

涉及现有文件:test/compare.pyproject/char_select_visual_test.gdproject/world_demo.gdproject/game_camera.gd

具体步骤

  1. 记录当前 Git 提交、未提交补丁摘要、Godot/导出模板版本、扩展库哈希、macOS/GPU、渲染后端、窗口和实际渲染分辨率。不能仅凭提交号标识脏工作区构建。
  2. 固定资产来源及哈希:身体、发型、武器、护甲、MSM/MSA、动画 GR2、纹理、地图和环境。两个上游与我们可能使用不同资源版本,必须先核对。
  3. 为每个场景保存地图名、服务器坐标、地图基点、转换后本地坐标、相机位置/目标/FOV、角色朝向、装备、动作及取样时间。
  4. 建立三种独立场景:无网络的单角色实验台;真实资产的 GameScene + FakeClient;实际服务器上的导出 .app
  5. 保存正面、侧面、背面和脚底近景,以及选角界面和游戏全景。待机/跑步/攻击每段至少取开始、中间、结束,过渡动作另测。
  6. 在现有截图工具基础上增加局部区域和差异图。脸、头发、脚底、技能栏不允许被动态遮罩排除;时间文字、随机粒子等可单列遮罩。
  7. 区分两类比较:同引擎修复前后用于自动回归;40250 对照用于保真评估。没有匹配的相机和动作时,只能做人工结构检查,不执行严格像素判定。

拟新增产物(尚未实现):test/rendering/cases.jsontest/rendering/README.mdtools/rendering/compare_regions.py。本地日志/截图/报告建议放 build/rendering/<run-id>/,不覆盖失败证据。

阈值:先在不改代码的条件下重复采样三次,测量阴影/抗锯齿噪声,再设置每区域阈值。报告包含 MAE、RMSE、变化像素比例和遮罩范围;不能为了让错误图片通过而放宽阈值。

完成条件:每条用例可由配置重复启动,并能将截图关联到确定的代码、资源、相机和构建;缺参考图的用例标为待验证。

5. R1:修复主角装配、地图定位与相机

涉及现有文件:project/game_scene.gdproject/net_world.gdproject/net/mapcoord.gdproject/ui/player_view.gdproject/game_camera.gd。必要时追踪 extension/src/net/m2_client.cpp 的事件顺序及 extension/src/metin2_world.cpp 的地图数据。

具体步骤

  1. 增加定点诊断:阶段进入、地图就绪、主角 VID 获得、实体行有效、模型创建、贴地定位、相机开始跟随。每条带角色/场景生命周期编号,避免把上一次登录数据误认为当前数据。
  2. 复核现有 _sync_main_character():明确“等待数据、加载中、成功、失败”的状态;缺 race 等必要数据时保持等待,重复事件不创建重复人物。
  3. 检查 _main_model_attempted 是否在失败后永久阻止恢复。数据或资源就绪条件变化时允许受控重试;永久资源错误显示明确失败原因,不能每帧重复加载或刷日志。
  4. 切角色、地图切换、重连时清理旧模型/相机目标/信号并重置对应状态。异步完成结果必须核对是否仍属于当前角色与场景。
  5. 统一通过 MapCoord 转换:服务器全局厘米 → 地图本地米。现有地图本地 Z 约定与上游不同,禁止直接复制对方轴翻转或地图中心公式。
  6. 验证坐标往返、合法边界及高度查询位置。(0,0) 可是合法地图基点,地图是否就绪应有明确状态,不能仅靠非零基点推断。
  7. 在地图可查询、角色位置有效后计算地面高度并绑定相机;模型就绪后移除胶囊。资源等待期间提供加载状态,不能把永久占位当正常游戏画面。

必须补充的回归

  • 主角 VID 在 setup 前已有、setup 后出现、实体行再晚一帧、多帧后出现。
  • 不发 entity_main_set,只更新实体存储,最终仍创建主角。
  • 重复主角事件只产生一个模型;缺 race 时不使用错误职业创建人物。
  • 加载失败有可见原因;条件恢复后可受控重试;旧加载不会覆盖新角色。
  • A1/C1 各一个合法出生点;基点为零的合成坐标用例;地图外坐标被明确诊断。
  • 切角色、重连、切地图后相机跟随当前人物,旧节点与旧状态不残留。

测试落点:扩展 project/gamescene_test.gdproject/app_flow_lifecycle_test.gd;新增真实资源的 GameScene 视觉用例。FakeClient 的字段/信号需匹配实际接口。

完成条件:自动回归通过;真实 GameScene 截图同时出现正确地表与真实主角。协议若中断,保留本地通过结果,但将实际联机验收标为未通过。

6. R2:修复身体、发型、武器与贴地

涉及现有文件:extension/src/metin2_model.cppextension/src/metin2_anim.cppextension/src/gr2_bridge.cppproject/ui/player_view.gdproject/ui/equip_model.gdproject/ui/race_spec.gdproject/ui/char_select_screen.gd

同资源对照与定位顺序

  1. 先选用户出错角色的确切身体/发型/装备;再建立男战士最小对照。GLB 转换必须使用同一组源文件,记录转换器版本与参数。生成文件留在隔离输出目录。
  2. 在统一实验台逐步加载:身体静态绑定姿势 → 身体动画 → 加发型 → 加武器 → 换护甲。每一步保存矩阵、包围盒、贴图绑定和截图。
  3. 检查模型初始 placement、单位缩放、轴转换、父子节点变换是否重复应用。分别核查三角形绕序和负缩放,不能以全局 flip_winding 代替定位。
  4. 对身体和发型按骨名核对 mesh binding、骨架索引、inverse bind、当前 world/skin 矩阵。验证发型是否错误重复乘身体变换,是否使用了另一套不兼容的绑定姿势。
  5. 检查附件共享的动画时间与过渡状态。身体切动作/换装时,发型不能停留在旧姿势。武器验证手部附件空间,避免在 model-local 与 world-space 之间重复转换。
  6. 先用不受光照影响的诊断材质、线框或深度视图判断几何是否错误;几何正确而发型仍盖脸时进入 R3 检查深度和混合。
  7. 数值正确后比较 GR2 CPU、GR2 GPU 与 GLB 的同时间画面。对关键骨骼矩阵、选定顶点和完整动画周期采样,保留已有 oracle 精度要求,不只检查 NaN。

贴地专项

  1. 分开记录实体基准高度、地形采样高度、模型局部原点和脚部接触点。
  2. 平地待机时用足底接触点判断高度;行走/攻击存在抬脚,不能用动画每帧 AABB 最低点强行贴地,否则人物会抖动。
  3. 测试坡地、台阶或可行走建筑表面,以及地图边界。确认所采样的是人物实际站立表面。
  4. 阴影检查与几何检查分开:阴影偏差可造成悬空观感,但不能用加深阴影掩盖高度错误。
  5. 建议初始平地接触误差上限为 2 cm;这是待以资源与 40250 校准的项目目标,不是原版已证实精度。腾空动作不使用该判据。

路线决策

实测结果 后续动作
GR2 数值正确,仅材质/装配错误 修复现有渲染与组合,不迁移动画
GLB 在选定资源上可靠,GR2 缺陷短期难解决 为该资源提供可回退的 GLB 适配,保持相同的角色表现接口
GLB 丢 shear 或过渡不一致 不将该角色纳入迁移范围;保留失败样本
静态 GLB/原生资源缓存画面一致且加载更快 单独推进静态物件缓存,与角色动画路线解耦

GLB 扩大范围前必须覆盖四职业、实际使用的性别/护甲/发型变体,以及待机、走、跑、攻击、受击、死亡和选角动作。缺少资产时明确未覆盖,不以一个战士替代全体结论。

完成条件:出错角色的正面/侧面/背面及关键动作通过;换装、重新选角后仍正确;记录 GR2/GLB 选择依据。清理临时强制绕序、调试打印和 /tmp 截图导出时只删除本轮明确归属的诊断代码。

7. R3:材质、地形与环境校准

涉及现有文件:extension/src/m2_material.cppextension/src/static_object.cppextension/src/terrain_splat.cppextension/src/environment_builder.cppextension/src/metin2_world.cppproject/ui/char_shadow.gd

具体步骤

  1. 逐 surface 输出源 mesh、tri-group、材质槽、最终纹理和渲染状态。确认 mesh-local 材质索引没有误当成全局索引,贴图回退有明确诊断。
  2. 身体、脸等不透明面验证深度写入;发丝按资产语义选择 alpha-test;真正半透明面单独混合。不要把所有头发强制成同一模式,也不要对所有表面关闭背面剔除。
  3. 同贴图下比较标准材质和自定义 shader,逐项打开光照、顶点色、透明、雾。检查 DDS 解码、颜色空间、alpha、mipmap 和双重 gamma。
  4. 身体、头发、武器分开检查投影与受影。正面、侧面、逆光及脸部重叠处验证深度;根据场景尺寸校准 shadow bias。
  5. 地形逐层验证 tile/alpha splat 对应、UV 比例、接缝和法线。固定至少一处道路/草地边界作为回归,防止纹理层顺序交换。
  6. 列出地图物件:应有、找到资源、解析成功、实例化、可见、失败原因。资源 CRC 可解析不等于模型已画出,树木占位必须单独计数。
  7. 基于原 .msenv 对照太阳方向、角色/背景光、环境光、雾距离与天空;逐项记录 Godot 近似规则。EnriqueGF 的固定 filmic/雾参数只作实验参照。
  8. 完成角色与场景单项校准后再组合全景,避免同时改曝光、贴图与相机导致无法归因。

完成条件:脸/身体不误透明,发丝边缘和遮挡正确;道路与草地层正确;主要建筑不缺材质;角色高度与阴影分别通过;每项与 40250 的剩余差异有说明。

8. R4:选角界面与技能栏

涉及现有文件:project/ui/char_select_screen.gdproject/ui/ui_assets.gdproject/ui/quickbar.gdproject/game_scene.gd

具体步骤

  1. 对照当前中文 40250 的 uiscript.sub、字体及图集,记录选角背景、角色相机、按钮、任务栏、快捷槽的来源与尺寸。
  2. 检查 .sub 裁剪边界、大小写路径、透明通道与纹理加载失败;输出 UI 依赖清单。
  3. 从 GameScene 开始检查快捷栏是否创建、挂在正确 CanvasLayer、可见、位于窗口内;核对缩放、锚点、裁剪和其他窗口遮挡。
  4. 跟踪 quickslots_changedrestore_from_server 的调用时序:初始槽位、晚到数据、重新登录均应恢复。保留现有服务端 36 槽与本地 4 页 × 8 格映射。
  5. 将空槽、未学习技能、图标缺失、技能不可用、冷却中区分呈现。不得通过凭空填充技能图标让截图看似正常。
  6. 使用确实已学习并已配置的技能验证图标、拖放、翻页、数字键/F 键和冷却;UI 对比先用 FakeClient,真实施放在适用测试环境下执行。
  7. 验证背包/聊天打开关闭后输入焦点,避免空格攻击误触 UI 按钮。
  8. 按 1280×720、1920×1080 和实际 Retina 窗口验证逻辑尺寸/物理像素关系;中文字体宽度单独检查。

完成条件:选角画面达到已记录的参考布局;底部栏始终可见,槽位状态真实;切角色/重连能恢复;技能冷却和输入行为保持正确。

9. R5:最终 macOS 包与验收证据

涉及现有文件:build-macos-client.shscript/live_smoke_test.shproject/live_smoke_test.gdproject/asset_pack.gdproject/bake_asset_index.gd

具体步骤

  1. 扩展构建清单,记录代码版本及脏补丁摘要、构建参数、扩展/模板版本、资源及实际产物哈希。
  2. 对实际 .app 检查身体、发型、装备、动作、地图、UI 图标依赖是否存在且可加载。清单生成后仍要启动包验证,不能用工作区资源替代包内检查。
  3. 从不依赖开发者资源路径的目录启动最终包;验证资源解析不会偷偷回退到源码 checkout。用受控资源根测试覆盖此情况。
  4. 执行只读登录冒烟,再运行有画面和输入的 GameScene 验收。现有脚本不是完整视觉测试;需新增或扩展包内测试场景并保存截图。
  5. 移动、攻击、换装等会改变服务端状态的用例使用适用测试角色/服务器;不要把现有冒烟默认包含的聊天发送当作渲染验收必需动作。
  6. 两客户端用例验证角色互见、移动朝向、武器/护甲变化、断开移除及重连重建;同时检查本地与远端外观。
  7. 排除凭据、私有日志、源码资产归档和临时探针出现在普通交付包中。若通过带探针的包定位问题,移除探针后还要对最终普通包重新冒烟。
  8. 将通过记录绑定到交付 .app 的确切构建。此后任何影响画面的代码、资源、导出设置变化,都使相关验收失效,需要重测受影响范围。

现有命令与覆盖边界

以下命令在仓库根目录运行。执行前检查本机 Godot、扩展和资源配置;这些是已存在的入口,本文没有执行它们或宣称通过。

godot --headless --path project --script gamescene_test.gd
godot --headless --path project --script equip_model_test.gd
godot --headless --path project --script environment_test.gd
godot --path project --script char_select_visual_test.gd
./build-macos-client.sh release
./script/live_smoke_test.sh --no-build --no-mutations --no-reconnect
  • Headless 测试仅证明其断言覆盖的逻辑,不能证明实际 GPU 画面正确。
  • 当前选角视觉脚本有实验性诊断,使用前复核其输出和退出行为;它不是完整 40250 比较器。
  • --no-mutations 不覆盖移动、聊天、重连、换装和攻击;此处同时给出 --no-reconnect 以明确首轮只读范围。
  • 当前脚本包含特定服务器地址与端口,运行前检查是否仍是目标环境;凭据只通过既有本地配置/环境提供,不写进报告。
  • 现有产物路径为 build/export/mtgodot-poc.app;日志在 build/live-smoke/。保留每次结果时复制到独立 run-id 归档,避免脚本固定路径覆盖历史。

第一版交付检查表

  • 首次进入、切角色、重连后出现真实主角,地图和相机正确。
  • 用户出错角色的身体/发型/装备在正面及动画中正常。
  • 平地和选定坡地测试通过,脚底与阴影分别正确。
  • 底部任务栏可见,已配置技能的图标、输入、恢复、冷却正确。
  • 材质与地图关键区域通过固定场景比较,差异图有人审阅。
  • 最终普通 .app 独立启动,关键资源无开发目录回退。
  • 协议错误未导致用例提前终止;未完成的联机项明确标注。
  • 日志、截图、报告和包哈希对应同一次构建。

上述为本次内部 macOS 修复版交付标准,不代表全游戏、所有地图或所有平台均已验收。

10. R6:性能、缓存与内容扩展

在 R1–R5 通过后执行,不以优化替代正确性修复。

  1. 固定场景采集 1/8/20/50 个角色的 CPU/GPU 帧时间、p95/p99、内存、冷加载与重复加载耗时,明确前台/后台和帧率限制。
  2. 量化当前 CPU 蒙皮逐帧构建网格的成本;GPU 路径先通过数值与画面回归,再扩大启用范围。
  3. 选择一组代表性静态建筑试缓存,缓存键至少包括源资源、转换器版本、坐标和材质选项;改变任一输入必须失效重建。
  4. 测量缓存对冷加载、包体和内存的影响,存在收益再扩大导入规模。保留原始资源来源和可重建流程。
  5. 树木、效果、LOD 和其他地图分别建立缺失清单与回归场景。占位替代物不能计入原版模型已完成数量。
  6. 最后扩展 Android/iOS 独立验证;macOS 的画面和性能结果不能直接外推。

11. 任务拆分与执行记录

每项宜形成一个可独立复查的补丁;无需先完成所有工具才修复已定位的问题。已有用户改动应在相邻上下文中保留,不因路线实施而重置。

任务 ID 交付物 前置条件 当前状态
R0-01 固定资源/场景基线与区域截图配置 可用参考资源 已有 A1/C1 实景入口和部分哈希;区域差异待补
R1-01 主角晚到、重复事件、重连回归与修复 R0 最小基线 晚到/重入/重试回归通过;真实重连待验
R1-02 地图坐标、高度、相机就绪检查 R1-01 A1/C1 晚到定位实景通过;坡地/边界待验
R2-01 同战士 GR2/GLB 实验台及对照报告 R0 实验台与悬空修复已完成并有数值断言;GLB 对照仍待执行
R2-02 发型/身体/附件修复及路线决定 R2-01 发型材质根因已修复并有八职业回归;导出包复验待做
R3-01 深度/材质/环境与地形校准 R1、R2 几何稳定 材质已有改动,待验证
R4-01 选角、技能栏装配与数据恢复 R1 待执行
R5-01 包资源清单、最终 .app 冒烟与画面报告 R1R4 待执行
R6-01 性能基线与静态资源缓存试验 R5 后续

每项完成记录必须包含:问题复现、确认根因、修改文件、源资源哈希、实际执行命令、断言/截图结果、包或库哈希、未覆盖范围。失败时记录下一步所需证据;不要把“方案合理”“截图已生成”更新成通过。

与现有文档的关系:本路线聚焦用户当前渲染故障和首个 macOS 修复包;SHINSOO-WORLD-RENDERING.md 提供世界渲染背景,PARITY-GAP.mdBACKLOG.md 保留更广范围。后续只在实测完成时同步这些文档的对应状态。

2026-09-08 第一批实施记录

  • 修改 game_scene.gd:setup 完成相关接线后才开始兜底;加载期间阻止网络事件重入;按 VID/race/资源根区分模型;失败间隔 1 秒最多尝试 3 次;丢弃构建期间失效的主角结果;避免重复动画订阅和每帧重置人物位置。
  • 发现地图路径原来只在 setup 时按已收到的主角位置选择。新增首次有效主角位置到达后的地图重选,复用已有地图重载和相机/移动同步基点定位函数。
  • 修改 ui/player_view.gd:拒绝非法 race、缺失身体文件及解析失败的源文件。模型节点在入树时才实际加载,因此使用源文件预检,不能把入树前为空的 get_info() 当成加载失败。
  • 新增 main_character_sync_test.gd:测试失败重试、晚到/无信号实体、重入、重复事件、移动不回滚、过期构建等,已执行得到 failures=0。注入失败用例的 warning 属预期。
  • gamescene_test.gdequip_model_test.gd 已通过;前者故意使用缺失地图,其预期加载错误不能当成真实地图测试通过。
  • 新增 gamescene_visual_test.gdApple M4 / Metal Forward+ / Godot 4.7.1 上已执行 A1/C1A1 加载 20 区块/601 物件,C1 加载 20 区块/565 物件,身体加发型共 4 个表面,实体定位断言通过。两者均使用 FakeClient,未连接实际服务器。
  • 截图人工观察:地图和真实人物出现;UI 仍有布局/风格问题,不能标为 R3/R4 完成。选角截图仍复现发型遮脸;单独翻转 winding 的试验未解决,未保留新的绕序修改。
  • 用例命令、输出位置和验证边界见 test/rendering/README.md。早期失败截图保留在 build/rendering/;后续脚本变化应重新执行,不复用旧报告作为新代码证据。
  • 尚未完成:GLB 对照、发型根因修复、完整视觉基线与区域阈值、真实服务器重连、最终 macOS 包验收、R6 优化。下一优先项是发型的身体/附件变换与深度隔离。

2026-09-08 第二批实施记录(R2-01 悬空 / R2-02 发型)

R2-01 人物整体悬空

  • 复现:gamescene_visual_test.gdA1、C1)与 char_bench_test.gd 的 feet 机位,角色脚底离地约一个骨盆高;tools 下的 foot_probe 预测偏差 0.146 cm 量级。
  • 确认根因:渲染桥把动画世界姿势按 Granny GrannyBuildWorldPoseOffset4x4 又乘了一次模型导出期的 InitialPlacement,而 Metin2 的动画 .gr2 根骨轨道本身已含骨盆高度,等于重复计一次(实测 IP.z:武士 99.443、刺客 92.589、修罗 103.674、萨满 95.473,单位 cm)。
  • 修改文件:libgr2/include/gr2/gr2.hlibgr2/src/gr2_skeleton.cppsample_pose 增加可选 root_offset,默认仍取 sk.initial_placementbind_pose 行为不变,oracle 数值一致性未放宽);extension/src/metin2_anim.cpp(渲染路径显式传单位阵)。
  • 断言:gamescene_visual_test.gd 新增 feet_gap(阈值 0.03 m)。A1、C1 实测均为 0.001373 mpassed: true
  • 未覆盖:只验证了 wait @0.5 s 与平地。坡地、跳跃/受击等位移较大的动作、以及 NPC/怪物模型未逐一复验。

R2-02 发型发白、看起来「挡脸」

  • 复现:char_bench_test.gdface_side 近景,额发和鬓角出现成片浅奶油色三角面。
  • 排除项(都不是原因):几何与挂接(发型权重 100% 在 Bip01 Head,骨骼按名重映射 74/74,蒙皮包围盒落在头部 z 149–178,bind_pose_self_check = 7.6e-05);贴图(hair/hair_1_1.dds 就是 gr2 材质里写的那张,256×256 DXT1,最亮纹素 RGB(90,81,90)0 个透明纹素,所以 alpha scissor 是空操作,dxt.cpp 无关);UV(全在 [0,1])。
  • 材质二分(同一机位逐项改一个开关):TRANSPARENCY_DISABLED 仍有、CULL_BACK 更糟、metallic_specular = 0 仍有、roughness = 0 干净SHADING_MODE_UNSHADED 干净
  • 确认根因:Metin2Model::_load_hair() 是全角色唯一没走 mtgodot::make_material 的表面,用的是裸 StandardMaterial3Droughness = 1.0,浅色斑是 Godot PBR 的高光响应,与身体/武器用的前向材质(shader 带 specular_disabled)光照模型不一致。
  • 修改文件:extension/src/metin2_model.cpp_load_hair() —— 改用 mtgodot::MaterialDesc{blend = AlphaTest, alpha_scissor = 0.5, two_sided = true, spec_power = 0}make_material;无贴图时用 modulate 保留原来的深棕兜底色。GPU 蒙皮路径本来就已经走 make_material,此改动使 CPU 路径与之一致。
  • 资源哈希(sha256):PC/ymir work/pc/warrior/warrior_novice.gr2 7ac881b0c60ced40c9388d6186bdf02a353a58382d12804345cb65fc3dc1fa74PC/ymir work/pc/warrior/hair/hair_1_1.gr2 2e3c00bda5397d579a2399cfb38a69974667e4f7b92dca6b2a709058a37d0a84PC/ymir work/pc/warrior/general/wait.msa 7bc14bc29a63a78708b9a76618aee9229286f255e725dd669c60ef838cec6a15。其余七个职业的身体/发型哈希记在各次 report.json 里。
  • 执行命令与结果:
./build.sh Debug
godot --headless --path project --script main_character_sync_test.gd   # failures=0
godot --headless --path project --script gamescene_test.gd             # PASS
godot --headless --path project --script equip_model_test.gd           # PASS
godot --path project --script char_select_visual_test.gd               # PASS
godot --path project --script gamescene_visual_test.gd                 # passed:true, feet_gap 0.001373
godot --path project --script gamescene_visual_test.gd -- --c1         # passed:true, feet_gap 0.001373
godot --path project --script char_bench_test.gd -- --race {0,1,2,3}   # 四职业近景人工复核,浅色斑消失
godot --path project --script model_render_test.gd                     # PASS(八职业)
  • 未覆盖:只验证了基础 *_novice 身体加 hair_1_1。5th 护甲等换装组合、hair_skin 覆写路径、GPU 蒙皮路径的实机画面未逐一复验;导出包内复验尚未完成(见下)。

新增:不启动游戏的模型渲染自动回归

  • 新增 project/model_render_test.gd 与基线 test/rendering/model_baseline.json。离线装配单角色(无地图、无网络、无服务器),固定灯光与机位,对像素算指标并断言,退出码即结论。
  • 指标与阈值、为什么用平均亮度而不是「炸白像素占比」、以及覆盖边界,见 test/rendering/README.md
  • 有效性已验证:把 _load_hair() 临时改回 StandardMaterial3D(roughness = 1) 重新编译后,八个职业全部失败(hair_mean_lum 偏离基线 0.0109…0.0371,阈值 0.006);改回 make_materialPASS。这条不是「截图已生成」式的通过。
  • 局限:基线绑定当前引擎版本、灯光和资源。改模型/发型材质/灯光都会使其失效,必须人工看过渲染结果再 --bless 并在此记录原因。

纠正前一批记录的一处误判

  • 第一批记录里「选角截图仍复现发型遮脸」中被一并当作缺陷的「武器平躺在地上」不成立:那是选角页 ui/char_select_screen.gd:517 建的 dragon_left/right_button_0{1,2,3}.sub 两个 2D 导航按钮((714,712) / (1024,712),160×44),不是武器挂点问题。武器挂接没有发现缺陷。

仍未完成

  • 导出包复验:包内没有离线渲染入口(导出模板不接受 --script 已解决(2026-09-08:新增 project/package_render_test.gd,由 client_main.gdMT_TEST_MODE=render 时挂到主场景下—— 绕开导出模板不收 --script 的限制,走真实 GameScene + Metin2World + PlayerView,不连服务器, 报告和截图落到 user://render-check/MT_RENDER_OUTPUT 可改)。

    MT_TEST_MODE=render build/export/mtgodot-poc.app/Contents/MacOS/mtgodot-poc
    

    首次在导出包内跑的结果(2026-09-08release 包):PASS —— 扩展三个类都注册、资源根解析到 Contents/Resources/assets、地图 20/20 chunk、model_built=truesurfaces=4feet_gap=0.001373, 截图是正常的草地 + 角色。R2-01/R2-02 的「导出包中复验」这一步由此补齐。 未覆盖:网络驱动的进场时序、40250 像素比对、性能。

  • 新发现的实机故障:进入游戏后 3D 画面整片灰紫、主角仍是蓝色胶囊,2D UI 与小地图正常 已修复(2026-09-08

    根因EntityStore::mut_despawn()GC_CHARACTER_DEL 命中主角 vid 时把 m_main_vid 清零了。 40250 服务器在进场那一两秒里会对主角自己的 vid 连发 GC_CHARACTER_DEL + GC_CHARACTER_ADD (视野重算,实测 10 次左右),于是主角被永久「取消主角身份」:get_main_vid() 返回 0 → _sync_main_character() 直接 return → 模型装配代码一次都没跑(main_attempts=0), 占位胶囊留在 y=0,相机跟着它钻到地形底下 → 满屏灰紫。地图其实一直是好的。

    参考端怎么做40250/ClientVS22/source/UserInterface/):主角 vid 归 CNetworkActorManager::m_dwMainVID 所有,只由 SetMainActorVID()GC_MAIN_CHARACTER)写、 只在管理器销毁时清;RemoveActor()NetworkActorManager.cpp:440)删 actor 行和角色实例, 不碰 m_dwMainVID。重新 add 时 __AppendCharacterManagerActor()kCreateData.m_isMain = __IsMainActorVID(dwVID) 把它重新认成主角,并再跑一次 SetMainCharacterIndex(dwVID)

    修法extension/src/net/entity_store.cpp,两处对齐参考端):

    1. mut_despawn() 不再清 m_main_vidm2dev 分支的 GC_CHARACTER_DEL 同样处理); 该字段现在只由 mut_spawn_main() 写、只由 reset_for_map_change() 清(对应参考端的销毁)。
    2. mut_spawn_full()vid == m_main_vid 就把 is_main 置回 true,并补一条 MainSet 变更,对应 __AppendCharacterManagerActor()m_isMain + SetMainCharacterIndex

    取证手段(保留在树里):

    • project/net_trace.gd(由 client_main.gd 挂在 AppFlow 之后)每帧比对 get_main_vid() 丢主角时打 NETTRACE: MAIN_LOST ... reason=despawn|silent 和最近 24 条事件环。 reason 直接区分「GC_CHARACTER_DEL 命中主角」和「reset_for_map_change() 静默清零」 (后者会清空 m_changes,不发 despawn 信号)。MT_NETTRACE=1 打全量。
    • game_scene.gd _diag_tick():进游戏若干秒后,_model_built_map_loaded() 任一为假就打一行 GAMESCENE_DIAG:race / pos_cm / map_path / map_base_set / main_attempts / main_sync_ready / map_report …)。MT_DIAG=1 强制输出并连打 6 次(间隔 2 s), MT_DIAG_AFTER_MS 改延时,MT_DIAG_SHOT=<png> 在最后一次存一张实机帧。

    实机复验(真服 192.168.21.203classic2026-09-08):

    • 修前 ./script/live_smoke_test.sh --no-build --no-mutations --no-reconnectFAIL: 主角色 VID 有效 / FAIL: 主角色不存在NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn
    • 修后同一命令 → LIVE_SMOKE RESULT: PASSmain_vid=25933 pos_cm=(963444.0, 266221.0, 0.0) entities=19
    • 40 秒实机连打诊断:model_built=truemain_attempts=1player_pos=(418.44, 177.67, 614.21) (站在 C1 地形上,不再是 y=0),16 次采样全程稳定;MT_DIAG_SHOT 存下的帧是正常的 地形 + 建筑 + NPC + 主角模型 + HUD + 小地图。

    未覆盖:主角材质在俯视机位下偏暗的观感问题——那属于光照/色调,受 PARITY-GAP §0 门禁约束, 不在这次修复范围内。

  • R2-01 的 GLB 对照、R0 完整视觉基线与区域阈值、R4 选角/快捷栏、R5 最终包验收、R6 性能,均未开始。