melでのvector活用術 ~その2~ RotatePlane IKを作ってみる②

おはこんばんちわ、絶賛リモートワーク継続中の山本ほっさんです。

前回の記事にひきつづき、melのvector型を活用してRotate plane IKを作成するようすについて、ご紹介いたします。

~~~
※この記事のシリーズは全三回を予定しております。他の記事は以下の通りです。
第一回:RotatePlaneIKを作ってみる① 「ジョイントをIKハンドルの位置で動かしてみる」
第二回:RotatePlaneIKを作ってみる① 「RotatePlane制御を実装してみる」←いまここ!
第三回:準備中!
~~~

前回は、平面上でのIKハンドルの挙動によって、骨を動かすところまで実装しました。

しかし、poleVectorの制御や、平面外のIKハンドルの挙動など、
 
まだまだ未実装の個所がある状態でした。

今回はpoleVectorの制御について、前回同様にmelのvector型を使いながら実装していきたいと思います。

そもそもRotatePlaneって何?

日本語で言うと「回転平面」にあたります。

肩・肘・手首の「頂点」=ポイントとして考えると、そこには三角形を描くことができます。

三角形は必ず平面になるので、RotatePlaneIKは「肩肘はかならずこの平面上で回転する」という考え方で回転の軸方向を制御をする方法なわけです。

つまり、この平面に対して、必ず直角な軸で肩肘を回転させればよいのです。

poleVectorとIKハンドルのベクトルから回転の軸方向を計算する

使うのは、PoleVectorを表すロケーターまでのベクトルと、IKハンドルまでのベクトルです。

この画像からわかるように、ここに三角形を描くことができます。

これが今回のRotatePlaneにあたります。

このRotatePlaneに垂直なベクトルがわかれば、回転軸がわかるわけですが、

ベクトル…
直角…

この二つのキーワードが出てきたときは、ベクトルの外積を使います。

ベクトルの外積・内積について

軽くベクトルの外積・内積に触れておくと

外積

二つのベクトルに垂直なベクトルを算出します。

内積

ベクトルAをベクトルBに成分分解したときの、そのベクトルの長さを算出します。

poleVectorのロケーターまでのベクトルと、IKハンドルまでのベクトルの外積ベクトルを計算すれば、RotatePlaneの挙動を再現できそうです。

ここまでをリグに実装してみよう

前回の実装で、

//IKctrlの位置ベクトルを取得
vector $IKPosVector = <<IKpos_decMatrix.outputTranslateX,
                    IKpos_decMatrix.outputTranslateY,
                    IKpos_decMatrix.outputTranslateZ>>;
//肩ジョイントの位置ベクトルを取得
vector $shoulderVector = <<shoulder.translateX,
                    shoulder.translateY,
                    shoulder.translateZ>>;
//肘ジョイントの位置ベクトルを取得
vector $elbowVector = <<elbow.translateX,
                    elbow.translateY,
                    elbow.translateZ>>;
//手首ジョイントの位置ベクトルを取得
vector $handVector = <<hand.translateX,
                    hand.translateY,
                    hand.translateZ>>;
//肩からIKctrlまでの長さを取得
vector $Shld_IKPosVector = $IKPosVector - $shoulderVector;
float $IKposDistance = mag($Shld_IKPosVector);
//肘ジョイントの長さを取得
float $elbowLength = mag($elbowVector);
//手首ジョイントの長さを取得
float $handLength = mag($handVector);

float $shoulderAng = 0.0;
float $elbowAng = 0.0;
float $elbowAng = 0.0;
if ($IKposDistance<$elbowLength+$handLength){
    //肩の回転角度を取得
    $shoulderAng = acosd(($IKposDistance * $elbowLength/($elbowLength+$handLength))/ $elbowLength);
    //手首の角の角度を取得
    $handAng = acosd(($IKposDistance * $handLength/($elbowLength+$handLength))/ $handLength);
    //肘の回転角度を取得
    $elbowAng = ($shoulderAng + $handAng)*-1.0;
}

shoulder.rotateY = $shoulderAng;
elbow.rotateY = $elbowAng;

ここまでのエクスプレッションと、

