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

70 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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|xy| = 0.85 ≤ 1`,越界 0 |
| 关节角(cos 值)有限且 ∈ [-1,1] | 范围 `[-1.000, 0.995]` |
| NDJSON 往返一致 / 缩略图有效 | 2901 行往返一致;PNG magic `89504e470d0a1a0a` |
| 动作非退化 | 左右腕 x 方向摆幅 > 0.1,鼻尖摆幅显著小于手腕 |
> 性能注意:2901 帧 × 33 点逐元素 `expect` 会拖满 vitest 默认 5 s 超时。
> 用例改成先聚合违规项、最后只断言一次(保留具体帧号与下标),并显式给
> 大用例 120–300 s 超时。