Files
opendance-web/tasks/013-bone-rebinding.md
T
zijunchiang 2da02ee423 test(real-samples): 用真实素材端到端验证 ①模型→VRM 与 ②动作内部标准
`model-archive.spec.ts` / `mmd-vmd.spec.ts` 用的都是合成夹具与假 Assimp,
证明得了边界,证明不了「真素材能出片」。新增 opt-in 的
`test/real-samples.spec.ts`(6 项),样例或 Assimp 缺失时整体跳过,
CI 与无素材的开发机不受影响:

① 模型 → VRM(两个真实 PMX 压缩包)
   - 初音未来 1.0 MB → 1.10 MB VRM,humanoid 19/22,2 张贴图全内嵌;
   - 奥黛塔 28.1 MB → 26.08 MB VRM,humanoid 21/22,18 张贴图全内嵌,
     39 材质(36 项双面已补回);两者 drivable = true;
   - 断言 VRMC_vrm 1.0、所有 images 带 bufferView 且无 uri、无 MMD 专属
     材质扩展泄漏、蒙皮下标不越界,并**逐字节复算 POSITION 访问器的
     min/max**(glTF 唯一强制填写的边界,客户端据此做视锥剔除与 AABB;
     读访问器必须按 byteStride 走,否则交错缓冲里的 u16 会被读成乱码)。

② 动作内部标准 + 骨骼重绑定(真实 VMD,0.8 MB / 96.7 s)
   - maxFrame 2900 → 2901 帧、标准骨架 31/31 命中、双侧腿部 IK 链齐备;
   - MMD 骨骼名 → joint-map 候选名的重绑定结果逐一比对(15 个键);
   - 15 个直接关节的置信度恒为 1.0(绝不退化成 0.5 的推算值),
     其余 18 点为 0.5,缺失为 0;骨盆归零后投影 max|xy| = 0.85 ≤ 1;
   - NDJSON 往返一致、缩略图 PNG 有效、手腕摆幅非退化。

顺带修 `assimp-locate.spec.ts` 的非密闭性:其用例断言的是 PATH/常见系统
目录查找,但会继承外部 ASSIMP_BIN(assimp-lib.sh 中 ASSIMP_BIN 优先级高于
PATH),开发者按本项目文档设过 ASSIMP_BIN 时 npm test 会假失败。

规模 24 文件 / 289 项 → 25 文件 / 295 项。产物与已发布的 v0.7.4 无关
(发布包不含 web-server/test/),故不新发版本。
2026-09-17 04:11:24 +08:00

3.6 KiB
Raw Blame History

T-WEB-013 动作内部标准与模型骨骼重绑定

  • 状态: done
  • 依赖: T-WEB-006 / T-WEB-012
  • 文档: v1.6 §4.10
  • 契约: skeleton-topology 0.2.0、data-models 0.7.0

目标

  1. 动作内部标准:所有动作(BVH / VMD / FBX / glTF)统一预处理成 MediaPipe 33 点关键点序列(mediapipe33@0.1.0),客户端只认这一种;
  2. 模型骨骼重绑定:任意命名风格的模型都能被关键点驱动 —— 按名字映射到 canonical humanoid 骨骼,而不是要求模型符合某一种命名规范。

方案

动作内部标准

preprocess.service.ts 按扩展名分派解析器,统一产出 NDJSON 关键点序列 + 9 个关节角特征(108 维),与 Android 端推流结构完全同构 —— 因此客户端 DTW 的两侧输入是同一套特征。

骨骼重绑定

项 做法
归一化 normalizeBoneName():保留 CJK 与假名(不能再只留 a-z0-9),全角 ASCII 折叠(足IK → 足ik)
候选表 HUMANOID_CANDIDATES 覆盖 VRM/VRoid、Mixamo、MMD 日文名、中文常见名
生成而非手写 Godot 端 src/model/humanoid_map.gd 的 CANDIDATES 由服务端表生成,并有跨端一致性测试
覆盖度 coverage() / missing_bones() 暴露给审核界面与客户端

踩坑记录:第一版 normalizeBoneName() 只保留 [a-z0-9],把 CJK 名字 整个抹成空串,导致 MMD 模型 humanoid 覆盖 0/22。修复后实测 奥黛塔 21/22、初音未来 19/22,两者 drivable = true 且 VRM 正常产出。

验收

验收项 结果
MMD(日文骨骼名)模型可驱动 ✅ 实测 21/22、19/22 覆盖;客户端可正常驱动
Godot 端候选表与服务端一致 ✅ 一致性测试(跨仓库生成 + 校验)
动作统一为 33 点 ✅ keypoint_format = mediapipe33@0.1.0;Android 端推流同构

真实样例回归(T-WEB-006)

test/real-samples.spec.ts 用仓库根 示例文件/【动作】霜雪千年.vmd(0.8 MB,真实舞蹈, 96.7 s)跑完整 parseVmd → vmdToWorldFrames → worldFramesToKeypointFrames 管线。 VMD 通路不依赖 Assimp,因此只要样例在就会真跑;样例缺失时整个 describe 跳过。

实测:maxFrame = 2900 → 2901 帧(30 fps / 96.7 s),轨道 31 条, 标准骨架 MMD_SKELETON 的 31 根全部命中(缺失 0),双侧腿部 IK 链 (左/右足IK + ひざ + 足首 + つま先)齐备,因此 IK 解算路径生效; 重定向 135 ms(0.05 ms/帧),产出 33 点 + 2 虚拟点 + 9 关节角,NDJSON 6.51 MB。

用例锁定的重绑定不变量:

断言 实测
世界帧键名恰为 joint-map 候选名(MMD 名 → 英文规范名的重绑定结果) 15 个键逐一比对
直接绑定的 15 个索引置信度只能是 1.0(不许退化成推算值) 0/11/12/13/14/15/16/23/24/25/26/27/28/31/32 → 全部 [1]
其余 18 个点由层级推算 全部 [0.5],缺失(0)为 0
骨盆归零 + 图像空间投影 `max
关节角(cos 值)有限且 ∈ [-1,1] 范围 [-1.000, 0.995]
NDJSON 往返一致 / 缩略图有效 2901 行往返一致;PNG magic 89504e470d0a1a0a
动作非退化 左右腕 x 方向摆幅 > 0.1,鼻尖摆幅显著小于手腕

性能注意:2901 帧 × 33 点逐元素 expect 会拖满 vitest 默认 5 s 超时。 用例改成先聚合违规项、最后只断言一次(保留具体帧号与下标),并显式给 大用例 120–300 s 超时。