melでのvector活用術 ~その3~ RotatePlaneIKを作ってみる③

おはこんばんちわ、在宅業務につき頻繁に子供を抱っこするようになったので心なしかマッスルになったような気がします、山本ほっさんです。

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

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

前回は、poleVector制御の実装まで行ってみました。

一見するとよさそうな見た目ですが、

このように、ジョイントがまだロールしていません。

今回は、このロール成分の実装を行って、mel(Expression)とベクトルを使ったRotatePlaneIKの実装を完了させたいと思います。


再設計:「回転平面」の「回転」を考える

まず、現在の実装状態を振り返ってみると、確かにジョイントにロールが適応されていませんが、

平面上のIK制御と回転平面の回転は正しく取得できているようです。

そこで、

  • 固定した平面上のIK制御によるジョイント回転
  • 回転平面と一緒に全体を回転

…という風に回転制御を分けて考えることにします。

図に表してみるとこんな感じでしょうか。

イメージとしては、回転平面の回転をIKリグの親空間にしてしまう感じです。


実装

それでは、上記再設計を念頭にリグを再構築していきます。

■IKの平面挙動の調整

第一回目のところでやり残したことがあります。

現在のIK挙動は一直線上の距離のみを処理していて、

上記のように、肩からIKハンドルへの向きが変わった場合は対応していません。

そこで下記図のように、

  • 肩からIKハンドルまでの向きによる肩の挙動
  • 肩からIKハンドルまでの長さによる肩の挙動

を計算して、角度を加算します。

まず、エクスプレッションで肩からIKハンドルまでの向きを表すベクトルの初期値を定義しておきます。

    //規定値の設定
    vector $orgNorlalizeIKPosVector = <<1.0,0.0,0.0>>;//IKハンドルの方向

こんな感じ。

次に、前々回の実装で計算済みの「肩からIKハンドルまでの現在のベクトル」$Shld_IKPosVectorと、先ほど定義した規定値 $orgNorlalizeIKPosVectorを使って、肩からIKハンドルまでの向きによる肩の回転を計算します。

なお、2つのベクトルの織りなす角度の計算は、melではコマンドでサポートされていて、
angle(ベクトル1,ベクトル2) 
…というコマンドで計算できます。 やっぱりお手軽ですね。
しかし、返される値はラジアンなので、度単位にコンバートしてあげないとなりません。
(ラジアンとは…?(wiki))

ラジアンから度単位へのコンバートもコマンドがあるので、
rad_to_deg(ラジアン) 
と書くことで、2つのベクトルの織りなす角度を度単位で取得できます。
よって、エクスプレッションでは…

    //水平方向の肩の回転を計算
    float $horizonAng =  rad_to_deg(angle($orgNorlalizeIKPosVector,$Shld_IKPosVector));

このように表記して、変数 $horizonAng にIKハンドルの方向による角度を格納しておきます。

また、Zマイナス方向にIKハンドルが移動した場合マイナスの角度がほしいですが、rad_to_degではマイナスの角度を返すことができません。
なので、

    if($IKPosVector.z>0) $horizonAng =  $horizonAng*-1;

上記のように、反転した場合のif分を挟んでマイナス方向の角度をサポートしておきます。

あとは、前々回の三角関数の計算で求まった肩の回転角度 $shoulderAng に加算して、

    $shoulderAng = $shoulderAng + $horizonAng;

としておきます。

今回のIK挙動における回転軸は?
なお今回のIK挙動は回転平面の変化を考慮せず、単一平面上で考えればいいので、poleVectorの座標は考慮する必要がなくなり、ある定数の軸方向になります。 よって、今回の回転軸は、ZX平面上にIKを作っているので、

    vector $rotateAxis = <<0.0,1.0,0.0>>;//回転軸の方向

…という規定値として定義しておきます。

あとは、ここまでで計算された、

  • $rotateAxis
  • $shoulderAng

そして、前々回計算しておいた、肘の回転

  • $elbowAng

