前項で触れた通り、基本的なボーン設定はvroidposeデータ内の「BoneDefinition」に入っています。
ただ、指(手)の形だけはVRoidStudioで指定したテンプレートが保存されており、ボーンとしては保存されていません。
なのでこのテンプレートをボーンの値に変換する必要があります(他のボーンはVRoidStudioでも使えるように書き出してくれているのに指だけは出してくれてないのは何か意図がありそうだけど……)。
vroidposeデータ内の以下が指の形に関係ある部分です。
・LeftHandAnimationName:左手に適用しているテンプレート
・LeftHandAnimationWeight:左手のテンプレートを適用している度合い
・RightHandAnimationName:右手に適用しているテンプレート
・RightHandAnimationWeight:右手のテンプレートを適用している度合い
これを以下のコードのようにボーン変換します。
/**
* VRoidStudioでの手のテンプレートから手のボーンに反映するための値
* https://ringo.ciao.jp/cont/view/20240305/ の値を一部改変して利用
*/
const HAND_TEMPLATE = {
Natural: {
Thumb: {
Metacarpal: [0, -0.11, 0.3],
Proximal: [0, -0.42, 0],
Distal: [-0.11, 0.16, -0.09],
},
Index: {
Proximal: [0, -0.05, 0.14],
Intermediate: [0, 0, 0.18],
Distal: [0, 0, 0.18],
},
Middle: {
Proximal: [0, 0, 0.38],
Intermediate: [0, 0, 0.18],
Distal: [0, 0, 0.36],
},
Ring: {
Proximal: [0, 0.05, 0.59],
Intermediate: [0, 0, 0.31],
Distal: [0, 0, 0.33],
},
Little: {
Proximal: [0, 0.06, 0.72],
Intermediate: [0, 0, 0.41],
Distal: [0, 0, 0.76],
},
},
Grip: {
Thumb: {
Metacarpal: [0, -0.3, 0.5],
Proximal: [0, -0.8, 0],
Distal: [0, -1.5, 0],
},
Index: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0.5, 0, 1.5],
},
Middle: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Ring: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Little: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
},
Open: {
Thumb: {
Metacarpal: [0, 0.05, -0.05],
Proximal: [0, 0.1, 0],
Distal: [0, 0.15, 0],
},
Index: {
Proximal: [0, 0.2, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Middle: {
Proximal: [0, 0, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Ring: {
Proximal: [0, -0.2, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Little: {
Proximal: [0, -0.4, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
},
Good: {
Thumb: {
Metacarpal: [0, 0.1, -0.1],
Proximal: [0, 0.2, 0],
Distal: [0, 0.3, 0],
},
Index: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0.5, 0, 1.5],
},
Middle: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Ring: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Little: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
},
V: {
Thumb: {
Metacarpal: [0.53, -0.3, 0.5],
Proximal: [0, -0.8, 0],
Distal: [0, -1.2, 0],
},
Index: {
Proximal: [0, 0.5, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Middle: {
Proximal: [0, 0, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Ring: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Little: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
},
Gao: {
Thumb: {
Metacarpal: [0, 0, 0.5],
Proximal: [0, 0, 0],
Distal: [0, -1.5, 0],
},
Index: {
Proximal: [0, 0.2, -0.4],
Intermediate: [0, 0, 0.8],
Distal: [0, 0, 1],
},
Middle: {
Proximal: [0, 0, -0.4],
Intermediate: [0, 0, 0.8],
Distal: [0, 0, 1],
},
Ring: {
Proximal: [0, -0.2, -0.4],
Intermediate: [0, 0, 0.8],
Distal: [0, 0, 1],
},
Little: {
Proximal: [0, -0.4, -0.4],
Intermediate: [0, 0, 0.8],
Distal: [0, 0, 1],
},
},
Index: {
Thumb: {
Metacarpal: [0.5, -0.3, 0.5],
Proximal: [0, -0.8, 0],
Distal: [0, -1.2, 0],
},
Index: {
Proximal: [0, 0, 0],
Intermediate: [0, 0, 0],
Distal: [0, 0, 0],
},
Middle: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Ring: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
Little: {
Proximal: [0, 0, 1.2],
Intermediate: [0, 0, 1.5],
Distal: [0, 0, 1.5],
},
},
};
// ポーズデータを読み込んだら、それをvrmに適用する
document.getElementById("poseDataInput").addEventListener('change', (event)=>{
const file = event.target.files[0];
if(file && vrm){
// ファイルを読み込む
const reader = new FileReader();
reader.onload = () => {
/* ~通常(指以外)のボーン読み込み処理は省略~ */
// 指の形を取り込む
setHandPose(poseData.LeftHandAnimationName, poseData.LeftHandAnimationWeight);
setHandPose(poseData.RightHandAnimationName, poseData.RightHandAnimationWeight);
};
reader.readAsText(file);
}
});
// 指の形を取り込む
function setHandPose(handAnimationName, weight){
if(!handAnimationName) return;
const handSide = handAnimationName.includes('L_') ? 'left' : 'right';
const handName = handAnimationName.replace(/^[LR]_/, "");
const handConfig = HAND_TEMPLATE[handName];
for(const finger in handConfig){
for(const joint in handConfig[finger]) {
const boneName = `${handSide}${finger}${joint}`;
const [rx, ry, rz] = handConfig[finger][joint];
const euler = (handSide === 'left')
? new THREE.Euler(rx * weight, ry * weight, rz * weight)
: new THREE.Euler(rx * weight, -ry * weight, -rz * weight);
const quat = new THREE.Quaternion().setFromEuler(euler);
// vrmにポーズを適用
const vrmBone = vrm.humanoid.getNormalizedBoneNode(boneName);
if(vrmBone){
vrmBone.quaternion.set(quat.x, quat.y, -quat.z, -quat.w);
}
}
}
}
左手と右手でほぼ同じ処理なので、「setHandPose」というメソッドでまとめています。
まずHAND_TEMPLATEというところで、「手の形のテンプレート」と「指のボーン」の対応表を作成しておきます。この値はコード中にも書いていますが、こちらの方が出した値をベースにしています。
この対応表に従い、「手の形のテンプレート」を「指のボーン」に変換してあとはそのボーンをVRoidに適用すればOKです。
ボーンに変換する前に「LeftHandAnimationWeight / RightHandAnimationWeight」のWeight値を掛けるのを忘れないようにしましょう。上のコードにある通り、Weight値はそのまま掛けてしまって良さそうです。