feat(models): 骨骼重绑定——修复 MMD 模型 humanoid 恒为 0/22(契约 skeleton-topology 0.2.0)
根因: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。
This commit is contained in:
@@ -12,7 +12,7 @@ export const CONTRACT_VERSIONS: Readonly<Record<string, string>> = Object.freeze
|
||||
'rest-api': '0.4.0',
|
||||
'websocket-keypoints': '0.1.0',
|
||||
'data-models': '0.5.0',
|
||||
'skeleton-topology': '0.1.0',
|
||||
'skeleton-topology': '0.2.0',
|
||||
'model-formats': '0.5.0',
|
||||
database: '0.3.0',
|
||||
});
|
||||
|
||||
@@ -143,21 +143,51 @@ export const STRIPPABLE_PREFIXES: readonly string[] = ['jbip', 'mixamorig', 'bip
|
||||
* 与客户端 `humanoid_map.gd` 的 CANDIDATES 逐字一致(测试会逐键比对)。
|
||||
*/
|
||||
export const HUMANOID_CANDIDATES: Record<string, string[]> = {
|
||||
hips: ['chips', 'hips', 'hip', 'pelvis', 'root'],
|
||||
spine: ['cspine', 'spine', 'spine1', 'spinemid'],
|
||||
chest: ['cchest', 'chest', 'spine2', 'spineupper'],
|
||||
upperChest: ['cupperchest', 'upperchest', 'chest2', 'spine3'],
|
||||
neck: ['cneck', 'neck', 'neck1'],
|
||||
head: ['chead', 'head'],
|
||||
leftShoulder: ['lshoulder', 'leftshoulder', 'shoulderl', 'shoulderleft', 'claviclel', 'lclavicle'],
|
||||
leftUpperArm: ['lupperarm', 'leftupperarm', 'larm', 'leftarm', 'luparm'],
|
||||
leftLowerArm: ['llowerarm', 'leftlowerarm', 'lforearm', 'leftforearm', 'lelbow'],
|
||||
leftHand: ['lhand', 'lefthand', 'lwrist', 'leftwrist'],
|
||||
rightShoulder: ['rshoulder', 'rightshoulder', 'shoulderr', 'shoulderright', 'clavicler', 'rclavicle'],
|
||||
rightUpperArm: ['rupperarm', 'rightupperarm', 'rarm', 'rightarm', 'ruparm'],
|
||||
rightLowerArm: ['rlowerarm', 'rightlowerarm', 'rforearm', 'rightforearm', 'relbow'],
|
||||
rightHand: ['rhand', 'righthand', 'rwrist', 'rightwrist'],
|
||||
leftUpperLeg: ['lupperleg', 'leftupperleg', 'lthigh', 'leftthigh', 'lupleg', 'lhip', 'lefthip'],
|
||||
hips: ['chips', 'hips', 'hip', 'pelvis', 'root',
|
||||
// MMD / 中文常见名
|
||||
'下半身', '腰', 'センター', 'center', '臀部', '髋', '骨盆',],
|
||||
spine: ['cspine', 'spine', 'spine1', 'spinemid',
|
||||
// MMD / 中文常见名
|
||||
'上半身', '脊椎', '脊柱',],
|
||||
chest: ['cchest', 'chest', 'spine2', 'spineupper',
|
||||
// MMD / 中文常见名
|
||||
'上半身2', '胸部', '胸',],
|
||||
upperChest: ['cupperchest', 'upperchest', 'chest2', 'spine3',
|
||||
// MMD / 中文常见名
|
||||
'上半身3',],
|
||||
neck: ['cneck', 'neck', 'neck1',
|
||||
// MMD / 中文常见名
|
||||
'首', '颈', '脖子',],
|
||||
head: ['chead', 'head',
|
||||
// MMD / 中文常见名
|
||||
'頭', '头', '头部',],
|
||||
leftShoulder: ['lshoulder', 'leftshoulder', 'shoulderl', 'shoulderleft', 'claviclel', 'lclavicle',
|
||||
// MMD / 中文常见名
|
||||
'左肩', '左锁骨',],
|
||||
leftUpperArm: ['lupperarm', 'leftupperarm', 'larm', 'leftarm', 'luparm',
|
||||
// MMD / 中文常见名
|
||||
'左腕', '左大臂', '左上臂', '左臂',],
|
||||
leftLowerArm: ['llowerarm', 'leftlowerarm', 'lforearm', 'leftforearm', 'lelbow',
|
||||
// MMD / 中文常见名
|
||||
'左ひじ', '左肘', '左小臂', '左前臂',],
|
||||
leftHand: ['lhand', 'lefthand', 'lwrist', 'leftwrist',
|
||||
// MMD / 中文常见名
|
||||
'左手首', '左手', '左手腕',],
|
||||
rightShoulder: ['rshoulder', 'rightshoulder', 'shoulderr', 'shoulderright', 'clavicler', 'rclavicle',
|
||||
// MMD / 中文常见名
|
||||
'右肩', '右锁骨',],
|
||||
rightUpperArm: ['rupperarm', 'rightupperarm', 'rarm', 'rightarm', 'ruparm',
|
||||
// MMD / 中文常见名
|
||||
'右腕', '右大臂', '右上臂', '右臂',],
|
||||
rightLowerArm: ['rlowerarm', 'rightlowerarm', 'rforearm', 'rightforearm', 'relbow',
|
||||
// MMD / 中文常见名
|
||||
'右ひじ', '右肘', '右小臂', '右前臂',],
|
||||
rightHand: ['rhand', 'righthand', 'rwrist', 'rightwrist',
|
||||
// MMD / 中文常见名
|
||||
'右手首', '右手', '右手腕',],
|
||||
leftUpperLeg: ['lupperleg', 'leftupperleg', 'lthigh', 'leftthigh', 'lupleg', 'lhip', 'lefthip',
|
||||
// MMD / 中文常见名
|
||||
'左足', '左大腿',],
|
||||
leftLowerLeg: [
|
||||
'llowerleg',
|
||||
'leftlowerleg',
|
||||
@@ -167,10 +197,17 @@ export const HUMANOID_CANDIDATES: Record<string, string[]> = {
|
||||
'leftshin',
|
||||
'lknee',
|
||||
'leftknee',
|
||||
],
|
||||
leftFoot: ['lfoot', 'leftfoot', 'lankle', 'leftankle'],
|
||||
leftToes: ['ltoes', 'lefttoes', 'ltoe', 'lefttoe', 'ltoebase', 'lefttoebase'],
|
||||
rightUpperLeg: ['rupperleg', 'rightupperleg', 'rthigh', 'rightthigh', 'rupleg', 'rhip', 'righthip'],
|
||||
// MMD / 中文常见名
|
||||
'左ひざ', '左膝', '左小腿', '左膝盖',],
|
||||
leftFoot: ['lfoot', 'leftfoot', 'lankle', 'leftankle',
|
||||
// MMD / 中文常见名
|
||||
'左足首', '左脚', '左脚踝',],
|
||||
leftToes: ['ltoes', 'lefttoes', 'ltoe', 'lefttoe', 'ltoebase', 'lefttoebase',
|
||||
// MMD / 中文常见名
|
||||
'左つま先', '左脚趾',],
|
||||
rightUpperLeg: ['rupperleg', 'rightupperleg', 'rthigh', 'rightthigh', 'rupleg', 'rhip', 'righthip',
|
||||
// MMD / 中文常见名
|
||||
'右足', '右大腿',],
|
||||
rightLowerLeg: [
|
||||
'rlowerleg',
|
||||
'rightlowerleg',
|
||||
@@ -180,9 +217,14 @@ export const HUMANOID_CANDIDATES: Record<string, string[]> = {
|
||||
'rightshin',
|
||||
'rknee',
|
||||
'rightknee',
|
||||
],
|
||||
rightFoot: ['rfoot', 'rightfoot', 'rankle', 'rightankle'],
|
||||
rightToes: ['rtoes', 'righttoes', 'rtoe', 'righttoe', 'rtoebase', 'righttoebase'],
|
||||
// MMD / 中文常见名
|
||||
'右ひざ', '右膝', '右小腿', '右膝盖',],
|
||||
rightFoot: ['rfoot', 'rightfoot', 'rankle', 'rightankle',
|
||||
// MMD / 中文常见名
|
||||
'右足首', '右脚', '右脚踝',],
|
||||
rightToes: ['rtoes', 'righttoes', 'rtoe', 'righttoe', 'rtoebase', 'righttoebase',
|
||||
// MMD / 中文常见名
|
||||
'右つま先', '右脚趾',],
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------- 客户端逻辑移植 */
|
||||
@@ -196,7 +238,25 @@ export function normalizeBoneName(nodeName: string): string {
|
||||
const lower = nodeName.toLowerCase();
|
||||
let out = '';
|
||||
for (const ch of lower) {
|
||||
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) out += ch;
|
||||
const code = ch.codePointAt(0) ?? 0;
|
||||
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
|
||||
out += ch;
|
||||
continue;
|
||||
}
|
||||
// 全角 ASCII(A-Z a-z 0-9)折成半角:MMD 常写「足IK」「つま先IK」
|
||||
if (code >= 0xff01 && code <= 0xff5e) {
|
||||
out += String.fromCharCode(code - 0xfee0);
|
||||
continue;
|
||||
}
|
||||
// **必须保留中文与日文**:MMD 骨名(上半身 / 左ひじ / 首 / 下半身)全是这些字符。
|
||||
// v0.3.0 之前只保留 a-z0-9,导致 MMD 模型所有骨名归一化成空串、humanoid 覆盖率恒为 0/22。
|
||||
if (
|
||||
(code >= 0x3040 && code <= 0x30ff) || // 平假名 / 片假名 / 长音符
|
||||
(code >= 0x4e00 && code <= 0x9fff) || // CJK 统一表意文字
|
||||
code === 0x3005 // 々
|
||||
) {
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user