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
10 KiB
10 KiB
M2 · 骨骼动画 + CPU 蒙皮
总纲:
../PLAN.md§04 / §06 / §07。演示切片见../DEMO-PLAN.mdD5(拉伸)。 本文件是 M2 的详细施工文档。
目标
解析 .msm / .msa,把 M0 读出的动画曲线采样成世界姿势,CPU 线性混合蒙皮,
让 warrior 播 idle / walk / dance,结果与 oracle 的**层①(裸 Granny)**逐帧数值一致。
装配一个多部件角色 + 挂点武器,验 LOD 一致性。
前置
- M1 完成:静态网格 + 贴图上屏。
- M0 T7(
gr2_anim子集)+ scaleshear 用量已知。 - oracle T3(层① dump)+ T5(蒙皮顶点 dump)+ T8(噪声地板 → ε)。
交付物
| 产物 | 位置 |
|---|---|
engine/animation.{h,cpp} 曲线采样 + 世界姿势累积 |
engine/ |
engine/skinning.{h,cpp} CPU LBS |
engine/ |
formats/msa.cpp(+ 视需要 msm.cpp) |
formats/ |
app/shaders/vs_pnt_skinned.sc |
app/shaders/ |
tools/anim_bake/(曲线退路,视 M0 结论决定是否要) |
tools/ |
| 数值对拍报告 | test/m2-numeric.json |
任务分解
T1 · formats/msa + msm 解析 + 验证门
.msa(文本):引用的动画.gr2、混合参数(blend time / ease)、事件。逻辑照RaceManager.cpp/EterGrnLib/Util.cpp。.msm(文本,仅多部件时需要):base model gr2、挂点、材质类型。- 验证门(PLAN §04 要求):dump 解析结果(挂点名、动作列表、混合参数),和源文本逐项目视核对。
- 完成判据:
dance_1.msa解析出的动画路径 + blend 参数与文本一致。
T2 · engine/animation —— 曲线采样 + 世界姿势累积
- 按局部时钟
t对每骨 position / orientation / scaleshear 曲线插值 → 局部granny_transform→ 沿父链累积 →world[bone]。 - 对应
GrannyBuildWorldPose/GrannyGetWorldPoseComposite4x4Array。 - ease-in/out 曲线、loop count、raw local clock 语义:照
EterGrnLib/Motion*+ModelInstanceMotion.cpp调用序列复刻。 - 非单位 scaleshear:M0 若报告普遍存在,
world[bone]要保留完整仿射(4×4 或 4×3),不能退化成刚体。 - 完成判据:
gr2_anim子集覆盖的动画能采样出每帧世界矩阵。
T3 · engine/skinning —— CPU LBS
skinMatrix[bone] = world[bone] · InverseWorld4x4[bone]。- 逐顶点:
v' = Σ weight[i] · skinMatrix[boneIndex[i]] · v(4 权重),法线用skinMatrix的 3×3(scaleshear 时要用逆转置)。 GrannyMeshIsRigid为真的网格不蒙皮,只按挂载骨骼刚体变换 —— 分支照搬。- 完成判据:bind pose(identity 动画)下 CPU 蒙皮结果 == 原始顶点。
T4 · vs_pnt_skinned.sc(GPU 版,D5 演示用)
mat4 skin = u_bones[a_indices.x]*a_weight.x + u_bones[a_indices.y]*a_weight.y + ...。- CPU 端
rhi::setBones(skinMatrix[], boneCount)上传 uniform 数组。 - 注意:M2 的数值门禁走 CPU 蒙皮(可 dump 顶点对拍);GPU shader 只是演示。CPU/GPU 一致性放 M4。
T5 · 多部件装配 + 挂点武器
.msm→ base gr2 + 额外 gr2(头发 / 时装);各自 mesh binding 到同一骨架。- 武器:
GrannyFindBoneByName("Bip01 R Hand"(或对应名))→ 取该骨世界矩阵 × 武器局部挂点矩阵 → 武器 gr2 的 model 变换。 - 完成判据:挂点武器的世界变换与 oracle 一致。
T6 · LOD 一致性
- 加载同模型 LOD 0–3(
warrior_cheongrin_lod_01/02/03.gr2),确认它们绑同一骨架、骨骼索引一致。 - 在某帧切 LOD,测顶点位移(对应骨骼的顶点,切换前后位置差)。
- 完成判据:切换帧顶点位移 < 阈值(无肉眼可见跳变)。
T7 · 曲线退路(tools/anim_bake,条件性)
- 仅当 M0 fuzz 报告曲线子类型超出"1–2 种可实现"范围时启用。
anim_bake:Windows 侧用 oracle 把动画按固定帧率(如 60fps)烘焙成密集关键帧(每骨每帧一个 TRS),存自有格式。libgr2/animation增加"读烘焙格式"分支,PoC 完全绕开曲线解码。- 完成判据:烘焙动画在 M2 数值对拍中照常通过。
门禁(分级,按序)
.msm/.msa解析子检查通过(T1)。- 骨骼世界矩阵与 oracle 层①(裸 Granny,
GrannyGetWorldPoseComposite4x4Array直出) 逐帧对拍,max|Δ| < ε_mat(noise_floor × 余量)—— 隔离曲线采样 + 姿势累积,先过。- M2 只对层①。层②(
ActorInstanceBlend+ LOD 骨骼裁剪)不在 PoC 范围,留正式移植。
- M2 只对层①。层②(
- 蒙皮顶点坐标与 oracle 逐帧对拍,
‖Δ‖ < ε_vtx(在矩阵已对上的前提下)—— 隔离蒙皮 + 顶点格式。 - 挂点武器世界变换与 oracle 一致(T5)。
- LOD 0–3 共享骨骼绑定,切换无跳变(T6)。
验证
- 数值层对拍:N 帧 × M 顶点、全部测试资产集,不是单文件。
- 必须按序:先骨骼矩阵,再顶点。否则动画错 + 蒙皮错相互抵消、最终顶点却"对",掩盖两个 bug。
- tie-break:libgr2 与 oracle 分歧又都合理时,用 Blender
io_scene_gr2在同t算的世界矩阵仲裁(也能抓 oracle 自己的 bug)。 - 视觉层:3 个确定姿势(idle 第 0 帧、走路中段、旋转量大的姿势),差异分类见 PLAN §07。
本步风险(从 PLAN §08 筛)
| 风险 | 状态 / 缓解 |
|---|---|
| Granny 曲线压缩格式多样 | 消除:M0 实测全 OldCurveType,degree ≤ 2,gr2_anim 全覆盖,无烘焙退路依赖 |
| ease/loop/局部时钟语义 | demo 用简单 loop(fmod(t, duration));ease / blend / accumulation 是正式移植照 EterGrnLib/Motion* 复刻,PoC 不需要 |
| 骨骼绑定顺序 / mesh binding 重映射 | gr2::sample_pose 按骨骼名 retarget(181 warrior 动画 NaN=0);skin 矩阵按 mesh.bone_bindings → skeleton 索引 |
| rigid + deformable mesh 混合 | engine/skinning.cpp 按 mesh.rigid 分支(单骨刚体 vs 4 权重混合) |
非单位 scaleshear 让 world·invBind 近似失效 |
全程完整仿射 4x4(mul4x3 保 3x3 + 平移);法线用 skin 3x3(scaleshear 严格应逆转置,M2 先近似,记 M4 收紧) |
| ε 拍脑袋 | 待 oracle T8 的 noise_floor × 10 |
已跑:macOS + Wine + MinGW 交叉编译的 oracle.exe(Granny 2.9.12)vs libgr2,骨骼矩阵 + 蒙皮顶点全 ≤ 6.5e-5(float 累积误差量级)。oracle/RUNBOOK.md / tools/oracle_diff |
DoD 清单
.msm/.msa解析 dump 与源文本核对通过(formats/textscripttoken 树 +msa/msm)- 骨骼世界矩阵 vs oracle 层①
max|Δ| ≤ 4.6e-5——oracle/run-diff-suite.sh跑 23 用例(warrior_cheongrin/lord ×4 LOD、assassin 双 root、shaman_lord v7、redthief2、snakeman、rabbit_backup、ox_01;dance_1/attack/run/wait @ 多个 t)。test/m2-numeric.json。 - 蒙皮顶点 vs oracle
‖Δ‖ ≤ 6.5e-5(同上,vsGrannyDeformVertices;法线用同 3x3,Granny 也不做逆转置 ——engine/skinning.cpp) test/noise_floor.json:同-DLL 确定性 0,跨实现地板 mat 4.6e-5 / vtx 6.5e-5,ε=1e-3app/render_fuzz全量渲染烟测:9166 个.gr2全过真管线(bgfx +Scene::build+ DXT 解码 + CPU 蒙皮 +submit;skinned 4055 / anim-only 5109 / rigid 2)——0 崩溃 / 0 空场景 / 0 NaN 姿势(test/render-fuzz.json)。- 顺带修的 libgr2 bug:
redthief_general/{back,front}_damage.gr2的 finger track 控制点在文件里就是NaN。read_old_curve现在遇到非有限 knot/control 整条曲线弃(回退 bind),不把 NaN 灌进蒙皮链。 - 抽样截图里「模型扭曲」的都是 render_fuzz 把某物种的动画 retarget 到别的 rig(如怪物动画播到 warrior 身上,或 warrior dance 播到怪物身上)—— 按名 retarget 到 bind pose 比例不同的骨架,肢体会拉长。不是 libgr2/engine bug(同 rig 的 warrior + dance_1/attack/run/wait 已对拍 Granny 1e-5)。render_fuzz 已改成只在 rig 匹配时才应用动画,其余画 bind pose。
- 顺带修的 libgr2 bug:
- [~] 挂点武器变换 ——
gr2::sample_pose出每骨世界矩阵,武器 =world[handBone]·mount;未接进 demo(无 .msm 武器数据),逻辑就绪 - LOD 0–3 共享骨架:
warrior_cheongrinlod_01/02/03 与 base 均 75 骨、骨骼名 + ParentIndex 逐项一致 → 切 LOD 骨骼索引不变、无跳变 xrender-demo按Space播dance_1:CPU LBS 每帧更新 dynamic VB,姿势连贯(test/golden/m2-*.png)- (条件)曲线退路
anim_bake—— 不需要:M0 实测曲线全OldCurveTypedegree ≤ 2,gr2_anim全覆盖
M2 实现结果
| 项 | 结果 |
|---|---|
T1 .msa/.msm |
formats/textscript.cpp 通用 token 树 + msa.cpp/msm.cpp。dance_1.msa→duration 28.333334 / accum 0;throw.msa→1 event(type10, t=0.824);warrior_w.msm→base + hair_path + 75 hairs。dump 与源文本逐项一致。 |
| T2 世界姿势 | gr2::sample_pose(skeleton, animation, t, world, skin)(libgr2,跨文件:model gr2 的 skeleton + anim gr2 的 tracks,按骨骼名 retarget)。world[i]=Composite(local[i])·(parent<0?InitialPlacement:world[parent]),skin[i]=InverseWorld4x4[i]·world[i]。bind pose 时 skin ≈ I(6.1e-5)。 |
| T3 CPU LBS | engine/skinning.cpp:v'=Σ wᵢ·skin[bᵢ]·v(4 权重归一),rigid mesh 走单骨刚体分支。skin_bind_pose_residual 自检 = 0(skin 全单位 → 输出 == 输入)。 |
| T4 GPU 蒙皮 shader | 跳过:M2 数值门禁走 CPU(可 dump 对拍),demo 也用 CPU + dynamic VB。vs_pnt_skinned.sc 留到 M4 CPU/GPU 一致性。 |
| 无 oracle 抽检 | 全部 181 个 pc/warrior/**.gr2:139 个带动画,各在 t=0/⅓/⅔/1 采样 —— NaN=0;56 个 track 全名匹配 skeleton(其余部分匹配,未匹配的骨退回 bind,安全)。 |
| demo 用法 | XR_ANIM=<anim.gr2|.msa> XR_ANIM_T=<0..1|秒> + 交互 Space 播放/暂停。 |