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

おはこんばんちわ。ベクトルおじさん 山本ほっさんです。

前回の記事では、Autosesk Maya®のスクリプティング言語であるmelでのベクトルの活用例として、オブジェクトの配置や法線の方向、近似頂点の検索などを紹介しました。
melはvector型を使えて、様々な計算処理がスクリプトでサポートされており、高度な演算がお手軽に利用できます。

melで使用できる、ということは。
エクスプレッションでも使えるんじゃ…?

そんな訳で、今回はvector型をエクスプレッションで使ってRigに応用する例を、複数回に分けてご紹介したいと思います。
~~~
※この記事のシリーズは全三回を予定しております。他の記事は以下の通りです。
第一回:RotatePlaneIKを作ってみる① 「ジョイントをIKハンドルの位置で動かしてみる」←いまここ!
第二回:RotatePlaneIKを作ってみる① 「RotatePlane制御を実装してみる」
第三回:RotatePlaneIKを作ってみる③「ロール制御を入れてみる」
~~~

エクスプレッションでRotatePlane IKを作ってみる。

RoltatePlane IKを実現するにあたって、

  • IKハンドルへの向きと距離
  • poleVectorのへの向き
  • それぞれのジョイントの長さ

…などから現在の各々ジョイントの回転を求める処理が必要になることが想像されます。
このキーワード、「向き」「距離」「位置」…というキーワードから、vectorを用いて実現ができそうです。

結果を先に

まず動作を見ていただきましょう。

通常のRotatePlane IK solver と遜色ない動作が確認できます。

行ったことは、

  • ベクトル計算によって、各ジョイントの回転Axisと回転Angleを求める
  • AxisAngleToQuaternionノード・マトリクス計算ノード系を使ってジョイントに接続

という感じ。

では、どういう仕組みで動いているのでしょうか。

IKハンドルの位置からジョイントの角度を求める。

例えば、以下のような図のとき。
それぞれの角度はどういう風になるのでしょうか。

この時点で分かるのは、

  • 肩からIKハンドルまでの距離
  • 骨の長さ

だけです。

・直角三角形に分解して、各辺の長さを考える。

ここで、上記ジョイントは織りなす三角形をこんな感じで分解して考えてみます。

こうすると、

  • それぞれのジョイントの長さ=斜辺の割合
  • 垂直に下した底辺の長さの割合

…は一致するので、底辺の長さが求まります。

こんなかんじです。

・三角関数を使って、角度を求める

直角三角形で、二辺の長さがわかっていれば、

その二辺の織りなす角度を三角関数で求めることができます。

ちなみに、僕が学生のとき使った、三角関数の覚えた方法は…

sine

cosine

tangent

でした。今でも大活躍してます。(汗

今回は、

ここの角度が知りたいので、cosinを使うのですが、

今回はcosineの値は既にわかっているので、cosineの値から角度を逆算したいわけです。

この場合、「逆三角関数」というものを使います。
arcsine(アークサイン)とか、arccosine(アークコサイン)というやつです。
ここでは、cosineの逆関数、arccosineを使いたいのですが、melにはその演算がスクリプトで提供されており、

    acos float

で計算できます。 お手軽ですね。

ただ、上記は度単位ではなくラジアン値で角度を返すところに注意が必要です。
ラジアン値を度単位に変換する必要があるのですが、melでは度単位で数値を返す逆関数も提供されており、

    acosd float

を使うことで解決します。 なんてお手軽。

・三角形の角度の総和は180度!…からの肘の角度を考える

小学校の算数で習う「三角形の角度の総和は180度」の知識を使えば、肘の角度がわかります。

ここまでの計算で、上記の角度が既にわかっています。

「三角形の角度の総和は180度」なので、

ここの角度は、こうなります。

ところで、肘が回転した角度というのは、

ここのところですが…。

そもそも、ここの角度は180度なわけで、

ここの角度は180度からの、他二角の和の差なのだから、

ここの角度、すなわち肘の回転角度は、単純に他二角の和、として考えられます。

ちなみに、回転方向は、肩とは逆方向なので、

-1を乗じて逆回転にしてあげましょう。

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

さて、これで、型・肘の回転角度が計算できました。
ここまでの仕組みをリグに実装してみます。

ノード構造は、このような形。

file

せっかくなので、通常のリグに合わせ、コントローラーのtranslate,rotateがゼロになるよう、オフセットノードを挟んで置きます。

肩のジョイント位置とIKコントローラーの位置を取得

肩の位置は、自身のトランスフォームの数値を使います。

しかし、IKコントローラーに関しては、オフセットノードを挟んでしまったので、自身のトランスフォームの数値では、肩と同じトランスフォーム空間における位置が取得できません。

ここでは、マトリクス計算を使って、肩ジョイントと同じ空間のトランスフォームを取得することにします。

肩ジョイントと、IKコントローラーの親ノードであるオフセットノードは、同じ空間にいるので、

IKコントローラーのマトリクスにオフセットノードのマトリクスを乗算してあげると、

IKコントローラーの位置を肩と同じ空間の数値で表すことができます。

こんな感じですね。

では、これをNode Editorで構築してみます。

IKコントローラーとオフセットノードのマトリクスをmultMatrixノードで乗算します。

マトリクスからTransformを取得できるよう、decomposeMatrixノードとつなぎ、

わかりやすいように「IKpos_decMatrix」とか名前をつけておきます。

エクスプレッション(mel)でベクトルを計算する

ここで、先ほど考えた角度の計算ロジックを、エクスプレッション(mel)を使って実装します。
コードは以下の通り

//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);

ちなみに、IKコントローラーが骨の長さよりも遠い距離にいる場合、腕が伸びきっていると言えるので、回転角度を取得するところに、

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;
}

