Files
opendance-web/tasks/012-vrm-first.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

66 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-012 模型主力格式:全面转为 VRM
- **状态**: done
- **依赖**: T-WEB-005 / T-WEB-007
- **文档**: v1.6 §4.9
- **契约**: `model-formats 0.5.0`、`data-models 0.7.0`
## 目标
所有入库模型统一转成 **VRM**(glTF 2.0 容器 + humanoid 扩展),
客户端只消费 VRM,避免三种格式各自的骨骼命名/坐标系差异在客户端叠加。
## 方案
| 项 | 做法 |
|--|--|
| 转换默认值 | `MODEL_CONVERT_FORMAT` 默认 `vrm`(`.env.example` 同步) |
| 直传 | `.vrm` / `.glb` 已经是 glTF 容器:直接入库、**不重编码** |
| 需转换 | `.pmx` / `.fbx` / `.zip` 由 Assimp 转 GLB,再补齐 VRM 扩展 |
| 可用性判定 | `isUsableModel()`:`format === 'vrm'` 才可用 |
| 不可用模型 | **入库但标记为「不可用」**,`unusableReason()` 说明原因;不进已发布目录 |
| 目录过滤 | `catalog/export` 只下发 `format = vrm` 的模型;`catalog/version` 指纹同样只算可用的 |
| 默认模型 | `resolveDefaultModelId()` 只在可用模型里挑 |
## 验收
| 验收项 | 结果 |
|--|--|
| 上传 PMX/FBX/zip 后产出 VRM | ✅ Assimp → GLB → VRM 扩展补齐;实测 MMD 模型(初音未来 / 奥黛塔)转换成功 |
| 非 VRM 模型不出现在客户端目录 | ✅ `catalog/export` 过滤 + 指纹过滤;`test/model-usability.spec.ts` 覆盖 |
| 不可用模型有明确原因 | ✅ DTO 增 `usable` / `unusableReason` 字段 |
| 三端契约一致 | ✅ `data-models 0.7.0` / `model-formats 0.5.0` 快照三端逐字节一致 |
## 真实样例回归(T-WEB-003 / T-WEB-011)
`test/real-samples.spec.ts` 是 **opt-in** 的真实素材端到端用例:合成夹具证明不了
「真的 MMD 素材能出片」,而这个文件直接用仓库根 `示例文件/` 下的真模型跑完整转换管线。
样例与 Assimp 任一缺失时整个 describe 跳过,因此无素材的开发机与 CI 依旧全绿。
```bash
# 跑真实素材(Assimp 不在 PATH 时用 ASSIMP_BIN 指绝对路径)
ASSIMP_BIN=/usr/bin/assimp npm test -- test/real-samples.spec.ts
# 样例放在别处时
OPENDANCE_SAMPLES_DIR=/path/to/示例文件 ASSIMP_BIN=/usr/bin/assimp npm test
```
实测(Assimp 6.0,2024 版素材):
| 样例 | 输入 | 产物 | humanoid | 贴图 | 材质 |
|--|--|--|--|--|--|
| 初音未来(PMX in zip) | 1.0 MB | 1.10 MB VRM | 19/22,`drivable` | 2 张全内嵌 | 3 |
| 奥黛塔(PMX in zip) | 28.1 MB | 26.08 MB VRM | 21/22,`drivable` | 18 张全内嵌 | 39(36 双面已补回) |
用例断言的不变量(与具体模型无关):
- `extensions.VRMC_vrm.specVersion === "1.0"`,`asset.version === "2.0"`
- **所有** `images` 都带 `bufferView`、无 `uri`(外链贴图离开原目录即坏图)
- 无 `KHR_materials_specular/volume/transmission/ior/emissive_strength` 泄漏(MMD 专属参数不能进标准 PBR 产物)
- **POSITION 访问器的 `min`/`max` 与逐元素读出的真实包围盒一致** —— 这是 glTF 规范唯一强制填写的边界,客户端拿它做视锥剔除与 AABB,填错会导致模型被整体剔除。读访问器时必须按 `byteStride` 走,否则交错缓冲里的 u16 数据会被读成乱码
- 蒙皮下标 `< skin.joints.length`(越界会让客户端取到错误矩阵)
> **已知的上游瑕疵(不影响可用性)**:Assimp 6.0 的 glTF2 导出器会给
> `JOINTS_0` 与 `inverseBindMatrices` 写偏保守的 `min`(前者把真实最小值 6 写成 0,
> 后者对角元 1 写成 `2.2e-308`)。两者都是**合法下界**(`min ≤ 真实值 ≤ max`),
> 没有任何运行时会据此做剔除,因此不改产物、只在用例里锁定真正要紧的 POSITION 边界。