1ボーンIKのフロアコンダクトリグを作る!

この記事はMaya Advent calendar 2020 22日目の記事です。

おはこんばんちわ。
つい最近まで日本酒の原酒で飲んでいたので普通のやつだと物足りなくなってしまいました、山本ほっさんです。

今日はフロアコンダクトリグの話を取り上げようと思います。

特定の平面との衝突を加味してくれるフロアコンダクトリグは、床や壁など接触を取るモーション作業において特に効果的なリグと思います。

このように、接触に合わせて2ボーンIKを動かすことができます。

しかし、1ボーンのジョイントではどうでしょうか。
2ボーンでは衝突に合わせてばねのように2つのジョイントが動くことで衝突面との距離を調整することができますが、1ボーンでは無理です。

こんな感じに、衝突に合わせてうまくジョイン問いに角度をつけ、「逃げ」てもらう必要があります。

今回は、1ボーンIKのフロアコンダクトリグの実装について、その1例をご紹介したいと思います。


結果を先に

衝突面とジョイント、ジョイントの根元の距離を測り、衝突している場合は三角関数を用いて滑る方向とその距離を算出し、IKハンドルの移動値をします。

vector $floorPos = <<condition1.outColorR,condition1.outColorG,condition1.outColorB>>;
vector $root = <<decomposeMatrix3.outputTranslateX,decomposeMatrix3.outputTranslateY,decomposeMatrix3.outputTranslateZ>>;
vector $RO = <<$root.x,0.0,$root.z>>;
vector $slideVector = $floorPos;

float $R = 2;
vector $localFloorPos = $floorPos - $root;

if (mag($localFloorPos)-$R<-0.0001){
    $slideVector = unit($floorPos - $RO);
    float $ang = acos($root.y/$R);

    float $slideLength = sin($ang) * $R;
    $slideVector = ($slideVector * $slideLength) + $RO;
}

composeMatrix1.inputTranslateX = $slideVector.x;
composeMatrix1.inputTranslateY = $slideVector.y;
composeMatrix1.inputTranslateZ = $slideVector.z;

では、そのロジックをひも解いてみましょう。


そもそも、普通に実装したらどうなるの?

いやしかし。
まずもって、そんな難しいことしなくてもよくない?
普通に衝突面との距離をみて、距離がゼロになったら押し上げる、ではだめなの?

実際にやってみましょう。

まず、ボーンとフロアコンダクト用にカーブを用意しておきます。

outliner上の表示はこんな感じ。

file

この時点で、ボーンにはsingleChainIKを適応しておきます。

IKを動かす用にlocatorを作成しておいて、トップノードをIKハンドルのところに移動し、末端のctrl_pos_trnsノードをIKハンドルにポイントコンストレインさせておきます。

こんな感じですね。

コントローラーのマトリクスをフロアコンダクト平面の子階層と仮定することで、シンプルロジック化

次にフロアコンダクト平面側ですが、

真下から衝突してくる分には、単にY軸に押し上げればよいだけですが…

せっかくなら、上記のような斜めの床から衝突を受けた際は、その法線方向に押し上げたいものです。
フロアコンダクト平面の法線ベクトルや現在の骨との距離など、考えないといけないところが目白押しですが、Matrixを使うとシンプルロジックにできます。

下記のように、フロアコンダクト平面と、IKコントローラーは、それぞれ別空間で独立しています。

しかしこんな感じに、もしもフロアコンダクト平面の子階層にIKコントローラーがあったと仮定したらどうでしょうか?

こう考えると、単にIKコントローラのY軸の値が常に0以上であればよいだけになります。


フロアコンダクト平面より下にいなければいいのですからね。

こうして計算されたマトリクスを、再度元の空間に戻してあげることでフロアコンダクト平面に押し上げる制御ができそうです。
単にY軸の高さだけを考えればよいだけになるので、とてもシンプル!

ではノードを組んでみます。
まずmultMatrixノードを作り、コントローラーのworldMatrixにフロアコンダクトリグのinverseWorldMatrixを乗じます。

これで、コントローラーのマトリクスをフロアコンダクト平面のマトリクス下に置いたことになります。
ここからdecomposeMatrixノードで成分分解し、translateの値を取り出します。