こんな感じで条件分岐をつけてあげます。

※この処理を入れておかないと、cosineの値が1以上になってしまって、melがエラーを起こし、Mayaのエラー表示がうっとおしくなります。

ここまでで、ノードエディタの様子を見てみると

こんな感じです。

※EvaluationはOn demnandにすること!※

エクスプレッションを使う上で注意なのですが、Evaluationオプションを「On demand」に設定することをお勧めいたします。

「On demand」に設定押しておくことによって、必要な時にだけ評価が走るので、Mayaに対するリグの処理負荷を軽減することができます。

結果を確認

さて、ここまでの計算を各ジョイントに接続して、挙動を見てみましょう。

エクスプレッションに

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

追記して、挙動を見ると…。

ハラショー!

回転平面の挙動や、そのほかの挙動については?

ここまでの実装で、一見IK挙動として良さそうにも見えますが、

PoleVectorの挙動はまだ未実装ですし…

また、平面外の挙動だったり…

こんな挙動のときなんか…
oh...

まだまだ、いろいろと考えないといけない要素がありますね。
このあたりの要素について、また回を改めて解説したいと思います。

まとめ

今回は、melでのvector型活用術として、リグへの応用例を紹介いたしました。
エクスプレッションの使用方法には少しコツが必要ですが、うまく活用してあげることで、複雑なvector演算をリグに組み込むことが可能になります。

melがサポートしている演算について、公式melドキュメントの「数学」カテゴリを流し見するだけでも、いろいろと発見があってとても面白いです。

cap.jpg

Maya2020 テクニカルドキュメント mel

ティータイムやコーヒーブレイクのお供にはうってつけの読み物ではないでしょか!

…え?そうでもない??

…あぁ…そう…ですか。。

ところで、気が付いたら一周年!

本当に気が付いたら、なのですが。
弊社スタッフと当ブログについて話していたところ、そういえば今回の記事でブログ開設からもう一年が経過しますね、という話題になりました。
時の経過は早いものです。

ブログの開始当初は、読者が付くのか、ネタが続くのか不安もありました。
しかし一年経過して、大変ありがたいことにアクセス数は毎回増えており、時たま「ブログの記事参考にしました!」などという嬉しいコメントもいただいております。
ネタの方も今のところ尽きることはなく、毎月の更新も問題なさそうです。

…というか、われわれTAの仕事というのは、常に「今そこにある問題の解決」の連続なので、ブログのネタは割と尽きることがないな、という印象です。
そこに「困った、何とかしたい」があるかぎり、われわれTAのお仕事は無くなることはないでしょう。

今後も当ブログでは、わたしたちの知りえた知見を、広く公開・共有していきたいと思っております。
当ブログをご覧いただくみなさまの、何かのヒントや問題解決の糸口になれれば幸いに、チーム一同思っております。

今後も当ブログと、COYOTE 3DCG STUDIO テクニカルチームを、よろしくお願いいたします。

Author: yamamototomohito

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