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

21 KiB
Raw Permalink Blame History

Demo 开发方案 —— bgfx 渲染 assets 下的 gr2 资源

本文件把 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.ddsDXT3 软解),材质正确 DXT 解码 + texture-stage + UV 正确
4 N 切法线可视化、按 W 切线框 自检工具,抓法线翻转 / 拓扑错
5(拉伸) Spaceaction/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 sectionsection 全 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 起步不碰 .msmwarrior_m.msm 引用 warrior_novice.GR2 + 61 组头发 + 一堆动作,太复杂)。直接 warrior_cheongrin.gr2 + 同名 .dds 是自足的 mesh+texture 对。.msm 解析留到 M2 多部件装配。

资产接入方式CMake 里配一个 XRENDER_ASSET_ROOT 指向 ../m2dev-client-main/assetsdemo 直接 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 优先)

# 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 里对每个 .scshaderc -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 QuartzCoremain.cpp 编成 .mmsokol_app 的 macOS 后端要 ObjC)。

5 · 模块与文件清单(职责 + 估行)

libgr2demo 子集,只读)

文件 职责 估行 demo 边界
gr2_file.cpp magicGRNFileMV_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(按材质分段)、BoneBindingsmesh→skeleton 骨骼名映射) 220 只支持上述三种顶点布局
gr2_anim.cpp Animation → TrackGroups → TransformTracks;每骨 position/orientation/scaleshear 曲线;曲线解码 + Animation::sample_local(t) 320 已实现(全覆盖)M0 实测 Metin2 曲线全部OldCurveTypeGranny 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 换成一个最小 shimtypedef uint8_t BYTE 等),去掉 windows.h
  • 备选:DXT3 解码就 ~150 行,嫌 EterImageLib 依赖脏可以自己写一个 dxt.cpp

engine/rhi.{h,cpp}(≈150 行)

对齐 StateManager 语义的 bgfx 薄封装:

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::submitbgfx::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_pnta_color0fs_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_skeletongr2dump 能打印 warrior_cheongrin.gr2 骨架树 + mesh 摘要 终端 dump 输出 骨骼数 / 名称 / 父索引与 Blender io_scene_gr2 导入一致;bind pose 自洽 PASS
D2 gr2_mesh + engine/scene + rhi + vs_pnt/fs_pntwarrior 白模绑定姿势上屏,轨道相机 能转的白模 轮廓 = Blender 里同模型;无背面/法线翻转(配合 D4 的法线可视化确认)
D3 reuse/EterImageLib DXT3 解码 → rhi::createTex2Dtexture_stage.sh;按 T 贴图 有材质的 warrior UV 无错位、无镜像;和客户端截图目视一致
D4 fs_normalviz + 线框 stateBGFX_STATE_PT_LINESBGFX_DEBUG_WIREFRAME);N / W 切换 法线彩图 / 线框 法线朝外;三角组分段正确
D5(拉伸) gr2_anim(子集)+ formats/msa + animation + skinningCPU LBS+ vs_pnt_skinnedSpacedance_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 → 逐 sectiongr2_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==0End)结尾。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::VertexLayoutgr2_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/Indicesbgfx::Attrib::Weight + IndicesAttribType::Uint8, normalized=weight true / indices false

GrannyCopyMeshVertices(mesh, dstType, dstBuf) 的效果自己实现:按源类型逐顶点拷到一个 demo 统一的打包结构,再 bgfx::createVertexBuffer。索引:gr2 是 u16 或 u32GrannyCopyMeshIndices 同理。三角组 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::mtxProjbx::Handedness 参数统一,和 basisFix 一起调到"warrior 正着站、面朝 +Z"。

7.6 bgfx 提交循环(main.cpp / rhi.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()->homogeneousDepthoriginBottomLeft 决定 bx::mtxProj 参数,别手写。

7.7 sokol_app ↔ bgfx 交接(main.cppD0 的验收)

// 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_GetWindowWMInfoNSWindow*,其余不变。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 实测全 OldCurveTypedegree ≤ 2gr2_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.InitialPlacementM0 gr2_fileinfo 已关联 model→skeleton
mediump 精度(移动端才有) macOS demo 无此问题 到 iOS demo 再管,骨骼矩阵用 highp

9 · 怎么跑 / 演示检查点

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 补性能采集。