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
1104 lines
55 KiB
Markdown
1104 lines
55 KiB
Markdown
# 神兽国 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`](./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`](./PARITY-GAP.md)(本文管「怎么渲出来」,
|
||
> 那文管「怎么做到参考图那样」)。
|
||
|
||
## 1. 文档目的
|
||
|
||
本文定义在「Phase 2 三设备 bring-up 完成」之后,从当前“单个 `.gr2` 角色展示 POC”推进到“能够直接加载原始客户端资源并渲染神兽国 A1 村庄画面”的完整开发方案(前置见上方「定位」)。
|
||
|
||
本文是实现和验收依据,解决以下问题:
|
||
|
||
1. 哪些数据直接来自 `assets/`;
|
||
2. 每种地图资源如何解析并转换为 Godot 运行时对象;
|
||
3. 新增模块与现有 `Metin2Model`、`libgr2`、DDS 解码器如何协作;
|
||
4. 按什么顺序实现,才能尽早得到可比较的场景截图;
|
||
5. 何时可以认为已经达到参考图中的地形、建筑、植被、角色、光照和 HUD 效果。
|
||
|
||
本文不要求还原网络、任务、战斗、商店等游戏逻辑。最终交付是可运行、可移动、可稳定截图的客户端渲染样板。
|
||
|
||
## 2. 最终目标
|
||
|
||
### 2.1 目标画面组成
|
||
|
||
最终 1920×1080 输出必须包含:
|
||
|
||
- 神兽国 A1 的原始地形高度与道路/草地/石地分布;
|
||
- 原始坐标中的村庄建筑、围栏、灯柱和其他静态摆件;
|
||
- 树木及透明叶片;
|
||
- 玩家角色、至少一个 NPC、角色动画和地面阴影;
|
||
- 与 `A1.msenv` 接近的天空、阳光、环境光、远景雾和整体色调;
|
||
- 可见水域时的基础水面;
|
||
- 第三人称游戏相机;
|
||
- 最终验收阶段的背包、快捷栏、状态栏、小地图和头顶文字。
|
||
|
||
### 2.2 两级交付
|
||
|
||
为了避免 UI、粒子或完整地图流送阻塞三维场景验证,交付分成两级:
|
||
|
||
| 级别 | 名称 | 必须包含 | 用途 |
|
||
|---|---|---|---|
|
||
| R1 | 世界渲染完成 | 地形、splat、建筑、树木、角色、相机、天空、光照、雾、阴影 | 验证截图主体是否成立 |
|
||
| R2 | 最终画面完成 | R1 + 水面 + 必要环境特效 + HUD/小地图/头顶文字 | 与参考图做最终视觉比较 |
|
||
|
||
R1 是后续工作的硬前置;R2 是本文的最终完成条件。
|
||
|
||
## 3. 当前基础与真实缺口
|
||
|
||
### 3.1 已可复用能力
|
||
|
||
当前仓库已经具备:
|
||
|
||
- `.gr2` v6/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.cpp`](../extension/src/metin2_model.cpp)
|
||
- [`extension/src/metin2_anim.cpp`](../extension/src/metin2_anim.cpp)
|
||
- [`extension/src/gr2_bridge.cpp`](../extension/src/gr2_bridge.cpp)
|
||
- [`extension/src/m2_material.cpp`](../extension/src/m2_material.cpp)
|
||
- [`extension/src/dxt.cpp`](../extension/src/dxt.cpp)
|
||
- [`formats/`](../formats/)
|
||
- [`libgr2/`](../libgr2/)
|
||
|
||
### 3.2 必须补齐的能力
|
||
|
||
当前不能直接输出目标画面的主要原因是以下系统尚不存在:
|
||
|
||
1. 统一虚拟资源路径与跨 pack 目录索引;
|
||
2. 地图设置、TextureSet、Environment、AreaData 和 Property 解析;
|
||
3. heightmap 地形网格;
|
||
4. `tile.raw` 多层地表 splatting;
|
||
5. 区块阴影贴图;
|
||
6. CRC 驱动的建筑/树木/特效实例化;
|
||
7. SpeedTree 替代或兼容路径;
|
||
8. 水面;
|
||
9. 地图区块流式加载和跨实例资源缓存;
|
||
10. 第三人称相机、NPC 放置和 HUD。
|
||
|
||
[`BACKLOG.md`](./BACKLOG.md) 中 E8、E9、E10、E7 的判断正确,但它们不足以覆盖地图渲染的全部前置:“地图文本格式”“区块二进制 + AreaData”“Property CRC 注册表”“虚拟资产解析器”“坐标统一”“地图回归”是独立工作项,已在 `BACKLOG.md` 补为 `E11`–`E13` / `G7` / `H7` / `I5`,不是 E8 内部可以忽略的小步骤。
|
||
|
||
## 4. 资源使用原则
|
||
|
||
### 4.1 直接读取原始资源
|
||
|
||
运行时默认直接读取:
|
||
|
||
```text
|
||
assets/
|
||
```
|
||
|
||
禁止为了构建一张演示图而手工重新摆放整座村庄。地形和对象位置必须由原地图数据驱动。
|
||
|
||
允许生成派生缓存,例如:
|
||
|
||
- 解码后的 DDS GPU 纹理缓存;
|
||
- `tile.raw` 生成的 splat alpha/权重纹理;
|
||
- GR2 转换后的静态 `ArrayMesh`;
|
||
- 树木离线转换出的普通 Mesh;
|
||
- 地图区块碰撞和导航缓存。
|
||
|
||
派生缓存必须可删除并从原始 assets 重建,不能成为新的唯一数据源。
|
||
|
||
### 4.2 虚拟路径不能简单拼接
|
||
|
||
资源内部经常使用:
|
||
|
||
```text
|
||
d:\ymir work\zone\...
|
||
d:/ymir work/environment/...
|
||
```
|
||
|
||
真实散文件按原 eterpack 名称分散在多个一级目录,例如:
|
||
|
||
```text
|
||
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/...`。
|
||
|
||
规范化规则:
|
||
|
||
1. `\` 转 `/`;
|
||
2. 移除盘符和开头 `/`;
|
||
3. ASCII 小写作为索引键,但保留真实路径用于打开文件;
|
||
4. 去除重复 `/`、`.`,拒绝越过 assets 根目录的 `..`;
|
||
5. 以“虚拟路径 → 真实散文件路径”建立索引;
|
||
6. 同名资源按显式 pack 优先级解析,冲突必须记录日志;
|
||
7. 开发模式允许输出全部候选,便于确认 patch 覆盖关系。
|
||
|
||
推荐优先级配置,而不是依赖目录遍历顺序:基础包在前,patch 包在后,后注册者覆盖前注册者。最终采用的顺序要写入测试快照。
|
||
|
||
### 4.3 授权约束
|
||
|
||
技术实现可以读取本地原客户端资源,但发布物不得默认携带这些资源。构建、测试和分发流程必须把代码许可与游戏素材许可分开处理。
|
||
|
||
## 5. A1 地图数据规格
|
||
|
||
目标地图位于:
|
||
|
||
```text
|
||
assets/OutdoorA1/metin2_map_a1
|
||
```
|
||
|
||
### 5.1 全局设置
|
||
|
||
[`setting.txt`](../assets/OutdoorA1/metin2_map_a1/setting.txt) 的当前数据为:
|
||
|
||
```text
|
||
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 × HeightScale` cm,再乘 `0.01` 变成 Godot 米。
|
||
|
||
`BasePosition` 是地图在全局 Metin2 世界中的坐标基准,不应该直接变成离原点几千米的 Godot 节点坐标。渲染场景使用 map-local 坐标,网络/地图逻辑需要时再通过原点偏移换算。
|
||
|
||
### 5.2 区块目录编号
|
||
|
||
区块目录编号采用:
|
||
|
||
```text
|
||
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`](../../m2dev-client-src-main/src/PRTerrainLib/Terrain.h) 和 [`Terrain.cpp`](../../m2dev-client-src-main/src/PRTerrainLib/Terrain.cpp) 为真值来源。
|
||
|
||
### 5.4 TextureSet
|
||
|
||
A1 使用:
|
||
|
||
```text
|
||
assets/textureset/textureset/metin2_a1.txt
|
||
```
|
||
|
||
当前文件含 17 个图层。每个图层记录:
|
||
|
||
```text
|
||
texture_path
|
||
u_scale
|
||
v_scale
|
||
u_offset
|
||
v_offset
|
||
splat_enabled
|
||
height_begin
|
||
height_end
|
||
```
|
||
|
||
索引 `0` 是原客户端内部的空图层;资源文件的 `Texture001` 对应运行时索引 `1`。不得产生一位偏移错误。
|
||
|
||
### 5.5 Environment
|
||
|
||
A1 环境资源可由虚拟路径解析到:
|
||
|
||
```text
|
||
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` 每个对象按以下字段解析:
|
||
|
||
```text
|
||
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.cpp`
|
||
> `place_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`](../../m2dev-client-src-main/src/GameLib/Area.cpp)、[`Property.cpp`](../../m2dev-client-src-main/src/GameLib/Property.cpp) 和 [`MapType.cpp`](../../m2dev-client-src-main/src/GameLib/MapType.cpp) 为依据。
|
||
|
||
## 6. 坐标系统
|
||
|
||
Metin2 资源使用厘米和 Z-up;Godot 使用米和 Y-up。全项目只能有一套转换:
|
||
|
||
```text
|
||
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 中复制不同公式。
|
||
|
||
建议新增:
|
||
|
||
```cpp
|
||
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. 目标运行时架构
|
||
|
||
```text
|
||
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)
|
||
```
|
||
|
||
数据流:
|
||
|
||
```text
|
||
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 建议新增源码布局
|
||
|
||
```text
|
||
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 节点:
|
||
|
||
```text
|
||
Metin2World : Node3D
|
||
```
|
||
|
||
建议暴露:
|
||
|
||
```gdscript
|
||
@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
|
||
```
|
||
|
||
信号:
|
||
|
||
```gdscript
|
||
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. 分阶段实施方案
|
||
|
||
> **贯穿全程的两条硬约束:**
|
||
> 1. **目标渲染器 = Godot Mobile renderer(Vulkan / Metal),不涉及 Compatibility(GLES3)。**
|
||
> 三台目标设备(Mac / 一加 13 / iPhone 16)都是现代硬件。R1 可在 macOS 上用 Forward+ 开发,但每个
|
||
> 新写的 shader(地形、splat、水、天空、`SRC_SKIN`)要尽早在一加 13 / iPhone 16 上各跑一次,
|
||
> 不把「移动端能不能跑」推到 W8。
|
||
> 2. **文本编码要显式声明。** `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/` 下已落地并通过 CTest `formats.map_formats`
|
||
> (内联单测 + `M2_ASSETS` 设时跑真实 A1 断言)+ `tools/map_probe` load 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`(`.msenv` Group/List 文本树;A1 `foglevel 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 可解析 / `000000` areadata
|
||
> 23 对象 / **136/136 个 areadata CRC 都命中 Property 注册表** / 全 975 对象坐标为地图全局 cm(W3 修正,见 §5.6) /
|
||
> CRC `569394331` = 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 阻塞。
|
||
|
||
实现:
|
||
|
||
1. `AssetResolver` 扫描 `assets` 并生成规范化索引;
|
||
2. 解析 `setting.txt`、TextureSet、`.msenv`;
|
||
3. 解析六类区块文件头和尺寸;
|
||
4. 扫描 Property 并建立 `uint32 CRC -> Property`;
|
||
5. 解析 `areadata.txt` 和 `areaambiencedata.txt`;
|
||
6. 所有 parser 返回结构化错误,不允许静默截断或越界读取;
|
||
7. 所有文本 parser 显式声明输入编码(见本节开头约束 2),非 ASCII 字段有测试覆盖。
|
||
|
||
验收:
|
||
|
||
> 以下多数项已由 `tools/map_probe` + CTest `formats.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.raw` 131×131 `uint16`(无头)/ `tile.raw` 258×258 `uint8`(无头);
|
||
- ✅ `attr.atr` magic 2634 / `water.wtr` magic 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}` = `Metin2World` GDExtension 节点(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 的显式断言、线框截图。
|
||
|
||
实现:
|
||
|
||
1. 每个区块生成 129×129 可见顶点和 128×128 个 quad;
|
||
2. `height.raw` 的 131×131 数据保留边界样本,用于相邻区块法线;
|
||
3. 三角线方向必须与原客户端 `GetHeight()` 的左右三角判定一致;
|
||
4. 法线按原客户端中心差分算法生成;
|
||
5. 先以 16×16 格 patch 为裁剪/LOD 单元;
|
||
6. 生成简单灰色材质和 `HeightMapShape3D` 或等价碰撞;
|
||
7. 提供 `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 乘法项(有则),光照交给 Godot `DirectionalLight`。
|
||
> **窗口渲染验证**:`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 层。当前 UV `128/scale` 也不等于原客户端公式。
|
||
> **剩余**:支持全部活动图层;把 UV 改为 `fTerrainTexCoordBase * Scale`(对当前区块归一化 UV 即
|
||
> U=`8*UScale`、V=`-8*VScale`,offset 同步符号);保留源分辨率/原 mip,严格 parity 时关闭程序化
|
||
> `macro_detail`;再用 minimap/受控帧验收,并跑 Mobile renderer。
|
||
|
||
兼容路径先实现原客户端语义:
|
||
|
||
1. `tile.raw` 的值是 TextureSet 图层编号;
|
||
2. 按原 `RAW_GenerateSplat` 逻辑生成每图层 258×258 alpha;
|
||
3. 图层按原顺序叠加;
|
||
4. 使用原公式 `fTerrainTexCoordBase * UScale`、`-fTerrainTexCoordBase * VScale` 和对应 offset 生成 UV;
|
||
5. `shadowmap.dds` 作为地表颜色乘法项;
|
||
6. 只有当前 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、重复寻址和各向异性过滤;
|
||
- 太阳漫反射;
|
||
- `.msenv` material 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)**:libgr2 `MaterialInfo` 加 `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` 精确解析。
|
||
|
||
实现:
|
||
|
||
1. 根据 AreaData CRC 查 Property;
|
||
2. 读取对象位置、YPR、heightBias 和 portal ids;
|
||
3. `Building`、`DungeonBlock` 复用 `libgr2`,但走专用静态资源路径;
|
||
4. 同一 GR2 只解析一次,Mesh 和 Material 跨实例共享;
|
||
5. 支持 `_lod_01.gr2`、`_lod_02.gr2`、`_lod_03.gr2`;
|
||
6. `ShadowFlag` 决定是否参与实时阴影;
|
||
7. `.mdatr` 用于静态碰撞,若暂未实现则必须在报告中列出;
|
||
8. 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 树种**;MultiMesh `visibility_range_end=420m`(+60m 淡出)。
|
||
> **剩余**:真实 branch/frond/leaf table、按树种精确尺寸、LOD/impostor、环境色调和阴影策略。
|
||
|
||
两条路径:
|
||
|
||
1. **当前 R2 proxy**:真实 bark/composite atlas + 确定性枝干/叶簇,先保证地图可运行和轮廓明显优于占位;
|
||
2. **最终 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_SKY` energy + `ambient_light_color`
|
||
> - `Fog`(`foglevel 6`)→ **深度雾** begin≈225m / end≈925m(`foglevel` 越大起雾越近;首版误用
|
||
> 指数密度把全图刷白,已改深度雾)
|
||
> - 色调 = Filmic + `adjustment`(saturation 1.12 / contrast 1.05)
|
||
>
|
||
> `Metin2World` load 时解析 `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 light `cull_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` +
|
||
> 玩家(`Metin2Model` warrior + `Metin2AnimPlayer`)+ NPC(shaman,`wait.msa`)+ 第三人称相机。
|
||
> - 玩家:WASD 相对相机朝向移动,`_player.rotation.y` 朝移动方向;`Metin2World.is_blocked()`
|
||
> (`attr.atr` bit0,新加 `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)**:NPC `Label3D` 名字牌;**玩家发型挂接**(`Metin2Model.hair_gr2`,骨骼按名重映射
|
||
> 到 base skeleton,并入 CPU 蒙皮,`alpha_scissor`;warrior 74/74 骨匹配)—— 角色不再是秃头。
|
||
> **剩余**:鼠标转视角交互壳、相机遮挡淡出、武器挂点、GPU 路径的 hair、确定性机位 preset。
|
||
|
||
实现:
|
||
|
||
1. 复用 `Metin2Model` 和 `Metin2AnimPlayer`;
|
||
2. 正式补齐头发、武器和共享骨架附件;
|
||
3. 玩家和 NPC 默认使用 GPU 自定义蒙皮;
|
||
4. 玩家 Y 位置实时贴合 `sample_height()`;
|
||
5. `attr.atr` 的 `ATTRIBUTE_BLOCK` 用于移动阻挡;
|
||
6. 相机包含跟随、yaw/pitch、缩放、近远裁剪和遮挡处理;
|
||
7. 调出与参考图相似的高度、距离和 FOV,并保存成确定性 preset;
|
||
8. 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_texture` sampler + `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 渲染器不要求逐像素一致。门禁分两层:
|
||
|
||
1. 硬门禁:加载成功、对象计数、NaN、缺资源、空 mesh、接缝和崩溃;
|
||
2. 视觉门禁:SSIM/边缘结构、区域直方图与人工签核结合。
|
||
|
||
视觉比较前必须锁定:Godot 版本、renderer、窗口尺寸、相机 transform/FOV、时间、动画帧、随机种子、曝光、天气和资源索引优先级。
|
||
|
||
> **原客户端参考帧要「受控」采集,不能只拿一张随手截图。** `ingame-shinsoo.png` 只用于早期
|
||
> 目视对齐。正式视觉门禁需要:在原 D3D 客户端里到一个**已知全局坐标 + 已知 yaw/pitch + 已知
|
||
> FOV + 关 HUD**(或裁掉 HUD 区)的机位截图,把这组参数记进 `test/maps/` 的机位 preset,
|
||
> Godot 侧用 §6 的坐标转换复现同一机位。原客户端若无法脚本化摆机位,则退而用「同一地标、
|
||
> 人工尽量对齐 + 只比 ROI 结构/直方图、不比全图 SSIM」,并在报告里注明是人工对齐。
|
||
|
||
## 12. 里程碑与依赖
|
||
|
||
```text
|
||
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`](./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 自动实例化;
|
||
- [x] 树木按 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:
|
||
|
||
1. 增加 `AssetResolver`;
|
||
2. 增加六个无 Godot 依赖的地图格式 parser;
|
||
3. 为真实 A1 文件建立单元测试和 load report;
|
||
4. 固化坐标转换(`mtgodot::coord`,见 I5)。
|
||
|
||
W0 的 parser 报告和坐标测试通过后,第一个 W1 切片才是「在 `Metin2World` 中只加载 `000000`、输出灰色地形」。
|
||
灰色地形 + W0 快照记录的对象数 + 已知 CRC 都通过测试后,再进入 W2 的 splat shader。这个顺序能最早暴露
|
||
路径、格式、坐标和索引错误,并避免在错误的数据基础上反复调整画面。
|