根因:normalizeBoneName() 只保留 a-z0-9,**中日文字符被整个丢掉**: '上半身'/'左ひじ'/'首' 归一化后都是空串,MMD 模型的骨名必然失配, humanoid 覆盖率恒为 0/22 -> 不可驱动 -> 只能回退交付 GLB 并标记不可用。 修复: 1. 归一化保留 CJK/平假名/片假名/长音符/々,并把全角 ASCII(足IK)折成半角; 2. 候选表补齐四类命名(22 根 x 8-13 个候选):VRoid / Mixamo / MMD 标准骨架(下半身/上半身/上半身2/首/頭/左肩/左腕/左ひじ/左手首/ 左足/左ひざ/左足首/左つま先)/ 常见中文名(臀部/头/脖子/左大臂/左大腿…); 精确匹配保证「左足IK」不会抢占「左足」; 3. Windows 客户端 humanoid_map.gd 的 CANDIDATES 由服务端表**生成写入**, normalize() 同步改造,parity 测试断言键/候选顺序完全一致; 4. 契约 skeleton-topology -> 0.2.0:新增 §6「内部动作标准与模型骨骼重绑定」 (三层标准、22/16 根骨、归一化与匹配规则、实测记录); 5. 新增 4 项测试;规模 17 文件 / 229 项。 实测:奥黛塔 PMX 0/22 -> 21/22、初音未来 PMX 0/22 -> 19/22,均 drivable=true 且产出 VRM。 版本 0.4.0。
8.3 KiB
8.3 KiB
contracts/skeleton-topology.md - 骨骼拓扑契约 v0.2.0
唯一拓扑:MediaPipe Pose 33 点。禁止引入 SMPL 及其衍生拓扑(许可风险)。
1. 33 点定义与索引
| idx | name | 中文 | 说明 |
|---|---|---|---|
| 0 | nose | 鼻 | |
| 1 | left_eye_inner | 左眼内 | |
| 2 | left_eye | 左眼 | |
| 3 | left_eye_outer | 左眼外 | |
| 4 | right_eye_inner | 右眼内 | |
| 5 | right_eye | 右眼 | |
| 6 | right_eye_outer | 右眼外 | |
| 7 | left_ear | 左耳 | |
| 8 | right_ear | 右耳 | |
| 9 | mouth_left | 左嘴角 | |
| 10 | mouth_right | 右嘴角 | |
| 11 | left_shoulder | 左肩 | |
| 12 | right_shoulder | 右肩 | |
| 13 | left_elbow | 左肘 | |
| 14 | right_elbow | 右肘 | |
| 15 | left_wrist | 左腕 | |
| 16 | right_wrist | 右腕 | |
| 17 | left_pinky | 左小指 | |
| 18 | right_pinky | 右小指 | |
| 19 | left_index | 左食指 | |
| 20 | right_index | 右食指 | |
| 21 | left_thumb | 左拇指 | |
| 22 | right_thumb | 右拇指 | |
| 23 | left_hip | 左髋 | |
| 24 | right_hip | 右髋 | |
| 25 | left_knee | 左膝 | |
| 26 | right_knee | 右膝 | |
| 27 | left_ankle | 左踝 | |
| 28 | right_ankle | 右踝 | |
| 29 | left_heel | 左足跟 | |
| 30 | right_heel | 右足跟 | |
| 31 | left_foot_index | 左足尖 | |
| 32 | right_foot_index | 右足尖 |
2. 虚拟关节(由 33 点派生,索引 33/34,仅用于层级与角度特征)
| idx | name | 计算 |
|---|---|---|
| 33 | neck | midpoint(left_shoulder(11), right_shoulder(12)) |
| 34 | pelvis | midpoint(left_hip(23), right_hip(24)) |
3. 父节点表(parent[i], -1 表示世界根)
对称树,便于骨骼重定向与关节角计算:
34 pelvis -> -1 (world root)
33 neck -> 34
23 left_hip -> 34
24 right_hip -> 34
11 left_shoulder -> 33
12 right_shoulder -> 33
0 nose -> 33
1 left_eye_inner -> 0
2 left_eye -> 0
3 left_eye_outer -> 0
4 right_eye_inner -> 0
5 right_eye -> 0
6 right_eye_outer -> 0
7 left_ear -> 3
8 right_ear -> 6
9 mouth_left -> 0
10 mouth_right -> 0
25 left_knee -> 23
26 right_knee -> 24
27 left_ankle -> 25
28 right_ankle -> 26
29 left_heel -> 27
30 right_heel -> 28
31 left_foot_index -> 29
32 right_foot_index -> 30
13 left_elbow -> 11
14 right_elbow -> 12
15 left_wrist -> 13
16 right_wrist -> 14
17 left_pinky -> 15
18 right_pinky -> 16
19 left_index -> 15
20 right_index -> 16
21 left_thumb -> 15
22 right_thumb -> 16
4. 坐标系
- 归一化图像坐标:
x, y ∈ [0, 1],原点左上角,y向下;x相对图像宽,y相对图像高。 - 深度:
z与x同尺度,原点位于髋中心,负值朝向相机。 - 镜像: Android 前置摄像头默认输出镜像画面;是否镜像由帧头
flags.bit0声明, 接收端据此决定是否翻转x(x' = 1 - x)。 - 置信度:
visibility ∈ [0, 1],落盘/传输为uint8(round(v * 255))。 归一化比例统一使用32767(见websocket-keypoints.md)。
5. 派生特征(DTW 输入,Windows 端实现,服务端预处理产物同构)
服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧结构完全一致, 因此 Windows 端 DTW 引擎只有一条输入通路。
关节角特征(弧度,余弦值便于插值):
shoulder_L = angle(left_elbow, left_shoulder, left_hip)
shoulder_R = angle(right_elbow, right_shoulder, right_hip)
elbow_L = angle(left_shoulder, left_elbow, left_wrist)
elbow_R = angle(right_shoulder, right_elbow, right_wrist)
hip_L = angle(left_shoulder, left_hip, left_knee)
hip_R = angle(right_shoulder, right_hip, right_knee)
knee_L = angle(left_hip, left_knee, left_ankle)
knee_R = angle(right_hip, right_knee, right_ankle)
neck = angle(nose, neck, pelvis)
spine = angle(neck, pelvis, midpoint(left_ankle, right_ankle))
angle(a, b, c) = 向量 ba 与 bc 的夹角余弦,即
dot(normalize(a-b), normalize(c-b)),落在 [-1, 1]。
DTW 序列结构(冻结):每帧 = [33 x (x, y, z), 9 个关节角余弦] = 108 维。
低置信度点(visibility < 0.5)在插值阶段由相邻帧线性补全,不参与代价计算。
6. 内部动作标准与模型骨骼重绑定(v0.2.0)
目的:动作数据必须能匹配系统内的任意模型,因此把"关键点标准 + 骨架标准 + humanoid 标准" 冻结成本节,模型上传时按此重绑定骨骼,再包装为 VRM。
6.1 三层标准
| 层 | 标准 | 权威实现 |
|---|---|---|
| 关键点 | MediaPipe Pose 33 点(顺序见 §1),动作一律先转成关键点序列(NDJSON)再入库 | preprocess/(BVH / VMD / FBX -> 33 点) |
| 动作骨架 | MMD 标准骨架(静止比例实测自参考 PMX; 足IK 带位移时走两骨骼腿部 IK,否则 FK) |
preprocess/mmd-skeleton.ts |
| 模型骨架 | 22 根 humanoid 规范骨(顺序即层级,父在前),其中 16 根为必需(§6.2) | models/model-inspector.ts 的 HUMANOID_CANDIDATES |
评分只依赖 33 点关键点序列,与模型骨架解耦;因此"动作能匹配任何模型"的充要条件是
模型可驱动(16 根必需骨齐全,drivable=true)。
6.2 22 根 humanoid 规范骨与 16 根必需骨
hips -> spine -> chest -> upperChest -> neck -> head
├── leftShoulder -> leftUpperArm -> leftLowerArm -> leftHand
└── rightShoulder -> rightUpperArm -> rightLowerArm -> rightHand
hips -> leftUpperLeg -> leftLowerLeg -> leftFoot -> leftToes
hips -> rightUpperLeg -> rightLowerLeg -> rightFoot -> rightToes
必需 16 根(缺一即 drivable=false):hips、spine、neck、head、
leftUpperArm、leftLowerArm、leftHand、rightUpperArm、rightLowerArm、rightHand、
leftUpperLeg、leftLowerLeg、leftFoot、rightUpperLeg、rightLowerLeg、rightFoot。
6.3 重绑定规则(模型上传时执行,服务端与三端一致)
- 归一化:小写 → 只保留
a-z0-9+ 中文/日文(CJK、平假名、片假名、长音符、々); 全角 ASCII(A-Z a-z 0-9)折成半角(MMD 常写「足IK」「つま先IK」)。 v0.2.0 修正:此前只保留a-z0-9, MMD 骨名(上半身/左ひじ/首)被归一化成空串, humanoid 覆盖率恒为 0/22。 - 剥前缀:只剥
J_Bip/mixamorig/Bip01; 不剥J_Aim_/J_Roll_/J_Sec_(VRoid 的扭转辅助骨,剥了会与真正的骨骼撞名)。 - 变体:
left/right与l/r互转(leftupperarm<->lupperarm)。 - 精确匹配:归一化+变体后的名字必须与候选表完全相等(前缀不匹配),
因此
左足IK(->左足ik)不会抢占左足(leftUpperLeg)。 - 候选表覆盖四类命名(顺序即优先级;服务端与客户端逐字一致,由测试断言):
VRoid(
J_Bip_C_Hips等)、Mixamo(mixamorig:LeftArm)、 MMD 标准骨架(下半身/上半身/上半身2/首/頭/左肩/左腕/左ひじ/左手首/左足/左ひざ/左足首/左つま先)、 常见中文名(臀部/头/脖子/左大臂/左小臂/左大腿/左脚等)。 - 结果写入 VRM:命中结果写成
VRMC_vrm.humanoid.humanBones的节点索引(VRM 规范 不要求节点名固定,因此以索引为准,不改动几何与贴图);未命中的骨骼保留原名作为附加骨。 - 结论必须上报:
models.stats.humanoidMapped/humanoidTotal/missingRequiredBones/drivable四项写库,审核界面展示;usable与是否进入客户端目录见 model-formats.md 1.1。
6.4 实测(v0.2.0 重绑定后)
| 样本 | 重绑定前 | 重绑定后 | 结果 |
|---|---|---|---|
【奥黛塔】_by_原神.zip(PMX,日文骨名) |
0/22,不可驱动 | 21/22, drivable=true | 交付 VRM |
模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip(PMX) |
0/22,不可驱动 | 19/22, drivable=true | 交付 VRM |
未命中的 1-3 根均为非必需骨(如 upperChest / leftToes),不影响驱动与评分。