依据 Gitea v1.27.3 modules/charset/escape_stream.go 的判定规则 (analyzeDetectResults:仅当左右相邻字符都不是非 ASCII 时才标记, 且 _common 表恒定生效),把说明文档中会被标记的全角标点 (,:;?!() 等)改为半角,并保证成对括号宽度一致。 中文语境中的全角标点(左右均为中文)保持原样,不动代码与契约语义。
10 KiB
contracts/model-formats.md - 模型与动作格式契约 v0.4.0
1. 角色模型(models)
系统以 GLB / VRM 为主力格式(两个客户端都原生加载),其余格式一律在服务端转换。
| 格式 | 扩展名 | 支持级别 | 服务端行为 |
|---|---|---|---|
| glTF Binary | .glb |
主力(P0) | 直接导入,不重编码 |
| VRM | .vrm |
主力(P0) | 直接导入; = GLB + VRM 扩展(humanoid / 表情 / springBone / meta) |
| glTF | .gltf (+ .bin, 贴图) |
P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) |
| MMD 压缩包 | .zip |
P1 | 自动搜索包内可用模型:优先直接导入 .vrm/.glb/.gltf,否则转换 .pmx/.fbx,见 1.1 |
| MMD 模型 | .pmx |
P2(输入) | 服务端转换(优先 GLB、其次 VRM);单文件上传无贴图 |
| FBX | .fbx |
P2(输入) | 服务端转换(优先 GLB、其次 VRM) |
1.1 上传与转换策略
.glb / .vrm / .gltf 直接导入(不重编码,零损失);.pmx / .fbx 由服务端转换:
优先 GLB,其次 VRM。
单文件上传
| 输入 | 行为 |
|---|---|
.glb / .vrm |
直接导入;.glb 若含 VRMC_vrm/VRM 扩展会被识别为 VRM |
.gltf |
直接导入(外部 .bin / 贴图不在同一文件里,会缺资源) |
.pmx / .fbx |
转换:ASSIMP_BIN 导出 -> 回读校验(见下) |
压缩包上传(自动搜索)
不再"只找 .pmx",而是按格式优先级自动搜索包内可用模型:
.vrm ->.glb ->.gltf (直接导入,优先)
.pmx ->.fbx (需要转换)
同一优先级内:MODEL_CONVERT_PMX 指定的文件名优先,其次目录最浅,最后文件最大。
压缩包原件以 model-archive 资产留存并写入 models.source_asset_id。
转换与校验(PMX / FBX)
| 步骤 | 规则 |
|---|---|
| 1. GLB 直出 | assimp export ->.glb,成功后把 Assimp 写成外部 URI 的贴图内嵌进 BIN chunk |
| 2. glTF 打包 | 上一步失败时 assimp export ->.gltf,由 gltf-packer.ts 把 .gltf + .bin + 贴图自行合成 GLB |
| 3. VRM 封装 | MODEL_CONVERT_FORMAT=vrm 时,在 GLB 之上补 VRMC_vrm 扩展(humanoid 骨骼来自骨骼名映射,见 model-inspector.ts 的候选表,与 Godot 客户端一致);humanoid 不全则回退交付 GLB,缺失骨骼写进 conversion_note |
| 回读校验 | 每一步产物都要 assimp info 回读。Assimp 会"写出损坏文件"(实测 FBX 导出退出码 11、产物回读报 FBX-DOM no Objects dictionary found),只看退出码会把坏文件入库 |
| 说明 | 每次尝试都写进 models.conversion_note(例如 Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图) |
| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> HTTP 422,不产生模型记录 |
入库后(自动)
| 项 | 说明 |
|---|---|
| 状态 | 一律 pending(待审核):模型必须审核通过后才可能被客户端消费,见 database.md 状态机 |
| 分析 | model-inspector.ts 计算面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性,写入 models.stats,供审核界面展示 |
| 缩略图 | model-thumbnail.ts 纯 TS 光栅化生成 PNG,写入 models.thumbnail_asset_id(失败不影响入库) |
| 默认模型 | 只在审核通过并勾选"同时设为默认"(或 POST /api/models/:id/primary)时设置,并把身份写进源码 src/config/default-model.ts,见 data-models.md 4.1 |
其它约定
| 项 | 规则 |
|---|---|
| 确定性 | 转换产物不是字节级确定的:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以上传压缩包的 sha256 为匹配键,产物 sha256 只作记录 |
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 ..(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE |
VRM 优先的选取顺序见 data-models.md 4.1。
VRM 版本识别:读取 GLB 的 JSON chunk,检查 extensions.VRM(0.x)或
extensions.VRMC_vrm(1.0),写入 models.vrm_version。
2. 动作数据(motions)
| 格式 | 扩展名 | 预处理支持 | 说明 |
|---|---|---|---|
| BVH | .bvh |
✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 -> FK -> MediaPipe33 |
| VMD | .vmd |
✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 -> MediaPipe33,见 2.3 |
| FBX | .fbx |
⚠️ 需 Assimp | 服务端调用 assimp 可执行文件导出为 BVH/glTF 后再解析;未安装则 preprocessState=failed |
| glTF/GLB | .gltf / .glb |
⚠️ 需 Assimp | 同上 |
| 关键点 JSON | .json |
✅ 直通校验 | 已是 mediapipe33@0.1.0 结构,仅做校验与归一化 |
2.1 预处理产物(冻结)
- 格式:NDJSON(
application/x-ndjson),每行一帧,UTF-8 无 BOM - 一行结构:
{"frame":0,"timeMs":0.0,"points":[[0.5,0.5,0.0,1.0],"...33 项"],"angles":[1.0,"...9 项"]}
points[i] = [x, y, z, confidence],语义与websocket-keypoints.md解码后完全一致angles为skeleton-topology.md5 定义的 9 个关节角余弦,顺序固定:[shoulder_L, shoulder_R, elbow_L, elbow_R, hip_L, hip_R, knee_L, knee_R, spine]- 帧率:保留源帧率;
fps写入motions.fps,缺省 30 - 坐标系换算:所有 3D 动作数据统一转到 Y 轴向上、右手系、骨盆为原点,
再投影到 MediaPipe 归一化图像空间:
x_img = 0.5 + x_world * scale,y_img = 0.5 - y_world * scale,z_img = -z_world * scale,scale = 0.35 / max(|x|,|y|,|z|)(按整段动作的最大范围自适应,保证不出画) - 缺失关节(BVH 中不存在对应骨骼):
confidence = 0.0,坐标沿用最近可用父节点
2.2 BVH 关节名映射(默认表,可在 web-server/src/preprocess/joint-map.ts 扩展)
| MediaPipe 点 | 常见 BVH 关节名(不区分大小写,按序匹配) |
|---|---|
| 0 nose | Head, Neck1, Head_Nub |
| 11 left_shoulder | LeftShoulder, LeftArm, LShoulder |
| 12 right_shoulder | RightShoulder, RightArm, RShoulder |
| 13 left_elbow | LeftForeArm, LeftElbow, LElbow |
| 14 right_elbow | RightForeArm, RightElbow, RElbow |
| 15 left_wrist | LeftHand, LeftWrist, LHand |
| 16 right_wrist | RightHand, RightWrist, RHand |
| 23 left_hip | LeftUpLeg, LeftHip, LHipJoint |
| 24 right_hip | RightUpLeg, RightHip, RHipJoint |
| 25 left_knee | LeftLeg, LeftKnee, LKnee |
| 26 right_knee | RightLeg, RightKnee, RKnee |
| 27 left_ankle | LeftFoot, LeftAnkle, LAnkle |
| 28 right_ankle | RightFoot, RightAnkle, RAnkle |
| 31 left_foot_index | LeftToeBase, LeftToe, LToe |
| 32 right_foot_index | RightToeBase, RightToe, RToe |
未映射到的点按 skeleton-topology.md 5 的层级由父节点推算,
并置 confidence 降低(0.5),表示估算而非实测。
2.3 VMD(MikuMikuDance)解析约定
VMD 是二进制格式:文件头 Vocaloid Motion Data 0002(30 字节)+ 模型名(20 字节,
Shift-JIS)+ 骨骼关键帧数组(每条 111 字节: 15 字节骨骼名、uint32 帧号、
float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)+ morph / 相机 /
灯光 / 自阴影 / IK / 可见性段落。VMD 不含骨骼长度,只有旋转(少数骨骼带位移),
因此服务端必须自带一套标准 MMD 骨架才能做 FK。
处理链路:vmd-parser.ts(二进制 + Shift-JIS + 贝塞尔插值)-> mmd-skeleton.ts
(标准骨架)-> mmd-pose.ts(FK + 腿部 IK)-> retarget.ts 的
worldFramesToKeypointFrames()(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 /
9 个关节角)。固定 30 fps(MMD 帧率恒定,上传时填写的 fps 被忽略)。
| MediaPipe 点 | MMD 标准骨骼 | 说明 |
|---|---|---|
| 0 nose | 頭 |
取头骨根部再上移,近似面部中心 |
| 11 / 12 left/right_shoulder | 左腕 / 右腕 |
肩关节(不是 左肩 骨骼) |
| 13 / 14 left/right_elbow | 左ひじ / 右ひじ |
|
| 15 / 16 left/right_wrist | 左手首 / 右手首 |
|
| 23 / 24 left/right_hip | 左足 / 右足 |
大腿根部 |
| 25 / 26 left/right_knee | 左ひざ / 右ひざ |
|
| 27 / 28 left/right_ankle | 左足首 / 右足首 |
|
| 31 / 32 left/right_foot_index | 左つま先 / 右つま先 |
骨骼名按 mmd-skeleton.ts 的别名表归一(含全角 IK / 半角 IK 两种写法);
未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。
腿部驱动判定: 足IK 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成
FK 旋转)。两份示例文件分别命中两种路径。
坐标系: MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色
左侧)。VMD 的四元数与位移统一做 Z 镜像((x,y,z,w) -> (-x,-y,z,w)、位移 z 取反)后
送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,zMirrored 下
膝盖朝前占比 94.1%,不转换时仅 5.9%(见 test/mmd-vmd.spec.ts)。
标准骨架数值来源:解析参考模型的 PMX 骨骼段实测得到(而非凭经验估计), 再去掉模型自身原点偏移并抹平左右不对称。关键比例:足首 1.43 / ひざ 6.57 / 髋 11.50 / センター 10.88 / 首 16.21 / 頭 17.16(总高约 19.4,髋高比约 59%); 大腿 4.94 + 小腿 5.15 ~ 腿长 10.07。
3. 上传限制
| 项 | 值 |
|---|---|
| 单文件上限 | 512 MiB(UPLOAD_MAX_BYTES 可覆盖) |
| 允许扩展名 | 上表全部;其他一律 422 |
| 存储路径 | ${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext} |
| 去重 | 相同 sha256 复用同一 assets 记录,不重复落盘 |
| 文件名 | 服务端不使用原始文件名做路径,只存 assets.original_name |
4. 开源依赖与许可证(引用 open-source-credits.md)
bvh(MIT)- BVH 解析参考实现Assimp(BSD-3-Clause)- FBX/glTF 转换,可选外部依赖@pixiv/three-vrm(MIT)- 管理端 VRM 预览three(MIT)- 管理端 3D 预览