conditionノードにそれぞれ接続して

このように、Yの高さが0以下だった場合、常にYの値が0以上になるようにします。

ここで調整されたtranslateの値を、composeMatrixノードを使って、再度matrixに戻します。
回転・スケール・シアの値は、前のdecomposeMatrixノードから引き継ぎます。

※回転は回転順序を加味したくないので、Quaternionの接続を使っています。
この場合、Use Euler Rotationのオプションをoffにすることをお忘れなく

そして、ここで計算されたMatrixはまだフロアコンダクト平面下のマトリクスなので、

フロアコンダクト平面のworldMatrixを乗じてworldMatrixに変換し、コントローラーのworldInverseMatrixを乗じて、コントローラー下のマトリクスに戻します。

あとはもう簡単で、decomposeMatrixノードで再度成分分解して

translateの値をIKハンドルに接続しているIK_ctrl_pos_trnsノードに接続してあげます。

すると…

おっ。できた?
斜めに押すと…

よしよし!

…でもまって…何かがおかしい。

ここのところ、何かおかしくないですか。
床面に合わせて骨が動くなら、本来はこうなるはず。

つまり、こういう事なのです。
1ボーンIKをフロアコンダクトで押し上げるには、単に押し上げるだけではダメなようです。
はて、なぜなのでしょうか。

骨の長さを半径とした円挙動を取るべき!

そのロジックを図で考えてみます。
ここまでの処理は、単に「IKコントローラーをフロアコンダクト平面に対して垂直に押し上げる」だけです。
2ボーンIKならば、ジョイント構造がばねのように動いてこの挙動で正しくふるまうわけですが、一方1ボーンIKの場合だと…

このように、単に押し上げるだけだとボーンはIKコントローラーに対してaimするだけで、本来フロアコンダクト面にいてほしいジョイント末端の方向を向かないのです。

つまりどういう事でしょうか。
いったん理想的な骨の挙動を下図に書いて、考えをまとめてみると。

円挙動が見えてきました。
どうやらこの問題を解決するには、IKハンドルがジョイントルートから見た骨の長さに等しい点に位置するよう、フロアコンダクト面を滑るような円挙動を取る必要がありそうです。


フロアコンダクト面を円挙動で滑らす!

状況をいったん図に書いて整理してみましょう

フロアコンダクト平面とジョイントを以下の図に示しました。

これはフロアコンダクト平面にsingleChainIKが押し出された図になっています。
円の半径はジョイントの長さに等しいものです。

上記図で、既にわかっている情報を書き出して見ると

  • ジョイントの長さ = R
  • ジョイントルートから見たフロアコンダクト平面との距離 = l
  • 単純にY方向に押し上げられた場合のIKハンドルの位置 = p
  • ジョイントから垂直に下した点RO
    …は既にわかっています。

するとどうでしょう…

直角三角形と、2辺の織りなす角度Θの図が見えてきましたね…
以前の記事「melでのvector活用術 ~その2~ RotatePlane IKを作ってみる①」で紹介した通り、直角三角形の2辺の長さがわかっていれば、その間の織りなす角度は三角関数で求めることができます。

この場合使用するのは、

cosΘなので、角度Θを求めるには逆cosΘを計算します。

次に、IKハンドルの位置を本来の正しい位置に移動させます。

単純にY方向に押し上げられた場合のP点が、本来のジョイントの位置に来てくれればよいので、点pが点p'に移動すればよい、と考えます。

つまり、ベクトルROPをベクトルROP'の長さに変更すれば、点p'の座標が求められることがわかります。

計算では、ベクトルopの単位ベクトルにベクトルop'の長さを掛ければ計算できそうです。
op'の長さは、今度はsinΘで計算できます。

上記の直角三角形は長編の長さが1の場合なので、

sinΘ × R

が、ベクトルop'の長さになります。


実装してみよう