を、前回ジョイントに接続した axisAngleToQuatノード

  • shoulderAngleToQuat
  • elbowAngleToQuat

に接続して、ジョイントへ角度を渡します。

エクスプレッションでは

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

    elbowAngleToQuat.inputAxisX = $rotateAxis.x;
    elbowAngleToQuat.inputAxisY = $rotateAxis.y;
    elbowAngleToQuat.inputAxisZ = $rotateAxis.z;
    elbowAngleToQuat.inputAngle = $elbowAng;

こんな感じ。

挙動は

ウン。良さそう。
ただ、これで回転平面の挙動をいったん切断したので、

当然のことながら、このようにpoleVectorのロケーターを操作しても反応しません。

■回転平面の回転を取得する

回転平面の法線の向きを表すベクトルは、前回の実装の時点で計算済みです。

    //回転平面の取得
    vector $Shld_VECPosVector = $VECPosVector - $shoulderVector;
    vector $crossAngle = cross($Shld_IKPosVector,$Shld_VECPosVector);

回転平面回転は、この法線ベクトルがどれくらい動いたかを見て計算取得します。

①初期値のベクトルと現在のベクトルの織りなす角度を求め、
②外積を使って、その回転軸を求めてみます。

  • ①初期値のベクトルと現在のベクトルの織りなす角度を求める
    まず、回転平面の法線ベクトル、すなわち$crossAngleの初期値を決めます。
    今回のIKリグはZX平面上に構築されているので、$crossAngleの初期値は、

        //回転平面の回転を取得
        vector $orgRotateAngle = <<0.0,1.0,0.0>>; //回転平面の初期値

    となります。 先ほどと同様に angle(ベクトル1,ベクトル2)を使って2つのベクトルの織りなす角度を計算します。
    帰ってくる値はラジアンなので、

        float $rotateplaneAngle = rad_to_deg(angle($orgRotateAngle,$crossAngle));

    このように表記し、回転平面の回転角度を float $rotateplaneAngle に格納しておきます。

  • ②回転軸を求める
    上記で求めた回転を適応するためには、どういうベクトルを軸に回転しているのか調べる必要があります。


    上記の画像からわかるように、回転軸は先ほど回転角度を求めた2つのベクトルに直角なベクトルに相当することがわかります。
    つまり、$orgRotateAngleと$crossAngleの外積を計算すればよいので…

        vector $rotatePlaneRotateAxis = cross($orgRotateAngle,$crossAngle);

    こうなります。

これで回転平面の回転を取得することができました。

■IKハンドルの座標から回転平面の回転成分を打ち消す

今回、回転平面とIKハンドルによるIKの平面挙動を別々に考えるため、IKハンドルの移動成分から
回転平面の回転成分をあらかじめ打ち消しておく必要があります。

そこで、先ほど計算した回転平面の回転成分をクォータニオンとマトリクスに変換します。

NodeEditorから「AxisAngleToQuat」ノードを作り、「rotatePlaneAngle」とか名前を付けておきます。

次にこのノードのinputAxisとinputAngleに対して、先ほど計算した回転と回転軸を接続します。

    rotatePlaneAngle.inputAxisX = $rotatePlaneRotateAxis.x;
    rotatePlaneAngle.inputAxisY = $rotatePlaneRotateAxis.y;
    rotatePlaneAngle.inputAxisZ = $rotatePlaneRotateAxis.z;
    rotatePlaneAngle.inputAngle = $rotateplaneAngle;

これで回転平面の回転成分をクォータニオンにすることができました。

次に、同じくノードエディタで「composeMatrix」ノードをつくり、

先ほどの「rotatePlaneAngle」を接続させます。

この時注意すべきなのが、「composeMatrix」のアトリビュート

file

「Use Euler Rotation」のチェックを外しておくことです。
ここのチェックを外しておかないと、Quaternionからマトリクスを計算してくれません。

これで回転平面の回転成分をマトリクスにすることができました。

