# 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 边界。