Files
mtgodot-poc/docs/PARITY-GAP.md
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
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
2026-08-31 20:02:12 +09:00

28 KiB
Raw Blame History

渲染一致性差距清单 —— 距 ingame-shinsoo.png 还差什么

更新:2026-08-29 · 配套 SHINSOO-WORLD-RENDERING.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 高光管线已端口但默认休眠(无装备层驱动 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.rawtile.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 .sptSpeedTree 树) 几何解析在闭源 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 不可用时继续用授权清晰的替代树 Lexporter spike 约 1 周;成功后转换与校形 2–3 周)
1.2 .mse(特效 / 粒子) 🟡 首版打通(见 CLIENT-ROADMAP.md P5 资产是 ASCII Group/List 文本;EffectLib/EffectData.cppParticleSystemData.cppEffectMesh.cppSimpleLightData.cpp 已包含 parser 和运行时真值 project/fx/mse.gd 文本树解析(真文件 6 particle)· effect_player.gd Particle→GPUParticles3D(发射形状/曲线/颜色渐变/billboard/加法混合)· effect_registry.gd 名字→.mse+缓存 · wire.h GC_SPECIAL/SPECIFIC_EFFECTM2Client.effect_cue .dds 粒子纹理、.mde mesh 解码、SimpleLight(本资产集未用)、绑骨骼挂点、时间轴位置动画 LXLparser 端口 + 运行时/材质/挂点,独立子项目)
1.3 lens flare .msenvLensFlare 块(已解析),但需屏幕空间实现 太阳屏幕投影 + 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::SetTreeCSpeedTreeForest::CreateInstance 在这条地图放置路径上只传 position、property CRC 和 treefile,实例本身只调用 SetPositionLoadTree 也没有从这里收到 TreeSize/TreeVariance。当前实现已移除这些随机变换;若替代树为避免重复确实需要 随机旋转,应作为明确的 fallback/增强开关,而不能污染基线。


2. 角色 / 模型保真