あとは、IKハンドルのマトリクスから上記マトリクスを打ち消します。
マトリクスを打ち消すには逆マトリクスにして掛け算すればよいので、

inverseMatrixノードとmultMatrixノードを作り、

file

inverseMatrixノードと回転平面の回転マトリクスを接続し、

file

multMatrixノードに、前々回作成した「IKpos_decMatrix」ノードに接続しているマトリクスと、上記のinversreMatrixノードを順に接続します。

file

あとは、このマトリクスからIKハンドルの座標を取得できるようにしたいので、decomposeMatrixノードを作り、「IKpos_decLocalMatrix」などの名前を付けておきます。

後は、先ほどのmultMatrixノードとつなげれば、OKです。

file

これでIKハンドルの座標取得の準備は完了です。

■回転平面空間用とIK空間用でエクスプレッションを分ける

これまでの実装で、角度計算などの処理を一つのエクスプレッションにまとめていました。
しかし、今のままの構造だとサイクルが発生してしまいます。

  • 回転平面空間の計算用
  • IK空間の計算用

としてエクスプレッションを分け、サイクルを回避したいと思います。

つまり、どういうことかというと…

これが前回まで状態です。
一つのエクスプレッションに対して、IKとpoleVectorのロケーターが接続して、骨に対して回転制御を行っています。

これを今回の、「回転平面の空間」を親として「IK空間の制御」を行う実装を行うと…

こんな感じで、一つのエクスプレッションノードを介してサイクルが発生してしまうわけです。

ですので、こんな風に

回転平面の空間用のエクスプレッションとIK空間用のエクスプレッションに分けることによって、サイクルを回避します。

今まで一つだったエクスプレッションを「rotatePlaneSpace_Expression」「ikSpace_Expression」などと名前を付け、以下のように処理を分けます。

ikSpace_Expression

    //規定値の設定
    vector $orgNorlalizeIKPosVector = <<1.0,0.0,0.0>>;//IKハンドルの方向
    vector $rotateAxis = <<0.0,1.0,0.0>>;//回転軸の方向

    //IKctrlの位置ベクトルを取得
    vector $IKPosVector = unit(<<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);

    //IKによる平面上の回転を計算
    float $shoulderAng = 0.0;
    float $handAng = 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;
    }

    //水平方向の肩の回転を計算
    float $horizonAng =  rad_to_deg(angle($orgNorlalizeIKPosVector,$Shld_IKPosVector));
    if($IKPosVector.z>0) $horizonAng =  $horizonAng*-1;
    $shoulderAng = $shoulderAng + $horizonAng;

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

    elbowAngleToQuat.inputAxisX = $rotateAxis.x;
    elbowAngleToQuat.inputAxisY = $rotateAxis.y;
    elbowAngleToQuat.inputAxisZ = $rotateAxis.z;
    elbowAngleToQuat.inputAngle = $elbowAng;

rotatePlaneSpace_Expression

    //規定値の設定
    vector $orgRotateAngle = <<0.0,1.0,0.0>>; //回転平面の初期値

    //IKctrlの位置ベクトルを取得
    vector $IKPosVector = <<IKpos_decMatrix.outputTranslateX,
                        IKpos_decMatrix.outputTranslateY,
                        IKpos_decMatrix.outputTranslateZ>>;

    //VECctrlの位置ベクトルを取得
    vector $VECPosVector = <<VECpos_decMatrix.outputTranslateX,
                        VECpos_decMatrix.outputTranslateY,
                        VECpos_decMatrix.outputTranslateZ>>;

    //肩ジョイントの位置ベクトルを取得
    vector $shoulderVector = <<shoulder.translateX,
                        shoulder.translateY,
                        shoulder.translateZ>>;

    //回転平面の取得
    vector $Shld_VECPosVector = $VECPosVector - $shoulderVector;
    vector $crossAngle = cross($IKPosVector,$Shld_VECPosVector);

    //回転平面の回転を取得
    vector $rotatePlaneRotateAxis = cross($orgRotateAngle,$crossAngle);
    float $rotateplaneAngle = rad_to_deg(angle($orgRotateAngle,$crossAngle));

    rotatePlaneAngle.inputAxisX = $rotatePlaneRotateAxis.x;
    rotatePlaneAngle.inputAxisY = $rotatePlaneRotateAxis.y;
    rotatePlaneAngle.inputAxisZ = $rotatePlaneRotateAxis.z;
    rotatePlaneAngle.inputAngle = $rotateplaneAngle;

