#27 pixiv/three-vrmでVRMに対応したIKを実装する https://gyazo.com/2171d7382360ed22cb113ecbab2724c9
基本情報
ソースコード
デモ
そのIKだけを取り出したサンプルを作成します。
実のところ、今回ほぼ全てのコードを書き直しました…
CCD-IK
IKのアルゴリズムとしてCCD(Cyclic-Coordinate-Descent)法を使用しました。
CCD-IKでは各関節ごとに最適化を行います。
まず、手先(エフェクタ)に近い関節から処理を行います。
手先位置が目標位置に最も近くなるように注目関節を回転させます。
そして注目関節の親関節にも同じ処理をしていきます。
https://gyazo.com/3c3c96bf7a3d26fa7a15969cbc5b84d9
根本まで到達したら同じ処理を再び手先側から行います。
これを繰り返すことで手先位置と目標位置の差を最小化していきます。
IK実装
関節ごとの1ステップの処理はこんな感じです。
目標位置のワールド座標を取得
注目関節のワールド座標を取得
注目関節からエフェクタまでのベクトルを求める
注目関節座標系に変換
正規化
注目関節から目標位置までのベクトルを求める
注目関節座標系に変換
正規化
2つのベクトル間の回転角を求める
回転軸を求める
注目関節を回転
そして次の関節へ処理を移動します。
これをコード化するとこんな感じです。
code:IKSolver.ts
// 注目関節のワールド座標・姿勢等を取得する
joint.bone.matrixWorld.decompose(_jointPosition, _jointQuaternionInverse, _jointScale);
_jointQuaternionInverse.invert();
// 注目関節 -> エフェクタのベクトル
ikChain.effector.getWorldPosition(_effectorPosition);
_joint2EffectorVector.subVectors(_effectorPosition, _jointPosition);
_joint2EffectorVector.applyQuaternion(_jointQuaternionInverse);
_joint2EffectorVector.normalize();
// 注目関節 -> 目標位置のベクトル
_joint2GoalVector.subVectors(_goalPosition, _jointPosition);
_joint2GoalVector.applyQuaternion(_jointQuaternionInverse);
_joint2GoalVector.normalize();
// 回転角
let deltaAngle = _joint2GoalVector.dot(_joint2EffectorVector);
if (deltaAngle > 1.0) {
deltaAngle = 1.0;
} else if (deltaAngle < -1.0) {
deltaAngle = - 1.0;
}
deltaAngle = Math.acos(deltaAngle);
// 回転軸
_axis.crossVectors(_joint2EffectorVector, _joint2GoalVector);
_axis.normalize();
// 回転
_quarternion.setFromAxisAngle(_axis, deltaAngle);
joint.bone.quaternion.multiply(_quarternion);
joint.bone.updateMatrixWorld(true);
角度制限
このままだと肘や膝が逆に曲がったりしてしまうので、関節の回転角度を制限します。
関節の最小・最大角度をオイラー角で設定します。
いったん関節を回転させた後、最小・最大角度を適用します。
また、オイラー角の回転順序も同時に指定します。
code:ts
// 回転
_quarternion.setFromAxisAngle(_axis, deltaAngle);
joint.bone.quaternion.multiply(_quarternion);
// 回転角・軸制限
joint.bone.rotation.setFromVector3(
joint.bone.rotation.toVector3(_vector).max(joint.rotationMin).min(joint.rotationMax), joint.order
);
VRMSchema.HumanoidBoneNameを参照して各関節ごとの回転角度制限を設定します。
肘は左右で回転方向を逆にしたいのでLowerArmのY軸の値を反転させています。
code:iconfig.ts
// Right Shoulder -> Hand
{
jointConfigs: [
{
boneName: VRMSchema.HumanoidBoneName.RightLowerArm,
order: 'YZX',
rotationMin: new Vector3(0, (0.1 / 180) * Math.PI, 0),
rotationMax: new Vector3(0, Math.PI, 0),
},
{
boneName: VRMSchema.HumanoidBoneName.RightUpperArm,
order: 'ZXY',
rotationMin: new Vector3(-Math.PI / 2, -Math.PI, -Math.PI),
rotationMax: new Vector3(Math.PI / 2, Math.PI, Math.PI),
},
{
boneName: VRMSchema.HumanoidBoneName.RightShoulder,
order: 'ZXY',
rotationMin: new Vector3(0, -(45 / 180) * Math.PI, 0),
rotationMax: new Vector3(0, (45 / 180) * Math.PI, (45 / 180) * Math.PI),
},
],
effectorBoneName: VRMSchema.HumanoidBoneName.RightHand
},
TransformControls
IKは実装できたので目標位置を移動できるようにコントローラを追加します。
目標位置として設定してあるオブジェクトにアタッチするだけで使えます。
カメラ操作にOrbitControlを使用している場合はマウス操作が競合するのでコントローラをドラッグ中は無効にするようにします。
code:UI.ts
export const setupIKController = (viewer: Viewer, avatar: Avatar) => {
avatar.vrmIK.ikChains.forEach(chain => {
const transCtrl = new TransformControls(viewer.camera, viewer.canvas);
transCtrl.attach(chain.goal);
transCtrl.addEventListener('dragging-changed', event => {
viewer.orbitControl.enabled = !event.value;
});
avatar.vrm.scene.add(transCtrl);
});
}
とりあえずこれでIKを使ったポージングが出来るようになりました。
https://gyazo.com/2171d7382360ed22cb113ecbab2724c9
実際に下記のリンクで試すことで出来ます。
結局、VRMお手軽ポーズからコードは全部書き直しになってしまいました…。
書き直した方が早いかなって思ってしまったので。
ただ、作り直したおかげで関節角度の制限などが指定しやすくなったので、肘の動きとかが良くなったかなと思います。
参考