ここまでのノード接続が組めているので、ここに新しく処理を挿入してみます。

  • ベクトルの外積を計算する
  • まず、肩のジョイントからpoleVectorのロケーターまでのベクトルを計算します。

    vector $Shld_VECPosVector = $VECPosVector - $shoulderVector;

    次に、poleVectorのベクトルとIKハンドルのベクトルの外積を計算します。
    melはベクトルの外積・内積をスクリプトでサポートしているので、

    vector $crossAngle = cross($Shld_IKPosVector,$Shld_VECPosVector);

    こんな感じで、お手軽に計算することができます。

    melの外積・内積のドキュメントはこちら…↓
    ・外積(cross)
    ・内積(dot)

    回転軸を可視化してみる

    念のため、正しい軸方向が取得できているのか、確認してみましょう。
    肩ジョイントと同階層にロケーターを置き、

    「rotateAxis_loc」などと名前を付けておきます。
    このロケーターのtranslateにベクトルの値を接続します。

    //test
    rotateAxis_loc.translateX = $crossAngle.x/mag($crossAngle);
    rotateAxis_loc.translateY = $crossAngle.y/mag($crossAngle);
    rotateAxis_loc.translateZ = $crossAngle.z/mag($crossAngle);

    ※あまり遠くに行かないよう、1の長さのベクトルになるよう事前にベクトルの長さで割っています。

    これでpoleVectorを動かしたとき、ロケーターがジョイントの回転軸方向に位置するように挙動するはずですが…。

    どうでしょう。
    正しい軸方向が取得できているように見えます。

    このベクトルを軸方向として、前回計算した回転値で回転すればrotatePlaneの制御が実装できそうです。

  • AshoulderAngleToQuatノードを使って、回転軸と回転をガッチャンコ
  • 回転軸と回転値から回転結果を取得するには。「shoulderAngleToQuat」ノードが便利です。
    その名の通り、回転軸(Axis)と回転数(Angle)を渡すことでQuaternionに変換することができます。

    ノードエディタでAxisAngleToQuatノードを、肩・肘に接続するよう二つ生成します。

    名前を「shoulderAngleToQuat」「elbowAngleToQuat」などとしておきます。

    次にエクスプレッションで、これらの「shoulderAngleToQuat」ノードの「Axis」と「Angle」に対して、それぞれ接続を行います。

    まず軸方向

    //quat回転
    shoulderAngleToQuat.inputAxisX = $crossAngle.x;
    shoulderAngleToQuat.inputAxisY = $crossAngle.y;
    shoulderAngleToQuat.inputAxisZ = $crossAngle.z;
    shoulderAngleToQuat.inputAngle = $shoulderAng;
    
    elbowAngleToQuat.inputAxisX = $crossAngle.x;
    elbowAngleToQuat.inputAxisY = $crossAngle.y;
    elbowAngleToQuat.inputAxisZ = $crossAngle.z;
    elbowAngleToQuat.inputAngle = $elbowAng;

    ここの「$shoulderAng」「$elbowAng;」は、前回計算した肩と肘の回転値になります。

    ノードエディタで確認すると

    こんな感じです。

  • QuatToEulerノードでジョイントに回転を接続

    あとは、QuaternionをEuler回転に変換して、各々rotateに接続すれば完了です。
    QUaternionをEulerに変換するには「QuatToEuler」ノードが便利です。
    ノードエディタで、肩・肘分のQuatToEulerノードを生成して、

    名前を「shoulderRotateOutput」「elbowRotateOutput」などとしておき、各々Quaternionの値をinputQuatに接続します。

    そして、各々ジョイントのrotateにoutputRotateを接続します。

    そして動かしてみると…

    このとおり!
    ハラショー!!

しかし…?

ここまでの実装で、一見IK挙動としては完成したように見えますが…。
ためしに、メッシュを階層に入れてみて、

回してみると…

…コレジャナイ。

想像してたのは

…こんな動きだったの。

何故このような挙動になったのでしょうか。

回転平面の回転を考える

今までの実装を振り返ってみると、poleVectorとIKハンドルvectorによって、回転する軸方向はわかったものの、骨に対ししてroll成分の回転をまだ与えていなかったことがわかります。

回転平面は外積によって判明していますが、

このように、「軸方向の変化」を「回転平面が回転した」として考えると、まだ考慮していなかった回転成分が見えてくると思います。

さて、この回転成分をどのように算出して、どのようにジョイントに接続したらよいのでしょうか。


今回はそろそろ文章が長くなってきましたので、この辺で。
次回、残す回転平面の回転成分の問題について解説して、RotatePlaneIKリグを完成させたいと思います。

いや~、リグって本当に面白いもんですね。
それでは、さよなら!さよなら!さよなら!!

Author: tomohito.yamamoto