# 现状 目标 工作 规模 BACKLOG
2.1 武器 mesh 🟡 首版已做 + 2026-08-31 复核Metin2Model.weapon_gr2 / weapon_bone(默认 equip_right_handwarrior bone[10])。加载武器 gr2 → build_meshrigid)→ 子 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-transformweapon_pre = mul4x3(weaponInvWorld[0], weaponLocal[0]),对齐客户端 weaponComposite[0] = ModelInstanceUpdate.cpp:171),update_weapon_posemul4x3(weapon_pre, world_pose[hand]);标准武器多校准 ~4–6 cm 握点。个别非标准文件(02010/03010/03150)骨骼带米级偏移 → 30 cm 夹断退回原始手部位姿(不回退)。mul4x3 提到 gr2_bridge.h 与 anim 共用 右手单持跟手 1:103150 类网格在文件里就离骨骼原点 ~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_skinformats.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::MaterialInfometin2_model::decide_blend() 优先 alpha_blenddump_materials 端口 Material.cpp:233 = Name Blend* + MapCount>1),decide_two_sided()two_sided+ hair/cloak 名兜底),m2_materialcull_disabled shader 变体。仅 hair 镂空 / effect 加法这两类 GR2 没编码的仍按表面名 复现最终机位实际触发的 alpha-test ref、多贴图组合 逐分支盘点 EterGrnLib/Material.cppGR2 不承载任意 D3D stage 状态 M B2/B4
2.6 顶点色 排查完毕:不适用。全资产 .gr2PC / 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_SKINspec_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 * powertex.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 逐部位驱动(,无物品系统);受控帧标定 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_poseworld_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 按相机距离切换渲染 meshlod_distances 属性,默认 [18,42,90]m)。切换时重建 cpu_mesh + 重解材质 + 重蒙皮;骨架共享故蒙皮不受影响。烟测:d=12→L1 d=21→L2 d=30→L3wait.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 本身忠实移植客户端 D3DXMatrixRotationYawPitchRollArea.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 层数断言 MP1,首版完成)
3.2 每图层平铺密度 首版已修layer_uv = (8·UScale, -8·VScale, UOffset, -VOffset),来自 TextureSet.cpp:185 + TexCoordBase = 1/(PATCH_XSIZE·CELLSCALE) = 1/3200minimap.dds / 受控参考帧的视觉验收 精确移植原客户端 UV 变换 SMP1,首版完成)
3.3 shadowmap.dds 调制 🟡 首版已分层:树 MultiMeshInstance3D + shadowflag≠1 的建筑/地牢块 → SHADOW_CASTING_SETTING_OFFshadowflag=1 建筑 + 玩家/NPC → 实时投影。terrain shader 仍 col *= shadowmap.rgb+ 角色接触阴影project/ui/char_shadow.gdCharShadow.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_detailTexture2DArray 尺寸取「用到的图层里最大源边长(≤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.ddsTexture2DArrayshader frame = (TIME·1000/70)%30MapOutdoorWater.cpp:43 实测 70ms/帧),UV 平铺 = 1/8mm_fWaterTexCoordBase = 1/(CELLSCALE·4))。 wrap/filter 细调、原客户端叠加 blend 状态 SMP1,首版完成)
4.2 水深透明与高度动画 首版已做:逐顶点 COLOR.awaterHeight - terrain_height_at() 算(60cm 深→近不透明),vertex shader 轻微高度浮动(近似 MapOutdoorWater 0..-15cm 的 0.6Hz 摆动)。 深度衰减系数按受控参考帧标定(现 A1 浅水偏透明) MP1,首版完成)
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;角色脚下清晰接触阴影 SM
5.4 昼夜 / 天气 若参考图之外还要,.msenv 多套 + 插值 M(可选)
5.5 色调校准 / LUT adjustment 饱和/对比 先用同机位、同曝光的多组 ROI/校准帧拟合曝光、曲线、饱和度;必要时再固化为 3D LUT。单张场景截图不足以唯一反推出 LUT SM

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 → NinePatchRectmargin=32/16),HP/MP 用 gauge_slot_{left,center,right} 框 + gauge_red/gauge_blue 填充,快捷栏 8 格 thinboard,小地图套 board 框。贴图按资产根 + 各 pack ymir work/ui/pattern/ 解析(.tgaImage.load)。 精确布局(rect/anchor/字体/描边)待 §0 ingame-shinsoo-ui.jsonlocale/<lang>/ui/ 本地化图;windows.dds atlas 子图 M(机制完成)
6.2 小地图 minimap.dds 拼图 + 玩家点 先保持地图 north-up、按玩家 heading 旋转箭头;若原客户端配置/受控帧证明整图旋转,再实现地图旋转。补缩放、NPC/怪/出口图标和交互 M
6.3 状态栏 / 快捷栏 / 背包 静态占位 真实布局 + 数值绑定 + 拖拽 + tooltip(不接服务端物品逻辑) ML
6.4 头顶文字 / 血条 Label3D 名字 + 屏幕空间血条跟随 SM
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 米,非缩放 shapeocc_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。
  • 所有新 shaderSRC_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.52 月) 7. 1.1 .spt reader/converter 通过授权与技术验证后批量离线转 Mesh,并接入统一 TreeDescriptor 8. 4.14.2 原客户端 30 帧水纹 + 深度 alpha;4.3–4.5 只按受控参考帧选择性增强 9. 5.1 云层 + 1.3 lens flare 10. 2.7 sphere-map 高光(管线已端口,默认休眠); 2.5 其余固定功能材质分支 / 2.8 角色专属光 11. 3.6 instanced grass(仅在取证确认后)

第 3 梯队(大工程 / 可并行 / 部分可选) 12. 1.2 .mse parser/EffectLib 语义到 Godot 的端口 —— 独立子项目,LXL 13. 2.3 换装系统、6.26.5 完整 HUD 14. 10. 移动端 bring-up

贯穿:每个梯队做完过一次 9.2 的 ROI 门禁 + 人工签核,用数据说话,别靠感觉。