# 渲染一致性差距清单 —— 距 `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 未按受控帧标定 **粗估**:把下面全部做到「目视等价」,单人 **≈ 3–6 个月**(不含 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` | S–M | ### 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.00–0.06 m;world_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` 分层 | S–M | 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 时自动加载 `_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 | — | S–M(首版完成) | | 3.5 | 地形 LOD / patch 裁剪 | 🟡 **首版已做**:`Metin2World.terrain_patches`(默认 4)—— 每区块地形拆成 N×N 个 `MeshInstance3D` patch(16 个,各 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–M(P1,首版完成) | | 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 完成后再做 | 可选 S–M | | 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-split,cascade 500m,bias 调过 | 逐场景标定 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//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.12,hdr 阈值 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 指回 MeshInstance3D;A1 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` 已进 CTest;golden 视觉门禁待接 | S | | 9.5 | 目标帧验收配置 | 在 `ingame-shinsoo.json` 为天空/草地/道路/建筑/树/角色分别保存 ROI、metric、threshold;UI 窗口 rect/锚点初始容差为 1 px | S | | 9.6 | 阈值冻结规则 | 同一机器连续渲染 5 次测噪声底;世界 ROI 阈值必须高于噪声并经一次人工签核后冻结。不得在回归失败时临时放宽阈值 | S | --- ## 10. 平台(SHINSOO 的前置,不是观感项但影响「能不能发」) - Android(一加 13 / Vulkan)+ iOS(iPhone 16 / Metal,静态链接)bring-up:**一次都没跑过**。见 `BACKLOG` F1–F7。 - 所有新 shader(`SRC_TERRAIN` / `SRC_WATER` / `SRC_LEAF` / `SRC_SKIN`)在 Mobile renderer 下未验。 - 移动端性能预算(流式 + `MTGODOT_GPUSKIN` 人群)未测真机。 --- ## 11. 建议推进顺序 **第 1 梯队(性价比最高,1–1.5 月)** 1. 0.1–0.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.2–6.5 完整 HUD 14. 10. 移动端 bring-up **贯穿**:每个梯队做完过一次 9.2 的 ROI 门禁 + 人工签核,用数据说话,别靠感觉。