チュートリアル / Mayaにおける魅惑のプロップリグ~モジュラーリギングシステムの基本と応用~
第2回:リグモジュールの開発とセットアップ その1
- GAZEN
- Maya
- アニメ
- キャラクター・リグ
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 学生・初心者
- 映画・TV
はじめに
前回簡易的なリグシステムを構築しましたので、 今回はそれを使用して実際にリグモジュールを開発しつつセットアップを行っていこうと思います。
今回のお題はこちらのモデルです。
シンプルなサイコロのモデル(diceModel_v01.ma)を用意しました。
このサイコロモデルにリグを入れてアニメーションし易くしたいと思います。
さていきなりセットアップの実作業開始する訳には行きませんので、まずは事前準備を行います。
セットアップの設計
まずはセットアップの内容を設計します。
実際の案件であれば こんな機能が欲しい・こんな動きをさせたい といった情報をアニメーター・モデラー・アートなど各部署にヒアリングしたり、コンテ・ストーリーボードといった演出情報から拾ってきます。
リリースした後に、
あ、こんな動きさせたいんで
とかこういう変形する想定だったんで
とか初耳の情報が多少出てくるのは仕方ないんですが、全く方向性が違ってると作業自体が無駄になってしまうのでなるべく事前に情報を集めます。
またイメージを具体化し、共有する助けとなる実際の物、アニメ、映画などのリファレンスも集めておくと色々役に立ちます。
訴求定義・動きの想像
今回は一人ぼっちなので、好き勝手に動きを想定します。
絵で描くとこのようなイメージです。
これをなるべく判り易い言葉で文章にしてみるとこうなります。
・サイコロを投げると空中で回転しながら落下していきます。
・地面にぶつかると、ちょっと潰れて戻りつつバウンドします。
・バウンドしたサイコロはまた空中で回転しながら放物線を描いて落下します。
・バウンドした後にまた着地して、転がりそうな感じで傾いて戻って止まります。
こんな動きができるセットアップを目指します。
要素抽出
動きの想定が出来たので、これらの動きに必要な要素を分解していきます。
動きの要素を分解することで必要なコントローラー・ギミックが洗い出せるので、
既存のリグモジュールで対応できるか? 新作する必要があるか? など実際の作業量を見積もることができます。
先述の動きの内容を個々に分解していくと以下のようになります。
・サイコロを投げると空中で回転しながら落下していきます。
全体移動 + モデル中心を支点にして回転
・地面にぶつかると、ちょっと潰れて戻りつつバウンドします。
モデル全体のスクワッシュ
・バウンドしたサイコロはまた空中で回転しながら放物線を描いて落下します。
全体移動 + モデル中心を支点にして回転
・何回かバウンドした後に着地して、転がりそうな感じで傾いて戻って止まります。
接地点を支点にして回転
これらを整理し、必要な要素として書き出すと
1. 全体移動
2. モデル中心を支点にして回転
3. 接地点を支点にして回転
4. モデル全体のスクワッシュ
となります。
4のスクワッシュに関しては、ストレッチ(伸びる)機能も足しておくと面白そうなので追加してみます。
作業量見積もり
抽出した要素を元に、作業内容を見積もっていきます。
1. 全体移動
前回作成したリグの基本構造にある全体移動コントローラーを使用
作業なし
2. モデル中心を支点にして回転
シンプルなコントローラーの設置モジュール作成
3. 接地点を支点にして回転
接地点を支点にするギミックの開発とモジュール作成
4. モデル全体のスクワッシュ
スクワッシュ・ストレッチのギミック開発とモジュール作成
ざっとこんな作業内容で、今の時点では新作するリグモジュールは3つになりそうです。
セットアップ作業
作業見積もりが出来たので、ここから実際にセットアップの作業を行っていきます。
1. 全体移動
モデル(diceModel_v01.ma)は準備済みなので、それに合わせて骨格データを作成します。
現段階ではモジュールは空で、root_jnt と 次の工程で使うであろうモデルの中心位置にjointを配置しておきます。
次工程での説明の都合でもう1つ中間にjointを追加しておきます。
このデータをdiceSkeleton_v01.maとして保存します。
前回作成したコマンドのパス部分を変えて実行します。
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v01.ma"
setupDataPath = "K:/dice/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
実行結果はこの通りです。
全体移動コントローラーが設置されたので、全体移動に関してはこれで終了にします。
2. モデル中心を支点にして回転
ここではシンプルなコントローラーの設置を行うリグモジュール作成します。
新規作成するリグモジュールの設計
リグモジュールの挙動としては、
・指定したjointの位置・向きにコントローラーを作成する
・作成したコントローラーで指定したjointを動かすことができる
というとてもシンプルな物を作成してみます。
試作
シンプルなモジュールですがいきなりスクリプト化はせず、まずは手作業で組んでみます。
まっさらな新規シーンから作ると大変なので、1. 全体移動 で作成したデータを元にして作業をします。
ただし、applyCleanup = True だと色々ロックされてやりづらいので、applyCleanup = False にして実行します。
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v01.ma"
setupDataPath = "K:/dice/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
クリーンナップが実行されないので、試作作業がしやすい状態で組み上がりました。
コントローラーの作成ルール
全体移動コントローラーは作成済みですが、root_jnt以下 のjointに対してコントローラーを設置するのは今回が初となります。 今後リグモジュールでコントローラーを設置する機会が多くなるので、ここでコントローラーの基本的な作成ルールを決めておきます。
root_jnt 以下のskelton階層は可能な限りセットアップ時にノードを追加したくないので コントローラーは ctrl_gp 以下の階層に格納します。
centerRot_jntにコントローラーを設置するとして、その親であるtestPoint_jntの位置・角度が変わった場合、設置したコントローラーもなるべく同じ動きをさせたいので、testPoint_jntと同じ動きをするノードをコントローラーの親として作成します。
この手のノード名も proxy とか dummy とか 色々と呼び名があるのですが、 今回は _proxy を末尾につけtestPoint_jnt_proxy を作成します。 全体スケールへ対応する為に、作成した testPoint_jnt_proxy を postTransform_ctrl の下にペアレントします。
次に親ノードとproxyノードの動きを合わせる為にtestPoint_jntからtestPoint_jnt_proxyへparentConstraint(offsetなし)を適用します。 ※matrixとかをうまく使うと軽くなったりもしますが、判り易さを優先して諸々コンストレインを使います。
スケールも踏襲させたいのですが、scaleConstraintではなく親ノードからproxyノードへコネクションします。
個人的な好みもあるのですが、scaleConstraintはconstraint元のworld空間でのscaleを拾ってしまうので少々使いづらいです。
コントローラーノードについてはスクリプトを使用して作成します。
前回触れていませんでしたが、コントローラーノードはjointにnurvsCurveShapeをparentして作成しています。
通常のtransformノードでも問題はないのですが、シアーとか色々考慮してjointにしています。
※前回のverにミスがありましたので、今回のverのライブラリを使用してください。
from SSRig import rigUtil
joint = "centerRot_jnt"
rigUtil.createCtrlNode("centerRot_ctrl","circle_y","joint",size = 10,position = joint,rotation =joint)
作成したコントローラーcenterRot_ctrlをtestPoint_jnt_proxyの下へ移動させます。
このままだとtranslateに数値が残ってしまうので、数値を0にするためにコントローラーと同じ位置・向きの数値リセット用ノードを作成し、testPoint_jnt_proxy
-> 移動値リセット用ノード ->
centerRot_ctrl となるように階層化します。
この数値リセット用ノードに関しても offset とか parentSpaceとか space とか色々呼び方があるのですが、 今回は _offset を末尾につけることにします。
最後にcenterRot_ctrlと対象centerRot_jntをつなげます。
試作では簡単に組み換えができるように
parentConstraintを使用します。
testPoint_jntを動かしてみると、centerRot_ctrl とcenterRot_jnt が一緒に動きます。
ここまでの内容を簡略化して描くとこのようになります。
最終的な実装ではconstraintではなく、matrixによるコネクションの方が都合がいい場面が多いので変更します。
親ノードがroot_jntの場合、postTransform_ctrl が既にroot_jntと同様の動きをしているのでproxyノードを作成せずにpostTransform_ctrlをコントローラーの親ノードとして使用します。
コントローラーからjointへのコネクションは、なるべくconstraintを使用せずにconnectAttrで直接つなげるか、matrixを利用して親階層空間内での変化だけを抽出してjointへ渡すようにします。
matrixでのやり取りは簡単に描くとこのようなイメージです。
prantJoint と parentJointProxy は同じ動きをさせているので、 それを前提としてparentJointProxy空間内でのコントローラーのmatrixを取得し、jointへ渡します。
実際にはjointOrientとかスケールとか色々ケアが必要ですが詳しい内容は割愛します。
ctrlOffsetが挟まっていなければ直接connectAttrが出来るので、セットアップの内容をみて調整します。
スクリプト化
試作した内容を再現する為に、リグモジュールとしてスクリプト化します。 リグモジュールの実行に必要な情報を考えます。
・コントローラーを接続したいjoint
・コントローラーの形状・色
・keyableにするアトリビュート
ひとまずこれらで作成します。
コントローラーの形状・色 に関しては予約済みアトリビュートを使用します。
予約済みアトリビュートはシステム内で予約されているので、オプション値はrigSettingsから呼び出されます。
コントローラーに接続したいjointについてはmultiにし、複数のjointに対して同設定のコントローラーを設置できるようにしてみます。
rigModules/pointCtrl.py
from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "ctrl"
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"joints",
"useTranslateX",
"useTranslateY",
"useTranslateZ",
"useRotateX",
"useRotateY",
"useRotateZ",
"useScaleX",
"useScaleY",
"useScaleZ",
##予約済みアトリビュート
"ctrlShape",
"ctrlColor"
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"joints": {"type":"message", "multi":True, "default":""},
"useTranslateX": {"type":"bool", "multi":True, "default":True},
"useTranslateY": {"type":"bool", "multi":True, "default":True},
"useTranslateZ": {"type":"bool", "multi":True, "default":True},
"useRotateX": {"type":"bool", "multi":True, "default":False},
"useRotateY": {"type":"bool", "multi":True, "default":False},
"useRotateZ": {"type":"bool", "multi":True, "default":False},
"useScaleX": {"type":"bool", "multi":True, "default":False},
"useScaleY": {"type":"bool", "multi":True, "default":False},
"useScaleZ": {"type":"bool", "multi":True, "default":False}
}
def setupPointCtrl(section,joint,ctrlShape,ctrlColor,ctrlSize,lockAttrs):
##get parentJoint
parentJoint = cmds.listRelatives(joint,p=True)[0]
parentProxy = rigSettings.NODENAME_DICT["ctrlWorldSpace"]
if parentJoint != rigSettings.NODENAME_DICT["rootJNT"]:
##create parentProxy
##connect jointParent to parentProxy
parentProxy = rigUtil.createParentProxy(parentJoint,rigSettings.NODENAME_DICT["ctrlWorldSpace"],attrs = ["t","r","s"])
##create ctrlNode
ctrlNode = rigUtil.createCtrlNode(
joint.replace(rigSettings.SUFFIX_DICT["jnt"],rigSettings.SUFFIX_DICT["ctrl"]),
ctrlShape,
"joint",
size = ctrlSize,
position = joint,
rotation =joint
)
cmds.parent(ctrlNode,parentProxy,a=True)
##add ctrlOffset
rigUtil.insertOffsetNode("joint",ctrlNode,"_offset")
##connect ctrl to joint
rigUtil.matrixConstraint(ctrlNode,joint,parentProxy,parentJoint,attrs = ["t","r"],offset = False)
rigUtil.connectAttrZip(ctrlNode,joint,["s"],["s"],True)
##不要なアトリビュートをロック
rigUtil.setAttrState([ctrlNode],lockAttrs,True,True,False)
##コントローラーの色を設定
rigUtil.setWireFrameColor([ctrlNode],ctrlColor, 0.8)
##コントローラーを setsへ登録
rigUtil.addToSet(rigSettings.NODENAME_DICT["allCtrlSet"] + "|" + section + "Ctrl" + rigSettings.SUFFIX_DICT["set"],[ctrlNode])
##セットアップ実行用
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)
return
この状態でモジュールを作成します。
※今回の記事用でrigProcessの内容が改修されていますので、前回と使用するコマンドが異なっていますのでご注意ください。
from SSRig import rigProcess
rigProcess.createRigModuleNode(section = "body",prefix = "point",side = "center",moduleName = "pointCtrl")
モジュールが作成されました。
対象のjointをモジュールノードに接続します。
このデータをdiceSkeleton_v02.ma として保存します。
試行
リグモジュールの記述が終わったら、実際に実行して結果を見てみます。
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v02.ma"
setupDataPath = "K:/dice/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
見た感じ問題なさそうです。
ただ今回は 回転 コントローラーなので、リグモジュールノードの設定値を変更してdiceSkeleton_v02.ma
を更新し、再度実行します。
回転コントローラーはこれで実装できました。
3. 接地点を支点にして回転
具体案を詰める
接地点を支点にコロンとさせたいわけですが、もうすこし具体的にどうしたいかを詰めてみます。
考えられるパターンを箇条書きにし、メリット・デメリット・疑問点も一緒に書いてみます。
1. 移動に併せて回転する?
移動値に併せてサイコロが転がっていくようにする
どこかの面で接地してる前提になる?
角で着地して、そこからスタートさせる場合にはどうすべきか?
2. その場で上下動にする?
回転すると、最下部が接地して見えるようにその場で上下に移動する
常に最下部で接地してることになるので、接地の調整は全体移動コントローラーで行うことになる。
そのままだと滑ってしまうので、横移動に関しては手動かスクリプトで補助することになる?
3. 傾けるだけにする?
完全に転がり切らずに、サイコロの辺・角を支点として傾けるだけにする
どこかの面で接地してる前提になる?
角で着地した場合にどのような操作になるか?
4. 支点を移動させられるようにする?
任意の辺・角に支点を移動できるようにする。
支点を変更する際に、ポーズを維持したまま変更できるか?
維持したままに出来ない場合はスクリプトで補助する?
任意の点ではなく、ある程度移動できる範囲を絞る?
本来であればアニメーターさんにこれらの内容を持ち込んでどのパターンで行くかを協議します。
その際、往々にしてあるのが どれがいいですか? と質問すると これとこれ両方で切り替えられるように! と藪蛇になる場合もありますね。
さて、今回は一人なので脳内アニメーターさんと協議してみます。
1については平面上しか対応できないので、使いどころがかなり限定されそうです。
この動きがメインとなる映像であれば使えると思いますが、今回は無しで。
※余談ですが、箱の中にジャイロとかモーターを仕込んで実際に箱が自分で転がるようにしたアート作品がありました。
参考:https://vimeo.com/112463532
2はスクリプトなりで位置合わせを補助したら使えそうですね。
3は使いどころは限られますが、今回の仮想演出的には使えそうです。
4汎用性は高そうですが、今回のモデルは角が丸くなってるので地味に手動調整するのは面倒そうです。
2か3が良さそうです。2の方が応用が利きそうなので今回は2で進めることにします。
ただし、常に上下動し続けると空中での回転がやりづらくなってしまうので、on/offできるようにアトリビュートで制御できるようにします。
さて、目標が決まったので、実装に必要な要素を洗い出します。
・回転しているサイコロオブジェクトの最下部を割り出す
・最下部が 接地点に来るように上下に移動させる(on/off あり)
大きく分けてこの2点かなと思います。
試作
・回転しているサイコロオブジェクトの最下部を割り出す
まずはこれを試作してみます。
メッシュの最下部の高さはBoundingBoxの最下部を取得できれば良さそうです。
ロケーターを作成し、shapeのアトリビュートからBBoxMinY を translateYへコネクトします。
transformノードを動かしても、ロケーターの位置は変化しませんがvtx等をつかんで動かすと位置が変化します。
transformノードにもBBoxアトリビュートが存在するのですが、こちらはtranslateノード階層下全てのノードを合わせたBBox情報を取得できるようなので今回はshapeの方を使用することにします。
この位置情報をjointへ戻してあげれば良いので、位置の調整は済んでませんが試しにつないでみます。
予想はしてましたが、サイクルのワーニングが出てしまいました。
今は最終的なデフォーム結果であるdice_geoShapeからBoundingBox情報を取得しているので、 rot jointの親階層である translate joint に変化が起きると、rot jointから skinClusterを介してdice_geoShapeへ伝わり、botPointY_loc を経由して translate joint へ戻て来て というサイクルを起こしています。
位置取り用のセットアップを追加してサイクルを回避します。
一見サイクルしているように見えますが、rot joint から rot joint Copyへは connectAttrでrotateをつないでいるので、rot jointの親階層の影響は受けない為サイクルが起きなくなりました。
具体的には
・サイコロオブジェクトを複製し、BoundingBox情報を取得するためのガイドモデルを用意します。
・回転値を参照するjointを複製し、rotateをコネクトします。
・複製したjointで複製したサイコロオブジェクトをskinClusterで拘束します。
スクリプト化?
試作と大体の設計ができたので、スクリプト化していこうと思います。
必要な情報は
・回転を取得するjoint
・移動値を与えるjoint
・ガイドモデル
・スウィッチアトリビュートを設置するノード
このリグモジュールではコントローラーノードは生成しませんので、作成するリグモジュールの種類としては setup にしようと思います。
実装に進む前にスウィッチアトリビュートを設置するノードの指定方法をどうすべきかを考えます。
1. 文字列で別のコントローラー名を指定する
指定したコントローラーが既に作成されている必要があります。
別のリグモジュールで生成されるものを指定する場合には、実行順を調整する必要があります。
2. root_gp に固定する
root_gp は rigProcess.preProcess で createBasicStructure.py が実行された時点で作成されますので 基本的には他のリグモジュールが評価される前に存在します。
ただ今後すべてのパラメーターをroot_gpに押し込めると、それはそれで使い勝手が悪そうです。
3. パラメーター用コントローラーを作成する
この手のパラメーターを格納する為だけのコントローラーを作成します。
ただし、任意に作れるようにしてしまうと収集が付かなくなってしまう恐れがあるので、 リグモジュール毎に設定している section毎に作成します。
現状であれば1ですが、今後のアップデートを考えると2,3の方が良さそうです。
パラメーターコントローラー用モジュール作成
section毎にパラメーターコントローラーを設置する為のリグモジュールを作成します。
パラメーター用と判り易くするために、色・形は固定にします。
rigModules/paramCtrl.py
from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "ctrl"
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"joint" #コントローラーを追従させるjoint
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"joint": {"type":"message", "multi":False, "default":""},
}
##--------------------------------------------------------------------------------------------
def setup(section,side,joint,globalScale,**kwargs):
sideString = rigUtil.getSideString(side)
##ctrl name/shape/scale/color は固定
ctrlScale = globalScale*0.5
ctrlShape = "diamond"
ctrlName = "PM_" + section + sideString + rigSettings.SUFFIX_DICT["ctrl"]
ctrlColor = "green"
if joint == "":
joint = None
if cmds.objExists(ctrlName) == False:
parentJoint = cmds.listRelatives(joint,p=True)[0]
parentProxy = rigSettings.NODENAME_DICT["ctrlWorldSpace"]
if parentJoint != rigSettings.NODENAME_DICT["rootJNT"]:
parentProxy = rigUtil.createParentProxy(parentJoint,rigSettings.NODENAME_DICT["ctrlWorldSpace"],attrs = ["t","r","s"])
ctrlNode = rigUtil.createCtrlNode(
ctrlName,
ctrlShape,
"joint",
size = ctrlScale,
position = joint,
rotation =joint
)
cmds.parent(ctrlNode,parentProxy,a=True)
##不要なアトリビュートをロック
lockAttrs = ["t","r","s","v"]
rigUtil.setAttrState([ctrlNode],lockAttrs,True,True,False)
##コントローラーの色を設定
rigUtil.setWireFrameColor([ctrlNode],ctrlColor, 1.0)
##コントローラーを setsへ登録
##setsをsection毎に分割
rigUtil.addToSet(rigSettings.NODENAME_DICT["allCtrlSet"] + "|" + section + "Ctrl" + rigSettings.SUFFIX_DICT["set"],[ctrlNode])
return ctrlName
def main(**kwargs):
setup(**kwargs)
return
あとはこのモジュールノード(以後 セクションノード)を作成するタイミングを考えます。
リグモジュールノードを作成する際にセクションノードを作成、作成したモジュールノードの親として階層化します。
リグモジュールノードのsectionとセクションノードのsectionは一致させますが、sideに関しては一致させると逆にとても使いづらくなってしまうので、セクションノード用のsideとして引数を追加します。
その為に、リグモジュールノード作成コマンドを改修しました。
同名のリグモジュールノードが既に存在する場合にはモジュールノードを最新の状態で作り直すようにします。
rigProcess.py
def createRigModuleNode(section,prefix,side,moduleName,moduleNodeName = None,parent=rigSettings.NODENAME_DICT["moduleGP"]):
if cmds.objExists(rigSettings.NODENAME_DICT["moduleGP"]) ==False:
cmds.createNode("transform", name = rigSettings.NODENAME_DICT["moduleGP"])
if moduleNodeName == None:
moduleNodeName = rigUtil.addPrefix(section,prefix) + rigUtil.capitalizeString(moduleName)+ rigUtil.getSideString(side) + rigSettings.SUFFIX_DICT["module"]
moduleNode = cmds.createNode("transform",name = moduleNodeName, p = parent)
##リグモジュールに問い合わせ
moduleType,attrs,attrDict = getModuleInfo(moduleName)
inputValueDict = {
"moduleName":moduleName,
"section":section,
"prefix":prefix,
"side":side,
}
##必須アトリビュートを先に追加
if moduleType in list(rigSettings.TYP_COMMONATTR_DICT.keys()):
commonAttrDict = rigSettings.TYP_COMMONATTR_DICT[moduleType]
for attr in list(commonAttrDict.keys()):
addAttrs(moduleNode,attr,commonAttrDict,{})
if attr in list(inputValueDict.keys()):
setValues(moduleNode,attr,"",commonAttrDict,inputValueDict[attr])
##リグモジュール固有アトリビュートを追加
for attr in attrs:
#予約済みアトリビュート
if attr in list(rigSettings.COMMON_ATTR_DICT.keys()):
addAttrs(moduleNode,attr,rigSettings.COMMON_ATTR_DICT,{})
else:
addAttrs(moduleNode,attr,attrDict,{})
return moduleNode
def updateModuleNode(moduleNodeName,keepOrg =False):
#現在の値を取得
valueDict = getModuleNodeValues(moduleNodeName)
curParent = cmds.listRelatives(moduleNodeName,p=True)[0]
#旧ノードをリネーム
cmds.parent(moduleNodeName,w=True)
oldNode = cmds.rename(moduleNodeName, moduleNodeName + "_old")
#新ノードを作成
moduleNodeName = createRigModuleNode(valueDict["section"],valueDict["prefix"],valueDict["side"],valueDict["moduleName"],moduleNodeName,curParent)
moduleType,attrs,attrDict = getModuleInfo(valueDict["moduleName"])
##旧ノードの値をセット
for attr in attrs:
if attr in list(valueDict.keys()):
if attr in list(rigSettings.COMMON_ATTR_DICT.keys()):
setValues(moduleNodeName,attr,"",rigSettings.COMMON_ATTR_DICT,valueDict[attr])
else:
setValues(moduleNodeName,attr,"",attrDict,valueDict[attr])
chNodes = cmds.listRelatives(oldNode,type = "transform") or None
if chNodes != None:
cmds.parent(chNodes,moduleNodeName,a =True)
if keepOrg ==False:
cmds.delete(oldNode)
def createSectionParamNode(section,side,keepOrg,update):
moduleName = "paramCtrl"
moduleNodeName = section + rigUtil.getSideString(side) + rigSettings.SUFFIX_DICT["module"]
if cmds.objExists(moduleNodeName) and update:
updateModuleNode(moduleNodeName,keepOrg)
else:
createRigModuleNode(section,"",side,moduleName,moduleNodeName,rigSettings.NODENAME_DICT["moduleGP"])
##orderOfExecutionは100で固定
setValues(moduleNodeName,"orderOfExecution","",rigSettings.COMMON_ATTR_DICT,100)
return moduleNodeName
def createModuleNode(section,sectionSide,prefix,side,moduleName,keepOrg = False,updateSection = False):
##リグモジュールノード名を生成
moduleNodeName = rigUtil.addPrefix(section,prefix) + rigUtil.capitalizeString(moduleName)+ rigUtil.getSideString(side) + rigSettings.SUFFIX_DICT["module"]
##リグモジュールノードグループを作成
if cmds.objExists(rigSettings.NODENAME_DICT["moduleGP"]) ==False:
cmds.createNode("transform", name = rigSettings.NODENAME_DICT["moduleGP"])
##sectionモジュールノードを作成
sectionNode = createSectionParamNode(section,sectionSide,keepOrg,updateSection)
if cmds.objExists(moduleNodeName):
##同名のモジュールノードが存在する場合には最新の内容で更新
updateModuleNode(moduleNodeName,keepOrg)
else:
createRigModuleNode(section,prefix,side,moduleName,moduleNodeName,sectionNode)
rigUtil.reParent(moduleNodeName,sectionNode)
return moduleNodeName
import importlib
from SSRig import rigProcess
importlib.reload(rigProcess)
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "point",side = "center",moduleName = "pointCtrl")
diceSkeleton_v02.ma で実行してみると モジュールノードが更新されつつ、セクションノードが追加されました。
これをdiceSkeleton_v03.ma として保存して実行してみます。 パラメーター用のコントローラーが生成されました。
パラメーターコントローラーを指定するためには paramCtrl.pyで使用した side 情報が必要になります。 そのため、リグモジュール実行部分に関しても手を入れておきます。
rigProcess.py
def excuteModule(moduleNode,baseOpt):
##使用するリグモジュール名を取得
moduleName = cmds.getAttr(moduleNode + ".moduleName")
##ノードからリグモジュールに渡す各値を取得
valueDict = getModuleNodeValues(moduleNode)
##リグモジュールによるセットアップを実行
valueDict.update(**baseOpt)
##sectionノード名・情報を取得
parentNode = cmds.listRelatives(moduleNode,p=True)
if parentNode != None:
if cmds.attributeQuery("moduleName", node = parentNode[0], exists =True):
parentValueDict = getModuleNodeValues(parentNode[0])
if parentValueDict["moduleName"] == "paramCtrl":
valueDict["sectionSide"] = parentValueDict["side"]
valueDict["section"] = parentValueDict["section"]
execModuleCmd(moduleName,valueDict)
改めてスクリプト化
パラメーター用コントローラーが作られるようになったので、それを考慮しつつ改めてスクリプト化を行います。
rigModules/boxPivotSetup.py
from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "setup"
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"rotateJoint",
"translateJoint",
"guideMesh",
"useParamCtrl",
"useParamPrefix"
##予約済みアトリビュート
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"rotateJoint": {"type":"message", "multi":False, "default":""},
"translateJoint": {"type":"message", "multi":False, "default":""},
"guideMesh": {"type":"message", "multi":False, "default":""},
"useParamCtrl": {"type":"enum", "enumList":["root","sectionParamCtrl"], "default":"sectionParamCtrl"},
"useParamPrefix": {"type":"enum","enumList":["none","section","section+side","prefix","prefix+side"],"default":"none"},
}
##--------------------------------------------------------------------------------------------
def setupBoxPivot(section,prefix,side,rotateJoint,translateJoint,guideMesh,useParamCtrl,useParamPrefix,sectionSide,**kwargs):
##create setupGP
##セットアップ用のアレコレを格納する階層を作成
##固有名になるように section / prefix / side から生成
sideString = rigUtil.getSideString(side)
sectionSetupGP = rigUtil.createSectionSetupGP(section,rigSettings.NODENAME_DICT["setupGP"])
setupSpace = rigUtil.createSetupSpace(section,prefix,side,None)
cmds.parent(setupSpace,sectionSetupGP,a=True)
##create rotSampleJoint
##セットアップで作成するノード名が固有になるように section / prefix / side から生成
rotSampleJoint = section + "_" + prefix + "_rotateSample" + sideString + "_jnt"
cmds.createNode("joint",name = rotSampleJoint)
rigUtil.snapPosition(rotateJoint,rotSampleJoint)
rigUtil.snapRotation(rotateJoint,rotSampleJoint)
cmds.parent(rotSampleJoint,setupSpace,a=True)
rigUtil.connectAttrZip(rotateJoint,rotSampleJoint,["rotate"],["rotate"])
##parent guideMesh
cmds.parent(guideMesh,setupSpace,a=True)
##create botPoint
##bbox底面張り付き用joint とりあえず原点・world向き
BBoxYJoint = section + "_" + prefix + "_BBOXBotY" + sideString + "_jnt"
cmds.createNode("transform",name = BBoxYJoint)
cmds.parent(BBoxYJoint,setupSpace,a=True)
##bind rotSampleJoint guideMesh
rigUtil.createSkinCluster(guideMesh,[rotSampleJoint])
##connect guideMesh bbox to botPoint translate
guideMeshShape = cmds.listRelatives(guideMesh,type ="shape")[0]
##bbox底面位置の変化差分だけを抽出
translateYValue = cmds.createNode("floatMath") ##sub
cmds.setAttr(translateYValue + ".operation",1)
cmds.connectAttr(guideMeshShape + ".boundingBox.boundingBoxMin.boundingBoxMinY",translateYValue + ".floatA")
cmds.setAttr(translateYValue + ".floatB",cmds.getAttr(guideMeshShape + ".boundingBox.boundingBoxMin.boundingBoxMinY"))
##bbox底面位置の変化差分を *weight でon/off
translateYWeight = cmds.createNode("floatMath") ##multi
cmds.setAttr(translateYWeight + ".operation",2)
cmds.connectAttr(translateYValue + ".outFloat",translateYWeight + ".floatA")
cmds.setAttr(translateYWeight + ".floatB",0)
##移動値を反転
translateYInv = cmds.createNode("floatMath") ##multi
cmds.setAttr(translateYInv + ".operation",2)
cmds.connectAttr(translateYWeight + ".outFloat",translateYInv + ".floatA")
cmds.setAttr(translateYInv + ".floatB",-1)
cmds.connectAttr(translateYInv + ".outFloat",BBoxYJoint + ".translateY")
# ##connect botPoint to translateJoint
# ##translateJointが 原点・world向き ではない場合を考慮して、translateJointを複製して出力用ノードを作成
translatePosJoint = section + "_" + prefix + "_transPos" + sideString + "_jnt"
cmds.createNode("joint",name = translatePosJoint)
rigUtil.snapPosition(translateJoint,translatePosJoint)
rigUtil.snapRotation(translateJoint,translatePosJoint)
cmds.parent(translatePosJoint,BBoxYJoint,a=True)
outputTranslateSpace = section + "_" + prefix + "_transSpace" + sideString + "_jnt"
translateParentJoint = cmds.listRelatives(translateJoint,p=True)[0]
cmds.createNode("joint",name = outputTranslateSpace)
rigUtil.snapPosition(translateParentJoint,outputTranslateSpace)
rigUtil.snapRotation(translateParentJoint,outputTranslateSpace)
cmds.parent(outputTranslateSpace,setupSpace,a=True)
outputTranslateJoint = section + "_" + prefix + "_trans" + sideString + "_jnt"
cmds.createNode("joint",name = outputTranslateJoint)
rigUtil.snapPosition(translateJoint,outputTranslateJoint)
rigUtil.snapRotation(translateJoint,outputTranslateJoint)
cmds.parent(outputTranslateJoint,outputTranslateSpace,a=True)
cmds.parentConstraint(translatePosJoint,outputTranslateJoint,mo=False)
# ##add switchAttr
paramCtrlnode = rigSettings.NODENAME_DICT["rootGP"]
if useParamCtrl == "sectionParamCtrl":
sectionSideString = rigUtil.getSideString(sectionSide)
paramCtrlnode = "PM_" + section + sectionSideString + rigSettings.SUFFIX_DICT["ctrl"]
##パラメーター名が他モジュールと被らないように section / side / prefix から生成
switchAttrName = rigUtil.geneParamName(useParamPrefix,"bboxPosWeight",section,side,prefix)
if cmds.attributeQuery(switchAttrName, node = paramCtrlnode, exists =True) == False:
cmds.addAttr(paramCtrlnode, ln = switchAttrName, at = "double", dv = 0, k =True, min = 0, max =1)
cmds.connectAttr(paramCtrlnode + "."+switchAttrName ,translateYWeight + ".floatB",f=True)
rigUtil.connectAttrZip(outputTranslateJoint,translateJoint,["t"],["t"])
##セットアップ実行用
def main(**kwargs):
setupBoxPivot(**kwargs)
return
試行
diceSkeleton_v03.maを開いて、リグモジュールノードを追加します。
from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "point",side = "center",moduleName = "boxPivotSetup")
リグモジュールノードが作成されたので、情報をセットします。
ついでにセクションノードの情報も整理します。
・testPoint_jnt を pivotPoint_jntにリネーム
・pivotPoint_jntの位置を調整
・centerRot_jnt_endを追加
・bodyPointBoxPivotSetup_mod用のガイドメッシュをdice_geo を複製してリネーム
これを diceSkeleton_v04.ma として保存し、リグのセットアップを実行します。
import importlib
from SSRig import rigProcess
importlib.reload(rigProcess)
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v04.ma"
setupDataPath = "K:/dice/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
無事生成されているようですが、dice_geoをcenterRot_jntでバインド(手作業)して挙動を確認します。
PM_body_ctrl.translateYWeight を1にし、centerRot_ctrlを回転してみます。
接地点にあわせて上下動してくれるようになりました。
skinWeightの管理
さて、毎回手作業でバインドするのが面倒になってきますのでskinWeightの管理方法を整理します。
保存
mayaの標準でskinWeightをjsonとかxmlで書き出せますね。
独自形式でも良いのですが、折角なので標準機能で書き出したxmlを使用します。
rigUtil.py
import maya.cmds as cmds
def getSkinCluster(node):
history = cmds.listHistory(node,pruneDagObjects =True)
skinNode = cmds.ls(history,type = "skinCluster") or None
if skinNode != None:
return skinNode[0]
return None
def removeUnuseInfs(skinNode):
allInf = cmds.skinCluster(skinNode,q =True, influence =True)
weightedInf= cmds.skinCluster(skinNode,q =True, weightedInfluence =True)
removeInf = list(set(allInf) - set(weightedInf))
for inf in removeInf:
cmds.skinCluster(skinNode,e =True, removeInfluence = inf)
def exportSkinWeight(dirPath,node,ext = "xml",cleanup = True,makeDir = True):
if cmds.nodeType(node) != "transform":
return
##skinClusterノードを取得
skinNode = getSkinCluster(node)
if skinNode == None:
return
##wieghtが振られていないinfluenceを除去
if cleanup:
removeUnuseInfs(skinNode)
##skinClsuter名がユニークになるように編集
if skinNode != node + rigSettings.SUFFIX_DICT["skinCluster"]:
skinNode = cmds.rename(skinNode, node + rigSettings.SUFFIX_DICT["skinCluster"])
fileName = node + "_skinWeight." + ext
if os.path.exists(dirPath) ==False:
if makeDir:
os.makedirs(dirPath)
else:
print("do not exists directory for export")
return None
cmds.deformerWeights(fileName, path = dirPath, deformer = skinNode, format = ext.upper(),ex = True)
return fileName
読み込み
こちらも標準機能で・・・・と行きたいところですが、標準機能はあくまでskinWeightの読み込みなので対象オブジェクトがバインド済みでなくては読み込みできません。
そこでxmlファイルからインフルエンスを読み取り、バインドした上でskinWeightを読み込む という処理を作成しました。
rigUtil.py
from xml.etree import ElementTree
import maya.cmds as cmds
def getSkinCluster(node):
history = cmds.listHistory(node,pruneDagObjects =True)
skinNode = cmds.ls(history,type = "skinCluster") or None
if skinNode != None:
return skinNode[0]
return None
def createSkinCluster(target,joints,nodeName = None):
if nodeName == None:
nodeName = target + rigSettings.SUFFIX_DICT["skinCluster"]
if cmds.objExists(nodeName) ==False:
## 細かいオプションが存在しますが、今回はシンプルな構成にしておきます
## skinMethod = リニア
## normalizeWeights = True
nodeName = cmds.skinCluster(joints,target,skinMethod = 0,normalizeWeights = 1,toSelectedBones =True,name = nodeName)[0]
return nodeName
def getInfluence(skinNode):
infs = cmds.skinCluster(skinNode, q =True, inf =True)
return infs
def addInfluence(target,joints):
skinNode = getSkinCluster(target)
if skinNode == None:
skinNode = createSkinCluster(target,joints)
curInfs = getInfluence(skinNode)
toAddInfs = list(set(joints) - set(curInfs))
for inf in toAddInfs:
cmds.skinCluster(skinNode, e =True, ai = inf, weight = 0)
return skinNode
def importSkinWeight(path,fileName,overWrite=False):
allInfsDict = {}
##XML からバインド情報を取得
tree = ElementTree.parse(path + fileName)
root = tree.getroot()
for e in root.findall('weights'):
inf = e.get("source")
shape = e.get("shape")
node = e.get("deformer")
if node in allInfsDict.keys():
allInfsDict[node]["infs"].append(inf)
else:
allInfsDict[node] = {
"infs":[inf],
"shape":shape
}
for skinNode in allInfsDict.keys():
allInfs = allInfsDict[node]["infs"]
shape = allInfsDict[node]["shape"]
##バインド済みの場合
if overWrite == False and getSkinCluster(shape) != None:
continue
##skinClusterが存在しなければ作成し、インフルエンスを追加する
skinNode = addInfluence(shape,allInfs)
if len(allInfs) != 1:
cmds.deformerWeights(fileName, path = path, deformer = skinNode, method = "index",im = True)
cmds.skinCluster(skinNode,e =True,forceNormalizeWeights = True)
管理
スキンウェイトの読み込みタイミングとして、
・他のデフォーマを併用し、かつ評価順を操作したい場合があります。
・バインドした後にjointでポーズを操作した上でコントローラーを設置したい場合があります。
という事があるので、正直 好きなタイミングで読み込めるようにしたいですね。
例えば、skinWeightモジュールノードを作成し、評価順を設定する。読み込むファイルを設定する。
という方法が考えられますが、ファイル個別はとても管理が面倒くさいです。
今回の様にメッシュ1つであれば良いのですが、200,300になってくると流石に管理が難しいです。
せめてファイル個別ではなく、フォルダ指定であればなんとか管理できる気がします。
フォルダ指定した場合、部位毎に更にフォルダ分けしたくなる気がするのでそれらも含めて考えると次のようになりました。
保存先
・setupDataPath 以下か、任意の場所のフォルダに保存する
・フォルダの階層の深さは任意
読み込み方法
・skinWeight読み込み用リグモジュールを作成
・読み込むフォルダのパスを指定する
・読み込むフォルダの階層下全部のファイルを読み込む
・基本読み込みタイミングは deformer と ctrl の間の600
・セットアップ実行時に指定する setupDataPath の階層下を指定する場合は相対パスで
・フルパスでも指定できるように
この流れで進めてみます。
リグモジュール作成
先述の要項を盛り込んだリグモジュールを作成します。
rigModules/importSkinWeight.py
from __future__ import (absolute_import, division,print_function)
import os
import pathlib
from .. import rigUtil
##モジュールの種類
_MODULETYPE = "skinWeight"
##アトリビュート名リスト
_ATTRS = [
##固有アトリビュート
"targetDirectories",
]
##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
"targetDirectories": {"type":"string", "multi":True, "default":""}
}
##--------------------------------------------------------------------------------------------
def importSkinWeightFromDir(targetDirectories,setupDataPath,**kwargs):
targetDirectories.sort()
for targetDirectory in targetDirectories:
##check fullpath or relatives
if os.path.exists(targetDirectory) == False:
targetDirectory = setupDataPath + targetDirectory
if os.path.exists(targetDirectory) == False:
continue
##get all files
targetDirectoryPath = pathlib.Path(targetDirectory)
filePathList = list(targetDirectoryPath.rglob("*/*.xml"))
filePathList.sort()
##import files
for filePath in filePathList:
dirpath = filePath.parent.replace(os.path.sep,'/') + "/"
fileName = filePath.name
rigUtil.importSkinWeight(dirpath,fileName)
##セットアップ実行用
def main(**kwargs):
importSkinWeightFromDir(**kwargs)
return
試行
まずはskinWeightをexportします。
K:/dice/ 直下に書き出しても良いのですが、念のためver分けして管理するような素振りをしておきます。
from SSRig import rigUtil
exportPath = "K:/dice/setupData_v01/weight/"
nodes = cmds.ls(sl =True) or []
for node in nodes:
rigUtil.exportSkinWeight(exportPath,node,ext = "xml",cleanup = True,makeDir = True)
無事書き出せました。
diceSkeleton_v04.maを開き、importSkinWeightのモジュールノードを作成します。
import importlib
from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "",side = "center",moduleName = "importSkinWeight")
まずは相対パスのでテストします。
作成されたモジュールノードに読み込みフォルダを設定します。
これをdiceSkeleton_v05.maとして保存しリグの構築を行います。
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v05.ma"
setupDataPath = "K:/dice/setupData_v01/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
読み込めました。
念のため絶対パスでのテストも行います。
dice_geo_skinWeight.xml を K:/dice/fullPathTest にコピーして、モジュールノードにK:/dice/fullPathTest/dice_geo_skinWeight.xmlを設定します。
これをdiceSkeleton_v05b.maとして保存しリグの構築を行います。
こちらも読み込めました。
これでskinWeightの再現も容易になったので、試行錯誤がより捗るようになりました。
テストアニメーション
データの保存
一旦現段階でテストアニメーションを作成してみます。 その為に、セットアップデータを完成に近い状態で作成します。 applyCleanup を Trueにしてリグの構築を実行します。
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v05.ma"
setupDataPath = "K:/dice/setupData_v01/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = True
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
完成、といいたいところですがrigModule_gpの主張が強すぎるので、ちょっとなんとかしたいと思います。
rigProcess内のcleanUpProcessでrigModule_gpを setup_gpの下に格納し、非表示になるように追記します。
rigProcess.py
def cleanUpProcess():
##非表示を設定
cmds.setAttr(rigSettings.NODENAME_DICT["setupGP"] + ".v", False)
cmds.setAttr(rigSettings.NODENAME_DICT["jointGP"] + ".v", False)
cmds.setAttr(rigSettings.NODENAME_DICT["moduleGP"] + ".v", False)
cmds.parent(rigSettings.NODENAME_DICT["moduleGP"],rigSettings.NODENAME_DICT["setupGP"])
##階層下のノードを分類して取得
nodeDict = rigUtil.listNodes(rigSettings.NODENAME_DICT["rootGP"],rigSettings.NODENAME_DICT["allCtrlSet"])
##コントローラーノードに デフォルトmatrix / worldMatrixを追加します
##コントローラーノードのアトリビュートにdefaultValueを設定します
setCtrlDefaultValues(nodeDict["ctrl"])
ignorAttrs = ["translate","rotate","scale","selectionChildHighlighting"]
rigUtil.setAttrState([rigSettings.NODENAME_DICT["scaleCtrl"]],["postScale"],lock=True,hide=True,keyable=False)
##IKJoint
lockNodeAttrs(nodeDict["IKJoints"],ignorAttrs)
##IKJoint はrotateを unKyeable に設定
rigUtil.setAttrState(nodeDict["IKJoints"],["rotate","rotateX","rotateY","rotateZ"],lock=False,hide=False,keyable=False)
##コンストレインノード
##アウトライナー内で非表示
for node in nodeDict["constraint"]:
cmds.setAttr(node + ".hiddenInOutliner",True)
lockNodeAttrs(nodeDict["constraint"],ignorAttrs)
##それ以外のノード
lockNodeAttrs(nodeDict["other"],ignorAttrs)
再度リグの構築を行います。
import importlib
from SSRig import rigProcess
importlib.reload(rigProcess)
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v05.ma"
setupDataPath = "K:/dice/setupData_v01/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = True
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)
これで一旦完成とします。
root_gp と allCtrl_sets を選択し、 export selectionから保存します。
save as でも良いのですが、export selection の方がより不要なノードが付随しにくくなります。
ただし必要なノードもコネクション次第ではexportされない場合もあるので、保存したデータは必ず開き直して挙動を確認するようにしています。
今回は diceAsset_v01.ma として保存しました。
テストアニメーション
完成したデータを使用してアニメーションを作成してみます。
※10年以上のブランクがあるので、クオリティには是非とも言及せずに温かい目で見てやってください。
はい、このくらいが精一杯です。
現段階だと、接地がちょっと楽かなぁくらいの印象です。
接地を取る機能が無くても力技でなんとかできる気はします。
おわりに
幾つか新規のリグモジュールを作成したことで、何となく私個人の進め方が見えてきているかと思います。
大まかには、
設計 -> 要素分解 -> 見積もり -> 試作 -> 実装
大体この流れに沿ってますね。
私はいきなり解像度の高い詳細な設計は出来ないので、まずはラフに設計を行います。
それを分解してなるべく要素をシンプルなものにし、全体像を判り易くします。
試作を経て問題点・改修案を明確にしたあと、実装を行い詳細を詰めていきます。
ラフから徐々に全体の解像度を上げていくので、デッサンみたいな感じですね。 ※飽くまで私個人に合った進め方なのでこれが絶対に正しいわけではありません。
次回は 4. モデル全体のスクワッシュ から再開します。