クリーク・アンド・リバー社の社内CGスタジオであり、ゲームの3DCGグラフィックス制作を中心に手がけているCOYOTE 3DCG STUDIO。本連載では、同社のTAチームによる制作に役立つ技術TIPSを紹介していく。
TEXT_山本智人 / Tomohito Yamamoto(COYOTE 3DCG STUDIO)
EDIT_小村仁美 / Hitomi Komura(CGWORLD)
連載開始に寄せて
おはこんばんちわ。COYOTE 3DCG STUDIO テクニカルアーティスト(以下、TA)の山本です。TAチームのリーダーとして従事しています。
普段は弊社のブログ(TECH-COYOTE)にていくつか記事を執筆しておりますが、このたびCGWORLD.jpへの寄稿の機会をいただき、私たちTAチームからも有用なテクニカルTips・業務サポートTipsなどを紹介していきたいと思います。
まず最初は、「ノードの回転制御」について、以下のように全3回に渡って取り上げます。
・ベクトルを使ったノードのBend制御
・Quaternionを使ったRoll成分の分解
・ベクトルの活用(内積・外積・三角関数)
今回は第1回として、Mayaにて「ベクトルを使ったノードのBend制御」、すなわち「捻り防止挙動」の実装例を紹介します。
Result:結果を先に
回転成分をBendとRollに分けて考えて、Bend成分のみでノードを回転させます。
手法は、
1:ノードの向きからMatrixを使ってベクトルを算出
2:初期の向きと現在の向きのベクトルからBend回転を取得して、ノードに接続
上記2の計算はUtilityノードでもできますが、エクスプレッションでMELを使って実装することも可能です。
では、上記実装にいたるまでを順に追っていきたいと思います。
Step 01:回転成分を「曲げ=Bend」と「捻り=Roll」に分けて考える
例えば、キャラクターの上腕や手首のジョイント制御を考えた時、捻りの制御を何とかしたいところです。
この問題の解決方法は、既にいくつかのアプローチが知られていますが、今回は回転成分を以下のように「曲げ=Bend」と「捻り=Roll」に分解し、Bend成分だけでジョイントを制御する方法を考えてみたいと思います。
オイラーは回避したい
あれ? なんでオイラー回転を使わないの? 軸方向と回転順序に注意して、ねじれの軸をロックしてしまえば実装できるんじゃ...??
確かに。それにオイラー回転は回転状態を説明するのにとても直感的でわかりやすいため、簡単そうです。
しかしながらオイラー回転の場合、必ずしも同じ値で1つの姿勢を定義するわけではなく、
また、同じ値でも回転順序によっては同じ姿勢にならず......
さらにジンバルロックもあったりして......アァー!(発狂
このことから、補助骨の制御には不向きといえます。
特に、同じ姿勢にもかかわらず値が大きく異なる特性については、ドリブンキーなど他の制御と組み合わせた際に不具合の原因になりやすいと思います。
実際、今まで私が見てきた中で、上腕の捻り防止をオイラー回転で実装した例では制御が安定しておらず、大体何らかの拍子にジョイントがフリップして腕があらぬ方向を向いてしまうという印象があります。
ですので個人的には、リギング・セットアップでの回転制御において、あまりオイラー回転の結果を信頼していません。
Step 02:現在の回転状態から向きを取得する
では、このBend成分を取得する方法を順に考えてみます。
Bend成分とは、つまり現在向いている方向(に、どれくらい回転していたったか)なので、「初期の向き」と「現在の向き」から差分の角度を調べることで実現できそうです。
▲初期の向き(青)、現在の向き(赤)
上記の画像の「向いている方向」を示す「座標」を取得する方法を考えてみます。今回の例では、ジョイントのx軸方向をprimaryAxis・Z軸方向をsecondaryAxisとします。
コンストレインを駆使して向きを取得
例えばparentConstraintなどを使えば、簡単に取得できそうです。まず、対象のジョイントと同じ階層にロケータを作成し、ジョイントのX軸方向正面+1の位置に配置します。
あとはこのロケータとジョイントをparentConstraintでつないでしまえば、この通り!
上の動画のアトリビュートの遷移を見れば、そのときの向きを示す座標が取得できているのがわかります。割と簡単ですね。
Matrixを使ってノードを節約
ただ、上記の方法ではロケータなどのTransformノードやparentConstraintノードが追加されるので、若干煩雑です。ここをMatrixを使ったノードリギングを行うことで、もう少しエレガントな実装が可能になります。
まず、Node Editorを開き、対象のジョイントを表示しておきます。
ここで「composeMatrix」ノードを追加します。このノードは与えられたTransformアトリビュートからMatrixを生成するUtilityノードです。
作成した「composeMatrix」を選択してAttribute Editorを開きます。ジョイントのX軸方向+1の座標を取得したいので、transformXの値に1.0と入力しておきます。
これで、X軸方向に+1移動したマトリクスを生成できました。
このMatrixは対象ジョイントの子階層として挙動してほしいため、対象ジョイントのMatrixの影響下に置く必要があります。そこで、「multMatrix」ノードをNode Editorに生成し、対象ジョイントと先ほどのcomposeMatrixを乗算させます。
composeMatrixのmatrixSumをmultMatrix.matrixIn[0]につなぎ、ジョイントのmatrixをmultMatrix.matrixIn[1]につなぎます。Matrixには計算順序がありますので、つなぐ際は順序に注意しましょう!
今度は、このMatrixから移動成分を抽出します。 Node Editorで「decompose Matrix」ノードを作成し、計算結果のMatrixをdecomposeMatrixのinputMatrixに接続します。
これでジョイントの向きを取得するノードが作成できました。
可視化のため、対象ジョイントにアトリビュートを追加して接続してみます。わかりやすいように、アトリビュートの名前を「bendVector」としておきます。
できました!
アトリビュート遷移を見ると、constraintで実装したときと同様に向きを示す座標が取得できているようです。
Constraintで実装したものと比較しても、同様の結果が返ってきていることがわかります。
次ページ:
Step 03:2つのベクトルの織りなす角度を求めて、回転した角度を計算する
Step 03:2つのベクトルの織りなす角度を求めて、回転した角度を計算する
現在の「向き」を示す「座標」はわかりました。ではこの情報からどうやって「角度」を求めるのでしょうか。
向きを示す「座標」は下図のように考えると、向きを示す「ベクトル」と捉えることができます。
つまり、初期値の向きを示すベクトルと現在の向きを示すベクトルの織りなす角度を計算すればよいのです。
Utilityノード「angleBetween」を使う
「angleBetween」ノードがこの問題を解決します。
そもそもこのノードは「2つのベクトルの織りなす角度を取得する」ためのものなので、もうズバリこれ。
NodeEditorで「angleBetween」ノードを作り、vector1とvector2に先ほどのbendVectorの値を接続します。
そして、vectorの接続を切ります。
すると、初期値として値が残ったと思います。Attribute Eritorから、アトリビュートに数値が残っていることが確認できます。
これでBend成分の角度取得が可能になりました。
試しに同じ階層にcubeを作成して、angleBetweenノードのEulerアトリビュートを接続してみると......
できました! 試しにねじらせてみても......
動かない! ハラショー!
こんな感じで3軸回転状態からBend成分のみを抽出することが可能になるわけです。
おまけ:Expressionでbendの回転成分を計算してみる
上記ではUtilityノードを使いましたが、Expressionでも実現が可能です。
MELコマンドにも「angleBetween」という「2つのベクトルの織りなす角度を取得する」ためのコマンドがあるので、これを使ってみます。
まず、bendVectorの初期値の定義と現在の値の取得を書きます。
float $defVec[] = {1.0,0.0,0.0};
float $aimVex[] = {decomposeMatrix1.outputTranslateX,
decomposeMatrix1.outputTranslateY,
decomposeMatrix1.outputTranslateZ};
次に、この2つの織りなす角度をangleBetweenコマンドで計算します。
float $angleBetween[] = `angleBetween -euler
-v1 $defVec[0] $defVec[1] $defVec[2]
-v2 $aimVex[0] $aimVex[1] $aimVex[2]`;
-eulerフラグを立てておけば、「anglenBetween」ノードと同様に結果をオイラー回転で返してくれるので、便利ですね。
この結果を、以下のようにcubeのrotateに接続すれば......
pCube1.rotateX = $angleBetween[0];
pCube1.rotateY = $angleBetween[1];
pCube1.rotateZ = $angleBetween[2];
できました! 挙動も良さそうです。
なお、Expressionでリギングを行う上での注意として、Evaluationを「Always」から「On demand」に変更しておいてください。
「Always」のままだと強制的に毎フレーム処理が走ってしまうので、場合によってはシーンに過負荷を与えてしまうことがあります。
いかがでしょうか!? 割と簡単に回転成分からBend成分だけを抽出することができたと思います。
「ベクトル」と聞くと何だか難しいイメージが先行してしまうかもしれませんが、3Dツールを通じて改めて見ると、「ノード制御に便利な考え方」としてみることができるのではないでしょうか。Mayaなど3D制作に従事する方ならば、きっとイメージもつかみやすいはず。
さて、今回はBend成分での制御のみを取り上げましたが、ではRoll成分での制御はいったいどのようにしたら良いのでしょうか。次回はQuaternionにも少し触れ、Roll制御について一例をご紹介したいと思います。
では今回はこの辺で!
サヨナラ! サヨナラ! サヨナラ!!
Profile.
-
山本智人/Tomohito Yamamoto(COYOTE 3DCG STUDIO)
ゲーム開発会社にてグラフィックアーティストとして、モデリング・モーション・エフェクト・UIなどオールラウンドのグラフィック制作を経験後、テクニカルアーティストに転身。現在は株式会社クリーク・アンド・リバー社 COYOTE 3DCG STUDIOにて、社内外問わずアーティストのDCCツールサポートや制作パイプラインの提案・作成・運用を行う。特にアニメーション制御やモーション制作周りのテクニカルサポート・リガーとして様々なプロジェクトに従事