feat(model): humanoid 骨名映射对齐 MMD/中文(契约 skeleton-topology 0.2.0)
- CANDIDATES 由服务端表生成写入(与 model-inspector.ts 逐字一致,parity 测试断言) - normalize() 保留 CJK/假名并把全角 ASCII 折半角,修复 MMD 骨名归一化成空串导致 humanoid 覆盖率恒为 0/22 的问题 - contracts/ 同步至 0.5.0 / 0.2.0
This commit is contained in:
+1
-1
@@ -3,6 +3,6 @@
|
|||||||
rest-api: 0.4.0
|
rest-api: 0.4.0
|
||||||
websocket-keypoints: 0.1.0
|
websocket-keypoints: 0.1.0
|
||||||
data-models: 0.5.0
|
data-models: 0.5.0
|
||||||
skeleton-topology: 0.1.0
|
skeleton-topology: 0.2.0
|
||||||
model-formats: 0.5.0
|
model-formats: 0.5.0
|
||||||
database: 0.3.0
|
database: 0.3.0
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ rejected 已驳回
|
|||||||
| `name` | string | ✅ | 模型名称,<=200 |
|
| `name` | string | ✅ | 模型名称,<=200 |
|
||||||
| `format` | enum | ✅ | `vrm`(主力)/ `glb`(回退或历史数据)/ `gltf` / `fbx`。**所有上传模型一律转换为 VRM**,只有 humanoid 不达标时回退为 `glb` 并置 `usable=false`,见 model-formats.md 1.1 |
|
| `format` | enum | ✅ | `vrm`(主力)/ `glb`(回退或历史数据)/ `gltf` / `fbx`。**所有上传模型一律转换为 VRM**,只有 humanoid 不达标时回退为 `glb` 并置 `usable=false`,见 model-formats.md 1.1 |
|
||||||
| `vrmVersion` | string\|null | 读 | `0.x` / `1.0`,非 VRM 为 null |
|
| `vrmVersion` | string\|null | 读 | `0.x` / `1.0`,非 VRM 为 null |
|
||||||
| `usable` | boolean | 读 | **是否可被客户端使用**:等价于"已产出 VRM"(`format=vrm`)。`false` 的模型**不出现在 `catalog/export`**(v0.5.0) |
|
| `usable` | boolean | 读 | **是否可被客户端使用**:等价于"已产出 VRM"(`format=vrm`)。`false` 的模型**不出现在 `catalog/export`**(v0.5.0) |
|
||||||
| `unusableReason` | string\|null | 读 | `usable=false` 时的原因,例如 `humanoid 不达标(0/22),未产出 VRM` |
|
| `unusableReason` | string\|null | 读 | `usable=false` 时的原因,例如 `humanoid 不达标(0/22),未产出 VRM` |
|
||||||
| `isPrimary` | boolean | | 全局默认模型;**同一时刻全库至多一个 `true`** |
|
| `isPrimary` | boolean | | 全局默认模型;**同一时刻全库至多一个 `true`** |
|
||||||
| `fileUrl` | string | 读 | |
|
| `fileUrl` | string | 读 | |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. 角色模型(models)
|
## 1. 角色模型(models)
|
||||||
|
|
||||||
**v0.5.0 起模型主力格式为 VRM**:**所有上传的模型都转换为 VRM**(`MODEL_CONVERT_FORMAT`
|
**v0.5.0 起模型主力格式为 VRM**: **所有上传的模型都转换为 VRM**(`MODEL_CONVERT_FORMAT`
|
||||||
默认 `vrm`);GLB 退为**转换中间产物**与 **humanoid 不达标时的回退交付**。
|
默认 `vrm`);GLB 退为**转换中间产物**与 **humanoid 不达标时的回退交付**。
|
||||||
两个客户端(Godot `godot-vrm` / 管理端 `@pixiv/three-vrm`)都原生加载 VRM。
|
两个客户端(Godot `godot-vrm` / 管理端 `@pixiv/three-vrm`)都原生加载 VRM。
|
||||||
|
|
||||||
@@ -17,23 +17,23 @@
|
|||||||
|
|
||||||
### 1.1 上传与转换策略
|
### 1.1 上传与转换策略
|
||||||
|
|
||||||
**v0.5.0 起的一律转换链路**:
|
**v0.5.0 起的一律转换链路**:
|
||||||
|
|
||||||
```
|
```
|
||||||
.vrm -> 直接导入(已是 VRM)
|
.vrm -> 直接导入(已是 VRM)
|
||||||
.glb / .gltf -> 包装为 VRM 1.0(写 VRMC_vrm)
|
.glb / .gltf -> 包装为 VRM 1.0(写 VRMC_vrm)
|
||||||
.pmx / .fbx -> assimp -> GLB -> 包装为 VRM 1.0
|
.pmx / .fbx -> assimp -> GLB -> 包装为 VRM 1.0
|
||||||
.zip -> 自动搜索包内模型,按上面三种处理
|
.zip -> 自动搜索包内模型,按上面三种处理
|
||||||
```
|
```
|
||||||
|
|
||||||
**humanoid 门槛(硬性)**:VRM 1.0 要求 humanoid 必需骨骼齐全。服务端判定规则:
|
**humanoid 门槛(硬性)**: VRM 1.0 要求 humanoid 必需骨骼齐全。服务端判定规则:
|
||||||
|
|
||||||
| 情况 | 交付 | `models.usable` | 是否进入客户端目录 |
|
| 情况 | 交付 | `models.usable` | 是否进入客户端目录 |
|
||||||
|--|--|--|--|
|
|--|--|--|--|
|
||||||
| humanoid 必需骨齐全 | **VRM**(`format=vrm`,`vrmVersion=1.0`) | `true` | ✅ 可发布、可选用 |
|
| humanoid 必需骨齐全 | **VRM**(`format=vrm`, `vrmVersion=1.0`) | `true` | ✅ 可发布、可选用 |
|
||||||
| humanoid 不齐(如 MMD 日文骨名未重绑定) | 回退交付 **GLB**,`vrmVersion=null`,`conversionNote` 注明原因 | `false` | ❌ 不进入 `catalog/export` |
|
| humanoid 不齐(如 MMD 日文骨名未重绑定) | 回退交付 **GLB**, `vrmVersion=null`, `conversionNote` 注明原因 | `false` | ❌ 不进入 `catalog/export` |
|
||||||
|
|
||||||
即:**不达标模型仍可入库与审核,但标记为不可用、不进入已发布目录**;
|
即:**不达标模型仍可入库与审核,但标记为不可用、不进入已发布目录**;
|
||||||
待骨骼按 `skeleton-topology.md` 的标准重绑定成功后重新转换即变为可用。
|
待骨骼按 `skeleton-topology.md` 的标准重绑定成功后重新转换即变为可用。
|
||||||
|
|
||||||
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
|
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# contracts/skeleton-topology.md - 骨骼拓扑契约 v0.1.0
|
# contracts/skeleton-topology.md - 骨骼拓扑契约 v0.2.0
|
||||||
|
|
||||||
**唯一拓扑:MediaPipe Pose 33 点。禁止引入 SMPL 及其衍生拓扑(许可风险)。**
|
**唯一拓扑:MediaPipe Pose 33 点。禁止引入 SMPL 及其衍生拓扑(许可风险)。**
|
||||||
|
|
||||||
@@ -123,3 +123,65 @@ spine = angle(neck, pelvis, midpoint(left_ankle, right_ankle))
|
|||||||
|
|
||||||
**DTW 序列结构(冻结)**:每帧 = `[33 x (x, y, z), 9 个关节角余弦] = 108 维`。
|
**DTW 序列结构(冻结)**:每帧 = `[33 x (x, y, z), 9 个关节角余弦] = 108 维`。
|
||||||
低置信度点(`visibility < 0.5`)在插值阶段由相邻帧线性补全,不参与代价计算。
|
低置信度点(`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 重绑定规则(模型上传时执行,服务端与三端一致)
|
||||||
|
|
||||||
|
1. **归一化**:小写 → 只保留 `a-z0-9` + **中文/日文(CJK、平假名、片假名、长音符、々)**;
|
||||||
|
**全角 ASCII(A-Z a-z 0-9)折成半角**(MMD 常写「足IK」「つま先IK」)。
|
||||||
|
*v0.2.0 修正*:此前只保留 `a-z0-9`, MMD 骨名(`上半身`/`左ひじ`/`首`)被归一化成空串,
|
||||||
|
humanoid 覆盖率恒为 **0/22**。
|
||||||
|
2. **剥前缀**:只剥 `J_Bip` / `mixamorig` / `Bip01`; **不剥** `J_Aim_` / `J_Roll_` / `J_Sec_`
|
||||||
|
(VRoid 的扭转辅助骨,剥了会与真正的骨骼撞名)。
|
||||||
|
3. **变体**: `left/right` 与 `l/r` 互转(`leftupperarm` <-> `lupperarm`)。
|
||||||
|
4. **精确匹配**:归一化+变体后的名字必须与候选表**完全相等**(前缀不匹配),
|
||||||
|
因此 `左足IK`(-> `左足ik`)不会抢占 `左足`(`leftUpperLeg`)。
|
||||||
|
5. **候选表覆盖四类命名**(顺序即优先级;服务端与客户端**逐字一致**,由测试断言):
|
||||||
|
VRoid(`J_Bip_C_Hips` 等)、Mixamo(`mixamorig:LeftArm`)、
|
||||||
|
**MMD 标准骨架(`下半身`/`上半身`/`上半身2`/`首`/`頭`/`左肩`/`左腕`/`左ひじ`/`左手首`/`左足`/`左ひざ`/`左足首`/`左つま先`)**、
|
||||||
|
常见中文名(`臀部`/`头`/`脖子`/`左大臂`/`左小臂`/`左大腿`/`左脚` 等)。
|
||||||
|
6. **结果写入 VRM**:命中结果写成 `VRMC_vrm.humanoid.humanBones` 的**节点索引**(VRM 规范
|
||||||
|
不要求节点名固定,因此以索引为准,不改动几何与贴图);未命中的骨骼保留原名作为附加骨。
|
||||||
|
7. **结论必须上报**: `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`),不影响驱动与评分。
|
||||||
|
|||||||
+30
-22
@@ -60,8 +60,16 @@ static func normalize(node_name: String) -> String:
|
|||||||
var out := ""
|
var out := ""
|
||||||
for i in lower.length():
|
for i in lower.length():
|
||||||
var c := lower[i]
|
var c := lower[i]
|
||||||
|
var code := c.unicode_at(0)
|
||||||
if (c >= "a" and c <= "z") or (c >= "0" and c <= "9"):
|
if (c >= "a" and c <= "z") or (c >= "0" and c <= "9"):
|
||||||
out += c
|
out += c
|
||||||
|
elif code >= 0xff01 and code <= 0xff5e:
|
||||||
|
# 全角 ASCII 折成半角:MMD 常写「足IK」「つま先IK」
|
||||||
|
out += String.chr(code - 0xfee0)
|
||||||
|
elif (code >= 0x3040 and code <= 0x30ff) or (code >= 0x4e00 and code <= 0x9fff) or code == 0x3005:
|
||||||
|
# 中文 / 日文必须保留:MMD 骨名(上半身 / 左ひじ / 首 / 下半身)全是这些字符;
|
||||||
|
# v0.3.0 之前只保留 a-z0-9,MMD 骨名会归一化成空串、humanoid 覆盖率恒为 0/22。
|
||||||
|
out += c
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -100,28 +108,28 @@ static func variants(node_name: String) -> PackedStringArray:
|
|||||||
|
|
||||||
## 每个 canonical 骨骼的候选名(全部为归一化后的形态,含 compact 与 expanded 写法)
|
## 每个 canonical 骨骼的候选名(全部为归一化后的形态,含 compact 与 expanded 写法)
|
||||||
const CANDIDATES := {
|
const CANDIDATES := {
|
||||||
"hips": ["chips", "hips", "hip", "pelvis", "root"],
|
"hips": ["chips", "hips", "hip", "pelvis", "root", "下半身", "腰", "センター", "center", "臀部", "髋", "骨盆"],
|
||||||
"spine": ["cspine", "spine", "spine1", "spinemid"],
|
"spine": ["cspine", "spine", "spine1", "spinemid", "上半身", "脊椎", "脊柱"],
|
||||||
"chest": ["cchest", "chest", "spine2", "spineupper"],
|
"chest": ["cchest", "chest", "spine2", "spineupper", "上半身2", "胸部", "胸"],
|
||||||
"upperChest": ["cupperchest", "upperchest", "chest2", "spine3"],
|
"upperChest": ["cupperchest", "upperchest", "chest2", "spine3", "上半身3"],
|
||||||
"neck": ["cneck", "neck", "neck1"],
|
"neck": ["cneck", "neck", "neck1", "首", "颈", "脖子"],
|
||||||
"head": ["chead", "head"],
|
"head": ["chead", "head", "頭", "头", "头部"],
|
||||||
"leftShoulder": ["lshoulder", "leftshoulder", "shoulderl", "shoulderleft", "claviclel", "lclavicle"],
|
"leftShoulder": ["lshoulder", "leftshoulder", "shoulderl", "shoulderleft", "claviclel", "lclavicle", "左肩", "左锁骨"],
|
||||||
"leftUpperArm": ["lupperarm", "leftupperarm", "larm", "leftarm", "luparm"],
|
"leftUpperArm": ["lupperarm", "leftupperarm", "larm", "leftarm", "luparm", "左腕", "左大臂", "左上臂", "左臂"],
|
||||||
"leftLowerArm": ["llowerarm", "leftlowerarm", "lforearm", "leftforearm", "lelbow"],
|
"leftLowerArm": ["llowerarm", "leftlowerarm", "lforearm", "leftforearm", "lelbow", "左ひじ", "左肘", "左小臂", "左前臂"],
|
||||||
"leftHand": ["lhand", "lefthand", "lwrist", "leftwrist"],
|
"leftHand": ["lhand", "lefthand", "lwrist", "leftwrist", "左手首", "左手", "左手腕"],
|
||||||
"rightShoulder": ["rshoulder", "rightshoulder", "shoulderr", "shoulderright", "clavicler", "rclavicle"],
|
"rightShoulder": ["rshoulder", "rightshoulder", "shoulderr", "shoulderright", "clavicler", "rclavicle", "右肩", "右锁骨"],
|
||||||
"rightUpperArm": ["rupperarm", "rightupperarm", "rarm", "rightarm", "ruparm"],
|
"rightUpperArm": ["rupperarm", "rightupperarm", "rarm", "rightarm", "ruparm", "右腕", "右大臂", "右上臂", "右臂"],
|
||||||
"rightLowerArm": ["rlowerarm", "rightlowerarm", "rforearm", "rightforearm", "relbow"],
|
"rightLowerArm": ["rlowerarm", "rightlowerarm", "rforearm", "rightforearm", "relbow", "右ひじ", "右肘", "右小臂", "右前臂"],
|
||||||
"rightHand": ["rhand", "righthand", "rwrist", "rightwrist"],
|
"rightHand": ["rhand", "righthand", "rwrist", "rightwrist", "右手首", "右手", "右手腕"],
|
||||||
"leftUpperLeg": ["lupperleg", "leftupperleg", "lthigh", "leftthigh", "lupleg", "lhip", "lefthip"],
|
"leftUpperLeg": ["lupperleg", "leftupperleg", "lthigh", "leftthigh", "lupleg", "lhip", "lefthip", "左足", "左大腿"],
|
||||||
"leftLowerLeg": ["llowerleg", "leftlowerleg", "lleg", "leftleg", "lshin", "leftshin", "lknee", "leftknee"],
|
"leftLowerLeg": ["llowerleg", "leftlowerleg", "lleg", "leftleg", "lshin", "leftshin", "lknee", "leftknee", "左ひざ", "左膝", "左小腿", "左膝盖"],
|
||||||
"leftFoot": ["lfoot", "leftfoot", "lankle", "leftankle"],
|
"leftFoot": ["lfoot", "leftfoot", "lankle", "leftankle", "左足首", "左脚", "左脚踝"],
|
||||||
"leftToes": ["ltoes", "lefttoes", "ltoe", "lefttoe", "ltoebase", "lefttoebase"],
|
"leftToes": ["ltoes", "lefttoes", "ltoe", "lefttoe", "ltoebase", "lefttoebase", "左つま先", "左脚趾"],
|
||||||
"rightUpperLeg": ["rupperleg", "rightupperleg", "rthigh", "rightthigh", "rupleg", "rhip", "righthip"],
|
"rightUpperLeg": ["rupperleg", "rightupperleg", "rthigh", "rightthigh", "rupleg", "rhip", "righthip", "右足", "右大腿"],
|
||||||
"rightLowerLeg": ["rlowerleg", "rightlowerleg", "rleg", "rightleg", "rshin", "rightshin", "rknee", "rightknee"],
|
"rightLowerLeg": ["rlowerleg", "rightlowerleg", "rleg", "rightleg", "rshin", "rightshin", "rknee", "rightknee", "右ひざ", "右膝", "右小腿", "右膝盖"],
|
||||||
"rightFoot": ["rfoot", "rightfoot", "rankle", "rightankle"],
|
"rightFoot": ["rfoot", "rightfoot", "rankle", "rightankle", "右足首", "右脚", "右脚踝"],
|
||||||
"rightToes": ["rtoes", "righttoes", "rtoe", "righttoe", "rtoebase", "righttoebase"],
|
"rightToes": ["rtoes", "righttoes", "rtoe", "righttoe", "rtoebase", "righttoebase", "右つま先", "右脚趾"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user