チュートリアル / Mayaにおける魅惑のプロップリグ~モジュラーリギングシステムの基本と応用~
第6回:リグモジュールの開発とセットアップ その5〜永遠に終わりが来ない廊下〜
- GAZEN
- Maya
- アニメ
- キャラクター・リグ
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 学生・初心者
- 映画・TV
今回の題材
前回・前々回は2回続けて宝箱のセットアップをやってみました。
今回は新しい題材でセットアップをやってみようかと思います。
モデル確認
さて、今回の題材はこちらです。
長そうでそんなに長くない廊下ですね。
そんなに長くないので、廊下を誰かに走ってもらうとすぐに終わりが来てしまいます。
なので永遠に終わりが来ない廊下を作ってみようと思います。
やりたいこと
ではいつも通り訴求内容を図にして考えようと思います。
要素分解
訴求を基に要素を分解してみます。
大きく分けて必要な要素は2つくらいになりそうです。
・廊下モデルの送り(ループ)
永遠を手に入れたいので、廊下のモデルをランニングマシーンの様にその場歩きができるようにしてみます。
手動だけではなく、前回宝箱の歯を動かしたようにパラメーターをonにすることで回り続けるようにします。
・全体の変形
こちらはlatticeによる変形でよさそうに思えます。
ただ、コントローラーをどのようにつけるかは一考したいところです。
試作
送り方を考える
このモデルを無限ループさせるたいわけですが、とりあえずパッと思いついたものを挙げてみます。
・全体でループさせる
全体を移動させ、手前に来たものを一番後ろへ を繰り返し行うことでループしているように見せます。
パーツ毎に特徴があっても全体を移動させているで、ループして見せることができます。
ロケット鉛筆という例えが頭をよぎりましたが、伝わらないかもしれませんね・・・
・個別でループさせる
それぞれのパーツが1パート分の距離を移動したら最初の位置に戻る 動きをすることで、ループしているように見せます。
ループしているように見せるためには全てのパーツが同一である必要があります。
・円形にしてぐるぐる回す
モデルの形状から相談できるのであれば、円形に配置して回転させる というのもありかもしれません。
平らな床にするにはちょっと工夫が必要になりそうです。
目がちかちかしてきたので、いったんこれくらいにします。
さて、ループについては1つ目が汎用性高そうかなぁと思います。
ただ長さの調整を考えた場合、2つ目は表示してる数を減らすだけなのでやりやすそうですね。
送り試作
個別でループさせる のパターンで試作をしてみます。
今回もexpressionによる制御でやってみました。
サンプルデータ:corridorA/mayaScenes/sampleScene/sample004.ma (記事の最後のダウンロードリンクからダウンロードできます。)
パラメーターは以下のものを用意してみました。
・ctrlNode.seqOn(自動送り on / off)
・ctrlNode.offsetFrame(手動による送り)
・ctrlNode.loopIn(1ループするフレーム数)
・ctrlNode.loopDistance(1ループする移動値)
・ctrlNode.partsNum(表示する数)
loopIn の間に loopDistance(Aの距離)を移動し、もとに位置に戻る
を繰り返させます。
実装時にはパーツ毎の初期値を考慮する必要がありますが、ループする値は全パーツ共通になります。
{
float $seqOn = ctrlNode.seqOn;
float $loopDistance = ctrlNode.loopDistance;
int $loopIn = ctrlNode.loopIn;
int $offset = ctrlNode.offsetFrame;
float $loopValue = ((frame + $offset)%$loopIn) * ($loopDistance / $loopIn);
corridorParts01_geo.translateZ = $loopValue;
corridorParts02_geo.translateZ = $loopValue;
corridorParts03_geo.translateZ = $loopValue;
corridorParts04_geo.translateZ = $loopValue;
corridorParts05_geo.translateZ = $loopValue;
corridorParts06_geo.translateZ = $loopValue;
corridorParts07_geo.translateZ = $loopValue;
corridorParts08_geo.translateZ = $loopValue;
corridorParts09_geo.translateZ = $loopValue;
corridorParts10_geo.translateZ = $loopValue;
}
外側から見るとこうなります。
内側から見るとこうなります。
ついでに可視性の制御を floatLogicノードを使用して実装しました。
準備されたモデルの個数以上の長さには出来ませんが、長さを調整することができます。
floatLogicノードは inputの2つの数値を比較して、bool値を返してくれるので何かと便利ですね。
別パターン試作
思った以上に簡単にできてしまったので、全体でループさせるのパターンも試作してみました。
サンプルデータ:
corridorA/mayaScenes/sampleScene/sample005.ma
(記事の最後のダウンロードリンクからダウンロードできます。)
制御パラメーターは共通にしました。
・ctrlNode.seqOn(自動送り on / off)
・ctrlNode.offsetFrame(手動による送り)
・ctrlNode.loopIn(1ループするフレーム数)
・ctrlNode.loopDistance(1ループする移動値)
・ctrlNode.partsNum(表示する数)
動きの内容を整理します。
・ctrlNode.loopIn の間に ctrlNode.partsNum * ctrlNode.loopDistance (A + B) を移動する。
ただし、Aの端に行きついたら B のスタート位置に移動する。
全長が動的なのが少々厄介です。
全体の移動値を 0.0 ~ 1.0 のパラメーターとして捉えてみます。
そうしてみると 1つ目のパーツのスタート位置は 0.9 となります。2つ目のパーツのスタート位置は0.8ですね。
これらを基にexpressionに起こしていきます。
まず時間による0.0 ~ 1.0 のループはこんな感じですね。
{
int $loopIn = ctrlNode.loopIn;
int $offset = ctrlNode.offsetFrame;
float $frameToParam = ((frame + $offset) % $loopIn)/$loopIn;
}
さらに、個々のオブジェクトのスタート位置対応と移動値への変換を追加します。
{
int $loopIn = ctrlNode.loopIn;
int $offset = ctrlNode.offsetFrame;
float $frameToParam = ((frame + $offset) % $loopIn)/$loopIn;
float $loopDistance = ctrlNode.loopDistance;
int $num = ctrlNode.partsNum;
//移動距離の全長
float $wholeLength = $loopDistance * $num;
{
//1つ目のオブジェクト
// start位置はここで変える
float $startParam = 1.0 - (1.0 / $num);
// start位置を考慮した 0.0 ~ 1.0 のループ値
float $frameParam = ($frameToParam + $startParam) - int(($frameToParam + $startParam));
//0.0 ~ 1.0 のループ値を移動値に変換
corridorParts01_geo.translateZ = ($frameParam * $wholeLength) - ($startParam*$wholeLength);
}
{
//2つ目のオブジェクト
// start位置はここで変える
float $startParam = 1.0 - (2.0 / $num);
float $frameParam = ($frameToParam + $startParam) - int(($frameToParam + $startParam));
corridorParts02_geo.translateZ = ($frameParam * $wholeLength) - ($startParam*$wholeLength);
}
//3~10 は省略
}
これを流してみるとこんな結果になりました。
1ループの移動距離が長いので、かなり高速になってしまっています。
どちらの方法でもできそうですが、今回は 個別でループさせる のパターンを採用しようと思います。
送り実装
frameTranslateLoopSwitch
試作を基にリグモジュールを作成しました。
前回作成した frameWaveSwitchを一部流用しています。
ひとまず1列で1系統制御できれば良いので、パラメーターは最小限にしてあります。
サンプルデータ: SSRig/rigModules/frameTranslateLoopSwitch.py (記事の最後のダウンロードリンクからダウンロードできます。)
_ATTRS = [
"ctrlNode",
"useParamPrefix",
"attrName",
"targets",
"useTranslateX",
"useTranslateY",
"useTranslateZ"
]
_ATTR_DICT = {
"ctrlNode": {"type":"string", "multi":False, "default":""},
"attrName": {"type":"string", "multi":False, "default":""},
"targets": {"type":"message","default":True,"multi":True},
"useTranslateX": {"type":"bool","multi":False, "default":True},
"useTranslateY": {"type":"bool","multi":False, "default":False},
"useTranslateZ": {"type":"bool","multi":False, "default":False},
}
リグモジュール パラメーター説明
・ctrlNode
制御用アトリビュートを設置するためのノード
・useParamPrefix
制御用アトリビュート名のprefix
・attrName
制御用アトリビュート名
・targets
制御するtransformノード
・useTranslateX,Y,Z
制御するアトリビュート
intVisSwitch
表示する数制御については、使い勝手が良さそうな気がするので別モジュールにしました。
サンプルデータ: SSRig/rigModules/intVisSwitch.py (記事の最後のダウンロードリンクからダウンロードできます。)
_ATTRS = [
"ctrlNode",
"useParamPrefix",
"attrName",
"targets",
"sortDirection",
]
_ATTR_DICT = {
"ctrlNode": {"type":"string", "multi":False, "default":""},
"attrName": {"type":"string","multi":False, "default":""},
"targets": {"type":"string","multi":True},
"sortDirection": {"type":"enum","enumList":["+","-","index"], "default":"+"},
}
リグモジュール パラメーター説明
・ctrlNode
制御用アトリビュートを設置するためのノード
・useParamPrefix
制御用アトリビュート名のprefix
・targets
制御するtransformノード
・sortDirection
targetsをソートする方向(名前昇順・降順・つないだ順)
これを使用してセットアップを行います。
skeleton の準備
骨格データを作成します。
各パートに1jointを配置し、これらをリグモジュールにて制御します。
frameTranslateLoopSwitch と intVisSwitch を作成し、それぞれ設定します。
これをcorridorA_skeleton_v0001.ma として保存します。
セットアップの実行
setupDataEditor を起動し、モデルと骨格データをそれぞれ登録します。
setupDataEditorなど各種ツールの使い方は同梱のマニュアルか、過去の記事をご参照ください。
for test でセットアップを実行します。
生成されたデータ上でモデルとjointを1:1でバインドし、再現用にweightデータを保存します。
root_gp の数値を調整して、実際に動かしてみます。
root_gpの設定を ctrlValue > 右クリック > export value で保存します。
corridorA_skeleton_v0001.ma を開き、importSkinWeight を追加しskinweightが読み込まれるようにします。
これをcorridorA_skeleton_v0002.ma として保存します。
ツールからskeletonデータの登録を変更し、再度ビルドを実行して問題なく再現されているかを確認します。
問題なさそうなので corridorA_asset_v0001.ma として保存しました。
lattice 追加
パース感とかいろいろ調整できるようにしたいので、latticeで全体を変形させます。
サイコロの回でやったように、送りのデフォームを行った後に latticeで変形させる流れです。
コントローラーの試作
latticeを制御するためのコントローラーを考えます。
訴求としては大まかにこの2つですね。
・カーブを作りたい
・部分的にスケールさせたい
これらを満たすためには
・FKのチェーンコントローラー
基本的にはこれでどうにでもできる気はします。
ただし、親から子へポーズを決めていくので途中部分の調整が発生した場合には大変そうです。
・splineIK を使用
nurbsCurveを使用するので、きれいなカーブを作ることができそうです。
ツイストの制御に癖があるので、そこは何とかしないとですね。
・並列にコントローラーを設置
これもまた究極的な策ですが、jointをそのまま触ってもらうのと大差ないですね。
自由度は最大ですが、その分手数がどうしても増えてしまうのが問題です。
なんとも悩ましいのですが、いろいろ合わせて試作してみました。
基本的には aimConstraintを使用してセットアップしています。
サンプルデータ: corridorA/mayaScenes/sampleScene/sample006.ma (記事の最後のダウンロードリンクからダウンロードできます。)
コントローラーは全部で3種類用意してみました。
ぞれぞれのコントローラーで競合しないように役割を分離しました。
上位のコントローラーから順に形状を詰めていくイメージです。
・primaryCtrl
全体の向きを調整するコントローラー(translateのみ)
・secondary1_ctrl ~ secondary3_ctrl
中間部分のカーブを調整するコントローラー(translate 2軸のみ)
・detail1_ctrl ~ detail7_ctrl
ひねり・太さ調整用コントローラー(rotate1軸,scale2軸)
動かしてみて2か所ほど課題が出てきました。
A, secondary_ctrlを動かした際に、曲がったlatticeの形状がつぶれてしまうので調整が必要。
スキンウェイトを0.5 / 0.5 で割り振っても良いですが、それでも結局細くなってしまうので、
曲がるjointに半分だけ回転が追従するjointを作成したいと思います。
B, secondary_ctrlを動かした際に、latticeの形状が均等に伸びていないので調整が必要。
これはダイス・箱の際に使用した lengthToScaleのリグモジュールで対応できそうです。
試作の内容を書き起こすとこのようになります。
半回転jointモジュール
元のjointから対象となるjointへ50%だけ回転値を送る仕組みをつくります。
今回は50%ですがこの値は任意に変えられた方が何かと便利に使えそうです。
50%回転値のといってもやり方は色々あるとおもいますが、今回はpairBlendノードを使用してみようかと思います。
コネクションはこのようにします。
今回 rotInterpolation は Quaternions に設定してあります。
Euler angles でも一応できますが、回転軸が複数になると望まない結果になることもあります。
pairBlandノードの挙動としては
weight = 0.0 の時に inRotate1 が 100% で出力されます。
weight = 1.0 の時に inRotate2 が 100% で出力されます。
なので、まとめるとこんな感じになります。
(inRotate1 * (1.0 - weight)) + (inRotate2 * weight) = outRotate
簡単にモジュール化してみました。
サンプルデータ: SSRig/rigModules/connectBlendRotate.py (記事の最後のダウンロードリンクからダウンロードできます。)
_ATTRS = [
"mirrorSetup",
"targets",
]
_ATTR_DICT = {
"targets": {"type":"compound", "multi":True, "ch":["sourceJoint","targetJoint","weight","connectTranslate","connectScale"]},
"sourceJoint": {"type":"message", "multi":False, "default":""},
"targetJoint": {"type":"message", "multi":False, "default":""},
"weight": {"type":"double", "multi":False, "default":0.5, "min":0.0, "max":1.0},
"connectTranslate": {"type":"bool", "multi":False, "default":False},
"connectScale": {"type":"bool", "multi":False, "default":False},
"mirrorSetup": {"type":"bool", "multi":False, "default":False}
}
リグモジュール パラメーター説明
・sourceJoint
回転値の出力元となるjoint
・targetJoint
sourceJointに weightを掛けた値を出力するjoint
・weight
sourceJointの回転値に掛ける値 0.0~
・connectTranslate
sourceJointから targetJointへ translateをコネクトするか否か
・connectScale
sourceJointから targetJointへ scaleをコネクトするか否か
注意点はいくつかあります。
・ソースとターゲットのjointOrient/rotateOrderが一致している必要があります。
・translateをコネクトする際には、それぞれのjointの親空間の軸が一致していないと、同じ方向に動きません。
・optimize scene size options の pairBelnd を実行するとpairBlendノードが削除されてしまいます。
skeletonの編集
試作の段階で secondaryはもう少し数があると良さそうに感じましたので、 コントローラーの数を決めてそこからlatticeの分割を決めてみます。
secondaryコントローラーの数を4とします。
detailを間に1個ずつ入れるとして、このくらいの数と分割にしようかと思います。
なめらかに変形させたいので、ffdノードのlocalInfluenceもここで決めておきます。
デフォルトの2,2,2だとパキっとした変形になってしまいます。
倍の4,4,4にすると、滑らかになりますが段差が出てしまっています。
8,8,8にするとかなり変形がぬるくなりますが、段差は消えました。
ぬるくならないようにしつつ、なめらかさを確保するにはlatticeの分割を増やす必要がありますが 今回は厳密な変形を求めているわけではないので8で行こうかと思います。
あとは、試作時に作成した概略図を参考にjointとリグモジュールを追加していきます。
サンプルデータ: corridorA_skeleton_v0003.ma (記事の最後のダウンロードリンクからダウンロードできます。)
build
パッケージのverを進め、早速 corridorA_skeleton_v0003.ma をセットしてtest buildを実行してみます。
detail01_jnt ~ detail09_jnt でlatticeShapeをbindし、weightを書き出します。
latticeが生成されるのが 500 としているので、すでに作成済みの skinWeightフォルダより後に読み込まれるようにする必要があります。
skinWeightAfter フォルダを作成し、そこに保存しました。
コントローラーをつかんで動かしてみます。
localInfluenceの値が大きいのでやはり変形がぬるい感じはしますが、一通り動かした感じ問題なさそうです。
コントローラーの形状を整え保存し、for shipping で生成します。
出来上がったデータをcorridorA_asset_v0002.ma として保存しました。
テストアニメーション
出来上がったアセットを使用してアニメーションを作成しました。
床と壁が流れ続けるだけだと味気ないので、必死に前に進むダイスを追加しました。
サンプルデータ: corridorA/mayaScenes/corridorA_animation_v0001.ma (記事の最後のダウンロードリンクからダウンロードできます。)
いつまでも出口にたどり着けないちょっとした悪夢のような動画が出来上がりました。
もう少し通路モデルにディテールがあると賑やかになりそうですね。
おわりに
今回はプロップというよりも背景寄りの題材でした。
メッシュを変えてしまえば何かしらプロップとしての運用もできるかと思います。
準備されたモデルの個数以上には伸ばせないという制約があるので、 対応しきれない場面も出てきてしまいそうですね。
もっと複雑・大規模なモデルを使用するのであれば、 MASHやbifrostなどを使用してプロシージャルなものを作った方がよいかもしれませんね。





























