实机故障:进入游戏后整屏灰紫,主角还是蓝色胶囊占位体,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
35 KiB
新客户端渲染修复实施路线
更新日期: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-godot:
078d39a41a3ef17c9e09fb9807956cb1a2f16641。 - kcanakdag/metin2-godotime:
6a82d793c1d83e34f3e71200fdfc67a78bcb96d3。
| 参考实现 | 本项目的应用方式 | 不应推导的结论 |
|---|---|---|
EnriqueGF:tools/compare_visual_golden.py |
引入区域、遮罩、误差阈值和差异图方法 | 截图存在或整体平均误差低不代表脸部正确 |
EnriqueGF:tools/build_character_glb.py、godot/client/online/presentation/warrior_actor.gd |
同一角色离线 GLB 对照;核对身体/发型/武器的组合和动作同步 | 转换包含 TRS 分解、舍弃 shear;不能假定动画无损 |
EnriqueGF:godot/client/online/presentation/avatar_registry.gd |
借鉴有效实体数据驱动创建、切地图清理、重复数据处理 | 其快照协议不能直接替代我们的原版协议事件 |
EnriqueGF:tools/prepare_legacy_ui_assets.py、godot/client/online/ui/ |
核对 .sub 裁剪、原版布局和按钮状态 |
西班牙语素材、字体、窗口尺寸不能直接替代当前中文配置 |
EnriqueGF:tools/audit_city_asset_dependencies.py |
地图资源依赖追踪、缺失分类和报告 | 全部依赖可解析不等于全部已经正确渲染 |
godotime:tools/export_playable.py |
实际包资源清单、哈希与加载审计,适配 macOS | Web/Linux 通过不代表 macOS 通过 |
godotime:content/profiles/p0-warrior-dog.json、client/scripts/content/ |
模型、动作、附件和依赖的统一验证清单 | 三个精选模型通过不代表四职业和全量换装通过 |
godotime:tools/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 完成。
- 保留原版网络协议与当前 GR2 主链路,以最小改动修复实际缺陷。
- 引入独立 GLB 对照场景,不先全量转换资源或更换运行时。
- 对照决定具体资源采用哪条路径;结果需要记录,不能预设 GR2 或 GLB 必然更好。
- 角色与静态物件分别决策。静态物件可先试离线 Godot 资源缓存;人物保留完整仿射矩阵的需求单独评估。
- 每项修复必须在真实 GameScene 和导出包中复验。单模型预览只能作为中间证据。
4. R0:建立可重复基线
涉及现有文件:test/compare.py、project/char_select_visual_test.gd、project/world_demo.gd、project/game_camera.gd。
具体步骤
- 记录当前 Git 提交、未提交补丁摘要、Godot/导出模板版本、扩展库哈希、macOS/GPU、渲染后端、窗口和实际渲染分辨率。不能仅凭提交号标识脏工作区构建。
- 固定资产来源及哈希:身体、发型、武器、护甲、MSM/MSA、动画 GR2、纹理、地图和环境。两个上游与我们可能使用不同资源版本,必须先核对。
- 为每个场景保存地图名、服务器坐标、地图基点、转换后本地坐标、相机位置/目标/FOV、角色朝向、装备、动作及取样时间。
- 建立三种独立场景:无网络的单角色实验台;真实资产的 GameScene + FakeClient;实际服务器上的导出
.app。 - 保存正面、侧面、背面和脚底近景,以及选角界面和游戏全景。待机/跑步/攻击每段至少取开始、中间、结束,过渡动作另测。
- 在现有截图工具基础上增加局部区域和差异图。脸、头发、脚底、技能栏不允许被动态遮罩排除;时间文字、随机粒子等可单列遮罩。
- 区分两类比较:同引擎修复前后用于自动回归;40250 对照用于保真评估。没有匹配的相机和动作时,只能做人工结构检查,不执行严格像素判定。
拟新增产物(尚未实现):test/rendering/cases.json、test/rendering/README.md、tools/rendering/compare_regions.py。本地日志/截图/报告建议放 build/rendering/<run-id>/,不覆盖失败证据。
阈值:先在不改代码的条件下重复采样三次,测量阴影/抗锯齿噪声,再设置每区域阈值。报告包含 MAE、RMSE、变化像素比例和遮罩范围;不能为了让错误图片通过而放宽阈值。
完成条件:每条用例可由配置重复启动,并能将截图关联到确定的代码、资源、相机和构建;缺参考图的用例标为待验证。
5. R1:修复主角装配、地图定位与相机
涉及现有文件:project/game_scene.gd、project/net_world.gd、project/net/mapcoord.gd、project/ui/player_view.gd、project/game_camera.gd。必要时追踪 extension/src/net/m2_client.cpp 的事件顺序及 extension/src/metin2_world.cpp 的地图数据。
具体步骤
- 增加定点诊断:阶段进入、地图就绪、主角 VID 获得、实体行有效、模型创建、贴地定位、相机开始跟随。每条带角色/场景生命周期编号,避免把上一次登录数据误认为当前数据。
- 复核现有
_sync_main_character():明确“等待数据、加载中、成功、失败”的状态;缺 race 等必要数据时保持等待,重复事件不创建重复人物。 - 检查
_main_model_attempted是否在失败后永久阻止恢复。数据或资源就绪条件变化时允许受控重试;永久资源错误显示明确失败原因,不能每帧重复加载或刷日志。 - 切角色、地图切换、重连时清理旧模型/相机目标/信号并重置对应状态。异步完成结果必须核对是否仍属于当前角色与场景。
- 统一通过 MapCoord 转换:服务器全局厘米 → 地图本地米。现有地图本地 Z 约定与上游不同,禁止直接复制对方轴翻转或地图中心公式。
- 验证坐标往返、合法边界及高度查询位置。
(0,0)可是合法地图基点,地图是否就绪应有明确状态,不能仅靠非零基点推断。 - 在地图可查询、角色位置有效后计算地面高度并绑定相机;模型就绪后移除胶囊。资源等待期间提供加载状态,不能把永久占位当正常游戏画面。
必须补充的回归
- 主角 VID 在 setup 前已有、setup 后出现、实体行再晚一帧、多帧后出现。
- 不发
entity_main_set,只更新实体存储,最终仍创建主角。 - 重复主角事件只产生一个模型;缺 race 时不使用错误职业创建人物。
- 加载失败有可见原因;条件恢复后可受控重试;旧加载不会覆盖新角色。
- A1/C1 各一个合法出生点;基点为零的合成坐标用例;地图外坐标被明确诊断。
- 切角色、重连、切地图后相机跟随当前人物,旧节点与旧状态不残留。
测试落点:扩展 project/gamescene_test.gd、project/app_flow_lifecycle_test.gd;新增真实资源的 GameScene 视觉用例。FakeClient 的字段/信号需匹配实际接口。
完成条件:自动回归通过;真实 GameScene 截图同时出现正确地表与真实主角。协议若中断,保留本地通过结果,但将实际联机验收标为未通过。
6. R2:修复身体、发型、武器与贴地
涉及现有文件:extension/src/metin2_model.cpp、extension/src/metin2_anim.cpp、extension/src/gr2_bridge.cpp、project/ui/player_view.gd、project/ui/equip_model.gd、project/ui/race_spec.gd、project/ui/char_select_screen.gd。
同资源对照与定位顺序
- 先选用户出错角色的确切身体/发型/装备;再建立男战士最小对照。GLB 转换必须使用同一组源文件,记录转换器版本与参数。生成文件留在隔离输出目录。
- 在统一实验台逐步加载:身体静态绑定姿势 → 身体动画 → 加发型 → 加武器 → 换护甲。每一步保存矩阵、包围盒、贴图绑定和截图。
- 检查模型初始 placement、单位缩放、轴转换、父子节点变换是否重复应用。分别核查三角形绕序和负缩放,不能以全局
flip_winding代替定位。 - 对身体和发型按骨名核对 mesh binding、骨架索引、inverse bind、当前 world/skin 矩阵。验证发型是否错误重复乘身体变换,是否使用了另一套不兼容的绑定姿势。
- 检查附件共享的动画时间与过渡状态。身体切动作/换装时,发型不能停留在旧姿势。武器验证手部附件空间,避免在 model-local 与 world-space 之间重复转换。
- 先用不受光照影响的诊断材质、线框或深度视图判断几何是否错误;几何正确而发型仍盖脸时进入 R3 检查深度和混合。
- 数值正确后比较 GR2 CPU、GR2 GPU 与 GLB 的同时间画面。对关键骨骼矩阵、选定顶点和完整动画周期采样,保留已有 oracle 精度要求,不只检查 NaN。
贴地专项
- 分开记录实体基准高度、地形采样高度、模型局部原点和脚部接触点。
- 平地待机时用足底接触点判断高度;行走/攻击存在抬脚,不能用动画每帧 AABB 最低点强行贴地,否则人物会抖动。
- 测试坡地、台阶或可行走建筑表面,以及地图边界。确认所采样的是人物实际站立表面。
- 阴影检查与几何检查分开:阴影偏差可造成悬空观感,但不能用加深阴影掩盖高度错误。
- 建议初始平地接触误差上限为 2 cm;这是待以资源与 40250 校准的项目目标,不是原版已证实精度。腾空动作不使用该判据。
路线决策
| 实测结果 | 后续动作 |
|---|---|
| GR2 数值正确,仅材质/装配错误 | 修复现有渲染与组合,不迁移动画 |
| GLB 在选定资源上可靠,GR2 缺陷短期难解决 | 为该资源提供可回退的 GLB 适配,保持相同的角色表现接口 |
| GLB 丢 shear 或过渡不一致 | 不将该角色纳入迁移范围;保留失败样本 |
| 静态 GLB/原生资源缓存画面一致且加载更快 | 单独推进静态物件缓存,与角色动画路线解耦 |
GLB 扩大范围前必须覆盖四职业、实际使用的性别/护甲/发型变体,以及待机、走、跑、攻击、受击、死亡和选角动作。缺少资产时明确未覆盖,不以一个战士替代全体结论。
完成条件:出错角色的正面/侧面/背面及关键动作通过;换装、重新选角后仍正确;记录 GR2/GLB 选择依据。清理临时强制绕序、调试打印和 /tmp 截图导出时只删除本轮明确归属的诊断代码。
7. R3:材质、地形与环境校准
涉及现有文件:extension/src/m2_material.cpp、extension/src/static_object.cpp、extension/src/terrain_splat.cpp、extension/src/environment_builder.cpp、extension/src/metin2_world.cpp、project/ui/char_shadow.gd。
具体步骤
- 逐 surface 输出源 mesh、tri-group、材质槽、最终纹理和渲染状态。确认 mesh-local 材质索引没有误当成全局索引,贴图回退有明确诊断。
- 身体、脸等不透明面验证深度写入;发丝按资产语义选择 alpha-test;真正半透明面单独混合。不要把所有头发强制成同一模式,也不要对所有表面关闭背面剔除。
- 同贴图下比较标准材质和自定义 shader,逐项打开光照、顶点色、透明、雾。检查 DDS 解码、颜色空间、alpha、mipmap 和双重 gamma。
- 身体、头发、武器分开检查投影与受影。正面、侧面、逆光及脸部重叠处验证深度;根据场景尺寸校准 shadow bias。
- 地形逐层验证 tile/alpha splat 对应、UV 比例、接缝和法线。固定至少一处道路/草地边界作为回归,防止纹理层顺序交换。
- 列出地图物件:应有、找到资源、解析成功、实例化、可见、失败原因。资源 CRC 可解析不等于模型已画出,树木占位必须单独计数。
- 基于原
.msenv对照太阳方向、角色/背景光、环境光、雾距离与天空;逐项记录 Godot 近似规则。EnriqueGF 的固定 filmic/雾参数只作实验参照。 - 完成角色与场景单项校准后再组合全景,避免同时改曝光、贴图与相机导致无法归因。
完成条件:脸/身体不误透明,发丝边缘和遮挡正确;道路与草地层正确;主要建筑不缺材质;角色高度与阴影分别通过;每项与 40250 的剩余差异有说明。
8. R4:选角界面与技能栏
涉及现有文件:project/ui/char_select_screen.gd、project/ui/ui_assets.gd、project/ui/quickbar.gd、project/game_scene.gd。
具体步骤
- 对照当前中文 40250 的
uiscript、.sub、字体及图集,记录选角背景、角色相机、按钮、任务栏、快捷槽的来源与尺寸。 - 检查
.sub裁剪边界、大小写路径、透明通道与纹理加载失败;输出 UI 依赖清单。 - 从 GameScene 开始检查快捷栏是否创建、挂在正确 CanvasLayer、可见、位于窗口内;核对缩放、锚点、裁剪和其他窗口遮挡。
- 跟踪
quickslots_changed与restore_from_server的调用时序:初始槽位、晚到数据、重新登录均应恢复。保留现有服务端 36 槽与本地 4 页 × 8 格映射。 - 将空槽、未学习技能、图标缺失、技能不可用、冷却中区分呈现。不得通过凭空填充技能图标让截图看似正常。
- 使用确实已学习并已配置的技能验证图标、拖放、翻页、数字键/F 键和冷却;UI 对比先用 FakeClient,真实施放在适用测试环境下执行。
- 验证背包/聊天打开关闭后输入焦点,避免空格攻击误触 UI 按钮。
- 按 1280×720、1920×1080 和实际 Retina 窗口验证逻辑尺寸/物理像素关系;中文字体宽度单独检查。
完成条件:选角画面达到已记录的参考布局;底部栏始终可见,槽位状态真实;切角色/重连能恢复;技能冷却和输入行为保持正确。
9. R5:最终 macOS 包与验收证据
涉及现有文件:build-macos-client.sh、script/live_smoke_test.sh、project/live_smoke_test.gd、project/asset_pack.gd、project/bake_asset_index.gd。
具体步骤
- 扩展构建清单,记录代码版本及脏补丁摘要、构建参数、扩展/模板版本、资源及实际产物哈希。
- 对实际
.app检查身体、发型、装备、动作、地图、UI 图标依赖是否存在且可加载。清单生成后仍要启动包验证,不能用工作区资源替代包内检查。 - 从不依赖开发者资源路径的目录启动最终包;验证资源解析不会偷偷回退到源码 checkout。用受控资源根测试覆盖此情况。
- 执行只读登录冒烟,再运行有画面和输入的 GameScene 验收。现有脚本不是完整视觉测试;需新增或扩展包内测试场景并保存截图。
- 移动、攻击、换装等会改变服务端状态的用例使用适用测试角色/服务器;不要把现有冒烟默认包含的聊天发送当作渲染验收必需动作。
- 两客户端用例验证角色互见、移动朝向、武器/护甲变化、断开移除及重连重建;同时检查本地与远端外观。
- 排除凭据、私有日志、源码资产归档和临时探针出现在普通交付包中。若通过带探针的包定位问题,移除探针后还要对最终普通包重新冒烟。
- 将通过记录绑定到交付
.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/8/20/50 个角色的 CPU/GPU 帧时间、p95/p99、内存、冷加载与重复加载耗时,明确前台/后台和帧率限制。
- 量化当前 CPU 蒙皮逐帧构建网格的成本;GPU 路径先通过数值与画面回归,再扩大启用范围。
- 选择一组代表性静态建筑试缓存,缓存键至少包括源资源、转换器版本、坐标和材质选项;改变任一输入必须失效重建。
- 测量缓存对冷加载、包体和内存的影响,存在收益再扩大导入规模。保留原始资源来源和可重建流程。
- 树木、效果、LOD 和其他地图分别建立缺失清单与回归场景。占位替代物不能计入原版模型已完成数量。
- 最后扩展 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 冒烟与画面报告 |
R1–R4 | 待执行 |
| R6-01 | 性能基线与静态资源缓存试验 | R5 | 后续 |
每项完成记录必须包含:问题复现、确认根因、修改文件、源资源哈希、实际执行命令、断言/截图结果、包或库哈希、未覆盖范围。失败时记录下一步所需证据;不要把“方案合理”“截图已生成”更新成通过。
与现有文档的关系:本路线聚焦用户当前渲染故障和首个 macOS 修复包;SHINSOO-WORLD-RENDERING.md 提供世界渲染背景,PARITY-GAP.md 和 BACKLOG.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.gd与equip_model_test.gd已通过;前者故意使用缺失地图,其预期加载错误不能当成真实地图测试通过。- 新增
gamescene_visual_test.gd:Apple M4 / Metal Forward+ / Godot 4.7.1 上已执行 A1/C1;A1 加载 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.gd(A1、C1)与char_bench_test.gd的 feet 机位,角色脚底离地约一个骨盆高;tools下的 foot_probe 预测偏差 0.146 cm 量级。 - 确认根因:渲染桥把动画世界姿势按 Granny
GrannyBuildWorldPose的Offset4x4又乘了一次模型导出期的InitialPlacement,而 Metin2 的动画.gr2根骨轨道本身已含骨盆高度,等于重复计一次(实测 IP.z:武士 99.443、刺客 92.589、修罗 103.674、萨满 95.473,单位 cm)。 - 修改文件:
libgr2/include/gr2/gr2.h、libgr2/src/gr2_skeleton.cpp(sample_pose增加可选root_offset,默认仍取sk.initial_placement,bind_pose行为不变,oracle 数值一致性未放宽);extension/src/metin2_anim.cpp(渲染路径显式传单位阵)。 - 断言:
gamescene_visual_test.gd新增feet_gap(阈值 0.03 m)。A1、C1 实测均为 0.001373 m,passed: true。 - 未覆盖:只验证了
wait@0.5 s 与平地。坡地、跳跃/受击等位移较大的动作、以及 NPC/怪物模型未逐一复验。
R2-02 发型发白、看起来「挡脸」
- 复现:
char_bench_test.gd的face_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的表面,用的是裸StandardMaterial3D且roughness = 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.gr27ac881b0c60ced40c9388d6186bdf02a353a58382d12804345cb65fc3dc1fa74;PC/ymir work/pc/warrior/hair/hair_1_1.gr22e3c00bda5397d579a2399cfb38a69974667e4f7b92dca6b2a709058a37d0a84;PC/ymir work/pc/warrior/general/wait.msa7bc14bc29a63a78708b9a76618aee9229286f255e725dd669c60ef838cec6a15。其余七个职业的身体/发型哈希记在各次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_material后PASS。这条不是「截图已生成」式的通过。 - 局限:基线绑定当前引擎版本、灯光和资源。改模型/发型材质/灯光都会使其失效,必须人工看过渲染结果再
--bless并在此记录原因。
纠正前一批记录的一处误判
- 第一批记录里「选角截图仍复现发型遮脸」中被一并当作缺陷的「武器平躺在地上」不成立:那是选角页
ui/char_select_screen.gd:517建的dragon_left/right_button_0{1,2,3}.sub两个 2D 导航按钮((714,712)/(1024,712),160×44),不是武器挂点问题。武器挂接没有发现缺陷。
仍未完成
-
导出包复验:包内没有离线渲染入口(导出模板不接受已解决(2026-09-08):新增--script)project/package_render_test.gd,由client_main.gd在MT_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-08,release 包):
PASS—— 扩展三个类都注册、资源根解析到Contents/Resources/assets、地图 20/20 chunk、model_built=true、surfaces=4、feet_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,两处对齐参考端):mut_despawn()不再清m_main_vid(m2dev 分支的GC_CHARACTER_DEL同样处理); 该字段现在只由mut_spawn_main()写、只由reset_for_map_change()清(对应参考端的销毁)。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.203,classic,2026-09-08):
- 修前
./script/live_smoke_test.sh --no-build --no-mutations --no-reconnect→FAIL: 主角色 VID 有效/FAIL: 主角色不存在;NETTRACE: MAIN_LOST was=25910 now=0 reason=despawn。 - 修后同一命令 →
LIVE_SMOKE RESULT: PASS,main_vid=25933 pos_cm=(963444.0, 266221.0, 0.0) entities=19。 - 40 秒实机连打诊断:
model_built=true、main_attempts=1、player_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 性能,均未开始。