Files
mtgodot-poc/docs/reference/steps/M1-static-render.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

184 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M1 · macOS 静态渲染
> 总纲:[`../PLAN.md`](../PLAN.md) §03 / §04 / §06。演示切片见 [`../DEMO-PLAN.md`](../DEMO-PLAN.md) D0D4。
> 本文件是 M1 的详细施工文档。
---
## 目标
`sokol_app`/SDL2 窗口 + bgfx(Metal),把 [M0](./M0-gr2-reader.md) 读出的 gr2 静态网格(绑定姿势)+ DDS 贴图渲染出来,
轨道相机可转。同时验证 **bgfx ↔ 窗口层的交接**PLAN §03 的高风险点)。
## 前置
- [M0](./M0-gr2-reader.md) 完成:`libgr2` 能出骨架 + 网格;`test/assets.list` 产出。
- [oracle](./00-oracle.md) 的 T6(确定态截图)—— M1 视觉对拍要它。
- `EterImageLib``CDXTCImage` 冻结拷进 `reuse/`
## 交付物
| 产物 | 位置 |
|---|---|
| `xrender-demo` 可执行(macOS | `build/app/` |
| `engine/rhi.{h,cpp}` bgfx 薄封装 | `engine/` |
| `engine/scene.{h,cpp}` `camera.{h,cpp}` | `engine/` |
| 着色器:`vs_pnt` / `fs_pnt` / `fs_normalviz` + `texture_stage.sh` | `app/shaders/` |
| 交接方案决议(sokol_app 还是 SDL2 | 记进 `app/README.md` |
---
## 任务分解(按 DEMO-PLAN 的 D0D4 切)
### T0a · toolchain 骨架(对应 D0a
- `CMakeLists.txt``add_subdirectory(third_party/bgfx.cmake)`pin bgfx 到与 `.sc` 草稿相近版本。
- 窗口层**默认用 bgfx 自带 `entry` 或 SDL2**(零风险,bgfx 所有 example 用的)——不是 sokol_app。
- 一个清屏 app`bgfx::setViewClear(0x303030ff)` + `bgfx::setDebug(BGFX_DEBUG_STATS)` + `bgfx::frame()`
- **编一个 dummy `.sc`**`vs_flat.sc` / `fs_flat.sc`),过 `shaderc -p metal --platform osx` + `bin2c`,断言 `*.sc.bin.h` 生成 —— 提前验证 shaderc 链路(否则要到 T2 才触发)。
- hi-dpiresize 事件里 `bgfx::reset(fbWidth, fbHeight)`retina 上用 framebuffer 像素,不是点)。
- **完成判据**:窗口出现纯色背景 + stats 角标;`*.sc.bin.h` 已生成。
### T0b · sokol_app ↔ bgfx 交接实验(对应 D0b,**不阻塞**)
- 另开一个 target,用 `sokol_app.h``sapp_metal_get_layer()`CAMetalLayer*)填 `bgfx::PlatformData::nwh``init.type = Metal`
- 已知障碍:sokol_app 会自建 MTKView + 自己 present,和 bgfx 抢 layerPLAN §03)。
- **成了** → M1 之后统一切 sokol_app(为移动端生命周期铺路)。**没成** → demo 全程用 T0a 的 SDL2/entrysokol_app 留到 [M3](./M3-mobile.md) 再单独攻。
- **完成判据**:二选一有明确结论并记进 `app/README.md`
### T1 · `engine/rhi` —— bgfx 薄封装(≈150 行)
- 接口签名见 DEMO-PLAN §5「engine/rhi」。语义对齐 `StateManager`
- 关键:`init(nativeHandle,w,h)``createVB/IB/Tex2D/Program``setModel/setBones/setTexture/setStageUniforms``submit``beginFrame/endFrame`
- 深度范围 / Y 翻转:用 `bgfx::getCaps()->homogeneousDepth` / `originBottomLeft` 决定 `bx::mtxProj` 参数,别手写。
### T2 · `engine/scene` + `vs_pnt/fs_pnt` —— 白模上屏(对应 D2)
- `scene``libgr2::Mesh` → 打包顶点 → `bgfx::VertexLayout`(映射表见 DEMO-PLAN §7.3)→ `createVB`;索引 → `createIB`;每 `TriGroup` 一个 draw item。
- `vs_pnt.sc` / `fs_pnt.sc`:从 `shaders.rar` 拷改。`vs_pnt``a_color0`fog 先关。
- `camera.{h,cpp}`:轨道相机(鼠标拖 = 绕轨道、滚轮 = 距离)。
- 坐标系:加固定 `basisFix`Granny art-tool basis → 左手 Y-up),用朝向明确的资产手调到"warrior 正着站、面朝 +Z",写进 `scene.cpp` 注释。
- **完成判据**:可转的白模,轮廓 = Blender 里同模型。
### T3 · DXT 解码 + 贴图(对应 D3)
- `reuse/EterImageLib``CDXTCImage``LoadHeaderFromMemory` + `LoadFromMemory` + `Decompress(0, rgba)`
- `warrior_cheongrin.dds` 实测 **512×512 DXT3 5mip** —— 先只传 level 0`BGFX_SAMPLER_MIN_POINT` 关 mip;T3 过后再补全 mip 链。
- `texture_stage.sh``shaders.rar` 原样拷,喂"单 stage MODULATE(TEXTURE, DIFFUSE)"的 uniform(等价 `tex * vertexColor`)。
-`T` 切贴图 / 白模。
- **完成判据**:UV 无错位、无镜像;和 oracle 确定态截图目视一致。
### T4 · 自检工具(对应 D4)
- `fs_normalviz.sc`10 行):`gl_FragColor = vec4(v_normal*0.5+0.5, 1)`。按 `N` 切。
- 线框:`BGFX_STATE_PT_LINES``bgfx::setDebug(BGFX_DEBUG_WIREFRAME)`。按 `W` 切。
- **完成判据**:法线朝外;三角组分段可见且正确。
### T5 · 多 stage 材质专项(PLAN §04 要求)
-`test/assets.list` 挑一个用了 `D3DTOP_MODULATE2X``ADDSIGNED` 的材质,单独渲一帧和 oracle 确定态截图对拍。
- 不能只靠整场景 SSIM 兜 `texture_stage.sh` 的正确性。
- **完成判据**:该材质的着色结果与 oracle 一致(差异分类内)。
---
## 门禁(go / no-go
- **子门禁 1(交接)**bgfx 经窗口层 handle 在 macOS 出画面 + 输入可用。sokol_app 啃不下 → 用 SDL2/entry,本门禁照样算过(方案已决议)。
- **子门禁 2(几何)**:网格拓扑、UV 正确;无背面剔除错误 / 法线翻转。
- **子门禁 3(材质)**:一个多 stage 材质的着色结果与 oracle 对上。
## 验证
- **对比场景两侧都喂同一张预解码 RGBA**(绕开 DXT),截图 diff 只反映几何 / 光照 / texture-stage,不被"软解 vs 硬件 S3TC"的 bit 级差异污染。
- oracle 侧进入确定态(T6:注入相机矩阵、固定光、绑定姿势、无程序化摇摆)。
- 与 oracle 截图像素 diff + SSIM;差异分类见 PLAN §07 视觉层。
- 法线可视化模式自检。
## 本步风险(从 PLAN §08 筛)
| 风险 | 状态 / 缓解 |
|---|---|
| sokol_app + bgfx 抢 context | **规避**M1 用 GLFW`GLFW_NO_API` + `glfwGetCocoaWindow``PlatformData.nwh`),零冲突。sokol_app 留到 M3。 |
| `.sc` 草稿针对某 bgfx 版本,shaderc 编不过 | **已解**`vs_pnt`/`fs_pnt``shaders.rar` 改写对上当前 pin 的 `bgfx_shader.sh``shaderc -p metal` 编过。 |
| `reuse/EterImageLib` 从没链接过 | **规避**DXT1/3/5 自研 `engine/dxt.cpp`~180 行),完全不碰 EterImageLib。 |
| 坐标系 basiswarrior 躺着 / 镜像 / 巨大 | **已解**`basis_fix = rotX(-90°)·scale(0.01)`warrior 正着站、比例约 1.7m。写死在 `scene.cpp`。 |
| DXT 软解 vs 客户端 GPU S3TC 色差 | 对拍时两侧都用软解 RGBA(`engine/dxt.cpp` 产出,喂给 oracle 侧同一张)。 |
| hi-dpi 视口 | `on_fb_size``glfwGetFramebufferSize``rhi::reset`framebuffer 像素)。 |
## DoD 清单
- [x] `xrender-demo` 在 macOS 起窗口(GLFW+ bgfx Metal init 成功(`renderer: Metal`)(T0a
- [x] 交接方案定为 **GLFW**,记进 `app/README.md`T0b)。sokol_app 不用(抢 layer),留到 M3。
- [x] warrior 绑定姿势上屏,正着站、`basis_fix` = rotX(-90°)·scale(0.01)Granny Z-up cm → Y-up m),轨道相机 + 键位 T/N/W/RT2
- [x] DXT1 / DXT3 软解(`engine/dxt.cpp`,绕开 `reuse/EterImageLib`),贴图 + UV 正确、无镜像(`warrior_cheongrin.dds` 512² DXT3、`warrior_face.dds` 256² DXT1)(T3
- [x] `N` 法线可视化(朝外、平滑)/ `W` 线框(拓扑正确)自检(T4
- [ ] 一个多 stage 材质与 oracle 对拍通过(T5)—— **待 oracle**`texture_stage.sh` 单独走 `fs_stage.sc`M1 主路径用固定 MODULATE(TEXTURE,DIFFUSE)
- [ ] 与 oracle 确定态截图 SSIM ≥ 0.98 或差异分类通过 —— **待 oracle**
## M1 实现结果
| 项 | 结果 |
|---|---|
| 依赖 | bgfx.cmake + bx/bimg/bgfx + glm + glfw`third_party/`pin 见 `VERSIONS.md`)。`tools/bootstrap-submodules.sh` 拉。 |
| 构建 | `cmake -B build -DXRENDER_BUILD_DEMO=ON -DXRENDER_BUILD_TOOLS=OFF``cmake --build build --target xrender-demo`。着色器经 bgfx 的 `shaderc` 编成 `metal` `.bin``cmake/xrender-shaders.cmake`),运行时加载。 |
| 无头验证 | `XR_HIDDEN=1`(隐藏窗口)+ `XR_SCREENSHOT=<path>`bgfx `requestScreenShot` → TGA`rhi` 自带 `CallbackI`+ `XR_MODE=normals|wireframe|bindpose` + `XR_FRAMES=n`。CI / 无显示器也能出确定态截图。 |
| 参考截图 | `test/golden/m1-warrior_cheongrin-bindpose-textured.png``-normals.png` |
| 几何 | 5 meshObject16 / face / Object03 / Object09 / body),顶点/索引数与 M0 一致;bind pose = Granny T-pose;朝向、比例正确 |
| 坑 | ① 近期 bgfx 把 `platform.h` 并进 `bgfx.h`;② `bgfx::setUniform` 要在每次 `submit` 前调(frame 头单调一次不可靠)→ `rhi` 缓存 light/stage 每 draw 重设;③ 截图 TGA 带 alpha 通道,shader 输出非 1 的 alpha 会让 PNG 查看器合成成白 → 不透明物体 shader 固定 `gl_FragColor.a = 1`,截图 writer 也强制 alpha 255 |
## 画质改进 pass(档 1 + 档 2)
用户反馈"渲染毛糙"后做的一轮画质提升。基线:无 MSAA / 无 mip / 平光 / 贴图靠文件名瞎猜 / 无 sRGB。
### 档 1 —— 采样与着色正确性(`engine/` + `app/shaders/`
| 项 | 做法 |
|---|---|
| MSAA | `rhi.cpp` `init`/`reset``BGFX_RESET_MSAA_X4`state 加 `BGFX_STATE_MSAA` |
| mip 链 | `engine/dxt.{h,cpp}` 重写:`decode()``dwMipMapCount`(off 28) 循环解每一级;`Image.mips` 存 level0..N。`scene.cpp` 把各级拼成一块传 `rhi::create_tex2d(packed, …, mips)`bgfx `hasMips = mips>1` |
| sRGB 正确性 | `create_tex2d``BGFX_TEXTURE_SRGB`(采样自动 sRGB→linear);`fs_pnt.sc` 线性空间着色,末尾 `pow(col, 1/2.2)` 编回显示空间 |
| 各向异性过滤 | `create_tex2d``BGFX_SAMPLER_{MIN,MAG}_ANISOTROPIC` |
| 半球环境光 | `fs_pnt.sc``mix(u_ambientGround, u_ambientSky, n.y*0.5+0.5)` 代替常数 ambient |
| 3 盏方向光 | `LightDesc``dir[3]`/`color[3]`key/fill/顶光),`.w` = 强度;`u_lightDir/u_lightColor``Vec4,3` uniform |
| 双面光照 | `fs_pnt.sc``abs(dot(n,l))`(薄片/头发/飘带两面都受光,和 Metin2 一致) |
| 法线 renormalize | vs 输出前 + fs `normalize(v_normal)` |
### 档 2 —— 材质绑定(`libgr2` + `engine/`
**这是"毛糙"的最大来源**:原来整个模型套一张 `<gr2 名>.dds``sura_lord` 根本没有 `sura_lord.dds` → 纯黑。
| 项 | 做法 |
|---|---|
| libgr2 解材质 | `gr2_mesh.cpp` `material_texture_name()``granny_material` → 直接 `.Texture.FromFileName`,或递归 `.Maps[].Map`**注意**:本版 Granny 里 `granny_material_map` 的子材质成员名是 `Map` 不是 `Material`)。`gr2_fileinfo.cpp``FileInfo.materials`(顶层表,调试用)。 |
| 每网格贴图表 | `Mesh.material_textures`(与 `granny_mesh.MaterialBindings` 平行);`tri_group.material_index` 索引它 |
| 逐 tri_group 上贴图 | `scene.cpp``SubMesh.ranges``DrawRange{ib,index_count,tex,alpha_cutout}`),一个网格按 tri_group 切多段,**每段切出独立 IB**,各自贴图。贴图按 `FromFileName` 的 basename 在 gr2 同目录里大小写不敏感查找,带缓存。找不到时回落老的 stem 猜测。 |
| bind pose 也要蒙皮 | 很多 Metin2 模型(shaman_lord 等)的 raw 顶点不在 bind 空间,直接上 raw 会整块错位。`Scene::set_bind_pose()` 有骨架时走 `File::bind_pose(0)` 的蒙皮矩阵(= `set_pose`);bounds/相机框选也按 bind 姿势的顶点算。 |
| alpha-test(镂空) | 解贴图时看 mip0 的 alpha 分布:**同时**有 >5% 近 0 且 >20% 近 255(双峰)才判 cutout(头发/飘带/树叶)。只看"低 alpha 比例"会误伤 alpha 平面全 0 的不透明 DXT3(整块被 discard)。`rhi::set_alpha_test` 逐 draw 开,`fs_pnt.sc``texColor.a < ref → discard`。 |
### 排障中发现并修掉的两个真 bug
| bug | 现象 | 根因 / 修复 |
|---|---|---|
| **多材质网格第 2 段起整块飞出视锥** | shaman_lord(1 网格 2 材质组)只渲出头顶一小撮;任何多组网格丢掉第一组之后的内容 | `Scene::draw``rhi::set_model()`(→ `bgfx::setTransform`)每网格只调一次,但 **bgfx 每次 `submit` 消费一次 transform**。第 2 个 draw range 没设 transform → 用单位阵 → 顶点(~120 单位的 skin 空间坐标)画在 basis_fix 之外。**修复**`set_model` 移进 range 循环,逐 submit 重设。 |
| **bind pose 直接上 raw 顶点** | shaman_lord 等模型整体错位、相机框选发飞(模型变成一个远处的点) | raw 顶点不在 bind 空间。**修复**:`set_bind_pose` 走蒙皮矩阵(见上表)。 |
调试加了 `XR_CAM_YAW` / `XR_CAM_PITCH` / `XR_CAM_DIST`(乘子)env 覆盖初始相机,`XR_VERBOSE` 打 bounds / 相机参数,方便无头抽查各角度。
### 结果
- `warrior_cheongrin` / `warrior_novice` / `sura_lord` / `assassin` / `shaman_lord` / `snakeman` 等正/背/动画各角度截图:贴图正确、比例朝向对、边缘平滑、明暗有层次(见 `test/render-samples/`
- **无回归**`gr2fuzz` 9166/9166、`render_fuzz` 9166/91660 crash/empty/nan)、oracle 逐字段对拍 23/23(≤6.5e-5)、iOS 交叉编译通过
### 明确不做(本 POC 范围外,记档)
| 项 | 原因 |
|---|---|
| 多 stage 材质混合(`fs_stage.sc` | Metin2 角色基本是单 stage MODULATE(TEXTURE,DIFFUSE);多 stage 主要用于地形。`texture_stage.sh` 已备,未接主路径 |
| alpha blend(半透明排序) | cutoutalpha-test)已覆盖头发/飘带主要场景;真半透明要 OIT 或按深度排序,收益低 |
| 顶点色 | 语料里角色网格全是 PNT332(无 color 分量) |
| `.msm` 装配(换发型/换肤) | `formats/msm.cpp` 已能解析,装配是模型组合子系统,非渲染画质 |
| 法线贴图 / 切线帧 | PNT332 无切线;Metin2 资产也没有法线贴图 |
| 阴影 / IBL / FXAA / LOD 选择 | 已有 MSAA x4;阴影/IBL 是独立子系统,env map 资产缺失;FXAA 在 MSAA 之上边际收益小 |
| 特效 / 粒子 / 地形 / 水 / SpeedTree | 独立子系统,POC(读+渲+动 `.gr2` 骨骼模型)范围外 |