Files
mtgodot-poc/docs/PARITY-GAP.md
T
shenleiandClaude Opus 5 fb4d2d222b docs: 文档审计——修正失效链接与过期结论
按 docs/ 目录逐篇核对,只改与事实/路径不符的部分,不动尚未验证的计划条目:

- `docs/steps/**` 在早前整理时已迁到 `docs/reference/steps/**`,全仓的旧路径
  引用(PLAN.md / README.md / M0-gr2-reader.md 及 libgr2 的头注释)一并改正。
- CLIENT-GAP.md / CLIENT-GAP-FIX.md / BACKLOG.md / MIDREVIEW.md:把已经落地的
  条目从「待办」改为已完成,删掉与代码现状矛盾的描述。
- ANDROID-TESTING.md / CLIENT-PORT.md / CLIENT-ROADMAP.md / GODOT-POC-PLAN.md /
  SHINSOO-WORLD-RENDERING.md / PARITY-GAP.md:同上,另补当前实际的构建/测试入口。
- THIRD-PARTY.md:补齐实际在用的第三方来源与许可说明。

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

249 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 渲染一致性差距清单 —— 距 `ingame-shinsoo.png` 还差什么
> 更新:2026-08-29 · 配套 [`SHINSOO-WORLD-RENDERING.md`](./SHINSOO-WORLD-RENDERING.md)(实施规格)、
> [`BACKLOG.md`](./BACKLOG.md)。
>
> **本文回答一个问题**:现在 `test/golden/world-a1-{persp,r2}.png` 到「与游戏内截图目视等价」之间,
> 还有哪些工作。不是 SHINSOO 的重复 —— SHINSOO 是「怎么把世界渲染出来」,本文是「怎么把观感做到
> 参考图那样」。当前已落地 W0–W6 的可运行纵切,以及 W7/W8 的部分首版;必要 `.mse`、真实 UI、
> 完整流式性能路径和移动端验证仍未完成。本文同时列出**正确性缺口**与**保真化缺口**。
---
## 0. 「一致」的定义与现状
**目标**:在同一全局坐标 + 同一 yaw/pitch/FOV + 同一时间/天气下,Godot 输出与原 D3D 客户端截图,
经 ROI 直方图 + 边缘结构(SSIM)+ 人工签核判为「同一场景、同一观感」。不要求逐像素(渲染器不同)。
**现状**`world-a1-persp.png` / `world-a1-r2.png`):
- ✅ 地貌 / 道路网 / 建筑布局 / 水域形状 = **认得出是神兽国 A1**
- ✅ 第三人称角色(带发型)在地形上跑、HUD 布局在位、`.msenv` 驱动的天空/光/雾
- ❌ 观感**不等价**:树仍是 proxy(虽已用真实 bark/composite atlas)、无 `.mse` 特效、水面已用真 30 帧序列但深度系数未标定、HUD 已用真实九宫格贴图但布局未按受控帧对齐、光照/色调未对受控帧校准、机位随手放、角色材质 sphere-map 高光已由装备层驱动(`equip_model.gd:283-288``item_proto specular/100`)但只覆盖身体/护甲一个部位、power 未按受控帧标定
**粗估**:把下面全部做到「目视等价」,单人 **≈ 36 个月**(不含 Phase 2 移动端 bring-up
需在地形/水面正确性修复和 `.spt` 转换可行性验证后重估)。`.spt` 当前缺少可分发的跨平台 reader;
`.mse` 是 ASCII 文本且原客户端带 parser,不属于不可读资产,但完整 EffectLib 运行时移植仍是大工程。
### 0.1 `ingame-shinsoo.png` 受控参考契约
本文的具体目标帧不是泛指「某张神兽国截图」。实施前必须创建
`test/maps/ingame-shinsoo.json`,把下列状态冻结;无法从截图可靠反推的值一律写成
`待采集`,不能凭观感猜一个值后继续调材质。
| 域 | 必须冻结的字段 |
|---|---|
| 输出 | 1920×1080、Godot 版本、renderer、MSAA/TAA、色彩空间、随机种子 |
| 地图 | map id、全局坐标、加载半径、可见区块 |
| 相机 | yaw、pitch、roll、与角色距离/高度、FOV、near/far |
| 角色 | race、sex、body/hair、武器/装备、动作名、动画时间/帧 |
| NPC | race、装备、全局坐标、动作帧、名字/标签状态 |
| 环境 | `.msenv`、时间/天气、太阳方向、雾、曝光、后处理参数 |
| UI | locale=`es`、背包打开、圆形小地图、聊天/状态栏/快捷栏以及顶部三行消息内容 |
**门禁**:上表未落盘前,只允许修格式/管线正确性问题,不接受「为匹配这张图」进行的相机、光照、
色调或材质调参。参考图本身应进入仓库为 `test/reference/ingame-shinsoo.png`,避免依赖个人目录路径。
### 0.2 单帧范围与完成定义
对这张目标图,阻塞交付的可见项按顺序是:固定机位 → 地形全部 splat/UV → 建筑与材质 →
非占位树 → 玩家/NPC/装备/阴影 → 光照与色调 → 真实 HUD。水体、完整 `.mse`、移动端流式和远离
机位的地图内容,只有在目标帧或其验收 ROI 内可见时才阻塞该单帧;被排除项必须在 preset 中记录
原因和对应 ROI,不能把「当前看不到」写成「已经支持」。
单帧完成同时要求:
- 世界层能辨认为同一位置、同一构图,主要建筑、道路、树冠和角色轮廓对齐;
- UI 层在 1920×1080 下使用真实贴图/字体,窗口边界、锚点、层级与参考图一致;
- 通过 §9 的世界 ROI、UI 几何门禁与人工签核;不同渲染器导致的逐像素差异不作为失败条件。
### 0.3 精确资产清单
另建 `test/maps/ingame-shinsoo-assets.json`,逐个登记目标帧里**实际可见**的资源,而不是只写资源类别:
| 可见对象 | 清单必须记录 |
|---|---|
| 地形 | map/区块、`height.raw``tile.raw`、TextureSet 图层及 shadow/minimap |
| 建筑/道具 | property CRC、buildingfile、实例变换、GR2、每个材质贴图 |
| 树 | property CRC、treefile、实例变换、最终使用的 mesh/LOD/impostor 与树皮/叶片贴图 |
| 玩家/NPC | MSM/MSA/GR2、body/hair、武器装备、动作和实例位置 |
| UI | 每个窗口的脚本/贴图/locale 图、字体、图标和九宫格来源 |
| 环境 | `.msenv`、天空/云/flare 贴图以及所有颜色、雾、曝光输入 |
每条资源保存原虚拟路径、`AssetResolver` 最终命中的磁盘路径、pack 优先级和内容 hash。这样 assets
目录内容变化后,golden 偏差能够定位到具体资源,而不是重新肉眼猜测。
---
## 1. 特殊资产与专项管线
| # | 项 | 为什么卡 | 实施路径 | 规模 |
|---|---|---|---|---|
| 1.1 | **`.spt`SpeedTree 树)** | 几何解析在闭源 `CSpeedTreeRT::LoadTree()`;仓库静态库实测为带符号的 **COFF x86-64**,不能链进 macOS/移动端且分发需确认授权。`formats/spt` 已能嗅探 bark/composite atlas,真实资产共 **118** 个 | (a) 先用随库 `SpeedTreeRT.h` + `.lib` 验证经授权的 Windows x64 离线 exporter,调用 `LoadTree/Compute/GetGeometry``.glb`;(b) 当前运行时 proxy 使用真实 bark/composite DDS(c) exporter 不可用时继续用授权清晰的替代树 | L(exporter spike 约 1 周;成功后转换与校形 2–3 周) |
| 1.2 | **`.mse`(特效 / 粒子)** 🟡 首版打通(见 `CLIENT-ROADMAP.md` P5| 资产是 ASCII Group/List 文本;`EffectLib/EffectData.cpp``ParticleSystemData.cpp``EffectMesh.cpp``SimpleLightData.cpp` 已包含 parser 和运行时真值 | ✅ `project/fx/``mse.gd` 文本树解析(真文件 6 particle)· `effect_player.gd` Particle→`GPUParticles3D`(发射形状/曲线/颜色渐变/billboard/加法混合)· `effect_registry.gd` 名字→`.mse`+缓存 · `wire.h` `GC_SPECIAL/SPECIFIC_EFFECT``M2Client.effect_cue`。⬜ `.dds` 粒子纹理、`.mde` mesh 解码、SimpleLight(本资产集未用)、绑骨骼挂点、时间轴位置动画 | L–XL(parser 端口 + 运行时/材质/挂点,独立子项目) |
| 1.3 | **lens flare** | `.msenv``LensFlare` 块(已解析),但需屏幕空间实现 | 太阳屏幕投影 + flare billboard;遮挡先用相机到太阳方向的物理射线,或在后处理里读深度。若使用低层 occlusion query,必须基于锁定的 Godot 4.7 API 实证,不使用不存在的 `get_1d_probe` | SM |
### 1.4 树木方案门禁
A1 已确认 **368 棵 / 14 树种**,全资产树库共 118 个 `.spt`;现有 R2 proxy 已按 treefile 分组为
`MultiMeshInstance3D`,从 `.spt` 嗅探真实树皮/composite atlas,并生成确定性枝干与多组交叉叶簇。
它仍没有真实 SpeedTree 几何、leaf table、LOD/impostor。批量开发前先从最终机位按屏幕投影面积选 3 个代表树种,
分别验证以下来源:
1. 具备授权且能实际读取这些旧 `.spt` 的 SDK/离线 converter
2. 保留原 treefile → 替代 mesh 映射的人工/半自动重建;
3. 改良程序化树,仅作为 converter 和替代 mesh 都不可行时的降级路径。
每条路径都必须产出同一份 `TreeDescriptor`(树干/叶片材质、LOD0/1、远景 impostor、包围盒、风参数),
并以树冠轮廓、树干比例、叶片 alpha、环境着色、阴影、LOD 跳变、批次数和授权可分发性签核。
未通过单树 spike 前不批量处理 118 个 `.spt`,运行时也不得直接依赖 Windows x64 专有 `.lib`
**实例语义修正(已落地)**:旧 `Metin2World` 会给每棵占位树额外生成随机 yaw 和 scale jitter;原客户端
`CArea::TObjectInstance::SetTree``CSpeedTreeForest::CreateInstance` 在这条地图放置路径上只传
position、property CRC 和 treefile,实例本身只调用 `SetPosition``LoadTree` 也没有从这里收到
TreeSize/TreeVariance。当前实现已移除这些随机变换;若替代树为避免重复确实需要
随机旋转,应作为明确的 fallback/增强开关,而不能污染基线。
---
## 2. 角色 / 模型保真
| # | 项 | 现状 | 目标 | 工作 | 规模 | BACKLOG |
|---|---|---|---|---|---|---|
| 2.1 | 武器 mesh | 🟡 **首版已做 + 2026-08-31 复核**`Metin2Model.weapon_gr2` / `weapon_bone`(默认 `equip_right_hand`warrior bone[10])。加载武器 gr2 → `build_mesh`rigid)→ 子 `MeshInstance3D`;每帧 `update_weapon_pose(world_pose[bone])`。**隔离探针实测**`warrior_novice`/`warrior_cheongrin` × 8 个武器文件,weapon MI 全局原点 == `equip_right_hand` 骨骼全局原点,Δ = 0.000.06 mworld_demo 目视剑握在手里。之前记忆里「武器偏 ~10 m」是 cpu_skin `>=` / MapCoord 修复前的旧状态,已失效。新增 **grip pre-transform**`weapon_pre = mul4x3(weaponInvWorld[0], weaponLocal[0])`,对齐客户端 `weaponComposite[0]` = `ModelInstanceUpdate.cpp:171`),`update_weapon_pose``mul4x3(weapon_pre, world_pose[hand])`;标准武器多校准 ~4–6 cm 握点。个别非标准文件(`02010`/`03010`/`03150`)骨骼带米级偏移 → 30 cm 夹断退回原始手部位姿(不回退)。`mul4x3` 提到 `gr2_bridge.h` 与 anim 共用 | 右手单持跟手 1:1;`03150` 类网格在文件里就离骨骼原点 ~3 m(资产缺陷,非变换问题)—— 需 per-weapon 偏移数据或修 .gr2。⬜ 左手/双持/盾、`_lod` 变体 | M | C5 / D2 |
| 2.2 | GPU 蒙皮路径的发型 | ✅ **已做**`_build_gpu_mesh()` —— `enable_gpu_skin` 带发型时,把发型 mesh 以 bind pose 并入 GPU mesh,每个 `ARRAY_BONES` 值经 `hair_bone_remap`(发型骨 idx → base 骨 idx)重映射。**无需改 shader / 第二骨索引区**warrior 74/74 全匹配)—— `SRC_SKIN` + 共享 `bones_tex` 直接形变。`_apply_materials` 给追加的发型 surface 上「skinned + alpha-test + hair 贴图」材质。实测 5 base + 1 hair surface,红发(TargetSkin)贴头、镂空干净、跟动画 | M(完成)| A1 |
| 2.3 | 换装 / 时装 / 翅膀 | 无 | 多 `MeshInstance3D` 绑同一骨架 + 部位遮挡 / 隐藏规则 | `.msm` + 物品系统的上层 | L | D1 / D4 |
| 2.4 | 贴图换色 `SourceSkin→TargetSkin` | ✅ **首版已做**`Metin2Model.hair_skin` —— 设了就用该 dds 当发型 albedo(代替 gr2 sibling 的 SourceSkin),对齐客户端 `SetMaterialImagePointer(part, SourceSkin, load(TargetSkin))``ActorInstanceData.cpp:152/225`)。`get_hair_options()` 现返回解析成绝对路径的 `source_skin` / `target_skin``formats.msm_hair` CTest 钉了 HairData 解析(同 gr2+SourceSkin、不同 TargetSkin)。demo 用红色变体 | ⬜ 非发型部位(护甲/时装)的 skin 表;`warrior_cheongrin` 头盔挡住发型,换色需换模型目视 | S(首版完成) | D3 |
| 2.5 | 固定功能材质状态覆盖 | 单 stage `MODULATE(TEX,DIFFUSE)` 近似;**sphere-map 分支已按源码端口**(见 §2.7)。**blend / two-sided 改读 `gr2::MaterialInfo`**`metin2_model::decide_blend()` 优先 `alpha_blend``dump_materials` 端口 `Material.cpp:233` = Name `Blend*` + MapCount>1),`decide_two_sided()``two_sided`+ hair/cloak 名兜底),`m2_material``cull_disabled` shader 变体。仅 hair 镂空 / effect 加法这两类 GR2 没编码的仍按表面名 | 复现最终机位实际触发的 alpha-test ref、多贴图组合 | 逐分支盘点 `EterGrnLib/Material.cpp`GR2 不承载任意 D3D stage 状态 | M | B2/B4 |
| 2.6 | 顶点色 | ✅ **排查完毕:不适用**。全资产 `.gr2`PC / monster2 / effect**零** `DiffuseColor`/`Color0` 顶点成员(grep 整个 assets 目录 = 0)。Metin2 的「逐顶点颜色」实际是:① 地形雾 —— `MapOutdoorRenderSTP` 运行时动态 VB 里 `dwDiffuse`/`dwSpecular` 存雾色/雾系数(非资产,已由 Godot `Environment` 雾覆盖);② `.mse` 特效 `colorfactor` / 粒子 `TimeEventColor`(属 §1.2 `.mse` 端口)。libgr2 无需改,`build_mesh` 无需补 `ARRAY_COLOR` | — | B7 |
| 2.7 | 镜面 / 球面反射(护甲金属光) | 🟡 **管线已端口**`SRC_MIX` / `SRC_SKIN``spec_map`/`spec_power`/`spec_enable`。按 `EterGrnLib/Material.cpp:305 __ApplySpecularRenderState` 复现:仅不透明面(客户端 alpha-blend 时回退纯 diffuse)、相机空间 `reflect(VERTEX,NORMAL).xy` 查 sphere map、`MODULATEALPHA_ADDCOLOR` = `EMISSION += sphere.rgb * tex.a * power``tex.a` = 护甲高光遮罩,`power` = `D3DRS_TEXTUREFACTOR.a`)。sphere map = 全局 `ETC/ymir work/special/spheremap.jpg`(客户端 index 0),`Metin2Model::_load_sphere_map()` 沿模型目录上溯解析。`Metin2Model.specular_power` 属性。**默认 0 = 完全休眠**,与之前逐字节一致 | 端口 `__ApplySpecularRenderState`(✅);接装备层用 `item_proto bSpecular/100` 驱动(✅ 身体/护甲:`project/ui/equip_model.gd:283-288` `_apply_body()`;⬜ 武器 / 发型 / 时装等其余部位未接);受控帧标定 power(⬜,§0 门禁) | M | B9 |
| 2.8 | 角色专属光(`DirectionalLight.Character` | 只用 `Background` 光 | 角色颜色与原客户端 Character light 接近 | 给 `SRC_MIX`/`SRC_SKIN` 加 character-light uniform,或用 light `cull_mask` 分层 | SM | E2 |
| 2.9 | 动画混合 / crossfade | ✅ **首版已做**`Metin2AnimPlayer.blend_time`(默认 0.15s)。切 `anim_path` 时保留旧 clip 冻结在当前时间,每帧对旧/新 `sample_pose``world_pose` 逐骨 slerp(rot)+lerp(pos/scale)smoothstep 权重),再 `skin = invWorld · world_blended` 重建;武器挂点跟混合后的手。shear 骨在 ≤blend_time 过渡内退化为无 shear(文档许可)。目视 wait→run 连续无 T-pose 闪 | ⬜ 受控帧核对过渡时长 | M(首版完成) | C2 |
| 2.10 | LOD | ✅ **首版已做**`Metin2Model` reload 时自动加载 `<base>_lod_01/02/03.gr2`(同 75 骨骼、抽面 mesh),`_process` 按相机距离切换渲染 mesh`lod_distances` 属性,默认 `[18,42,90]`m)。切换时重建 `cpu_mesh` + 重解材质 + 重蒙皮;骨架共享故蒙皮不受影响。烟测:`d=12→L1 d=21→L2 d=30→L3``wait.msa` 播放中不崩),4 级渲染均干净。⬜ 淡入淡出过渡、GPU 蒙皮路径的 LOD | M(首版完成)| C9 |
| 2.11 | `ypr_basis` 符号终核 | ✅ **发现并修复真实 bug**:物体旋转 `yaw#pitch#roll` 之前**未做** Z-up→Y-up 轴共轭就塞进 Godot Basis,导致 66% 的 A1 物体(roll≠0 = 朝向)绕 Godot Z 翻滚而非绕 Y 转向。新增 `m2_coord::object_basis_godot()` = `ypr_basis()` 共轭 `rotate(-90°,X)``metin2_world` 放置改用它。已核对 `ypr_basis` 本身忠实移植客户端 `D3DXMatrixRotationYawPitchRoll``Area.cpp:520/619` + `GrpObjectInstance.cpp:165`;单值旋转=roll=朝向,`Area.cpp:820`)。`formats.map_formats` 加断言(identity / roll=90→绕 Godot+Y / 正交 det=+1)。目视:A1 建筑竖直、门楼与桥沿路对齐 | ⬜ 对原客户端截图的逐建筑朝向复核(§0 受控帧) | S(首版完成) | — |
---
## 3. 地形 / splat 保真
| # | 项 | 现状 | 目标 | 工作 | 规模 |
|---|---|---|---|---|---|
| 3.1 | 活动图层上限 | ✅ **首版已修**shader 支持 **16 层**4 张 RGBA8 权重贴图),`min(8)``min(16)`A1 单区块最多 12 层(`000004`/`003004`)不再静默丢 | 每区块全部活动图层按原顺序混合 | ⬜ 按 patch 分批省带宽;真实 A1 层数断言 | M(P1,首版完成) |
| 3.2 | 每图层平铺密度 | ✅ **首版已修**`layer_uv = (8·UScale, -8·VScale, UOffset, -VOffset)`,来自 `TextureSet.cpp:185` + `TexCoordBase = 1/(PATCH_XSIZE·CELLSCALE) = 1/3200`。⬜ 对 `minimap.dds` / 受控参考帧的视觉验收 | 精确移植原客户端 UV 变换 | — | S–M(P1,首版完成) |
| 3.3 | `shadowmap.dds` 调制 | 🟡 **首版已分层**:树 `MultiMeshInstance3D` + `shadowflag≠1` 的建筑/地牢块 → `SHADOW_CASTING_SETTING_OFF``shadowflag=1` 建筑 + 玩家/NPC → 实时投影。terrain shader 仍 `col *= shadowmap.rgb`。**+ 角色接触阴影**`project/ui/char_shadow.gd``CharShadow.attach`)—— `player_view`/`mob_view` 建完模型后强制所有 `MeshInstance3D` `cast_shadow=ON`(防被 world 的 OFF 波及)+ 脚下一张径向渐变 `Decal` 投影到地形/物体(对齐 `GameLib/MapOutdoorCharacterShadow.cpp` 的结果,不复刻 RT)。太阳接近正午实时投影很短时也有接触暗影。`eterngrn_polish_test.gd` 覆盖 | ⬜ baked/realtime 精确不叠加的 mask、Decal 参数受控帧标定 | M(首版完成) |
| 3.4 | 近景清晰度 | ✅ **首版已修**:删掉程序化 `macro_detail`Texture2DArray 尺寸取「用到的图层里最大源边长(≤1024)」,只放大不硬降 512² 地表贴图 | 严格 parity 基线不叠加现代化增强;确认需要后再加可开关 detail map | — | SM(首版完成) |
| 3.5 | 地形 LOD / patch 裁剪 | 🟡 **首版已做**`Metin2World.terrain_patches`(默认 4)—— 每区块地形拆成 N×N 个 `MeshInstance3D` patch16 个,各 32×32 quad),共享 splat 材质。逐 patch **视锥剔除**Godot 自动,主要增益)+ `visibility_range_end`(默认 3500m 硬剔)远景整片剔除。顶点/法线/UV 从整块 `TerrainMesh` 拷贝 → **无裂缝无接缝**(顶视 + 斜视验证)。`terrain_patches=1` = 旧单 mesh | ⬜ 逐 patch 几何 LOD(抽稀索引集 + skirt 防裂);碰撞体也按 patch | M(首版完成)|
| 3.6 | 草(instanced grass) | 无;当前参考截图不能明确证明存在独立草片几何 | 只有原客户端源码、属性或受控近景帧确认后,才在对应区域撒 `MultiMesh` 草片 + 风摆 | 先取证,避免把现代化增强混入 parity 基线 | 可选 M |
| 3.7 | 相邻区块共享边断言 | ✅ 已加 `formats.map_formats` seam 断言:`000000` col-128 高度 == `001000` col-0(边界样本)+ 生成顶点位置一致,实测无接缝 | — | — | S(完成) |
---
## 4. 水面
| # | 项 | 现状 | 目标 | 工作 | 规模 |
|---|---|---|---|---|---|
| 4.1 | 原客户端水纹序列 | ✅ **首版已做**`special/water/01..30.dds``Texture2DArray`shader `frame = (TIME·1000/70)%30``MapOutdoorWater.cpp:43` 实测 70ms/帧),UV 平铺 = 1/8m`m_fWaterTexCoordBase = 1/(CELLSCALE·4)`)。⬜ wrap/filter 细调、原客户端叠加 blend 状态 | — | S–MP1,首版完成) |
| 4.2 | 水深透明与高度动画 | ✅ **首版已做**:逐顶点 `COLOR.a``waterHeight - terrain_height_at()` 算(60cm 深→近不透明),vertex shader 轻微高度浮动(近似 `MapOutdoorWater` 0..-15cm 的 0.6Hz 摆动)。⬜ 深度衰减系数按受控参考帧标定(现 A1 浅水偏透明) | — | M(P1,首版完成) |
| 4.3 | 反射 | 无真实反射 | 若受控参考帧证明必要,使用镜像相机 + `SubViewport` 做平面反射,或以 SSR 近似 | `ReflectionProbe` 是 cubemap,不作为平面反射实现 | 可选 M |
| 4.4 | 折射 | 无 | Godot 4 sampler `hint_screen_texture` + `SCREEN_UV` 扰动,结合 depth sampler 衰减 | 属于增强项;原客户端水面 parity 完成后再做 | 可选 SM |
| 4.5 | 岸线泡沫 / 法线贴图 | 无 | 仅在参考帧明确需要时增加;不宣称原客户端使用 normal map | 与严格 parity 材质做开关和 golden 对照 | 可选 S |
| 4.6 | 网格合并 | 同水层/区块已经是一个 mesh surface,内部按行生成 span quad | 行列矩形合并只减少顶点/索引;要减少 draw call 需跨区块/水层合批或统一 surface | 分别记录 vertex/index 与 draw-call 指标,避免把几何优化写成 draw-call 优化 | S |
---
## 5. 环境 / 光照 / 色调
| # | 项 | 现状 | 目标 | 规模 |
|---|---|---|---|---|
| 5.1 | 云层 | `.msenv` 有 cloud texture/scale/height/speed(已解析),未渲染 | 天空 shader 加滚动云层 + `List CloudColor` 着色 | M |
| 5.2 | 光照对受控参考帧校准 | `.msenv` → Godot 近似,肉眼调 | 在原 D3D 客户端到已知机位截图 → Godot 复现 → ROI 亮度/饱和/主色迭代到匹配(SHINSOO §11.4) | M(含反复采集) |
| 5.3 | 阴影质量 | 4-splitcascade 500mbias 调过 | 逐场景标定 cascade split / normal bias / peter-panning;角色脚下清晰接触阴影 | S–M |
| 5.4 | 昼夜 / 天气 | 无 | 若参考图之外还要,`.msenv` 多套 + 插值 | M(可选) |
| 5.5 | 色调校准 / LUT | `adjustment` 饱和/对比 | 先用同机位、同曝光的多组 ROI/校准帧拟合曝光、曲线、饱和度;必要时再固化为 3D LUT。单张场景截图不足以唯一反推出 LUT | S–M |
---
## 6. HUD / UI
目标截图的 UI 不是「有相似窗口即可」。建立 `test/maps/ingame-shinsoo-ui.json`,为圆形小地图、右侧已打开
背包、底部聊天/HP/MP/快捷栏以及顶部 objective/action/绿色提示逐项保存:1920×1080 像素 rect、anchor、
pivot、z-index、九宫格边距、贴图/字体路径、字号、描边、颜色、文本和 visible 状态。布局比对直接读取
该文件,禁止在测试里散落第二套魔法数字。
| # | 项 | 现状 | 目标 | 规模 |
|---|---|---|---|---|
| 6.1 | UI 美术 | 🟡 **机制已做**`hud.gd` 用真实 `ETC/ymir work/ui/pattern/` 贴图 —— `board`/`thinboard` 九宫格(4 角 + 4 边 + `*_base` 合成 atlas → `NinePatchRect`margin=32/16),HP/MP 用 `gauge_slot_{left,center,right}` 框 + `gauge_red`/`gauge_blue` 填充,快捷栏 8 格 `thinboard`,小地图套 `board` 框。贴图按资产根 + 各 pack `ymir work/ui/pattern/` 解析(`.tga``Image.load`)。⬜ 精确布局(rect/anchor/字体/描边)待 §0 `ingame-shinsoo-ui.json``locale/<lang>/ui/` 本地化图;`windows.dds` atlas 子图 | M(机制完成) |
| 6.2 | 小地图 | `minimap.dds` 拼图 + 玩家点 | 先保持地图 north-up、按玩家 heading 旋转箭头;若原客户端配置/受控帧证明整图旋转,再实现地图旋转。补缩放、NPC/怪/出口图标和交互 | M |
| 6.3 | 状态栏 / 快捷栏 / 背包 | 静态占位 | 真实布局 + 数值绑定 + 拖拽 + tooltip(不接服务端物品逻辑) | M–L |
| 6.4 | 头顶文字 / 血条 | 无 | `Label3D` 名字 + 屏幕空间血条跟随 | S–M |
| 6.5 | 聊天框 / 系统消息 | 无 | `RichTextLabel` + 频道 tab(参考图里通常有) | S |
| 6.6 | 1920×1080 布局 | 1152 测试分辨率 | 目标分辨率像素对齐 + 缩放策略 | S |
---
## 7. 后处理
| # | 项 | 现状 | 目标 | 规模 |
|---|---|---|---|---|
| 7.1 | Bloom / glow | 🟡 已收敛(intensity 0.35→0.12hdr 阈值 1.1→1.5,bloom→0;曾让角色发糊)。参考帧未落盘前保守 | 按参考图高光溢出量标定 | S |
| 7.2 | SSAO | 🟡 已收敛(intensity 1.4→0.7)。参考帧未落盘前保守 | 标定强度 / 半径 | S |
| 7.3 | 抗锯齿 | MSAA 4× | + TAA 或 FXAA(移动端权衡) | S |
| 7.4 | 景深 | 无 | 参考图若有远景虚化则加 | S(可选) |
| 7.5 | 暗角 / 色差 | 无 | 参考图若有则轻加,禁过度 | S |
---
## 8. 相机 / 构图
| # | 项 | 现状 | 目标 | 规模 |
|---|---|---|---|---|
| 8.1 | 机位匹配 | 🟡 渲染分辨率已提到 1920×1080(`project.godot`);机位仍是脚本里随手放(fov 60) | 精确复现参考图高度/距离/pitch/FOV,存 `test/maps/` 确定性 preset | S |
| 8.2 | 相机遮挡淡出 | ✅ **首版已做**`Metin2World` 给每个静态物体加层 2 盒碰撞(mesh AABB → Y-up 米,非缩放 shape`occ_mesh` meta 指回 MeshInstance3DA1 601 个)。`game_camera.gd::_fade_occluders` 每帧 cam→player 射线(ray exclude 逐个穿,最多 4 层)→ 命中建筑 `GeometryInstance3D.transparency = 0.72`,不挡时恢复 0。⬜ 树(MultiMesh 无逐实例透明)、渐变淡入淡出 | M(首版完成) |
| 8.3 | 相机碰撞 | ✅ **首版已做**`game_camera.gd::_desired_pos` head→want 射线打层 2,命中建筑就把相机拉到撞点前 0.3m(叠加已有地形采样防穿)。静态物体碰撞体作为 8.2 的副产品一并落地(`player_controller` 仍用 `attr.atr` 网格,可后续接物理)。⬜ `SpringArm3D` 平滑、`.mdatr` 精确碰撞体 | M(首版完成) |
---
## 9. 验证方法论(做「一致」的前提)
| # | 项 | 说明 | 规模 |
|---|---|---|---|
| 9.1 | 受控参考帧采集 | 在原 D3D 客户端脚本化到同一「已知全局坐标 + yaw/pitch + FOV」连续采两帧:关 HUD 的世界帧与开 HUD 的合成帧;参数存进 `test/maps/`。原客户端不能脚本化则「同地标 + 人工对齐 + 只比 ROI」 | M |
| 9.2 | ROI 直方图 + SSIM 门禁 | `test/compare.py` 扩:世界帧按天空 / 道路 / 草地 / 建筑 / 树 / 角色分区,比均亮 / 饱和 / 主色 / 边缘结构;合成帧另做 UI rect 与文本基线比对 | M |
| 9.3 | golden 锁定项 | Godot 版本 / renderer / 窗口 / 相机 / 时间 / 动画帧 / 随机种子 / 曝光 / 天气 / 资源优先级 全锁 | S |
| 9.4 | `world-a1-r1/r2.png` 进 CI | `formats/tests` 已进 CTestgolden 视觉门禁待接 | S |
| 9.5 | 目标帧验收配置 | 在 `ingame-shinsoo.json` 为天空/草地/道路/建筑/树/角色分别保存 ROI、metric、thresholdUI 窗口 rect/锚点初始容差为 1 px | S |
| 9.6 | 阈值冻结规则 | 同一机器连续渲染 5 次测噪声底;世界 ROI 阈值必须高于噪声并经一次人工签核后冻结。不得在回归失败时临时放宽阈值 | S |
---
## 10. 平台(SHINSOO 的前置,不是观感项但影响「能不能发」)
- Android(一加 13 / Vulkan+ iOSiPhone 16 / Metal,静态链接)bring-up**一次都没跑过**。见 `BACKLOG` F1F7。
- 所有新 shader`SRC_TERRAIN` / `SRC_WATER` / `SRC_LEAF` / `SRC_SKIN`)在 Mobile renderer 下未验。
- 移动端性能预算(流式 + `MTGODOT_GPUSKIN` 人群)未测真机。
---
## 11. 建议推进顺序
**第 1 梯队(性价比最高,1–1.5 月)**
1. 0.10.3 目标帧/资产/UI 契约 + 8.1 机位 preset + 9.1/9.5 受控参考与验收配置 —— 没有基准就没法谈「一致」
2. 3.1 全活动 splat 图层 + 3.2 原始 UV 公式 —— 先修复会丢内容/尺度错误的地表正确性问题
3. 1.4 最终机位可见树种清单 + 单树 spike;先用替代 mesh 达到轮廓,再并行验证授权 converter
4. 5.2 光照校准 + 5.5 色调拟合 —— 全画面观感一次性拉近
5. ✅ 2.1 武器(右手单持)+ ✅ 2.11 ypr 终核(修了漏轴转换 bug+ ✅ 2.9 crossfade + ✅ 2.4 换色 —— 角色补齐(首版)
6. 🟡 6.1 HUD 真美术 —— 九宫格机制已上,布局待 §0;✅ 3.3 阴影分层首版
**第 2 梯队(保真化,1.5–2 月)**
7. 1.1 `.spt` reader/converter 通过授权与技术验证后批量离线转 Mesh,并接入统一 `TreeDescriptor`
8. 4.1–4.2 原客户端 30 帧水纹 + 深度 alpha;4.3–4.5 只按受控参考帧选择性增强
9. 5.1 云层 + 1.3 lens flare
10. ✅ 2.7 sphere-map 高光(管线已端口 + 装备层已驱动身体部位;逐部位与 power 标定仍缺);⬜ 2.5 其余固定功能材质分支 / 2.8 角色专属光
11. 3.6 instanced grass(仅在取证确认后)
**第 3 梯队(大工程 / 可并行 / 部分可选)**
12. 1.2 `.mse` parser/EffectLib 语义到 Godot 的端口 —— 独立子项目,L–XL
13. 2.3 换装系统、6.26.5 完整 HUD
14. 10. 移动端 bring-up
**贯穿**:每个梯队做完过一次 9.2 的 ROI 门禁 + 人工签核,用数据说话,别靠感觉。