Files
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

346 lines
21 KiB
Markdown
Raw Permalink 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.
# Demo 开发方案 —— bgfx 渲染 assets 下的 gr2 资源
> 本文件把 [`PLAN.md`](./PLAN.md) 的 M0 + M1+ M2 拉伸目标)落成一个**可演示的 demo** 的文件级施工图。
> demo 只做 macOSMetal),跑通后 iOS/Android 是同一套代码换 toolchain。
> 前置事实、坐标/格式细节、风险都以 `PLAN.md` 为准,这里不重复论证。
---
## 1 · Demo 目标与演示脚本
**一句话**:一个原生 macOS 窗口,直接从 `m2dev-client-main/assets/` 读一个 `.gr2` + 它的 `.dds`,用 bgfx/Metal 把这个带蒙皮的角色渲染出来,轨道相机可转,能切线框 / 法线可视化;拉伸目标是让它播一个 `.msa` 动作。
**演示脚本(给人看的顺序)**
| 步骤 | 屏幕上 | 证明了 |
|---|---|---|
| 1 | 终端跑 `gr2dump warrior_cheongrin.gr2`,打印骨架树(骨骼名 + 父索引 + 层级缩进)、网格摘要(顶点数 / 索引数 / 三角组)、动画列表 | libgr2 能脱离 Granny 读结构 |
| 2 | 窗口出现,warrior 网格以**绑定姿势**显示,白模,轨道相机可拖转、滚轮缩放 | gr2 顶点 / 索引 / 骨架 → bgfx,交接成立 |
| 3 | 按 `T` 贴上 `warrior_cheongrin.dds`(DXT3 软解),材质正确 | DXT 解码 + texture-stage + UV 正确 |
| 4 | 按 `N` 切法线可视化、按 `W` 切线框 | 自检工具,抓法线翻转 / 拓扑错 |
| 5(拉伸)| 按 `Space``action/dance_1.gr2` + `dance_1.msa`,角色跳舞,CPU 蒙皮 | 曲线采样 + LBS 成立,M2 的核心 |
| 6 | 角标 HUD 显示 FPS / draw call / 三角数(bgfx 自带 `showStats` | 有性能可读数 |
**不做**:地形、特效、UI、多部件装配、eterpackdemo 用散装文件)、iOS/Android(同代码后续换 toolchain)。
---
## 2 · 演示用资产(具体文件,散装,无需解包)
| 用途 | 路径(相对 `m2dev-client-main/` | 事实 |
|---|---|---|
| 主模型 | `assets/PC/ymir work/pc/warrior/warrior_cheongrin.gr2` | 92 770 Bgr2 格式 v68 section**section 全 Oodle1 压缩**(实测)|
| 主贴图 | `assets/PC/ymir work/pc/warrior/warrior_cheongrin.dds` | **512×512 DXT35 mip** |
| LOD 对照 | `warrior_cheongrin_lod_01/02/03.gr2` | 同骨架、减面,用于 M2 的 LOD 一致性检查 |
| 动作(拉伸)| `assets/PC/ymir work/pc/warrior/action/dance_1.gr2` + `dance_1.msa` | 动画单独一个 gr2 + 文本 msa |
| 刚体静态件(对照)| `assets/Zone/ymir work/zone/oxevent/ox_01.gr2` | 无骨骼,验证 rigid 分支 |
> demo 起步**不碰 `.msm`**`warrior_m.msm` 引用 `warrior_novice.GR2` + 61 组头发 + 一堆动作,太复杂)。直接 `warrior_cheongrin.gr2` + 同名 `.dds` 是自足的 mesh+texture 对。`.msm` 解析留到 M2 多部件装配。
**资产接入方式**CMake 里配一个 `XRENDER_ASSET_ROOT` 指向 `../m2dev-client-main/assets`demo 直接 `fopen` / mmap 读,不复制。
---
## 3 · 依赖与仓库骨架
```
xrender-poc/
third_party/
bgfx.cmake/ submodule → github.com/bkaradzic/bgfx.cmake(拉 bx/bimg/bgfx
⚠ pin 到与 shaders.rar 的 .sc 草稿相近的 bgfx 版本
sokol/ 只放 sokol_app.h(单文件,手动 vendor
glm/ submodule 或单目录 vendor
cgltf/ 单头文件(gr2dump 导出 glTF 用)
reuse/
EterImageLib/ 从 ../MobileSource 冻结拷入:DXTCImage.{h,cpp} + StdAfx + 依赖的最小集
EterBase/ (拉伸)CFileBase / CMappedFile —— demo 初期可先用裸 fopen
libgr2/
include/gr2.h
src/gr2_file.cpp header + section table + fixup 重定位
src/gr2_typetree.cpp 自描述 data_type_definition 遍历器
src/gr2_fileinfo.cpp FileInfo 根对象 → skeleton/mesh/material/animation 视图
src/gr2_skeleton.cpp 骨骼数组 + bind pose 自洽检查
src/gr2_mesh.cpp 顶点/索引/三角组/BoneBindings 提取
src/gr2_anim.cpp TrackGroup → 每骨曲线;曲线子类型解码(demo 子集)
src/gr2_decompress.cpp section 解压分派
src/oodle1.c Granny Oodle1 解码(已实现,端口自泄露 SDK;9166/9166 验证通过)
engine/
rhi.h/.cpp ≈150 行 bgfx 薄封装
camera.h/.cpp 轨道相机
skinning.h/.cpp CPU LBS(拉伸)
animation.h/.cpp 曲线采样 + 世界姿势累积(拉伸)
scene.h/.cpp 把 libgr2 视图 → GPU buffer + draw item
formats/
msa.cpp (拉伸)文本 msa 解析
app/
main.cpp sokol_app 回调 + bgfx init + 输入 + demo 状态机
shaders/
varying.def.sc 从 shaders.rar 拷 + 加 a_indices/a_weight
vs_pnt.sc fs_pnt.sc 从 shaders.rar 拷改
vs_pnt_skinned.sc 新写(骨骼矩阵 uniform 数组)
texture_stage.sh 从 shaders.rar 原样拷
compile.cmake shaderc → metal + bin2c
tools/
gr2dump/main.cpp CLI:结构化 dump + glTF 导出
oracle/ Windows-only,见 PLAN §07demo 阶段可先跳,用 Blender io_scene_gr2 当参照)
cmake/
macos.cmake
CMakeLists.txt
```
---
## 4 · 构建(macOS 优先)
```bash
# 1. 拉依赖
git submodule add https://github.com/bkaradzic/bgfx.cmake third_party/bgfx.cmake
cd third_party/bgfx.cmake && git submodule update --init && cd -
# 固定 bgfx 版本(示例):cd third_party/bgfx.cmake/bgfx && git checkout <pinned-tag>
# 2. 配置 + 编
cmake -B build -DXRENDER_ASSET_ROOT=../m2dev-client-main/assets -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
# 3. 跑
./build/tools/gr2dump/gr2dump "$XRENDER_ASSET_ROOT/PC/ymir work/pc/warrior/warrior_cheongrin.gr2"
./build/app/xrender-demo
```
**`CMakeLists.txt` 要点**
- `add_subdirectory(third_party/bgfx.cmake)` → 得到 `bgfx` / `bx` / `bimg` / `shaderc` 目标。
- 着色器编译:`app/shaders/compile.cmake` 里对每个 `.sc``shaderc -p metal --platform osx`,产物过 `bin2c` 生成 `*.sc.bin.h`,作为 `xrender-demo` 的生成依赖。
- `reuse/EterImageLib` 编成静态库 `xr_eterimage``-Wno-*` 压掉老代码告警;可能要 `-DXR_STANDALONE` 剥掉 `StdAfx.h` 里的 Windows include。
- macOS`-framework Cocoa -framework Metal -framework QuartzCore``main.cpp` 编成 `.mm`sokol_app 的 macOS 后端要 ObjC)。
---
## 5 · 模块与文件清单(职责 + 估行)
### libgr2demo 子集,只读)
| 文件 | 职责 | 估行 | demo 边界 |
|---|---|---|---|
| `gr2_file.cpp` | magic`GRNFileMV_Old`+ `grn_file_header` + section 表(**已实现**9166/9166 跑通)+ fixup 重定位 | 250 | v6 / 32-bit LE |
| `gr2_decompress.cpp` + `oodle1.c` | section 解压分派 + Granny Oodle1 解码器 | 40 + 450 | **已实现**:从泄露 SDK 端口解码路径,9166/9166 展开到精确 `ExpandedDataSize`,内嵌串验证 |
| `gr2_typetree.cpp` | 按 `granny_data_type_definition`(文件内嵌,自描述)递归走类型树,把裸内存映射成可访问的字段 | 300 | 通用遍历器,不硬编码结构;只需支持 gr2 实际用到的成员类型(Real32 / Int32 / Ref / ReferenceToArray / Inline / String |
| `gr2_fileinfo.cpp` | 定位 `FileInfo` 根对象,暴露 `Skeletons[] / VertexDatas[] / TriTopologies[] / Meshes[] / Materials[] / Textures[] / Models[] / Animations[]` 的 span 视图 | 150 | — |
| `gr2_skeleton.cpp` | 骨骼数组(`Name / ParentIndex / LocalTransform(SRT) / InverseWorld4x4`);**bind pose 自洽检查**:重建 `world_bind`,验 `world_bind[i]·InverseWorld4x4[i]≈I` | 180 | — |
| `gr2_mesh.cpp` | 顶点(识别 `PNT332` / `PNT3322` / 带 `BoneWeights+BoneIndices` 的蒙皮变体)、索引、`TriGroups`(按材质分段)、`BoneBindings`mesh→skeleton 骨骼名映射) | 220 | 只支持上述三种顶点布局 |
| `gr2_anim.cpp` | `Animation → TrackGroups → TransformTracks`;每骨 position/orientation/scaleshear 曲线;曲线解码 + `Animation::sample_local(t)` | 320 | **已实现(全覆盖)**M0 实测 Metin2 曲线**全部**是 `OldCurveType`Granny 2.4`{Degree; Knots[]; Controls[]}`,无压缩变体)。degree 0(常量)/ 1(线性)/ 2(二次 B 样条)+ 四元数归一。**degree 3 全样本 0 个**,无需烘焙退路。精度待 oracle 层① 对拍 |
`include/gr2.h` 对外暴露纯 POD 视图(`gr2::Skeleton` / `gr2::Mesh` / `gr2::Animation`),不泄露内部指针 —— 上层只依赖这个头。
### reuse/EterImageLib(冻结拷入)
- `DXTCImage.{h,cpp}``LoadHeaderFromMemory` + `LoadFromMemory` + `Decompress(level, DWORD* out)` → RGBA8。demo 用 DXT3 分支。
- 依赖裁剪:把 `StdAfx.h` 换成一个最小 shim`typedef uint8_t BYTE` 等),去掉 `windows.h`
- 备选:DXT3 解码就 ~150 行,嫌 EterImageLib 依赖脏可以自己写一个 `dxt.cpp`
### engine/rhi.{h,cpp}(≈150 行)
对齐 `StateManager` 语义的 bgfx 薄封装:
```cpp
namespace rhi {
void init(void* nativeWindowHandle, int w, int h); // bgfx::PlatformData + bgfx::init
void resize(int w, int h);
void beginFrame(const glm::mat4& view, const glm::mat4& proj);
Handle createVB(const void* data, uint32_t size, const bgfx::VertexLayout&);
Handle createIB(const void* data, uint32_t size, bool i32);
Handle createTex2D(const void* rgba, uint16_t w, uint16_t h, uint16_t mips);
Handle createProgram(const uint8_t* vs, uint32_t vsLen, const uint8_t* fs, uint32_t fsLen);
void setModel(const glm::mat4&);
void setBones(const glm::mat4* mtx, uint16_t n); // uniform 数组,vs_pnt_skinned 用
void setTexture(Handle);
void setStageUniforms(const StageDesc&); // texture_stage.sh 的 uniform
void submit(Handle vb, Handle ib, Handle prog, uint64_t state);
void endFrame();
}
```
### app/main.cpp
- `sokol_app` 描述:`.window_title``.high_dpi=true`、macOS 后端;**不让 sokol_app 建 GL/Metal 设备** —— 用 `sapp_metal_get_layer()` / `sapp_macos_get_window()` 取原生 handle 传给 `rhi::init`(这就是 PLAN §03 的交接点,D2 的验收)。
- `frame_cb`:更新相机 → `rhi::beginFrame` → 遍历 scene draw items → `rhi::submit``bgfx::frame()`
- `event_cb`:鼠标拖 = 轨道、滚轮 = 缩放、键 `T/N/W/Space`
- demo 状态机:`enum { BindPose, Textured, NormalViz, Wireframe, Animating }`
### app/shaders
| 文件 | 来源 | 改动 |
|---|---|---|
| `varying.def.sc` | shaders.rar 拷 | 加 `int4 a_indices : BLENDINDICES;` `vec4 a_weight : BLENDWEIGHT;` |
| `vs_pnt.sc` / `fs_pnt.sc` | shaders.rar 拷 | `vs_pnt``a_color0``fs_pnt` 加 fog(demo 可先关);确认 `#include <bgfx_shader.sh>` 对上 pin 的 bgfx 版本 |
| `vs_pnt_skinned.sc` | 新写 | `mat4 skin = u_bones[a_indices.x]*a_weight.x + …`4 权重);其余同 `vs_pnt` |
| `texture_stage.sh` | shaders.rar 原样 | 不改;demo 里给它喂"单 stage MODULATE(TEXTURE, DIFFUSE)"的 uniform,等价于 `tex * vertexColor` |
| `fs_normalviz.sc` | 新写(10 行) | `gl_FragColor = vec4(v_normal*0.5+0.5, 1)` |
### tools/gr2dump/main.cpp
- 参数:`gr2dump <file.gr2> [--gltf out.glb]`
- stdout:骨架树(缩进)、每 mesh 的顶点/索引/三角组/顶点布局、每 animation 的时长 + track 数 + 曲线子类型直方图、bind pose 自洽检查结果(PASS/FAIL + max 偏差)。
- `--gltf`:用 cgltf 写骨架 + 第一个 mesh 的 bind pose + (若实现了)第一个 animation。**仅供 Blender 目视**,动画正确性不认它(PLAN §06 M0)。
---
## 6 · 实现顺序(每步一个可演示切片)
| 步 | 交付 | 演示点 | 验收 |
|---|---|---|---|
| **D0** | CMake 骨架编过:空 `xrender-demo` 开一个 bgfx 清屏窗口(纯色)+ `bgfx::showStats` | 窗口出现、FPS 角标在跳 | bgfx 经 sokol_app handle 起来了(**PLAN §03 交接子门禁的最小版**) |
| **D1** | `gr2_file` + `gr2_typetree` + `gr2_fileinfo` + `gr2_skeleton``gr2dump` 能打印 `warrior_cheongrin.gr2` 骨架树 + mesh 摘要 | 终端 dump 输出 | 骨骼数 / 名称 / 父索引与 Blender `io_scene_gr2` 导入一致;bind pose 自洽 PASS |
| **D2** | `gr2_mesh` + `engine/scene` + `rhi` + `vs_pnt/fs_pnt`warrior **白模绑定姿势**上屏,轨道相机 | 能转的白模 | 轮廓 = Blender 里同模型;无背面/法线翻转(配合 D4 的法线可视化确认) |
| **D3** | `reuse/EterImageLib` DXT3 解码 → `rhi::createTex2D``texture_stage.sh`;按 `T` 贴图 | 有材质的 warrior | UV 无错位、无镜像;和客户端截图目视一致 |
| **D4** | `fs_normalviz` + 线框 state`BGFX_STATE_PT_LINES``BGFX_DEBUG_WIREFRAME`);`N` / `W` 切换 | 法线彩图 / 线框 | 法线朝外;三角组分段正确 |
| **D5(拉伸)** | `gr2_anim`(子集)+ `formats/msa` + `animation` + `skinning`CPU LBS+ `vs_pnt_skinned``Space``dance_1` | 角色跳舞 | 与 Blender 导入的同一动画逐帧目视一致;无肢体飞出/顶点塌陷。数值对拍要 oracle(见 §8) |
**D0D4 = 可交付的 demo**(静态带贴图 + 自检工具)。D5 是加分项,卡住不影响 demo 成立。
---
## 7 · 关键实现细节
### 7.1 gr2 v6 文件结构(`gr2_file.cpp`
```
0x00 BYTE magic[16] // B8 67 B0 CA F8 6D B1 0F 84 72 8C 7E 5E 19 00 1E ← 认版本/字节序
0x10 u32 headerSize // 0x1B8
0x14 u32 headerFormat // 0(这不是 section 压缩!section 压缩看每段 grn_section.Format
0x18 u32 reserved[2]
--- GrannyFileHeader ---
0x20 u32 version // 6
0x24 u32 totalSize // == 文件大小,用来自检解析对齐
0x28 u32 crc32
0x2C u32 sectionArrayOffset // 0x38
0x30 u32 sectionArrayCount
0x34 u32 rootObjectTypeSection / rootObjectTypeOffset / rootObjectSection / rootObjectOffset
...
--- Section[sectionArrayCount] --- 每项 ~44 B
u32 Format // 0=none 1=Oodle0 2=Oodle1Metin2 实测全 2
u32 dataOffset, dataSize
u32 expandedDataSize
u32 alignment
u32 first16Bit / first8Bit // marshalling 边界
u32 pointerFixupArrayOffset, pointerFixupArrayCount
u32 mixedMarshallingFixupArrayOffset, mixedMarshallingFixupArrayCount
```
**流程**:读 header → 逐 section`gr2_decompress` 展开到 `expandedDataSize` 的缓冲 → 应用 pointer fixup(把文件内偏移改写成进程内指针)→ (小端机上 mixed-marshalling fixup 可跳过,big-endian 才需要)→ 得到一组可随机访问的 section 内存块。root object 在 `(rootObjectSection, rootObjectOffset)`,其类型定义在 `(rootObjectTypeSection, rootObjectTypeOffset)`
### 7.2 自描述类型树(`gr2_typetree.cpp`
`granny_data_type_definition` 是数组,每项:`{ MemberType(u32), Name(char*), ReferenceType(def*), ArrayWidth(i32), Extra[3], Ignored }`,以 `MemberType==0`End)结尾。`MemberType` 枚举含 `Inline / Reference / ReferenceToArray / ArrayOfReferences / Real32 / Int32 / UInt32 / String / Transform / …`
写一个 `walk(void* obj, const TypeDef* type, Visitor&)`:按成员类型算 stride、递归 `Reference`/`ReferenceToArray`。**不要硬编码 struct 布局** —— 不同 Granny 小版本字段顺序会变。上层 `gr2_fileinfo` 按**成员名**`"Skeletons"`, `"Meshes"` …)取字段,用 `GrannyFindMatchingMember` 式的按名查找。
### 7.3 顶点布局 → `bgfx::VertexLayout``gr2_mesh.cpp` + `scene.cpp`
gr2 mesh 的顶点类型在文件里(`GrannyGetMeshVertexType`)。demo 支持:
| gr2 顶点类型 | 成员 | bgfx layout |
|---|---|---|
| `PNT332` | Pos3f, Norm3f, UV2f | Position/Normal/TexCoord0 |
| `PNT3322` | Pos3f, Norm3f, UV2f, UV2f | + TexCoord1 |
| 蒙皮变体 | + BoneWeights(4×u8 归一) + BoneIndices(4×u8) | + Weight/Indices`bgfx::Attrib::Weight` + `Indices``AttribType::Uint8`, normalized=weight true / indices false|
`GrannyCopyMeshVertices(mesh, dstType, dstBuf)` 的效果自己实现:按源类型逐顶点拷到一个 demo 统一的打包结构,再 `bgfx::createVertexBuffer`。索引:gr2 是 u16 或 u32`GrannyCopyMeshIndices` 同理。三角组 `TriGroups``(materialIndex, triFirst, triCount)`,每组一次 `rhi::submit`
### 7.4 DXT3 → bgfx 纹理(`scene.cpp`
demo 走**运行时软解**PLAN §08:移动端无 S3TC,且要和 oracle 对齐时贴图要能预解码):
```
CDXTCImage img;
img.LoadHeaderFromMemory(ddsBytes); // 认 512x512 DXT3 5mip
img.LoadFromMemory(ddsBytes);
std::vector<uint32_t> rgba(w*h);
img.Decompress(0, rgba.data()); // level 0mip 链 demo 可先不传,让 bgfx 不采样 mip
rhi::createTex2D(rgba.data(), 512, 512, 1);
```
`bgfx::createTexture2D` + `BGFX_SAMPLER_MIN_POINT` 之类先关 mip,D3 通过后再补全 mip 链。)
### 7.5 坐标系(`scene.cpp` / `camera.cpp`
PLAN §07**约定写死**。demo 取"左手 Y-up、单位 = gr2 原始单位、根变换已 apply"。gr2 里骨架/网格是 Granny 约定(通常右手 Z-up 或文件指定的 art tool basis)。demo 先加一个固定 `basisFix`(可能是绕 X -90° + Z 翻转,M0 用朝向明确的资产标定),乘进 model 矩阵。**左右手系**bgfx 用 `bx::mtxLookAt` / `bx::mtxProj``bx::Handedness` 参数统一,和 `basisFix` 一起调到"warrior 正着站、面朝 +Z"。
### 7.6 bgfx 提交循环(`main.cpp` / `rhi.cpp`
```cpp
bgfx::setViewRect(0, 0,0, w,h);
bgfx::setViewClear(0, BGFX_CLEAR_COLOR|BGFX_CLEAR_DEPTH, 0x303030ff, 1.0f);
bgfx::setViewTransform(0, &view, &proj);
// per draw item:
bgfx::setTransform(&model);
bgfx::setVertexBuffer(0, vb);
bgfx::setIndexBuffer(ib);
bgfx::setTexture(0, s_texColor, tex);
bgfx::setUniform(u_stageColor, &stage, 1);
if (skinned) bgfx::setUniform(u_bones, bones, boneCount);
bgfx::setState(BGFX_STATE_WRITE_RGB|BGFX_STATE_WRITE_A|BGFX_STATE_WRITE_Z
|BGFX_STATE_DEPTH_TEST_LESS|BGFX_STATE_CULL_CW
|BGFX_STATE_MSAA);
bgfx::submit(0, prog);
// end:
bgfx::frame();
```
深度范围 / Y 翻转:用 `bgfx::getCaps()->homogeneousDepth``originBottomLeft` 决定 `bx::mtxProj` 参数,别手写。
### 7.7 sokol_app ↔ bgfx 交接(`main.cpp`D0 的验收)
```cpp
// sokol_app 里关掉它自己的渲染循环意图,只要窗口 + 事件
bgfx::PlatformData pd{};
pd.nwh = sapp_macos_get_window(); // NSWindow*
pd.ndt = nullptr;
// Metal: 也可以 pd.nwh = (__bridge void*)sapp_metal_get_layer(); // CAMetalLayer*
bgfx::Init init;
init.type = bgfx::RendererType::Metal;
init.platformData = pd;
init.resolution.width = sapp_width();
init.resolution.height = sapp_height();
bgfx::init(init);
```
若这条在 macOS 上出不了画面(sokol_app 和 bgfx 抢 layer)——按 PLAN §03 退路:`app/` 换成 SDL2 窗口,`SDL_GetWindowWMInfo``NSWindow*`,其余不变。**D0 就是来验证这个的**,别拖。
---
## 8 · demo 阶段相关的坑(从 PLAN §08 摘)
| 坑 | 在 demo 里的表现 | demo 阶段怎么办 |
|---|---|---|
| sokol_app + bgfx 抢 context | D0 黑屏 / 崩 | D0 卡死就切 SDL2,别硬啃 |
| `.sc` 草稿针对某 bgfx 版本 | shaderc 编不过 / uniform 名对不上 | submodule pin 到相近版本;编不过就照 `bgfx_shader.sh` 手改宏 |
| DXT 软解 vs 客户端 GPU S3TC | D3 贴图和客户端截图有细微色差 | demo 目视够了;要数值对拍时两侧都用软解 RGBA |
| 坐标系 basis | D2 warrior 躺着 / 镜像 / 巨大 | 用朝向明确的资产手调 `basisFix`,记进 `scene.cpp` 注释 |
| ~~曲线子类型超出实现~~ | — | **已消除**M0 实测全 `OldCurveType`degree ≤ 2`gr2_anim.cpp` 全覆盖 |
| `InverseWorld4x4` 读错但一致 | D2 白模看着对,D5 蒙皮炸 | **已兜住**M0 self-check 全量 9166 跑过,`warrior_cheongrin` 6.1e-5 PASS |
| root 偏移放法两种(`InitialPlacement` vs 烘进 root local | 世界姿势整体偏移 ~100 单位 | **已处理**`world[root] = Composite(local[root]) · model.InitialPlacement`M0 gr2_fileinfo 已关联 model→skeleton|
| mediump 精度(移动端才有) | macOS demo 无此问题 | 到 iOS demo 再管,骨骼矩阵用 highp |
---
## 9 · 怎么跑 / 演示检查点
```bash
export XRENDER_ASSET_ROOT="$PWD/../m2dev-client-main/assets"
cmake -B build -DXRENDER_ASSET_ROOT="$XRENDER_ASSET_ROOT" && cmake --build build -j
# 结构(D1
./build/tools/gr2dump/gr2dump "$XRENDER_ASSET_ROOT/PC/ymir work/pc/warrior/warrior_cheongrin.gr2" --gltf /tmp/warrior.glb
# → 期望:骨架树打印、mesh 摘要、"bind pose self-check: PASS (max 3.1e-6)"
# → 把 /tmp/warrior.glb 拖进 Blender,骨架和 T-pose 网格应正常
# 渲染(D2D4
./build/app/xrender-demo
# 拖拽转视角;T 贴图;N 法线;W 线框;Space 播 dance_1D5
```
**演示成立的判据**D0D4 全绿 = "bgfx 能从 assets 的散装 gr2 + dds 渲出正确的带贴图 warrior,且有自检工具" —— 这就把 PLAN 的 M1 用一个能给人看的东西证了。D5 绿 = M2 的核心(曲线采样 + LBS)也站得住。
**下一步**demo 之后):接 oracleWindows dump)做 D5 的数值对拍 → 换 iOS/Android toolchain 跑 D2D4 → 按 PLAN M3 补性能采集。