では先ほど失敗したシーンを修正しながら、ここまで設計した内容を実際に実装してみましょう。

  1. ジョイントルートのマトリクスを取得
    まず、ジョイントルートのマトリクスを、IKハンドル同様にフロアコンダクト平面下で使用できるようにノードを構築します。

    後で座標を取得できるように、deconposeMatrixノードにも接続しておきます。

  2. エクスプレッションで三角関数・ベクトルの処理を作成
    エクスプレッションで処理を作成します。
    まず、最初の失敗した例で取得している、「単純にY方向に押し上げた座標」と、先ほど取得しているジョイントルートの座標をベクトルにしていきます。

上記のノードコネクションで、それぞれdecomposeMatrixノートのoutTranslateの値に座標があるので…

vector $floorPos = <<condition1.outColorR,condition1.outColorG,condition1.outColorB>>;
vector $root = <<decomposeMatrix3.outputTranslateX,decomposeMatrix3.outputTranslateY,decomposeMatrix3.outputTranslateZ>>;

あわせて、ジョイントルートを垂直に下した頂点ROを取得しておきます。

vector $RO = <<$root.x,0.0,$root.z>>;

次に、半径Rとフロアコンダクト平面までの距離を取得します。

float $R = 2;
vector $localFloorPos = $floorPos - $root;

※ここでは、定数2としてハードコーディングしていますが、ジョイントのtransformの値からdistanceを取るようにすると、ソースコードが汎用的になると思います。
続けてIKハンドルを滑らすベクトルを計算しますが、まず滑らすベクトルの規定値を設定しておきます。

vector $slideVector = $floorPos;

基本的に、滑っていなければ単純にY方向に押し出された座標に一致するので、規定では上記のように同じ値になるよう指定しておきます。
次に、条件分岐を入れます。

if (mag($localFloorPos)-$R<-0.0001){

フロアコンダクト平面の距離がジョイントの長さ以下かをチェックします。もし、ジョイントの長さ以上離れていた場合、フロアコンダクトしないので、余計な処理を走らせる必要はありません。

そして、滑らすベクトルを計算する処理部分に進みます。
まず、単純に押し上げられただけの座標ベクトルを単位ベクトルにしておきます。

    $slideVector = unit($floorPos - $RO);

次にジョイントの回転角度Θを取得します。

    float $ang = acos($root.y/$R);

上記で求まった回転角度Θから、滑らすベクトルの長さをsinΘを使って計算し、単位ベクトルに乗算します。

    float $slideLength = sin($ang) * $R;
    $slideVector = ($slideVector * $slideLength) + $RO;
}

ここまでが条件分岐。
あとはこのベクトルの値を、マトリクス計算ノードに渡して、マトリクス空間を元通りに戻します。

composeMatrix1.inputTranslateX = $slideVector.x;
composeMatrix1.inputTranslateY = $slideVector.y;
composeMatrix1.inputTranslateZ = $slideVector.z;

ノードエディタでは、こうなります。


ちょうど、最初に作ったノードの間にエクスプレッションが挟まった形になりますね。

では結果を見てみましょう。

おっ、よしよし。

ナナメにしても

ちゃんとにげるぞ!
ハラショー!!

あ。エクスプレッションを使用する際は

file

EvaluationをOn demandに変えておくことをお忘れなく!
毎フレーム処理されてしまいますからね。


いかがでしたでしょうか。
1ボーンIKのフロアコンダクト、割と簡単と思いきや挙動について少し詰めてみると以外にも複雑だったと思います。

しかし、この知見をうまく駆使することで、よりトリッキーなフロアコンダクトリグの実装も可能になると思います。
ぜひリギングに取り入れてみてはいかがでしょうか!

年末年始、フロアコンダクト制御について掘り下げてみるのも一興かもしれませんよ…フフ。

え?そばともち食べる?
あぁ…そう。ですか。。

さて、明日のMaya Advent calendar 2020、は赤めがね@redglasses67さんの「【Maya/Python】modelPanelの上部にあるボタンをスクリプトから切り替えてみよう」ですね!

今年もう終わりが近づいてまいりましたが、はやり病にはくれぐれもお気をつけいただきつつ、よき年末年始をお過ごしください!

今年の記事更新はこれで最後ですが、来年もCOYOTE 3DCG STUDIOをよろしくお願いいたします!

Author: yamamototomohito

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