チュートリアル / Mayaにおける魅惑のプロップリグ~モジュラーリギングシステムの基本と応用~
第4回:リグモジュールの開発とセットアップ その3〜宝箱のアニメーション〜
- GAZEN
- Maya
- アニメ
- キャラクター・リグ
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 学生・初心者
- 映画・TV
さて、今回もプロップのセットアップを行っていきたいと思います。
しかし、その前にちょこっとだけやる事がでてきました。
システムのアップデート
前回の作業中に自分で使っていてちょっとかなりイライラしてしまったので、 リグシステムを調整しました。
イライラポイント
・リグモジュールノードを作る際にスクリプト実行なので面倒
・リグモジュールの一覧が無いので面倒
・セットアップで使用したモデルと骨格の組み合わせが判らなくなる
・セットアップ実行時の追加オプション(restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)のon/offを書き換えて実行するのが面倒
・セットアップ実行がスクリプトエディタからなので、データの設定変えて実行してmayaが落ちた際に書き直しなので面倒
これらを解消する為に以下2点を行いました。
・セットアップデータの管理方法設定
・GUI作成
セットアップデータの管理方法設定
セットアップの内容をパッケージでまとめてバージョンをつけて管理し、 任意のタイミングで任意のバージョンのセットアップを再現できるようにしたくなりました。
その為に、管理したい内容と管理方法を決めていきます。
管理したい内容
現在セットアップ実行時のオプションはこのくらいあります。
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v10.ma"
setupDataPath = "K:/dice/setupData_v01/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
これらのオプションについては本番ビルド時には固定にしたいので、情報を残す必要はなさそうです。
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
なので、記憶しておきたいのはこの辺ですね。
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v10.ma"
setupDataPath = "K:/dice/setupData_v01/"
globalScale = 2.0
jsonファイルにこの情報を記載して、セットアップ時の情報を管理しようと思います。
外部に保存しておけば 後日再現しやすいですし、万が一作業中にmayaがクラッシュしてもダメージが最小限に抑えられます。
このjsonファイルの内容を専用ツールによって読み書きできるようにすればイライラが解消されそうです。
このjsonファイルとweightデータとrestoreCtrlValue・restoreCtrlSahpe・restorePostTransform用のjsonファイルをまとめて1つのパッケージとしてバージョン管理出来れば、バージョン毎の再現が容易になります。
管理方法・置き場所
管理するとなると、データの置き場所も含めて設定したいですね。
置き場所のルールを設定しておくとツールでのアクセスが容易になります。
今回はこのように階層ルールを設定します。
セットアップのデータ・情報の管理についてはこのようにしてみました。
モデルと骨格データはパッケージのバージョンが上がっても同一のものを使用する可能性があるので、
セットアップデータ置き場とは別の場所の方が都合が良さそうです。
こちらも専用の格納場所とネーミングルールを決めてしまえばもっと管理が楽になりそうですね。
管理ツールの作成とテスト
このルールを基にして、パッケージを管理する為のツールを作成しました。
詳しい使用方法については同梱のマニュアルをご覧ください。
試しに前回記事で作成したサイコロリグのパッケージを作成し、ビルドできるか試してみます。
まずは階層を準備します。
今回は
rootPath を
"K:/proj/AREA/assets/" としました。
assetTypeはautodeskFlowPTを参考にpropとしておきます。
assetName は diceでフォルダを作成します。
その下にmayaScenes とsetupPackagesフォルダを作成します。
※数が多くなるときっとイライラし始めるとおもうので、そのうちフォルダ作成機能をつけると思います。
ツールを起動し、rootPathを設定します。
パッケージが無いので、createで v001 を作成します。 使用したモデル・骨格データを とりあえずmayaScenesフォルダに移動し、 drag&dropでフィールドへ登録します。
使用したweightを setaupData_dev_v001フォルダ下へ移動します。
for shipping にてビルドを実行してみます。
無事データが組み上がりました。
これで少しはイライラが解消されると思います。
もう一つのイライラ リグモジュールノード
もう1つのイライラを解消するために、 リグモジュールノードの作成・閲覧をサポートするツールも作成しました。
これで作業効率が少し上がりそうです。
他調整
ツールの作成に伴いリグシステムの他の部分も調整を行いました。
・rigProcess.cleanUpProcess()
keyableなアトリビュートが存在しないparamCtrlを削除するようにしました。
コントローラーに接続されているhistoryをhideするようにしました。
・rigProcess.setupFromJson()
新規追加 パッケージ管理用jsonファイルを渡すことで、rigProcess.setupMainProcess()を呼び出せます。
・rigProcess.setupMainProcess()
setupFromJsonの実装に伴い、引数を変更しました。
今回のお題
閑話休題。
今回のお題はこちらのモデルです。
いわゆる、宝箱ですね。
色んな作品で登場する宝箱は色々動きがあって面白いですね。
・単純に開閉
・スクワッシュして飛び上がる
・ゴトゴト動く
・中身が輝く
・歯と舌が生えてて人を食べる
・手足が生えて襲ってくる
後半はちょっとトラウマが混ざってますね。
さて、どういった動きをさせようか脳内アニメーターさんと相談してみます。
このような回答が返ってきました。
要素分解
訴求を元に必要なセットアップの要素を抽出し、作業量を見積ります。
・蓋の開閉
単純な回転コントローラーで問題なさそう。
前回作成した pointCtrlモジュールを使用予定
・側面の取っ手
回転・移動が必要
こちらもpointCtrlモジュールを使用予定
・全体スクワッシュ
前回作成のlattice等を使用すれば出来そう。
蓋を開けた際に、BBOXサイズが変わるのでそれに対応できるか確認が必要
・底面の縁を支点に傾ける
新規モジュール作成必要
まずは試作から
こんなところですね。
それでは順番に進めていきます。
蓋と取っ手のセットアップ
作業階層とパッケージの準備
まずは作業階層を準備します。
assetType : prop
assetName : treasureChest
で作成します。
パッケージもv001を作成します。
骨格データとモジュールの設定
さて、ひとまず簡単な蓋と取っ手のセットアップを済ませてしまいます。
蝶番など円形で回転するパーツは、jointの位置が正しくないと動かした時に目立ってしまうのでキチンと配置します。
※この時モデルでヒンジパーツ同士の中心がずれていたり、形状がゆがんでたりするとどうにもならないので差し戻します。
取っ手用の両側分jointを配置しました。
リグモジュールを設置していきます。
折角なのでセクションを body(箱部分) と topLid(蓋部分)で分けておきます。
蓋のモジュールはpointCtrlを作成し、このように設定しました。
次に取っ手のモジュールも同じくpointCtrlを作成し・・・・・・
さて、ちょっと悩ましいですね。
モジュールを1つにして左右のjointを登録してしまうか、左右個別にモジュールを作成するか。
どちらにしろ左jointを修正して右jointをmirrorJointで生成しなおした場合、 モジュールへのコネクションが切れてしまうのでモジュールへの登録がやり直しになってしまいますね。
1つならまだ頑張れますが人型で全身とかだと無理ですね。心が。
イラっとしたのでモジュール改修(反対側もセットアップ機能の追加)
そんなわけで pointCtrlモジュールを改修して、反対側のセットアップも実行機能を追加します。
処理としては、
・登録されているjoint名を、命名規則に沿って反対側のjoint名に変換
・反対側のjointが存在する場合は、同じ設定でセットアップ
これらを追加しようと思います。
追加するパラメーターはmirrorSetupSettingを親として、
mirrorSetup
反対側もセットアップするか否かのbool
mirrorCtrlColor
反対側のコントローラーの色を変えたくなる事もあるので、反対側の色
この2つを追加します。
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"joints",
"useTranslateX",
"useTranslateY",
"useTranslateZ",
"useRotateX",
"useRotateY",
"useRotateZ",
"useScaleX",
"useScaleY",
"useScaleZ",
##予約済みアトリビュート
"ctrlShape",
"ctrlColor",
##固有アトリビュート 反対側用
"mirrorSetupSetting"
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"joints": {"type":"message", "multi":True, "default":""},
"useTranslateX": {"type":"bool", "multi":False, "default":True},
"useTranslateY": {"type":"bool", "multi":False, "default":True},
"useTranslateZ": {"type":"bool", "multi":False, "default":True},
"useRotateX": {"type":"bool", "multi":False, "default":False},
"useRotateY": {"type":"bool", "multi":False, "default":False},
"useRotateZ": {"type":"bool", "multi":False, "default":False},
"useScaleX": {"type":"bool", "multi":False, "default":False},
"useScaleY": {"type":"bool", "multi":False, "default":False},
"useScaleZ": {"type":"bool", "multi":False, "default":False},
"mirrorSetupSetting":{"type":"compound","ch":["mirrorSetup","mirrorCtrlColor"],"multi":False},
"mirrorSetup": {"type":"bool", "multi":False, "default":False},
"mirrorCtrlColor": {"type":"enum","enumList": rigSettings.COLORS_LIST,"default":"yellow"},
}
次に処理を追加します。
mirrorSetup の値を見て、jointsを順番に処理していきます。
##セットアップ実行用
def main(**kwargs):
lockAttrs = ["v"]
useAttrKeys = [
"useTranslateX",
"useTranslateY",
"useTranslateZ",
"useRotateX",
"useRotateY",
"useRotateZ",
"useScaleX",
"useScaleY",
"useScaleZ",
]
for useAttrKey in useAttrKeys:
if kwargs[useAttrKey] ==False:
attr = useAttrKey.replace("use","")
attr = attr[0].lower() + attr[1:]
lockAttrs.append(attr)
for joint in kwargs["joints"]:
setupPointCtrl(kwargs["section"],joint,kwargs["ctrlShape"],kwargs["ctrlColor"],kwargs["ctrlScale"],lockAttrs)
##反対側セットアップ---------------------
if kwargs["mirrorSetupSetting"]["mirrorSetup"]:
for joint in kwargs["joints"]:
##rigSettings.SWITCH_SIDE_DICTを元に、対となるjoint名を返す
otherSideJoint = rigUtil.getOtherSideJoint(joint)
##重複処理回避 + 存在しない場合は次へ
if otherSideJoint in kwargs["joints"] or cmds.objExists(otherSideJoint) == False:
continue
if otherSideJoint not in kwargs["joints"]:
setupPointCtrl(
kwargs["section"],otherSideJoint,kwargs["ctrlShape"],kwargs["mirrorSetupSetting"]["mirrorCtrlColor"],kwargs["ctrlScale"],lockAttrs)
return
さて処理の追加が出来たので、モジュールノードを追加してみます。
それを骨格データとして保存し、skeletonFileにセットします。
for test でビルドしてみます。
無事に反対側も生成されました。
現状で蓋・取っ手・本体箱をそれぞれjointにsmoothBindを行います。
パッケージフォルダ内にskinweight格納用のフォルダを作成し、それらのskinWeightを書き出します。
骨格データにも importSkinweightモジュールを作成し、該当のフォルダのskinWeightを読み込む様に設定します。
再度ビルドを実行しますと
とりあえず最低限動かせる物が出来ました。
全体スクワッシュ
次に全体のスクワッシュを追加します。
パッケージのバーションを1つ上げて v002で作業を行います。
※今回は要素毎に判り易くバージョンを上げて行きますが、あまりバージョンを消費し過ぎると桁数が足らなくなるという事態になることもあります。(ありました)
モジュールの追加
構造的にはサイコロのスクワッシュを入れ込む事になるのですが、BBOXの取得をどうするかですが とりあえず前回のセットアップ内容とモジュールの構成を眺めつつ、モジュールを追加していきます。
眺めてたら移植できそうだったのでjointとモジュールを移植してきました。
latticeのガイドと、BBoxPosのガイドメッシュは今回にあわせて更新してあります。
実行順だけ念のためBBoxPosモジュールが一番最初に実行されるように調整しておきます。
こちらを treasureChest_skeleton_v02.ma として保存し、v002パッケージのskeletonFileに登録します。
され、これで一度ビルドしてみます。
topLid_ctrlをつかんで開いてみると、大きなエラーは無さそうですが調整は必要そうです。
データを細かく確認していくと調整点が幾つか見えてきました。
・squashRoot_jnt は底面合わせで良さそう。
・getBBoxPosSetup モジュールではガイドメッシュのバインドが強制されてるので、ウェイトをいじれない。モジュールのオプション足して対応。
・よく見たらモデルがY=0より下にはみ出てたので修正必要。
※postTransform機能で調整することも可能ですが、状況が許すのであればモデルから修正したほうが確実です。
getBBoxPosSetupモジュール改修
ここですね。guideMeshを強制的にguideSampleJoint1つでバインドしてますね。
これをon/offできるようにパラメーターを追加します。
##bind guideSampleJoint guideMesh
rigUtil.createSkinCluster(guideMesh,[guideSampleJoint])
bindGuideMesh というboolのパラメーターを追加しました。
この際、defaultを Trueに設定しておきます。
diceで再ビルドを行った場合、リグモジュールのアップデートが行われていないので bindGuideMesh パラメーターに関してはdefault値が採用されます。
そうすると、今まではbindされていたguideMeshがbindされなくなってしまい挙動が変わってしまいます。
もちろんそのアップデートを把握していて対応すれば問題ないのですが、アセット数が増えてくると対応が煩雑になるのでモジュールのアップデートは既存の処理結果が変わらない様にしています。
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"guideJoint",
"useTranslate",
"useRotate",
"useScale",
"bindGuideMesh",
"guideMesh",
"outputs",
"useParamCtrl",
"useParamPrefix"
##予約済みアトリビュート
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"guideJoint": {"type":"message", "multi":False, "default":""},
"guideMesh": {"type":"message", "multi":False, "default":""},
"useTranslate": {"type":"bool", "multi":False, "default":False},
"useRotate": {"type":"bool", "multi":False, "default":True},
"useScale": {"type":"bool", "multi":False, "default":False},
"bindGuideMesh": {"type":"bool", "multi":False, "default":True},
"outputs": {"type":"compound", "multi":True, "ch":["outputTarget","outputPosition","outputX","outputY","outputZ","useOffset","connectWeight"]},
"outputTarget": {"type":"message", "multi":False, "default":""},
"outputPosition": {"type":"enum", "enumList":["Min","Max"], "default":"Min"},
"outputX": {"type":"enum", "enumList":["+","-","none"], "default":"+"},
"outputY": {"type":"enum", "enumList":["+","-","none"], "default":"+"},
"outputZ": {"type":"enum", "enumList":["+","-","none"], "default":"+"},
"useOffset": {"type":"bool", "multi":False, "default":True},
"connectWeight": {"type":"bool", "multi":False, "default":True},
"useParamCtrl": {"type":"enum", "enumList":["root","sectionParamCtrl"], "default":"sectionParamCtrl"},
"useParamPrefix": {"type":"enum","enumList":["none","section","section+side","prefix","prefix+side"],"default":"none"},
}
該当箇所はこんな感じに改修
##bind guideSampleJoint guideMesh
if bindGuideMesh:
rigUtil.createSkinCluster(guideMesh,[guideSampleJoint])
他の修正も行って、treasureChest_skeleton_v02b.ma として保存しました。
モデルの位置も修正して treasureChest_model_v01b.ma として保存しました。
これらをセットしてビルドを行います。
BBoxGuideMeshはバインドされていません。
bodySquash_BBGuide_space bodySquash_BBGuide_jnt でバインドし、蓋だけ開くようにします。
上下のコントローラーの位置を見た感じ、BBOXの変形内容は取得できていそうですね。
あとはlatticeを所定のjointでバインドします。
ここでサイコロのセットアップでもあったように、デフォーマーが掛かる順番に留意する必要があります。
箱の蓋が開く -> latticeで変形する の順番になるようにモジュールの実行順を調整します。
latticeのskinweightは別のフォルダを作成して、そちらに保存します。
今回は650_skinweight というフォルダを作成しました。
このウェイトを読み込むためのリグモジュールを作成しつつ、実行順を調整します。
これを treasureChest_skeleton_v02c.ma として保存・セットし、再度ビルドを行います。
動かしてみると、ちゃんと蓋の開閉を考慮したBBoxサイズでスクワッシュしてくれました。
縁でガタゴト
手法案
支点を倒す方向に合わせて動的に変えたい訳ですが、
どっちに倒れているか
の判別が必要です。
判別方法はいくつか思いつきました。
1. 倒れるjointのrotate値を見て判別する
一見簡単で確実にみえますがrotateOrderの都合でrotate値と見た目の角度が一致してない事があるので、
もしやるのであればrotate値そのままではなく、軸毎の回転を抽出する必要があります。
2. nurbsCurveを配置し、最近接点を取ることで方向を判別
nurbsCurveを配置してjointの先端などの位置から、nurbsuCurve上の最近接点を取得することで倒れている方向を判別します。
割と簡単なのですがnurbsCurveを四角にしてセンシングした場合、四隅に向いたときにピッタリ行ってくれるかが懸念です。
3. 倒れるjointの先端の位置を取得して、2次元的に判別
倒れるjointの上方向に子のjointを設置し、その位置が倒れるjointに対して横方向にどの程度動いたかで判別します。
translate値を取得するので、1つ目の様な問題は起きません。
どれを採用しても実現はできるんですが、一番簡単そうなのは3なので3にします。
では具体的に支点はどこに行ってくれば良いのでしょうか?
簡単に図を描いて考えてみます。
まず、前後に傾けた際はこのエッジ上に支点があって欲しいですね。
つぎに、左右に傾けた際にはこのエッジ上にあって欲しいですね。
前後・左右1方向に傾くのであればこれで良いのですが、 斜め方向に傾いたりもするとおもうので両者を重ねてみます。
そうすると、領域が4分割されて見えます。
その上で両者の支点が共存できる場所を考えると、4隅に限定されそうです。
では逆に傾いていないときは中心にあるべきでしょうか?
傾いていないので、支点はどこでも関係ないような気がしますね。
試作
方針も決まったので、早速試作してみようと思います。
支点移動するノードはjointを使いたいところですが、jointにはrotatePivotがないのでtransformノードを使用します。
途中まで組んでみましたが、アニメーション時はrotateでの制御とtranslateでの制御どちらがやり易いでしょうか?
translateの制御であれば、わざわざ新たにセンシング用のjointを設置しなくてもコントローラーの値から引っ張って来た方がシンプルに組めそうですね。
今回コントローラーはtranslate制御にしたいと思います。
試作を進めてひとまず動くようにしてみました。(treasureChest_sample_v01.ma)
trans_jnt を移動させると動きます。
コアとなる支点移動と移動方向の判別部分はこのように組んであります。
pivotの距離が固定値であればfloatConditionノードから直接rotPivotへ繋ぐことも出来ます。
使い道がありそうかなと思い、pivotの距離に対して方向を掛ける様にすることで位置を調整できるようにしてみました。
ここで1点懸念がでてきました。
今回の試作では、inputRot_jnt の -Y = front、 -Z = left と定義して組みました。
別の軸も front / left として定義出来た方が使いやすそうに思えます。
モジュールのパラメーターで指定してセットアップ内で組めばできると思いますが、 先述の移動軸の+/-方向を判別部分を軸の定義に合わせて設定が必要になりそうで、
モジュールの修正が必要になった際にイライラしそうだなぁと。
trans_jnt と rotPivot あたりの軸の向きを固定にしてしまい(例えば -Y = front、 -Z = left)、 軸の定義に合わせてこれらのノードの向きを調整すればシンプルな処理に出来そうです。
この辺りを留意しつつ、実装に進みます。
モジュールへ実装
tiltCtrl という名前でリグモジュールを作成します。
まずはパラメーターの設定を行います。
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
##回転させたいjoint
"rotJoint",
##固有アトリビュート
##コントローラーを設置する距離
"aimPointDistance",
##固有アトリビュート
##回転させたいjointのコントローラーを設置したい軸の方向
"aimAxis",
##固有アトリビュート
##回転させたいjointの正面としたい軸の方向
"frontAxis",
##予約済みアトリビュート
##コントローラー名を生成する際のprefixの設定(enum : "section","section+prefix","prefix")
"setCtrlPrefix",
##予約済みアトリビュート
##コントローラーのワイヤーフレームカラー
"ctrlColor",
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"rotJoint": {"type":"message", "multi":False, "default":""},
"aimAxis": {"type":"enum", "enumList":["x","y","z","-x","-y","-z"], "default":"x"},
"frontAxis": {"type":"enum", "enumList":["x","y","z","-x","-y","-z"], "default":"-y"},
"aimPointDistance": {"type":"double", "multi":False, "default":0.0, "min":0.0, "max":100},
}
最終的な構造はこのようになりました。(空間リセット用の階層は省略してあります)
SSRig/rigModules/tiltCtrl.py
試作から変わった部分としましては、
・pivotまでの距離は、コントローラーに設置したアトリビュートで調整できるようにしました。
・pivot位置が目視しやすいように template表示のnurbsCurveを追加しました。
・ctrl・ctrlJoint・pivot は常に front = -y, left = -z になるように調整されます。
・rotJointSetupの傾きの制御はikHandleではなくaimConstraintを使用しました。その際upVectorはfrontAxisを反転させたものを採用します。
骨格データへ入れ込み
まずはパッケージのバージョンを1つあげてv003を作成します。 次に倒す用のjointを階層に差し込み、tiltCtrlのモジュールノードを作成して各種情報を設定します。
これを treasureChest_skeleton_v03.ma として保存し、v003 のskeletonFileとして登録します。
データをビルドして動かしてみると・・・
箱の本体部分は今までroot_jntにバインドされていたので、追加したbody_jntへバインドし直す必要があります。
また、pivotの距離アトリビュートはデフォルトで10としているので結構ずれてしまっています。
この値も調整し、defautValueとして書き出す必要があります。
コントローラーの形状も整え、各種情報を書き出し for shippingでビルドします。
動かしてチェックしてみると、問題なさそうです。
treasureChest_asset_v01.ma として /treasureChest/mayaScenes/に保存しました。
テストアニメーション
さて、一通り動かしてみての感想をまとめてみました。
・傾けた状態で、位置・角度を微調整できるコントローラーがあるともう少し使い勝手が良さそうです。
・傾けた状態で、水平方向に回転できるコントローラーがあっても良いかなと思いました。
・取っ手のコントローラーは IK制御のコントローラーにし、位置調整用のコントローラーを別途根元につけた方が制御しやすそうです。
などなど、調整の余地は色々とありそうです。
おわりに
データの再現をより簡単にする為に、パッケージ管理機能を追加してみました。
これによって差分の追加更新がやり易くなりました。
ただ管理機能を充実させていくと、そのうちパイプラインの方へも手が伸びていき
気が付いたら全部整備してるなんてこともありますので、ご注意ください。
さてやはり宝箱なので、なにかしら中身が欲しくなりますね。
次回は中身を足していきたいと思います。