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
55 KiB
神兽国 A1 场景最终渲染开发文档
状态:实施规格(Implementation Specification)
目标工程:mtgodot-poc
资源来源:仓库内assets/(gitignored,MT_ASSETS可覆盖)
原客户端行为参考:同级仓库m2dev-client-src-main/src
目标参考图:ingame-shinsoo.png,1920×1080 的神兽国村庄第三人称画面
定位(先读)
这不是 Phase 1 / POC 收尾,是「正式移植」的一个独立子项目。
GODOT-POC-PLAN.md的 POC 判定问题是「能否脱离 Granny 正确读+渲+动.gr2骨骼模型、且跨平台」——已答「能」。本文是世界渲染工程,前置 = Phase 2 的三设备 bring-up (Mac / 一加 13 / iPhone 16)完成——即确认整套桥接在两台移动设备上也能跑。项目自用、不对外 发布,没有正式 go/no-go 门禁;W0 可随时开始(纯解析、无 Godot 依赖),W1 及以后建议等 bring-up 有结果,避免在移动端还没验证时就投入几个月的重 shader 工作。本文也扩大了 POC 的排除范围:POC §01 把「UI / Python 脚本层」列为「明确不做」;本文 R2 把 HUD / 小地图 / 头顶文字纳入交付。这是有意的范围扩张,因为最终画面对标的是完整客户端。
规模粗估(置信度低,排期须细化):重写客户端的地形 + 地图对象 + 环境系统,参照原
PRTerrainLib/GameLib/Area/Property/MapType(原客户端里数千行)。
阶段 单人粗估 W0(AssetResolver + 6 个格式 parser + 坐标 + 单测) 2–3 周 W1–W2(地形几何 + splat + 烘焙阴影) 3–5 周 W3(Property/AreaData 静态对象) 2–3 周 W4(树木;含 .spt工具链未知,见 §9 W4)2–6 周 W5–W6(环境/光照 + 玩家/NPC/相机) 3–4 周 R1 合计 ≈ 3.5–5.5 月 W7(水 + 必要 .mse+ HUD)3–6 周 W8(流送 + 缓存 + 移动端性能) 4–8 周(含一加 13 / iPhone 16 实测) R2 合计(在 R1 之上) ≈ +2–3.5 月 叠加 Phase 2 的 bring-up。多人并行可压缩 W1/W3/W4/W6,但 W0 和坐标模块是串行硬前置。
距参考图「目视等价」的保真化剩余量,见
PARITY-GAP.md(本文管「怎么渲出来」, 那文管「怎么做到参考图那样」)。
1. 文档目的
本文定义在「Phase 2 三设备 bring-up 完成」之后,从当前“单个 .gr2 角色展示 POC”推进到“能够直接加载原始客户端资源并渲染神兽国 A1 村庄画面”的完整开发方案(前置见上方「定位」)。
本文是实现和验收依据,解决以下问题:
- 哪些数据直接来自
assets/; - 每种地图资源如何解析并转换为 Godot 运行时对象;
- 新增模块与现有
Metin2Model、libgr2、DDS 解码器如何协作; - 按什么顺序实现,才能尽早得到可比较的场景截图;
- 何时可以认为已经达到参考图中的地形、建筑、植被、角色、光照和 HUD 效果。
本文不要求还原网络、任务、战斗、商店等游戏逻辑。最终交付是可运行、可移动、可稳定截图的客户端渲染样板。
2. 最终目标
2.1 目标画面组成
最终 1920×1080 输出必须包含:
- 神兽国 A1 的原始地形高度与道路/草地/石地分布;
- 原始坐标中的村庄建筑、围栏、灯柱和其他静态摆件;
- 树木及透明叶片;
- 玩家角色、至少一个 NPC、角色动画和地面阴影;
- 与
A1.msenv接近的天空、阳光、环境光、远景雾和整体色调; - 可见水域时的基础水面;
- 第三人称游戏相机;
- 最终验收阶段的背包、快捷栏、状态栏、小地图和头顶文字。
2.2 两级交付
为了避免 UI、粒子或完整地图流送阻塞三维场景验证,交付分成两级:
| 级别 | 名称 | 必须包含 | 用途 |
|---|---|---|---|
| R1 | 世界渲染完成 | 地形、splat、建筑、树木、角色、相机、天空、光照、雾、阴影 | 验证截图主体是否成立 |
| R2 | 最终画面完成 | R1 + 水面 + 必要环境特效 + HUD/小地图/头顶文字 | 与参考图做最终视觉比较 |
R1 是后续工作的硬前置;R2 是本文的最终完成条件。
3. 当前基础与真实缺口
3.1 已可复用能力
当前仓库已经具备:
.gr2v6/v7 解析;- 骨架、网格、UV、四骨权重和动画采样;
- CPU 蒙皮和保留 Granny shear 的自定义 GPU 蒙皮;
.msa、.msm基础解析;- DDS/DXT level 0 软解码;
- opaque、alpha、alpha-test、additive 的近似材质;
- Godot
DirectionalLight3D阴影; - procedural sky、Filmic tonemap、MSAA 4× 和可选深度雾;
- 固定时间截图及部分 golden 回归能力。
对应实现主要位于:
extension/src/metin2_model.cppextension/src/metin2_anim.cppextension/src/gr2_bridge.cppextension/src/m2_material.cppextension/src/dxt.cppformats/libgr2/
3.2 必须补齐的能力
当前不能直接输出目标画面的主要原因是以下系统尚不存在:
- 统一虚拟资源路径与跨 pack 目录索引;
- 地图设置、TextureSet、Environment、AreaData 和 Property 解析;
- heightmap 地形网格;
tile.raw多层地表 splatting;- 区块阴影贴图;
- CRC 驱动的建筑/树木/特效实例化;
- SpeedTree 替代或兼容路径;
- 水面;
- 地图区块流式加载和跨实例资源缓存;
- 第三人称相机、NPC 放置和 HUD。
BACKLOG.md 中 E8、E9、E10、E7 的判断正确,但它们不足以覆盖地图渲染的全部前置:“地图文本格式”“区块二进制 + AreaData”“Property CRC 注册表”“虚拟资产解析器”“坐标统一”“地图回归”是独立工作项,已在 BACKLOG.md 补为 E11–E13 / G7 / H7 / I5,不是 E8 内部可以忽略的小步骤。
4. 资源使用原则
4.1 直接读取原始资源
运行时默认直接读取:
assets/
禁止为了构建一张演示图而手工重新摆放整座村庄。地形和对象位置必须由原地图数据驱动。
允许生成派生缓存,例如:
- 解码后的 DDS GPU 纹理缓存;
tile.raw生成的 splat alpha/权重纹理;- GR2 转换后的静态
ArrayMesh; - 树木离线转换出的普通 Mesh;
- 地图区块碰撞和导航缓存。
派生缓存必须可删除并从原始 assets 重建,不能成为新的唯一数据源。
4.2 虚拟路径不能简单拼接
资源内部经常使用:
d:\ymir work\zone\...
d:/ymir work/environment/...
真实散文件按原 eterpack 名称分散在多个一级目录,例如:
assets/Zone/ymir work/...
assets/Terrain/ymir work/...
assets/ETC/ymir work/...
assets/PC/ymir work/...
assets/metin2_patch_*/ymir work/...
因此必须建立 AssetResolver,不能假设文件总在 assets/ymir work/...。
规范化规则:
\转/;- 移除盘符和开头
/; - ASCII 小写作为索引键,但保留真实路径用于打开文件;
- 去除重复
/、.,拒绝越过 assets 根目录的..; - 以“虚拟路径 → 真实散文件路径”建立索引;
- 同名资源按显式 pack 优先级解析,冲突必须记录日志;
- 开发模式允许输出全部候选,便于确认 patch 覆盖关系。
推荐优先级配置,而不是依赖目录遍历顺序:基础包在前,patch 包在后,后注册者覆盖前注册者。最终采用的顺序要写入测试快照。
4.3 授权约束
技术实现可以读取本地原客户端资源,但发布物不得默认携带这些资源。构建、测试和分发流程必须把代码许可与游戏素材许可分开处理。
5. A1 地图数据规格
目标地图位于:
assets/OutdoorA1/metin2_map_a1
5.1 全局设置
setting.txt 的当前数据为:
CellScale 200
HeightScale 0.5
ViewRadius 128
MapSize 4 5
BasePosition 409600 896000
TextureSet textureset\metin2_A1.txt
Environment A1.msenv
由原客户端常量可得:
- 每个区块:128×128 个地形格;
- 每格:200 cm,即 2 m;
- 每个区块:256 m×256 m;
- A1 全图:4×5 个区块,即约 1024 m×1280 m;
- 高度:
uint16_value × HeightScalecm,再乘0.01变成 Godot 米。
BasePosition 是地图在全局 Metin2 世界中的坐标基准,不应该直接变成离原点几千米的 Godot 节点坐标。渲染场景使用 map-local 坐标,网络/地图逻辑需要时再通过原点偏移换算。
5.2 区块目录编号
区块目录编号采用:
tile_id = tile_x * 1000 + tile_y
directory = six_digit_decimal(tile_id)
例如 (0, 0) → 000000,(1, 3) → 001003。
加载器不得把六位目录名按连续序号理解。
5.3 区块文件
| 文件 | 规格 | 用途 |
|---|---|---|
height.raw |
131×131,little-endian uint16,34322 B |
含一圈边界样本的高度图 |
tile.raw |
258×258,uint8,66564 B |
TextureSet 图层编号 |
attr.atr |
6 B header + 256×256 uint8 |
阻挡、水域、禁 PK 等属性 |
water.wtr |
7 B header + 128×128 layer id + layer heights | 水层索引与高度 |
shadowmap.dds |
256×256 DDS | 地形烘焙阴影/颜色调制 |
shadowmap.raw |
256×256 RGB565,131072 B | CPU 阴影颜色查询 |
minimap.dds |
区块小地图 | HUD 小地图与地形分布校验 |
areadata.txt |
对象记录 | 坐标、CRC、YPR、heightBias、portal id |
areaambiencedata.txt |
环境声音对象 | R2 或后续音频阶段使用 |
areaproperty.txt |
区块元数据 | AreaName、NumWater |
二进制常量以原客户端 Terrain.h 和 Terrain.cpp 为真值来源。
5.4 TextureSet
A1 使用:
assets/textureset/textureset/metin2_a1.txt
当前文件含 17 个图层。每个图层记录:
texture_path
u_scale
v_scale
u_offset
v_offset
splat_enabled
height_begin
height_end
索引 0 是原客户端内部的空图层;资源文件的 Texture001 对应运行时索引 1。不得产生一位偏移错误。
5.5 Environment
A1 环境资源可由虚拟路径解析到:
assets/ETC/ymir work/environment/a1.msenv
当前包含:
- background 和 character 两组方向光;
- material diffuse/ambient/emissive;
- fog level 和雾色;
- sky gradient;
- cloud texture、scale、height、speed;
- lens flare 参数。
文件头中的 EnvrionmentData 保留了原格式拼写,解析器应接受该拼写,并对 token 大小写不敏感。
真值来源(W0 已定位):
.msenv是文本(Group NAME { ... }+List NAME { ... }匿名 rgba 行), 首行ScriptType EnvrionmentData(原格式拼写错误,照收)。A1 实盘用foglevel <n>(非老版Enable/NearDistance/FarDistance),10 行List Gradient。formats/environment.cpp已解析。.wtr头 ={u16 magic=5426, u16 w=128, u16 h=128, u8 layerCount}+u8[128*128]+layerCount×(u16|u32)层高(Terrain.cpp LoadWaterMapFile两种都收);formats/terrain_files.cpp已实现。
5.6 AreaData 与 Property
areadata.txt 每个对象按以下字段解析:
position.x position.y position.z
property_crc
yaw#pitch#roll
height_bias
optional_portal_ids...
position的坐标系 —— W3 修正(2026-08-29):地图全局厘米,不是区块本地。 W0 时只看了000000(在地图原点附近,全局≈本地)误判成本地;实测 20 个区块:001003的 x∈[25665,49665]、003003的 x∈[77531,94553] —— 明显是tile_x·25600 + 区块内偏移。Area::SetCoordinate不偏移, 正因为 position 已是全局。x正=东,y负=南(|y|随 tile_y 增大)。渲染时直接position_to_godot(x, y, z+heightBias),不再加区块原点。extension/src/metin2_world.cppplace_objects()已修正,formats/tests/map_formats_test.cpp有001003的全局断言。
所有 assets/Property/property/**/* Property 文件都必须注册进 CRC 索引。格式(W0 实测):行1 = YPRT
fourcc;行2 = 十进制 uint32 CRC;行3+ = <key>\t"<value>"(propertyname / propertytype +
类型专属键)。A1 扫出 1330 CRC(Building 1084 / Tree 85 / Effect 76 / DungeonBlock 84 / Ambience 2),
formats/property.cpp 的 PropertyRegistry 已实现,A1 areadata 的 136 个 CRC 全部命中。
支持的原始对象类型:
| PropertyType | 扩展名 | 关键资源字段 | R1 处理 |
|---|---|---|---|
Tree |
.prt |
TreeFile、TreeSize、TreeVariance |
转换树木并实例化 |
Building |
.prb |
BuildingFile、ShadowFlag |
用 GR2 静态渲染器实例化 |
Effect |
.pre |
EffectFile |
R1 可占位,R2 必须实现必要效果 |
Ambience |
.pra |
声音与播放参数 | 不影响 R1 画面 |
DungeonBlock |
.prd |
模型和属性文件 | 按静态建筑处理 |
解析和实例化行为以 Area.cpp、Property.cpp 和 MapType.cpp 为依据。
6. 坐标系统
Metin2 资源使用厘米和 Z-up;Godot 使用米和 Y-up。全项目只能有一套转换:
Godot.x = Metin2.x * 0.01
Godot.y = Metin2.z * 0.01
Godot.z = -Metin2.y * 0.01
这与当前 make_conv(unit_scale=0.01, flip_z=false) 的 -90° X 转换一致。
要求:
- 地形顶点、建筑位置、树木、角色、特效、相机目标和碰撞全部调用同一个转换函数;
- 法线只做轴转换,不做平移和单位缩放;
- YPR 使用与原客户端相同的 yaw/pitch/roll 顺序,先通过已知 A1 建筑验证方向,再固化成测试;
- AreaData
position是地图全局 cm(§5.6,W3 修正)—— 直接做 §6 轴转换,不加区块原点; heightBias加在原始 Z 上,再转换到 Godot Y;- 不允许分别在 GDScript、地形 builder 和 GR2 loader 中复制不同公式。
建议新增:
namespace mtgodot::coord {
godot::Vector3 position_to_godot(double x_cm, double y_cm, double z_cm);
godot::Vector3 direction_to_godot(double x, double y, double z);
godot::Basis ypr_to_godot(double yaw_deg, double pitch_deg, double roll_deg);
}
7. 目标运行时架构
Metin2World (Node3D)
├── AssetResolver
├── PropertyRegistry
├── MapDefinition
├── TerrainStreamer
│ └── TerrainChunk[0..N]
│ ├── Terrain Mesh/Material
│ ├── Water Mesh
│ └── Collision
├── AreaObjectStreamer
│ ├── StaticBuilding instances
│ ├── Tree MultiMesh groups
│ └── Effect instances
├── CharacterRoot
│ ├── Player Metin2Model
│ └── NPC Metin2Model
├── WorldEnvironment
├── DirectionalLight3D
├── ThirdPersonCamera
└── HUD (CanvasLayer)
数据流:
assets_root
-> AssetResolver index
-> map setting / textureset / environment
-> visible tile set
-> binary parse on worker thread
-> mesh/mask/instance description
-> Godot resource creation on main/render thread
-> cache + scene attach
7.1 建议新增源码布局
formats/
map_setting.{h,cpp}
texture_set.{h,cpp}
environment.{h,cpp}
terrain_files.{h,cpp}
area_data.{h,cpp}
property.{h,cpp}
tests/map_formats_test.cpp
extension/src/
asset_resolver.{h,cpp}
metin2_world.{h,cpp}
terrain_builder.{h,cpp}
terrain_material.{h,cpp}
property_registry.{h,cpp}
area_object_loader.{h,cpp}
metin2_static_model.{h,cpp}
environment_builder.{h,cpp}
water_builder.{h,cpp}
resource_cache.{h,cpp}
coordinate.{h,cpp}
project/
world_demo.gd
third_person_camera.gd
hud/
test/
maps/
golden/world-a1-r1.png
golden/world-a1-r2.png
格式解析层不得依赖 godot-cpp,以便使用 CTest 做快速单元测试。Godot 对象创建留在 extension/src。
8. Godot API 契约
新增一个 GDExtension 节点:
Metin2World : Node3D
建议暴露:
@export var assets_root: String
@export var map_path: String = "OutdoorA1/metin2_map_a1"
@export var load_radius_tiles: int = 1
@export var player_global_position_cm: Vector3
@export var enable_water: bool = true
@export var enable_effects: bool = true
@export var enable_collision: bool = true
func load_map() -> bool
func unload_map() -> void
func set_focus_position(global_position_cm: Vector3) -> void
func sample_height(global_x_cm: float, global_y_cm: float) -> float
func sample_attribute(global_x_cm: float, global_y_cm: float) -> int
func get_load_report() -> Dictionary
func capture_reference_frame(path: String) -> void
信号:
signal map_loaded(report: Dictionary)
signal tile_loaded(tile_x: int, tile_y: int)
signal tile_unloaded(tile_x: int, tile_y: int)
signal asset_missing(virtual_path: String, context: String)
signal load_failed(stage: String, detail: String)
get_load_report() 至少返回:地图尺寸、已加载区块、活动 terrain layers、AreaData 对象数、各 PropertyType 成功数、缺失资源数、缓存命中率和加载耗时。
9. 分阶段实施方案
贯穿全程的两条硬约束:
- 目标渲染器 = Godot Mobile renderer(Vulkan / Metal),不涉及 Compatibility(GLES3)。 三台目标设备(Mac / 一加 13 / iPhone 16)都是现代硬件。R1 可在 macOS 上用 Forward+ 开发,但每个 新写的 shader(地形、splat、水、天空、
SRC_SKIN)要尽早在一加 13 / iPhone 16 上各跑一次, 不把「移动端能不能跑」推到 W8。- 文本编码要显式声明。
setting.txt/areadata.txt/areaproperty.txt/ Property 文本、 AreaName 等可能是 CP949/EUC-KR(原版)或 ASCII/UTF-8(m2dev fork)。每个文本 parser 必须声明它按 什么编码读,formats/tests要有一个非 ASCII 用例。
Phase W0:资源索引与格式真值测试
目标:先证明所有后续系统能够稳定找到并读懂数据。
进度(2026-08-29)—— W0 解析层基本完成。
formats/下已落地并通过 CTestformats.map_formats(内联单测 +M2_ASSETS设时跑真实 A1 断言)+tools/map_probeload report:
m2_tokvec(LoadMultipleTextData等价)、m2_coord(I5:坐标 / 单位 / 区块编号)map_setting(setting.txt)、texture_set(17 层)、area_data(areadata / ambience / property)terrain_files(height/tile/attr/water,magic 2634/5426 + 尺寸校验,真值取自Terrain.cpp)environment(.msenvGroup/List 文本树;A1foglevel 6+ 10 行 Gradient)property(.prb/.prt/.pre/.prd/.pra+ CRC 注册表:A1 扫出 1330 CRC,Building 1084 / Tree 85 …)asset_resolver(虚拟路径 → 散文件;索引 54212 文件 / 26106 ymir key)A1 实测:MapSize 4×5 / 20 区块全可发现 / TextureSet 17 层且贴图 17/17 可解析 /
000000areadata 23 对象 / 136/136 个 areadata CRC 都命中 Property 注册表 / 全 975 对象坐标为地图全局 cm(W3 修正,见 §5.6) / CRC569394331= Object000 = Property "Pagoda1"(类型 Tree,非 Building)。剩余:
ingame-shinsoo.png= A1 的 minimap 目视核对;ypr_to_godot(YPR→Basis,留 W3 用已知 A1 建筑验顺序);extension 侧Metin2World节点(W1);Property 模型引用 1200/1244 可解析,缺口 是部分.spt树 / 未包含的 patch 包,非 W0 阻塞。
实现:
AssetResolver扫描assets并生成规范化索引;- 解析
setting.txt、TextureSet、.msenv; - 解析六类区块文件头和尺寸;
- 扫描 Property 并建立
uint32 CRC -> Property; - 解析
areadata.txt和areaambiencedata.txt; - 所有 parser 返回结构化错误,不允许静默截断或越界读取;
- 所有文本 parser 显式声明输入编码(见本节开头约束 2),非 ASCII 字段有测试覆盖。
验收:
以下多数项已由
tools/map_probe+ CTestformats.map_formats对真实 A1 跑通(2026-08-29,见上方 进度块)。仍需人工完成的只剩第一条(参考图 = A1 的目视核对)和最后的落点校验。
- 先确认参考图 = A1(人工):核对
ingame-shinsoo.png里的建筑 / 地貌 / 远景与OutdoorA1/metin2_map_a1的minimap.dds拼图一致;若不是 A1,改用实际地图目录,§5 常量随之更新; - ✅
MapSize=4 5(字段序x y:目录 000000..003004 = tile_x 0..3 / tile_y 0..4); - ✅ 20 个区块目录全部可发现;
- ✅
height.raw131×131uint16(无头)/tile.raw258×258uint8(无头); - ✅
attr.atrmagic 2634 /water.wtrmagic 5426(真值取自Terrain.cpp); - ✅ TextureSet 17 层,运行时保留 index 0 空图层,17/17 贴图经
AssetResolver可解析; - ✅
000000/areadata.txt声明 23 = 解析 23;.msenv/.prb.prt.pre.prd.pra全部解析; - ✅ Property CRC 注册表:A1 全 136 个 distinct areadata CRC 都命中;CRC
569394331= Object000 = Property "Pagoda1"(类型 Tree,非 Building —— §9-W0 原示例猜错了); - ✅ AreaData
position= 地图全局 cm(§5.6 W3 修正;001003全局断言在map_formats_test.cpp); - ⬜ 落点校验(人工):取一个已知
Building对象(从map_probe输出里挑一个 type=Building 的 CRC),其position经 §6「加区块原点 + 轴转换」后落点与minimap.dds上该建筑一致; - ⬜
AssetResolver的未解析路径 / 重复虚拟路径进报告(map_probe已打印计数,需补明细导出)。
Phase W1:地形几何
目标:在 Godot 中正确显示 A1 高度地形。
进度(2026-08-29)—— W1 几何做通。
formats/terrain_mesh.{h,cpp}(纯数学,CTest 覆盖): 129×129 顶点 / 128×128 quad,对角线 TL→BR(=CTerrain::GetHeight的 h1/h2),中心差分法线用height.raw边界样本(跨区块无缝),坐标经m2_coord转 Godot 空间 + 区块原点。terrain_height_at()逐字节照AreaTerrain.cpp CTerrain::GetHeight的左/右三角插值。extension/src/metin2_world.{h,cpp}=Metin2WorldGDExtension 节点(SHINSOO §8 API 子集:assets_root/map_path/load_radius_tiles/load_map()/unload_map()/sample_height()/get_load_report())。headless 冒烟project/world_probe.gd:A1 20/20 区块建成MeshInstance3D(165ms),sample_height(128,128)=175.7m(与 areadata z≈178m 一致),地图外返回 0,RESULT PASS。 打磨(2026-08-29):每区块StaticBody3D+HeightMapShape3D(129²,缩放承载格距)已加。 剩余:16×16 patch 裁剪单元、相邻区块共享边 = 0 的显式断言、线框截图。
实现:
- 每个区块生成 129×129 可见顶点和 128×128 个 quad;
height.raw的 131×131 数据保留边界样本,用于相邻区块法线;- 三角线方向必须与原客户端
GetHeight()的左右三角判定一致; - 法线按原客户端中心差分算法生成;
- 先以 16×16 格 patch 为裁剪/LOD 单元;
- 生成简单灰色材质和
HeightMapShape3D或等价碰撞; - 提供
sample_height()并与网格表面交叉验证。
线程:R1 主线程同步构建。 W1–W7 的区块解析 + mesh/splat 生成都在主线程同步做,接受加载时的 短暂卡顿(截图样板不追求流畅游览)。§7 数据流里的 worker-thread 拆分是 W8 的事,别提前引入
RenderingServer跨线程调用的复杂度。
验收:
- 四个相邻区块的共享边高度误差为 0;
- 接缝处法线连续,无可见裂缝;
- 已知 AreaData 物体落在正确地形高度附近;
- 地图尺寸和区块原点换算正确;
- 线框截图能辨认出 A1 地貌。
Phase W2:地表 splatting 与烘焙阴影
目标:得到参考图中最关键的道路/草地分布和明暗结构。
进度(2026-08-29)—— W2 多图层地表着色器。
formats/splat.{h,cpp}逐字节照AreaTerrain.cpp RAW_GenerateSplat(tile.raw→ 每图层 258² alpha + 1px 羽化)。extension/src/terrain_splat.{h,cpp}=build_chunk_terrain_material():当前把图层 alpha 打进 ≤2 张 RGBA8 权重贴图;图层色 DDS(dxt.cpp)解码 + 强制 resize 512² + 运行时 mipmap 后堆成Texture2DArray;ShaderMaterial(SRC_TERRAIN)逐图层按权重mix,UV 按TextureLayer.u_scale/v_scale/offset平铺,shadowmap.dds作 albedo 乘法项(有则),光照交给 GodotDirectionalLight。 窗口渲染验证:project/world_shot.gd出test/golden/world-a1-terrain-top.png+world-a1-persp.png—— 当前 8 层子集下道路 / 草地 / 沙滩 / 岩壁轮廓可辨且跨区块无明显接缝; 在补齐最多 13 个活动层之前,不能据此宣称完整地表分布已与原客户端一致。 已确认的 P1 缺口:当前min(8, splat.layers.size())静默截断活动图层;A1 单区块最多有 13 个非零图层,最坏丢 5 层。当前 UV128/scale也不等于原客户端公式。 剩余:支持全部活动图层;把 UV 改为fTerrainTexCoordBase * Scale(对当前区块归一化 UV 即 U=8*UScale、V=-8*VScale,offset 同步符号);保留源分辨率/原 mip,严格 parity 时关闭程序化macro_detail;再用 minimap/受控帧验收,并跑 Mobile renderer。
兼容路径先实现原客户端语义:
tile.raw的值是 TextureSet 图层编号;- 按原
RAW_GenerateSplat逻辑生成每图层 258×258 alpha; - 图层按原顺序叠加;
- 使用原公式
fTerrainTexCoordBase * UScale、-fTerrainTexCoordBase * VScale和对应 offset 生成 UV; shadowmap.dds作为地表颜色乘法项;- 只有当前 patch 出现的图层才提交绘制,但不得按固定 8 层上限截断。
第一版允许“一图层一 pass”来优先保证准确。R1 达标后,再把每 4 个权重打包到 RGBA 或使用纹理数组减少 draw call。优化版本必须与兼容路径做截图对比,不能改变道路边缘。
移动端提前验:17 图层「一图层一 pass」在移动 GPU 上仍然很重。W2 里就在一加 13 / iPhone 16 上 跑一次单区块 splat,确认合批路径(RGBA 权重打包 or
sampler2DArray—— Vulkan / Metal 都支持数组 采样)帧率可接受;不行则地表方案在 W2 内定,别拖到 W8 才返工。
材质最低要求:
- sRGB 地表纹理;
- mipmap、重复寻址和各向异性过滤;
- 太阳漫反射;
.msenvmaterial ambient/emissive 近似;shadowmap.dds调制;- 可选距离宏细节衰减,不能破坏原纹理分布。
验收:
- 道路、草地、石地和沙地轮廓与
minimap.dds对齐; - 区块边缘没有 mask 接缝;
Texture001..017不发生索引偏移;- 真实 A1 最多 13 个非零层的区块不会丢层,并有自动化断言;
- 固定俯视相机截图可稳定生成 golden;
- 关闭实时阴影时,烘焙阴影仍可见。
Phase W3:Property 驱动的静态场景
目标:不用手工摆放即可恢复村庄建筑和摆件。
进度(2026-08-29)—— W3 首版。决策落地:不新建
Metin2StaticModel节点,改用轻量自由函数extension/src/static_object.{h,cpp}get_static_mesh()—— 复用gr2_bridge的build_parts/build_mesh,逐 surface 从 gr2 material binding(Mesh::material_textures)取贴图,走AssetResolver定位 +dxt.cpp解码,按真实路径缓存、跨实例共享同一ArrayMesh,不建 Skeleton3D。Metin2World::place_objects():遍历各区块 areadata →PropertyRegistry查 CRC →Building用buildingfile/DungeonBlock用dungeonblockfile→ 实例化MeshInstance3D。 A1 实测:975 个 areadata → 601 个 Building/DungeonBlock 实例,119 个共享 mesh,0 缺模型; 6 个跳过(Effect/Ambience;368 个 Tree 走 W4)。 W3 尾巴已补:位置改用地图全局 cm(W0 误判为区块本地,见 §5.6 修正 —— 视觉核对时发现 建筑飘到地图外才抓到);ShadowFlag=0/空→SHADOW_CASTING_OFF;portal_ids存进节点 meta; 材质默认CULL_BACK(贴图名含 leaf/grass/fence/ivy 才 alpha-scissor + 双面);有同名.mdatr的实例计数(A1 = 601,碰撞未建)。ypr_basis()=Ry(yaw)·Rx(pitch)·Rz(roll)(Metin2 Z-up 空间,忠实 移植D3DXMatrixRotationYawPitchRoll);放置用object_basis_godot()= 其共轭到 Godot Y-up(C·R·C⁻¹,C=rotate(-90°,X))—— 修了之前漏轴转换的 bug(roll=朝向被当成绕 Godot Z 翻滚,影响 66% A1 物体)。 CTest 钉了 identity / roll=90→绕 Godot+Y / 正交性。 B1 材质数据(2026-08-29):libgr2MaterialInfo加map_count/alpha_blend(Name~"Blend"+maps>1,照EterGrnLib Material.cpp:233)/two_sided(名字关键字近似 ExtendedData "Two-sided")。static_object据此 切TRANSPARENCY_ALPHA/CULL_DISABLED。gr2fuzz 9166/9166 不回归。 剩余:ypr逐建筑朝向对原客户端截图复核(轴共轭 bug 已修,见上);.mdatr碰撞体;_lod_01..03距离切换;ExtendedData精确解析。
实现:
- 根据 AreaData CRC 查 Property;
- 读取对象位置、YPR、heightBias 和 portal ids;
Building、DungeonBlock复用libgr2,但走专用静态资源路径;- 同一 GR2 只解析一次,Mesh 和 Material 跨实例共享;
- 支持
_lod_01.gr2、_lod_02.gr2、_lod_03.gr2; ShadowFlag决定是否参与实时阴影;.mdatr用于静态碰撞,若暂未实现则必须在报告中列出;- portal 信息先保存,R1 可不做室内 portal 裁剪。
不要为每栋静态建筑创建完整动画播放器和独立纹理。W3 开工前须定方向:新增 Metin2StaticModel,还是
把 Metin2Model 的“资源解析”与“节点实例化”拆开。它决定 W3 的大部分工作量,别拖到撞上再定。
验收:
000000的每条 AreaData(W0 实测 23,全部命中 Property 注册表)都有明确结果:成功、暂不支持或缺资源;- 一个已知
Building对象(从map_probe输出挑一个 type=Building 的 CRC;569394331是 Tree,不能用) 的位置和旋转正确; - 建筑屋顶朝向、正反面剔除和贴图没有明显错误;
- 重复建筑共享 Mesh/Texture;
- 缺失资源只影响单个实例,不导致地图加载失败。
Phase W4:树木与植被
目标:恢复参考图中影响轮廓和色彩最大的树冠。
.spt调研修正(2026-08-29):几何 parser 仍在闭源CSpeedTreeRT::LoadTree(),但仓库speedtree_static.lib实测是保留大量类/函数符号的 COFF x86-64,并附完整SpeedTreeRT.h;它不能 链进 macOS/移动端,授权与转换产物分发也必须先确认。优先方案是 Windows x64 离线 exporter 调用LoadTree/Compute/GetGeometry,不是先完整反编译.spt。W4/R2(真实纹理 proxy,已落地):
formats/spt.{h,cpp}除 magic/内嵌贴图外,会提取二进制 尾标中的CompositeMap*/CompositeShadowMap*;extension/src/tree_placeholder.{h,cpp}从 AssetResolver 加载真实 bark DDS 和 B1/B2/B3/N1/N2 composite atlas,生成确定性树干/分枝与多组 交叉叶簇,SRC_LEAF使用 alpha-scissor、mipmap anisotropic 和按实例错相风摆。解析失败才回退到 程序化叶片。Metin2World::place_chunk_objects()按treefile缓存共享 Mesh → 每组一个MultiMeshInstance3D; 逐实例只使用 areadata 的全局位置,已移除旧 proxy 未经原客户端证实的随机 yaw/scale jitter。 A1 实测 368 棵 / 14 树种;MultiMeshvisibility_range_end=420m(+60m 淡出)。 剩余:真实 branch/frond/leaf table、按树种精确尺寸、LOD/impostor、环境色调和阴影策略。
两条路径:
- 当前 R2 proxy:真实 bark/composite atlas + 确定性枝干/叶簇,先保证地图可运行和轮廓明显优于占位;
- 最终 parity 路径:验证授权后,用 Windows x64 离线 exporter 将
.spt转成普通 branch/frond/leaf Mesh、LOD 与 impostor。
离线 exporter 仍是待决项:真实资产共 118 个;先拿最终机位的 3 个代表树种,在兼容的 Windows x64/MSVC 环境验证
LoadTree → Compute(seed=1) → GetTextures/GetGeometry。导出器需保留 branch/frond strip、leaf cluster、wind weight/index、每级 LOD、billboard、包围盒和纹理名;未确认 授权前不提交专有库、反编译实现或转换资产。
R1 树木材质要求:
- 叶片
alpha_scissor,避免全透明区域参与排序; cull_disabled双面;- 树干 opaque;
- 叶片颜色受环境光和太阳影响;
- 简单顶点风摆;
- 相同树种使用
MultiMeshInstance3D; - 远距离使用低模或 billboard。
TreeSize 和 TreeVariance 必须参与实例尺度,随机结果以 CRC/对象索引为种子,保证每次截图一致。
验收:
- A1 村庄主要树木的位置、尺度和树种一致;
- 叶片无黑色方块、严重白边或单面消失;
- 固定截图中的树冠轮廓稳定;
- 树木实例不会导致每帧大量 CPU 更新。
Phase W5:环境、灯光和画面风格
观感打磨(2026-08-29 第二轮,向参考图靠):
environment_builder—— 环境光改AMBIENT_SOURCE_COLOR用.msenv Material.Ambient暖色提亮阴影面;4-split 阴影 + 收紧 cascade(500m)+ bias 调;adjustment饱和 1.22 / 对比 1.10;开轻 glow(HDR 溢出)+ 轻 SSAO。地形着色器加各向异性过滤 + 低频宏观明暗破平铺。static_object用 B1(见下)的 alpha/two-sided 切材质。整体色调更接近 Metin2 高对比高饱和暖调。
目标:从“资源正确”推进到参考图的经典高亮、高饱和视觉。
进度(2026-08-29)—— W5 首版。
extension/src/environment_builder.{h,cpp}apply_environment()把fmt::Environment(.msenv)落到 Godot:
DirectionalLight.Background→ 场景Sun(方向经direction_to_godot+Basis::looking_at, 色 =bg_diffuse,energy 按亮度,shadow=on)SkyBox.Gradient(10 行)→ProceduralSkyMaterial三段色(zenith / horizon / ground)Material.Ambient→AMBIENT_SOURCE_SKYenergy +ambient_light_colorFog(foglevel 6)→ 深度雾 begin≈225m / end≈925m(foglevel越大起雾越近;首版误用 指数密度把全图刷白,已改深度雾)- 色调 = Filmic +
adjustment(saturation 1.12 / contrast 1.05)
Metin2Worldload 时解析A1.msenv(resolver定位)→apply_environment(this),建Sun+WorldEnv子节点。world-a1-{persp,terrain-top}.png重出:蓝天渐变 + 远山轻霭 + 定向光阴影 + 偏饱和绿地。 剩余:DirectionalLight.Character角色光(留角色材质 uniform,§9-W5 note);SkyBox云层 texture/speed;lens flare(R2);烘焙shadowmap.dds与实时阴影的双影规避;同机位 ROI 直方图对 参考帧。
实现映射:
.msenv 数据 |
Godot 映射 |
|---|---|
| DirectionalLight.Background | 地形/建筑主方向光 |
| DirectionalLight.Character | 角色灯光参数或角色材质修正 |
| Material Ambient/Emissive | WorldEnvironment + 材质参数 |
| Fog Color/Level | 深度雾颜色与距离 |
| SkyBox Gradient | 自定义天空 shader |
| Cloud texture/speed | 天空云层 shader |
| LensFlare | R2 屏幕空间 flare/billboard |
DirectionalLight.Character落地方式 W5 开工前定:Godot Forward+ 不易做「地形一套光 / 角色另一 套光」。候选:给SRC_MIX/SRC_SKIN加一组 character-light uniform,或用 Godot lightcull_mask把角色光单独分层。别留到调色阶段才发现角色偏色是光照分层没做。
阴影:
- 地形使用烘焙 shadowmap,并继续接收玩家/NPC 的实时接触阴影;
- 角色实时投影;静态树木/建筑按是否已经烘焙选择性关闭实时投影,避免双影;
- 根据米制场景重新标定 shadow distance、cascade split、normal bias 和 bias;
- 角色脚下必须有清晰接触阴影;
- 避免烘焙阴影与实时建筑阴影造成不可接受的双影。
色调:
- 保留 Filmic 作为起点,但最终以参考图为准;
- 增加曝光、对比度、饱和度和暖色调参数;
- 不以“物理正确 PBR”作为验收目标;
- 禁止用极端曝光掩盖贴图、材质或光照错误。
验收采用同机位图像统计:天空、道路、草地、建筑和角色分别取 ROI,对比平均亮度、饱和度、主色和边缘结构。
Phase W6:玩家、NPC 与游戏相机
目标:把当前模型演示器变成第三人称场景。
进度(2026-08-29)—— W6 首版。
project/world_demo.gd(SceneTree脚本):Metin2World+ 玩家(Metin2Modelwarrior +Metin2AnimPlayer)+ NPC(shaman,wait.msa)+ 第三人称相机。
- 玩家:WASD 相对相机朝向移动,
_player.rotation.y朝移动方向;Metin2World.is_blocked()(attr.atrbit0,新加sample_attribute())挡移动;sample_height()逐帧贴地- 动画状态机:speed 0 →
general/wait.msa,< 阈值 →walk,否则run(切anim_path触发重载)- 相机:
yaw/pitch/dist环绕 + 沿视线采sample_height的防穿逼近- 截图模式跑脚本化「前进 + 右转」→
test/golden/world-a1-r1.png:warrior 跑姿在地形上, 朝向正确,相机跟随不穿地,背景是 W1–W5 的完整世界 打磨(2026-08-29):NPCLabel3D名字牌;玩家发型挂接(Metin2Model.hair_gr2,骨骼按名重映射 到 base skeleton,并入 CPU 蒙皮,alpha_scissor;warrior 74/74 骨匹配)—— 角色不再是秃头。 剩余:鼠标转视角交互壳、相机遮挡淡出、武器挂点、GPU 路径的 hair、确定性机位 preset。
实现:
- 复用
Metin2Model和Metin2AnimPlayer; - 正式补齐头发、武器和共享骨架附件;
- 玩家和 NPC 默认使用 GPU 自定义蒙皮;
- 玩家 Y 位置实时贴合
sample_height(); attr.atr的ATTRIBUTE_BLOCK用于移动阻挡;- 相机包含跟随、yaw/pitch、缩放、近远裁剪和遮挡处理;
- 调出与参考图相似的高度、距离和 FOV,并保存成确定性 preset;
- NPC 名称和交互提示使用
Label3D或屏幕空间跟随 UI。
R1 不要求完整角色控制器,但必须能用键盘移动、转向、从 idle 切到 walk/run,并稳定截图。
R1 的「可移动」= 贴地(sample_height)+ attr.atr 阻挡;建筑 / 摆件的实体碰撞(.mdatr,见 W3 第 7 项)
是 R2 或之后,不阻塞 R1。
验收:
- 玩家比例与建筑、道路一致;
- 角色脚不明显悬空或陷地;
- 相机不会穿过地面;
- 固定 preset 能重复生成相同构图;
- 玩家和 NPC 动画正常,无 CPU/GPU 蒙皮差异回归。
Phase W7:水面、必要特效与 HUD
目标:完成 R2 最终画面。
进度(2026-08-29)—— W7 首版。
- 水面:
extension/src/water_builder.{h,cpp}——water.wtr(formats已解析)逐层按 texel 行合并 span 生成掩膜网格,Y =heights[layer]·HeightScale·0.01;同层/区块为一个 mesh surface。SRC_WATER仍是程序化波纹法线 + 菲涅耳 alpha + 深/浅色 mix + 高 SPECULAR 太阳光斑。 A1 = 28 片水面 / 19 区块。- HUD:
project/hud.gd(CanvasLayer)—— 小地图 = 4×5 张minimap.dds(Metin2World.load_dds新方法解 DDS)拼图 + 玩家黄点;HP/MP 条、8 格快捷栏、I开关背包(占位)。test/golden/world-a1-r2.png= 当前已实现部分的叠加(第三人称 + 占位 HUD + 程序化水 +.msenv), 不代表 W7 的必要特效和真实 UI 已完成。.mse特效尚未端口:资产是 ASCII 文本,原客户端 EffectLib 已有 parser 和语义真值;缺口是 Godot 数据模型、粒子/mesh/light 运行时、材质与挂点,不是格式逆向。 剩余:原客户端 30 帧水纹、逐顶点水深 alpha/高度动画、.mse最小子集、HUD 真实 UI 贴图, 以及玩家朝向箭头/缩放。
水面:
- 解析
water.wtr的 128×128 layer id; 0xFF表示无水,其余值引用层高度;- 按连续水域生成合并网格,不能一格一个节点;
- parity 路径加载
special/water/01.dds…30.dds,约每 70ms 翻页,并使用相机空间位置 UV; - 按
waterHeight - terrainHeight复现逐顶点 alpha,并复现轻微高度动画; ReflectionProbe是 cubemap,不作为平面反射;需要时用镜像相机 + SubViewport 或 SSR;- Godot 4 折射使用
hint_screen_texturesampler +SCREEN_UV,不能沿用 Godot 3 的SCREEN_TEXTURE; - normal map、折射、岸线泡沫和高成本反射属于增强项,只有参考机位明确可见时才纳入 R2;
- 行列矩形合并只减少顶点/索引;减少 draw call 需跨区块/水层合批,必须分别测量。
特效:
真值来源已定位:
.mse是 ASCII Group/List 文本。顶层见EffectLib/EffectData.cpp,粒子见ParticleSystemData.cpp,mesh effect 见EffectMesh.cpp,light 见SimpleLightData.cpp;运行时由CEffectManager等类驱动。端口这些结构和时间事件语义,不凭字段名重做一套推断 parser。
- 先统计最终机位内实际出现的
.mse; - 为必要的 billboard、粒子和 mesh emitter 实现最小兼容子集;
- 未出现在最终机位的完整技能特效不阻塞本里程碑;
- 不允许用永久静态贴图替代会明显影响参考画面的动态环境效果。
HUD:
- 用 Godot
CanvasLayer/Control构建; - UI 贴图通过
AssetResolver从各 pack 的ymir work/ui/和assets/locale/locale/<lang>/ui/读取; - 小地图由各区块
minimap.dds拼接并按玩家全局坐标定位;先保持 north-up、旋转玩家 heading 箭头, 只有原客户端配置/受控帧证明整图旋转时才旋转地图; - 背包和快捷栏只要求展示与基础开关,不要求服务端物品逻辑;
- UI 在 1920×1080 下先做像素对齐,再补缩放策略;
- 字体、描边、层级和透明度需接近参考图。
验收:最终帧包含参考图中相同类别的 HUD 元素,并且不改变三维场景机位和曝光。
Phase W8:流式加载、缓存和性能
目标:从固定截图样板变成可稳定游览的 A1 地图。
进度(2026-08-29)—— W8 首版。
Metin2World重构成逐区块自包含:每个区块一个Chunk_<id>根节点,挂 terrain + water + 该区块的建筑 + 该区块的树 MultiMesh;卸载 = free 根节点。
- 流式:
load_radius_tiles >= 0时按focus的 (2r+1)² 装/卸。set_focus_position(gx,gz)→ 算 focus tile,变了就stream_update()(卸半径外 + 入队半径内)。_process每帧建stream_budget个(默认 1)。load_radius_tiles = -1保留「一次全建」。- 缓存:
PropertyRegistry(1330 CRC)、AssetResolver(5.4 万文件)、StaticMeshCache(建筑 gr2 → ArrayMesh,119 份)、DDS 解码缓存跨区块装卸复用。- 性能报告:
get_perf()→ fps / process_ms / draw_calls / primitives / video_mem / tex_mem / objects_3d / nodes / resident_chunks。world_demo.gd实测(radius=1,3×3):resident_chunks 9(非 20),稳定 30fps(headless 上限), ~34ms/帧,draw 450–550,显存 320MB / 纹理 221MB。 剩余:worker thread 解析(现主线程同步,stream_budget分摊)、建筑_lod_*距离切换、 全活动图层/精确 UV/源 mip 修复后再测、真机(一加 13 / iPhone 16)基线。
实现:
- 玩家所在区块 + 周围 3×3 区块常驻;
- 更远区块只保留低成本远景或卸载;
- 文件读取、文本解析、height/splat 生成可在 worker thread;
- Godot Resource/Node 创建和场景树修改在允许的主线程阶段完成;
- Mesh、Texture、Material、Property 和解析结果分别缓存;
- 静态建筑批次、树木 MultiMesh、LOD、可见距离和遮挡裁剪;
- DDS 完整 mip 链和平台压缩纹理;
- 加载有每帧预算,移动时不能出现长时间主线程停顿。
建议 R2 性能门槛:
| 平台 | 分辨率 | 目标 | 说明 |
|---|---|---|---|
| Mac 开发机 | 1920×1080 | 60 FPS | 最终截图基线 |
| iPhone 16 / 一加 13 | 设备原生或动态分辨率 | 稳定可玩(旗舰基线) | 自用参考,非发布门槛 |
性能报告必须同时记录 CPU frame、GPU frame、draw calls、三角形、纹理显存、活跃对象和区块加载峰值,不能只写平均 FPS。
10. 材质兼容要求
世界场景会放大当前启发式材质的问题。R1 前至少完成:
- 从 GR2 暴露材质实际承载的纹理/双面等数据,并按原客户端源码分支复现 blend、alpha test、cull、sphere-map 和必要的多贴图状态;
- 支持
D3DCULL_NONE,尤其是屋檐、围栏、叶片和薄片; - 支持顶点色;
- alpha-test 阈值来自材质或兼容配置,不再只看表面名;
- 区分 sRGB 颜色纹理与线性 mask/权重纹理;
- 恢复 DDS mip 链;
- 同材质参数生成稳定 cache key;
- 透明和 additive 设置明确的排序/优先级。
完整固定功能材质分支可以渐进实现,但最终机位内出现的错误材质必须在 R1 前修复,不能以“已列入 BACKLOG”为通过理由。
11. 测试方案
11.1 格式单元测试
放入 formats/tests,不启动 Godot:
- map setting 正常/缺字段/非法值;
- 131×131 height 数据及截断输入;
- attr/water magic、宽高和 payload 校验;
- tile layer 越界;
- TextureSet 的 index 0 规则;
YPRT、CRC、五种 PropertyType;- AreaData 的 YPR、heightBias、portal ids;
- Windows 路径、大小写、空格和重复 pack 冲突。
11.2 数值测试
地图数据没有
.gr2那种外部 oracle(Granny DLL)。这里的「对拍」= 把Terrain.cpp/Area.cpp的公式移植过来做自洽测试 + 与minimap.dds目视核对;正确性上限取决于移植是否忠实。
- 高度采样与原客户端三角插值公式对拍;
- 法线与中心差分公式对拍;
- 区块目录 id 和原点换算;
- Metin2 → Godot 坐标和 YPR(AreaData
position= 地图全局 cm,见 §5.6); attr.atr位标记;- water layer 和高度;
- Property CRC 命中率和对象分类计数。
11.3 Godot 冒烟测试
无窗口或固定窗口运行:
- 加载单个
000000; - 加载 2×2 区块检查接缝;
- 加载 A1 3×3 活动区域;
- 实例化已知建筑 CRC;
- 同时实例化玩家和 NPC;
- 缺失单一资源时继续加载;
- 反复 load/unload 后无节点和纹理持续增长。
11.4 Golden 截图
至少维护以下固定相机:
| Golden | 内容 | 主要检查 |
|---|---|---|
world-a1-terrain-top.png |
俯视地形 | splat 分布、区块接缝 |
world-a1-objects.png |
村庄静态对象 | CRC、位置、旋转、贴图 |
world-a1-lighting.png |
无 HUD 场景 | 天空、雾、阴影、色调 |
world-a1-r1.png |
玩家第三人称 | R1 完整构图 |
world-a1-r2.png |
最终 HUD 画面 | 最终交付 |
Godot 与原 Direct3D 渲染器不要求逐像素一致。门禁分两层:
- 硬门禁:加载成功、对象计数、NaN、缺资源、空 mesh、接缝和崩溃;
- 视觉门禁:SSIM/边缘结构、区域直方图与人工签核结合。
视觉比较前必须锁定:Godot 版本、renderer、窗口尺寸、相机 transform/FOV、时间、动画帧、随机种子、曝光、天气和资源索引优先级。
原客户端参考帧要「受控」采集,不能只拿一张随手截图。
ingame-shinsoo.png只用于早期 目视对齐。正式视觉门禁需要:在原 D3D 客户端里到一个已知全局坐标 + 已知 yaw/pitch + 已知 FOV + 关 HUD(或裁掉 HUD 区)的机位截图,把这组参数记进test/maps/的机位 preset, Godot 侧用 §6 的坐标转换复现同一机位。原客户端若无法脚本化摆机位,则退而用「同一地标、 人工尽量对齐 + 只比 ROI 结构/直方图、不比全图 SSIM」,并在报告里注明是人工对齐。
12. 里程碑与依赖
W0 格式/路径
├─> W1 地形几何 ─> W2 splat/阴影 ─┐
├─> W3 静态对象 ─> W4 树木 ──────┼─> W5 环境/光照
└─> 角色资源路径 ─> W6 玩家/相机 ─┘
└─> R1
R1 ─> W7 水/必要特效/HUD ─> R2
R1 ─> W8 流送/性能,可与 W7 并行推进
W0 和 §9-W4 的
.spt调研都无 Godot 依赖,可在 Phase 2 的 bring-up 期间并行提前做;W1 及以后建议 等 bring-up 有结果(见开头「定位」)。上图是 bring-up 之后的阶段依赖,不是日历排期。
阶段完成定义:
| 里程碑 | 完成条件 |
|---|---|
| M-W0 | A1 全部地图、Property 和依赖路径有结构化解析报告 |
| M-W1 | 2×2 地形无高度/法线接缝,高度采样测试通过 |
| M-W2 | 道路/草地与 minimap 对齐,shadowmap 正确 |
| M-W3 | 最终机位内静态对象自动摆放且材质基本正确 |
| M-W4 | 树木位置正确;proxy 的真实 bark/composite atlas、叶片透明、风摆和 MultiMesh 通过;最终 parity 另需 exporter 的真实几何/LOD |
| M-W5 | 天空、雾、太阳、曝光和阴影达到参考风格 |
| M-W6 | 玩家/NPC/相机组成 R1 golden |
| M-W7 | 水、必要特效和 HUD 组成 R2 golden |
| M-W8 | 3×3 流送稳定,并有 CPU/GPU/显存报告 |
13. 与 BACKLOG 的映射
| 本文阶段 | BACKLOG ID | 处理方式 |
|---|---|---|
| W0 | E11、E12、G7、I5、H7(+ G1/G2/G3 的前置) |
Map formats、AssetResolver、PropertyRegistry、坐标模块、地图回归 |
| W1/W2 | E8 |
将 E8 拆为 geometry、splat、attr、shadow 四项 |
| W3 | E13(+ E12 的 AreaData 部分) |
AreaData/Property 对象实例化 |
| W4 | E10 |
当前运行时使用真实纹理 proxy;最终 parity 走可授权的 Windows x64 离线转换 |
| W5 | E1/E2/E3/E4/E5、B1–B11 | 以最终机位出现的问题作为 R1 门禁 |
| W6 | A1/A2、C2/C5、D1/D2、I4 | 完成角色装配和游戏相机 |
| W7 | E7/E9、UI | .mse 只先做最终场景必要子集;UI 纳入 R2 |
| W8 | C9、G2/G3/G5/G6、F4 | 正式流送、LOD 和平台性能 |
以下新 ID 已加入 BACKLOG.md(E / G / H / I 段):
- E11:MapSetting/TextureSet/Environment 解析;
- E12:区块二进制 + AreaData/AreaAmbienceData 解析;
- E13:Property CRC 注册与五类对象实例化;
- G7:跨 pack 虚拟路径索引与覆盖优先级;
- H7:A1 地图格式/数值/golden 回归;
- I5:地图坐标、全局坐标和 Godot 坐标统一转换。
上表左列引用的其余 ID(E1–E10 / B1–B11 / A1–A2 / C2–C9 / D1–D2 / G1–G6 /
F4 / I4)以 BACKLOG.md 的当前定义为准 —— W0 开工时先对一遍,该文件近期改动频繁,ID 含义可能已漂移。
14. 风险与处理
| 风险 | 影响 | 处理 |
|---|---|---|
| pack 目录中存在大小写或重复虚拟路径 | 加载到错误资源 | 确定性索引、冲突报告、优先级快照 |
| SpeedTree 格式/运行时授权不可用 | 无法获得原始几何/LOD | 保留当前真实纹理 proxy 或使用授权清晰的替代 Mesh;不在运行时分发专有库 |
| GR2 材质数据仍不完整 | 建筑/叶片观感错误 | B1 前置,最终机位逐材质审计 |
| 一图层一 pass draw call 高 | 地形 GPU 开销大 | 先保真,再以 RGBA/array 合批并对比 golden |
| 所有静态模型走现有角色节点 | 内存和加载时间爆炸 | 静态资源/实例分离、跨实例缓存 |
| 坐标/旋转符号错误 | 建筑镜像、道路错位 | 单一转换模块 + 已知对象数值测试 |
| DDS 只解 level 0 | 远景闪烁、显存高 | 恢复完整 mip,平台格式转 BC/ASTC |
| UI 抢占世界渲染进度 | R1 长期不可验收 | R1/R2 分级,HUD 不阻塞 R1 |
| 原资源许可不明确 | 无法分发 | 代码与资产分离,发布前完成许可审查 |
15. 最终验收清单
R1:世界场景
- 直接读取
assets/,没有手工重建村庄布局; - A1 地形数据(
MapSize,示例 4×5)可解析,最终机位周围至少加载 3×3; - 地形高度、法线和区块边界无明显错误;
- 道路/草地/石地分布与 minimap 和参考图一致;
- 地形烘焙阴影正确;
- 建筑、围栏、灯柱等由 AreaData + Property 自动实例化;
- 树木按 AreaData 位置放置,使用真实 bark/composite atlas、alpha-scissor、风摆和 MultiMesh;
- 代表树种经离线 exporter 获得真实 branch/frond/leaf、尺寸、LOD/impostor,并通过最终机位轮廓验收;
- 玩家和 NPC 的比例、贴图、动画和阴影正常;
- 第三人称机位与参考图相近且可确定性复现;
- 天空、雾、阳光、曝光和饱和度达到签核标准;
- 固定机位
world-a1-r1.png通过结构化对比和人工评审。
R2:最终画面
- 可见水域正确;
- 最终机位中必要
.mse环境效果正常; - 小地图、状态栏、快捷栏、背包和头顶文字完成;
- 1920×1080 UI 布局不遮挡错误区域;
- 场景可持续移动浏览,无明显区块加载卡顿;
world-a1-r2.png达到最终视觉签核;- macOS 性能报告、缺失资源报告和测试结果归档;
- 发布物不意外包含无授权的原始资产。
16. 开发起点
W0 + §9-W4 的
.spt调研可以在 Phase 2 的 bring-up 期间并行做(纯调研 / 无 Godot 依赖); W1 及以后建议等 bring-up 有结果(见开头「定位」)。
第一批代码只做 W0(无 Godot 依赖),不应立刻编写水 shader 或 HUD:
- 增加
AssetResolver; - 增加六个无 Godot 依赖的地图格式 parser;
- 为真实 A1 文件建立单元测试和 load report;
- 固化坐标转换(
mtgodot::coord,见 I5)。
W0 的 parser 报告和坐标测试通过后,第一个 W1 切片才是「在 Metin2World 中只加载 000000、输出灰色地形」。
灰色地形 + W0 快照记录的对象数 + 已知 CRC 都通过测试后,再进入 W2 的 splat shader。这个顺序能最早暴露
路径、格式、坐标和索引错误,并避免在错误的数据基础上反复调整画面。