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
184 lines
14 KiB
Markdown
184 lines
14 KiB
Markdown
# M1 · macOS 静态渲染
|
||
|
||
> 总纲:[`../PLAN.md`](../PLAN.md) §03 / §04 / §06。演示切片见 [`../DEMO-PLAN.md`](../DEMO-PLAN.md) D0–D4。
|
||
> 本文件是 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 的 D0–D4 切)
|
||
|
||
### 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-dpi:resize 事件里 `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 抢 layer(PLAN §03)。
|
||
- **成了** → M1 之后统一切 sokol_app(为移动端生命周期铺路)。**没成** → demo 全程用 T0a 的 SDL2/entry,sokol_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。 |
|
||
| 坐标系 basis:warrior 躺着 / 镜像 / 巨大 | **已解**:`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/R(T2)
|
||
- [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 mesh(Object16 / 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/9166(0 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(半透明排序) | cutout(alpha-test)已覆盖头发/飘带主要场景;真半透明要 OIT 或按深度排序,收益低 |
|
||
| 顶点色 | 语料里角色网格全是 PNT332(无 color 分量) |
|
||
| `.msm` 装配(换发型/换肤) | `formats/msm.cpp` 已能解析,装配是模型组合子系统,非渲染画质 |
|
||
| 法线贴图 / 切线帧 | PNT332 无切线;Metin2 资产也没有法线贴图 |
|
||
| 阴影 / IBL / FXAA / LOD 选择 | 已有 MSAA x4;阴影/IBL 是独立子系统,env map 资产缺失;FXAA 在 MSAA 之上边际收益小 |
|
||
| 特效 / 粒子 / 地形 / 水 / SpeedTree | 独立子系统,POC(读+渲+动 `.gr2` 骨骼模型)范围外 |
|