こんな感じです。
なお、エクスプレッション「ikSpace_Expression」ではIKハンドルの座標は、回転平面の回転成分を打ち消したものの座標値を使いたいので、vector $IKPosVectorの値の取得個所を…

    vector $IKPosVector = <<IKpos_decLocalMatrix.outputTranslateX,
                        IKpos_decLocalMatrix.outputTranslateY,
                        IKpos_decLocalMatrix.outputTranslateZ>>;

このように、先ほど作った「IKpos_decLocalMatrix」から取得するように直してます。

これで、

  • 平面上のIK挙動の計算
  • 回転平面の回転計算

を分割することができました。

■「平面上のIK挙動の計算」と「回転平面の回転計算」を合体する

あとは、分割した「回転平面の計算」と「IK挙動の計算」の回転結果をガッチャンコすれば、rotatePlaneIKは正しく動いてくれるはずです。

クォータニオンの合成は掛け算なので、
ik挙動の回転分のクォータニオン⇒回転平面の回転分のクォータニオン
…という順番に掛け算します。

NodeEditor上で「quatProd」ノードを生成して、

file

「mergeShoulderRotate」などと名前を付けておきます。

次に、前々回作成した肩のIK挙動を計算している「shoulderAngleToQuat」と、先ほど作成した回転平面のクォータニオン回転を計算している「rotatePlaneAngle」を、それぞれ上記「mergeShoulderRotate」に接続します。

そして、前回作成した方のオイラー回転を計算している「shoulderRotateOutput」ノードの接続を

以下のように、「mergeShoulderRotate」との接続に変更します。

これでIK挙動の制御と回転平面によるロールの制御が正しく接続できたはずです。

動かしてみると…

おっ。いい感じ!

PoleVectorを動かすと…

きました! ハラショー!!

念のため、Maya標準のRotatePlaneIKリグと重ねてみます。
青が今回作成したIKリグ、緑がMayaのRotatePlaneIKです。

ぴったり一致して動いているようなので、問題なさそうです。

いや、長かった…ここまで長かった…!


最終的なノードとExpressionの内容

ようやく、ここまでですべての実装が完了しました。
最終的なoutlinerの内容は

file

こうなりました。

ちなみに、NodeEditorの構造をセクションごとに見てみると…

こんな感じになります。

※Expressionの内容は既に上記「■回転平面空間用とIK空間用でエクスプレッションを分ける」で明記したので、こちらでは割愛。


さて、今回melのvector活用例として全3回にわたって紹介いたしました、「RotatePlaneIKを作ってみる」ですが、いかがだったでしょうか。
意外とmel(Expression)とUtilityノードだけでも結構高度なリグが組めるものですね。

また、vector型の様々な活用方法もご紹介できたと思います。
みなさんの身の回りでも「向き」とか「角度」とか「長さ」などのワードが出たら、「…ベクトルかな?」などとvectorに思いを馳せてみてはいかがでしょうか。。

え?べつに馳せない?
あぁ…そう…ですか。

あと、あくまで今回はvectorの使用例の応用ケースとしてご紹介したので、マトリクスやクォータニオンに関してあまり掘り下げられませんでした。
この辺りは、また回を改めて取り上げたいと思います。

それでは、今回はこの辺で。
サヨナラ!サヨナラ!サヨナラ!!

Author: yamamototomohito

COYOTE 3DCG STUDIO モーション・リグ・パイプライン系TA。 他にやる人がいないのでチームリーダーです。 二児の父。パパじゃないぞ。おとうさんだ!