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
This commit is contained in:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+39 -34
View File
@@ -1,15 +1,20 @@
# Godot 渲染 Demo — 开发计划
> 内部研究方案,不对外公开。
> 与 [`PLAN.md`](./PLAN.md) 是**两条并行的跨平台基座候选**:本文件评估「Godot 4 + 自研资源 loader」,`PLAN.md` 评估「自研引擎 + bgfx RHI」。
> 复用 [`../libgr2`](../libgr2)、[`../formats`](../formats) 与 [`../oracle`](../oracle) 的真值数据。
>
> **2026-08-29 更新**:并行的 bgfx 自研引擎方案(`xrender-poc` / [`reference/PLAN.md`](./reference/PLAN.md)
> 经中期评审([`MIDREVIEW.md`](./MIDREVIEW.md)**停止开发**,本方案成为唯一在研路线。
> `libgr2` / `formats` / `oracle` / `tools` 已从 xrender-poc **vendored 进本仓库**
> `../libgr2`、`../formats`、`../oracle`、`../tools`),不再是 submodule / sibling 引用。
> 下文中出现的 `xrender-poc/xxx` 路径按 [`reference/README.md`](./reference/README.md) 的映射表读。
> bgfx demo 截图作为交叉核对基线留在 `../test/bgfx-reference/`。
| | |
|---|---|
| 状态 | PoC 规划 |
| 技术栈 | Godot **4.7.1** stable · GDExtension`godot-cpp` `4.7` 分支)· C++20 |
| 目标平台 | **Phase 1macOS**(当前)→ Phase 2iOS · Android |
| 判定周期 | Phase 1 ≈ 3.5–5 周(单人)→ **中期评审**(不下结论)Phase 2 后 → **go / no-go** |
| 状态 | Phase 1macOS)基本完成,见 [`MIDREVIEW.md`](./MIDREVIEW.md);未完成工作分层清单见 [`BACKLOG.md`](./BACKLOG.md) |
| 技术栈 | Godot **4.7.1** stable · GDExtension`godot-cpp` 精确提交 `101ae38`4.7 API)· C++20 |
| 目标平台 | **Phase 1macOS**(当前)→ Phase 2Android(一加 13 / Vulkan)· iOSiPhone 16 / Metal)。自用,三台现代设备,不涉及 GLES3 / Compatibility |
| 判定周期 | Phase 1 ≈ 3.5–5 周(单人)→ **中期评审**Phase 2 = 三设备各一次 bring-up + 性能留档(自用,无对外 go/no-go) |
| 复用 | `libgr2`gr2 v6 读取器)· `formats/`textscript · msa · msm)· `oracle/` golden · bgfx demo 截图 |
| 自研 | `Metin2Model` GDExtension 节点(gr2 → `Skeleton3D` + `ArrayMesh` + 材质) |
@@ -20,11 +25,11 @@
| 阶段 | 平台 | 里程碑 | 回答什么 | 产出 |
|---|---|---|---|---|
| **Phase 1** | macOS | M0' · M1 · M2 · **M2.5** | ① gr2 → Godot 场景对象能否正确映射;② 动画运行时(采样 / retarget / 事件)能否移植;③ **Metin2 观感能否在 Godot 里还原** | 中期评审报告:桥接层工作量实测 + 观感对比 + 剩余风险 |
| **Phase 2** | iOS · Android | M3(·M4 可选) | ④ 跨平台三端一致;⑤ 移动端性能 / 生命周期 | **go / no-go 结论** |
| **Phase 2** | Android(一加 13)· iOSiPhone 16 | M3(·M4 可选) | ④ 三端渲染一致;⑤ 两台移动设备性能 / 生命周期 | 三设备 bring-up 报告 + 性能留档 |
**关键澄清(写给评审)**Godot 在 macOS 上渲染一个蒙皮 gr2 **几乎不可能失败**。Phase 1 不是用来判断"该不该押 Godot"的——那个只有 Phase 2 能答。Phase 1 的价值是把**平台无关**的 GDExtension 工程(占本方案工程量大头)做完并确认视觉保真度可达,从而让 Phase 2 只剩"移动端 + 性能"这一个真未知
**关键澄清**Godot 在 macOS 上渲染一个蒙皮 gr2 **几乎不可能失败**Phase 1 的价值是把**平台无关**的 GDExtension 工程(占工程量大头)做完并确认视觉保真度可达。bgfx 自研引擎方案已于 2026-08-29 停止开发,Godot 是唯一在研路线;本项目自用、不对外发布,**没有"押哪条路线"的产品决策**。Phase 2 只剩"三台目标设备(Mac / 一加 13 / iPhone 16)上能不能跑 + 性能够不够自己用"
**进入 Phase 2 的前置**M2.5 门禁通过,且中期评审判定"桥接层工作量与观感可接受"。否则在此转回 bgfx route,损失仅为 GDExtension 桥接层(≈ glTF importer 量级),`libgr2` 完整保留
**进入 Phase 2 的前置**M2.5 门禁通过中期评审确认桥接层与观感可接受即可推进。三台目标设备都是现代硬件,全程不涉及 GLES3 / Compatibility 渲染器,不做中低端 / 老机器
---
@@ -38,8 +43,8 @@
### Phase 2 要证明的两件事(移动端,本轮不做)
4. **跨平台一致** — 同一 GDExtension + Godot 工程在 iOS 真机 + Android 真机(Vulkan + Compatibility)渲染与桌面一致。
5. **性能 / 生命周期达标**中端机单角色 / 多角色帧率达标,后台 / 恢复稳,Godot 节点开销可控。
4. **三端一致** — 同一 GDExtension + Godot 工程在 iPhone 16Metal+ 一加 13Vulkan)渲染与 Mac 一致。
5. **性能 / 生命周期够自用**两台设备单角色 / 多角色帧率可接受(旗舰基线,非发布门槛),后台 / 恢复稳,Godot 节点开销可控。
### 明确排除(两个阶段都不做)
@@ -49,13 +54,11 @@ SpeedTree 植被、地形(Metin2 户外地图)、UI / Python 脚本层、ete
### 判定口径
- **M2.5 门禁通过 + 中期评审"可接受"** = 进 Phase 2。
- **门禁失败区分两类**(同 `PLAN.md` §01 口径):
- **工程性延期** — 材质差色、某曲线子类型没实现、某 API 用错。有明确修法,记下继续
- **方案性死路** — 例如 Godot 节点 / 骨骼模型无法表达 Metin2 的多部件挂点体系、或多 stage 固定管线在 `ShaderMaterial` 里无法近似到可接受、或每帧写骨骼在合理规模下 Mac 上就已经卡。这才触发"转回 bgfx route"。
- **最终交付(Phase 2 末)** 一页纸对比结论:与 bgfx route 在 **工作量 / 性能 / 视觉保真度 / 长期维护成本** 四轴上的取舍。
- **M2.5 门禁通过 + 中期评审"可接受"** = 进 Phase 2。中期评审已判定桥接层工作量与观感均可接受([`MIDREVIEW.md`](./MIDREVIEW.md));bgfx route 已停,无「转回」分支。
- **Phase 2 若撞上方案性死路**(自写蒙皮着色器在 iPhone 16 / 一加 13 上跑不出、Godot 节点开销在自用规模下就卡)——记录清楚、就地想办法(回落 CPU LBS / `RenderingServer` 直调 / uniform 传骨骼),不再有另一条引擎路线兜底。
- **Phase 2 末交付**:三设备 bring-up + 性能留档报告(自用参考,不做对外选型结论)
### 与 bgfx route 的关系
### 与 bgfx route 的关系(历史对照,bgfx route 已于 2026-08-29 停止开发)
| | bgfx route`PLAN.md` | Godot route(本文件) |
|---|---|---|
@@ -82,8 +85,7 @@ mtgodot-poc/
gr2_bridge.{h,cpp} libgr2 POD 视图 → Godot 数组打包;basis + 单位换算(集中一处)
dds_loader.{h,cpp} DDS → Godot Image(散文件路径用;res:// 里的 .dds 交 Godot 原生导入)
m2_material.{h,cpp} texture-stage 描述 → ShaderMaterialM2.5
libgr2/ submodule → ../xrender-poc/libgr2
formats/ submodule → ../xrender-poc/formatsmsa/msm
(libgr2 / formats 在仓库根,顶层 CMake add_subdirectory 进来)
SConstruct / CMakeLists godot-cpp 构建(Phase 1 只出 macOS arm64
project/ Godot 4.7.1 demo 工程
main.tscn orbit 相机 + DirectionalLight3D(+shadow) + WorldEnvironment(sky) + Metin2Model
@@ -93,7 +95,7 @@ mtgodot-poc/
export_presets.cfg macOSPhase 1
test/
golden/ godot-macos-*.png
compare.py 与 ../xrender-poc bgfx demo / oracle 对拍
compare.py 与 test/bgfx-reference/ 参考截图 + oracle 对拍
docs/
GODOT-POC-PLAN.md 本文件
steps/ 按需拆
@@ -122,16 +124,16 @@ mtgodot-poc/
| 项 | 选择 | 理由 |
|---|---|---|
| 引擎版本 | **Godot 4.7.1 stable**(本机已装),锁死不随手升 | GDExtension ABI 绑 minor;升级作独立任务 |
| 扩展机制 | `godot-cpp` **`4.7` 分支**GDExtension(非模块编译) | 用官方导出模板,不自定义 Godot 构建 |
| 扩展机制 | `godot-cpp` **`101ae38034304346a46ea9ea84ae156d3e860496`**GDExtension(非模块编译) | gitlink 精确锁定,目标 API 4.7;升级作独立任务 |
| 构建工具 | `godot-cpp`**CMake 路径**(本机已有 cmake;不装 SCons)或 `brew install scons` 二选一 | Phase 1 只出 macOS arm64,摩擦最小 |
| gr2 解析 | `xrender-poc/libgr2` 静态库嵌入 extension | 与 bgfx route 同一份,已过 9166 fuzz + oracle |
| 动画方案 | **B(每帧写骨骼)**A(烘 `Animation`**决策推迟到 Phase 2** | B 直接复用 `libgr2` 采样、最保真;A 的动机(多角色 CPU)在 Mac 上压不出来 |
| 骨骼权重 | `ArrayMesh` 4 权重优先;>4 影响则 `ARRAY_FLAG_USE_8_BONE_WEIGHTS` | M1 T1.2 早验,三种骨架都查 |
| 贴图 | 见 §02 实操约定(`.dds` 原生导入 vs 运行时 `dds_loader`T1.3 定) | 两条都留 |
| 材质 | **自定义 `ShaderMaterial`**`light()` 自定义着色贴 Metin2 观感);M1 先 `StandardMaterial3D` 上屏,M2.5 换 | 多 stage 只做近似,列差距清单 |
| 渲染器 | Forward+macOS | Phase 1 不涉及 Compatibility |
| 渲染器 | Forward+ / Mobile renderer | 三台目标设备(Mac / 一加 13 / iPhone 16)都是现代硬件,全程不涉及 Compatibility(GLES3) |
| 阴影 / sky | Godot 内置 `DirectionalLight3D` shadow + `WorldEnvironment` procedural skyM2.5 接入 | 成本近零,Mac 观感必要 |
| 资产来源 | 从 `m2dev-client-main/assets` **warrior + 另 2 种骨架** 的 gr2/dds 到 `project/assets/`,散文件 | 不接 eterpackPhase 2 / M4 |
| 资产来源 | 从 `assets/` **warrior + 另 2 种骨架** 的 gr2/dds 到 `project/assets/`,散文件 | 不接 eterpackPhase 2 / M4 |
| 真值源 | `xrender-poc/oracle` golden + bgfx demo 同机位 / pose / t 截图 | 复用现成对拍基建 |
| 仓库 | 新建 `mtgodot-poc/``libgr2` / `formats` submodule 指向 `xrender-poc` | 隔离,不污染 bgfx route |
@@ -210,9 +212,9 @@ mtgodot-poc/
---
### M3 ·(Phase 2,本轮不做)iOS + Android + 性能
### M3 ·(Phase 2,本轮不做)Android + iOS bring-up + 性能
移动端真机、三端一致、性能、生命周期、Compatibility 渲染器、每帧写骨骼的 A/B 决策 —— **go / no-go 结论在此产生**。详见后续 `steps/M3-mobile.md`(届时再写)。
一加 13Vulkan+ iPhone 16Metal)真机跑起来、三端一致、性能留档、生命周期。iOS 需把 GDExtension 按 arm64 **静态库**编出来(链进 app 二进制)。不涉及 Compatibility 渲染器。详见后续 `steps/M3-mobile.md`(届时再写)。
### M4 ·(Phase 2 可选拉伸)
@@ -244,7 +246,7 @@ mtgodot-poc/
| `.gr2` 被 Godot import 流程扫到报错 | 打开工程时一堆 import 错误 | `project/assets/.gdignore``.gr2` 走运行时绝对路径 / `user://`T1 前定死 |
| gr2 basis / 单位换算到 Godot(左手 → 右手 Y-up + 缩放) | M1 模型躺 / 镜像 / 骨骼反向 / 大小离谱 | `gr2_bridge` 一次性处理,定 `M2_TO_GODOT`,用朝向明确资产手校,写注释 |
| `Skeleton3D` 骨骼数上限 / 8 权重路径行为 | M1 T1.2 蒙皮爆开 / 部位错位 | 三种骨架都查 `gr2dump`>4 权重走 8-bone flag |
| **多 stage 固定管线材质无法在 `ShaderMaterial` 近似到可接受** | M2.5 T2.5.2/3 关键装备观感明显错 | **这是 Phase 1 主要方案性风险**;差距清单区分"可延期"vs"死路",后者触发转回 bgfx route |
| **多 stage 固定管线材质无法在 `ShaderMaterial` 近似到可接受** | M2.5 T2.5.2/3 关键装备观感明显错 | **这是 Phase 1 主要方案性风险**;差距清单区分"可延期"vs"死路"。中期评审已判定为可延期(观感打磨,见 `MIDREVIEW.md`);bgfx route 已停,无「转回」分支 |
| 半透明 / additive 排序 corner case | M2.5 T2.5.3 发光部件穿插 | 记入差距清单;Godot 的 `render_priority` / `depth_draw` 调,不追求 100% |
| motion event 语义(`.msa`/`.msm` 帧事件类型) | M2 T2.3 事件类型没覆盖全 | Phase 1 只需打点回调,实际特效 Phase 2;未覆盖类型列清单 |
@@ -252,11 +254,11 @@ mtgodot-poc/
| 风险 | 缓解 / 退路 |
|---|---|
| 每帧 GDExtension 写骨骼的 CPU 成本(多角色) | M2 T2.7 已留档;转 A 方案(烘 `Animation` + `AnimationMixer` |
| Godot Compatibility(GLES3) 老安卓 shader 兼容 | M2.5 的 `ShaderMaterial` 写保守,避开 Compatibility 不支持特性 |
| 每帧 CPU LBS 全量重建 mesh 的成本(多角色) | `MTGODOT_GPUSKIN=1` 自写蒙皮着色器(已实现);多线程构建 + eterpack 共享资产 |
| 自写蒙皮着色器(`texelFetch` + RGBAF + `BONE_INDICES`)在 iPhone 16 Metal / 一加 13 Vulkan 上的行为 | Phase 2 各跑一次;出问题回落 CPU LBS 或改 uniform 数组传骨骼。三台都是现代设备,无 GLES3 约束 |
| Godot 节点开销(大场景规模) | `MultiMesh` / `RenderingServer` 直调兜底 |
| GDExtension ABI 绑 Godot minor | 锁 4.7.1,升级作独立任务 |
| iOS 交叉编译 `libgr2` 静态库 | 提前出 iOS `.a` 预编译产物 |
| iOSGDExtension 须静态链接进 app 二进制(`libgr2` 出 iOS arm64 `.a`),签名 / 模板脾气大 | 自用免费 provisioning 够用;预留 23 天 bring-up |
---
@@ -286,25 +288,28 @@ mtgodot-poc/
| 产物 |
|---|
| iOS `.app` + Android APK/AAB + 真机性能 / 生命周期报告 |
| **go/no-go 对比结论(一页纸)**:与 bgfx route 在工作量 / 性能 / 视觉保真度 / 长期维护四轴对比,给出客户端跨平台基座选型建议 |
| iOS `.ipa`iPhone 16+ Android APK(一加 13+ 两台真机性能 / 生命周期报告 |
| 三设备 bring-up 报告:三端渲染一致性截图 + 各设备帧率留档(自用参考,不做对外选型结论) |
---
## 选型变更记录
- 2026-08-29 · **设备范围定死(自用)**:目标 = MacmacOS / Metal+ 一加 13Android arm64 / Vulkan+ iPhone 16iOS arm64 / Metal),各一台。取消对外 go/no-go 门禁(bgfx route 已停、项目自用);Phase 2 收敛为「三设备各一次 bring-up + 性能留档」。**全程不涉及 GLES3 / Compatibility 渲染器**,不做中低端 / 老机器。SHINSOO 的 W0→W1+ 因此无外部门禁。
- 2026-08-29 · 范围收敛为 **Phase 1 = macOS only**;新增 **M2.5 材质与观感保真** 作为 Phase 1 核心门禁;动画 A/B 决策推迟到 Phase 2Godot 版本钉死 4.7.1 / godot-cpp 4.7。
- 2026-08-29 · **M1 T1.3**:贴图走 **运行时 `dds_loader`**`src/dxt.cpp`,端口自 xrender-poc)。理由:Godot 4 的 `Image.load()` 不支持 `.dds`,且资产从 `res://` 外的绝对路径加载(`.gr2``.gdignore` 同理)。DXT1/3/5 + BGRA8level 0 + `generate_mipmaps()`
- 2026-08-29 · **动画方案**确认走 **B 方案** —— `_process``gr2::sample_pose``Skeleton3D.set_bone_global_pose(i, gr2_to_godot(world[i]))`Godot GPU 蒙皮 `skin_matrix = global_pose(i) * bind_pose(i)` 恒等于 gr2 deformer 矩阵(`gr2_bridge.h` 有推导)。A 方案(烘 `Animation`)仍推迟到 Phase 2 按多角色 CPU 采样决定
- 2026-08-29 · **动画方案**B 方案(每帧 `gr2::sample_pose` 驱动)。**两条蒙皮路径**,都用 libgr2 的 `skin` 矩阵完整仿射(shear 保留):默认 **CPU LBS**`metin2_anim.cpp``Metin2Model::cpu_skin`,每帧重建 `ArrayMesh`);`MTGODOT_GPUSKIN=1`**自写蒙皮顶点着色器**`m2_material` `SRC_SKIN`,逐骨 4×3 矩阵存 RGBAF 纹理,`Metin2Model::gpu_skin` 每帧更新,绕过 `Skeleton3D`)。原因:Godot 内置 GPU 蒙皮 / `set_bone_pose` 会正交化骨骼矩阵、**丢掉 Granny 烘在 rig 骨(`bone_front_*`/`Bone_shoulder_*`warrior 7 / sura 16)上的 shear**`tools/anim_probe` 定位,非 libgr2 —— `world[]` 对拍 Granny ≤4.1e-5)。旧 `Skeleton3D::set_bone_pose` 路径已删。A 方案(烘 `Animation`)走 `Skeleton3D` 骨骼、同样丢 shear,不用
- 2026-08-29 · **坐标/单位**`gr2_to_godot()` = gr2 行主序 Mat4 的 4×4 转置 → Godot `Transform3D`Z-up cm → Y-up m`rotate(-90°,X)` + `scale(0.01)`)放在 `Metin2Model` 节点自身 transform`make_conv`)。warrior/warrior_lord/assassin/shaman_lord 实测 `flip_z=false``flip_winding=false``unit_scale=0.01` 正立、朝 +Z、无镜像。
### M2.5 T2.5.6 — 材质无法 1:1 还原 / 待办项清单
> **不再单独维护** —— 全部并入 [`BACKLOG.md`](./BACKLOG.md) B 段(`B1``B11`)。下表为历史快照。
| 项 | 现状 | 影响 | 出路 |
|---|---|---|---|
| **libgr2 POD API 不暴露材质数据** —— 只有 `material_count`/`texture_count`,无材质名、无贴图文件名、无 texture-stage op、无 `D3DBLEND_*`/alpha-test 标志 | 贴图靠**文件名约定 + 目录大小写不敏感扫描**(`_resolve_texture`);blend 靠**表面名关键字启发式**(`guess_blend`hair→alpha-test、cape/skirt→alpha、effect/glow→add,其余 opaque | warrior 5/5、assassin 17/17、shaman 1/1 表面命中贴图,但**多材质槽 → 具体贴图/混合的精确映射做不到**;换皮/多贴图变体、精确 alpha-test 阈值、MODULATE2X/ADDSIGNED 等 stage op 无法复现 | **需要给 libgr2 加材质 API**(新文件 `gr2_material.cpp`,走类型树取 `Materials[].Name` + `Maps[].Texture.FromFileName` + shader/blend hint)。这也正是 xrender-poc M1 T5「多 stage 材质对拍待 oracle」的同一缺口,两条 route 共用修法。 |
| **顶点色** | `gr2::Vertex` POD 无 color 字段;shader 已留 `COLOR` 通道,默认白 | Metin2 PC 模型基本不用顶点色,影响小;特效/地形会用 | libgr2 若暴露则 `build_mesh``ARRAY_COLOR` |
| **`TriGroup.material_index` 未接线** | libgr2 已解析每个三角组的 material_index,但 `build_mesh` 目前一个 gr2 mesh = 一个 surface(未按 material_index 再拆分) | 单 mesh 内多材质的模型会用同一张贴图 | `build_mesh``tri_groups` 的 material_index 分 surface(有材质 API 后一起做) |
| ~~`TriGroup.material_index` 未接线~~ **已做(2026-08-29** | `gr2_bridge::build_parts()` 一个 gr2 mesh `tri_groups` 拆成 N 个 `RenderPart` → N 个 Godot surface,贴图取 `Mesh::material_textures[matidx]`mesh-local,非全局 `Materials[]`)。GPU 和 CPU 蒙皮路径都走 `RenderPart`。 | — |
| **ShaderMaterial 静态 `Shader` 退出泄漏** | `shader_for()` 用 function-static `Ref<Shader>`Godot 退出时报 "1 shader never freed" | 仅退出时一条 warning,无功能影响 | 改成每材质自持 Shader,或注册 cleanup |
| **alpha 排序 corner case** | 半透明/additive 用 `render_priority` 粗排,未做逐三角/OIT | 多层半透明部件可能穿插 | 记录,不追求 100%Phase 2 M2.5 打磨) |
| **法线未做逆转置** | 走 Godot GPU 蒙皮,其法线处理与 Granny runtime 的「上 3×3 不逆转置」近似不完全一致 | 非均匀缩放骨骼下法线略有差异;warrior 类资产无明显问题 | Phase 2 逐帧对拍时量化 |
| **法线变换** | CPU 与 GPU`SRC_SKIN` 着色器)两条路径都用混合矩阵的**上 3×3、不逆转置** —— 与 Granny runtime 一致(见 `docs/reference/steps/M2-anim-skinning.md`)。非均匀缩放骨骼下这是 Granny 自己的近似,我们与之一致。 | 已一致 | — |