# Demo 开发方案 —— bgfx 渲染 assets 下的 gr2 资源 > 本文件把 [`PLAN.md`](./PLAN.md) 的 M0 + M1(+ M2 拉伸目标)落成一个**可演示的 demo** 的文件级施工图。 > demo 只做 macOS(Metal),跑通后 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、多部件装配、eterpack(demo 用散装文件)、iOS/Android(同代码后续换 toolchain)。 --- ## 2 · 演示用资产(具体文件,散装,无需解包) | 用途 | 路径(相对 `m2dev-client-main/`) | 事实 | |---|---|---| | 主模型 | `assets/PC/ymir work/pc/warrior/warrior_cheongrin.gr2` | 92 770 B,gr2 格式 v6,8 section,**section 全 Oodle1 压缩**(实测)| | 主贴图 | `assets/PC/ymir work/pc/warrior/warrior_cheongrin.dds` | **512×512 DXT3,5 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 §07(demo 阶段可先跳,用 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 # 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 · 模块与文件清单(职责 + 估行) ### libgr2(demo 子集,只读) | 文件 | 职责 | 估行 | 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 ` 对上 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 [--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) | **D0–D4 = 可交付的 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=Oodle1(Metin2 实测全 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 rgba(w*h); img.Decompress(0, rgba.data()); // level 0;mip 链 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 网格应正常 # 渲染(D2–D4) ./build/app/xrender-demo # 拖拽转视角;T 贴图;N 法线;W 线框;Space 播 dance_1(D5) ``` **演示成立的判据**:D0–D4 全绿 = "bgfx 能从 assets 的散装 gr2 + dds 渲出正确的带贴图 warrior,且有自检工具" —— 这就把 PLAN 的 M1 用一个能给人看的东西证了。D5 绿 = M2 的核心(曲线采样 + LBS)也站得住。 **下一步**(demo 之后):接 oracle(Windows dump)做 D5 的数值对拍 → 换 iOS/Android toolchain 跑 D2–D4 → 按 PLAN M3 补性能采集。