チュートリアル / Mayaにおける魅惑のプロップリグ~モジュラーリギングシステムの基本と応用~
第1回:リグの仕様作成とリグシステムの設計

  • GAZEN
  • Maya
  • アニメ
  • キャラクター・リグ
  • ゲーム
  • コラム
  • スクリプト・API
  • チュートリアル
  • 学生・初心者
  • 映画・TV

はじめに

皆さまはじめまして、久保です。
普段は主に映像作品のセットアップ作業とツールの作成を行っています。

今回から数回に渡って、プロップ(小物)のリグについて色々とお話させて頂ければと思います。
普段からスクリプトでリギング作業を行うことが多いので、内容的にはスクリプトのお話が多くなります。

さて、いきなりお題プロップのリギング作業を始めてしまっても良いのですが、まずはリグの仕様を決め、それに則した簡易的なリグシステムを構築してしまおうと思います。

リグの仕様作成

まずはリグの基本仕様を決めます。
具体的には命名規則と基本の階層構造の仕様を作成します。

ただこの辺の話を掘り下げると長くなってしまう上に色々と論争が勃発しそうなので、今回はこちらで予め決めた内容で進めたいと思います。

ノードのネーミングルール設定

まずはネーミングルールを作成しますが、今回は下記の通りに設定します。

transformノード名
・グループ名:xxxxx_gp
・メッシュ名:xxxxx_geo
・joint名:xxxxx_jnt
・コントローラー名:xxxxx_ctrl

左右判別文字列
・_L_
・_R_

命名例
・arm_L_jnt
・arm_R_jnt
・leg_L_ctrl
・leg_R_ctrl
・head_ctrl

最低限この辺りが決まっていれば問題ないかと思います。

リグデータの基本構造設定

次にリグの基本的な階層構造を作成します。
こちらも色々と掘り下げるとアレなので、今回は下記の通りに設定します。

リグデータの基本構造

各階層とノードの説明は以下の通りです。

・root_gp
最上位のノード
移動・回転・スケールはロックしておく

・ctrl_gp
コントローラー類を格納する階層

・all_ctrl/allSub_ctrl/allScale_ctrl
全体移動用コントローラー
allScale_ctrlのみスケールを解放する。

・postTransform_gp
all_ctrl/allSub_ctrl/allScale_ctrl はそのままに、全体の位置・スケール初期値を調整するためのノード

・joint_gp
ジョイントチェーンを格納する階層

・root_jnt
ジョイントチェーンの最上位joint
原点位置にworld向きで設置

・setup_gp
リグのセットアップに必要なノード類を格納する階層
この階層の中身を非表示にする。

・model_gp
モデルデータを格納する階層

・allCtrl_sets
キーフレームを設定できるノードを登録するobjectSets

リグシステムの設計

基本仕様が決まったのでリグシステムを構築していきます。

リグシステムのメリットについて

構築する前に、リグシステムを使用するメリットについて考えてみました。
各種リグシステムの特性にもよりますが、リグシステムの有用性として一番に挙げられるのは
セットアップの再現が容易
という点だと思っています。

これによって得られる影響が以下の通りです。

・同一作業でのミスが減る
セットアップの再現が容易な為、
同一仕様での複数セットアップ作業などでヒューマンエラーによるミスを減らせます。
モジュラーリグシステムであれば、左右パーツのセットアップや類似パーツのセットアップで同じくミスを減らせます。

・仕様の徹底がしやすい
アセットデータとしての基本仕様など臨機応変に変更する必要が無い個所については、予め決め込んだものを再現させることで全体の仕様を徹底できます。

・作業フローを組みやすい
システムに則した手順があるのでセットアップ全体の作業フローが組みやすくなります。
その為、作業コストの見積もりも出しやすくなります。

・開発時の試行錯誤が行いやすい
基本となるリグをくみ上げる場合、アニメーターさんの要望を取り入れて一部だけ組み替えて再度確認してもらうといった案件別のリグの調整が行いやすいです。
またモジュラーリグシステムであれば、新しい部位の開発が行いやすくなります。

一方短所としては、

・基礎部分の学習機会が減る
リグシステムを使用しての作業においては、作業者がシステムが肩代わりしている内容を理解していなくても問題なく作業できてしまうのでそういった基礎部分を学習する機会が減ってしまいます。
その為リグシステムでの作業しかやったことが無い作業者の場合、リグシステムを使えない局面では対応できない可能性がでてきます。

・メンテナンスが必要
DCCツール等のアップデートに合わせて、リグシステムのメンテナンスが必要になります。

・リグシステムが変わる場合は新規学習
異なるリグシステムを使用する場合には、新たにそのシステムの操作を学習する必要があります。

リグシステムの方針

システムの基本方針を考えます。

・既定の作業については自動化する。
セットアップを行うにあたり、必ずやる作業についてはなるべく自動化し手間を減らします。

・データの再現に手動での調整を工程として組み込まない
アップデートの度に同じ内容の手動による調整を行うのは、いつか忘れたり間違えたりするので危険です。
データの再現を徹底したいので、手動による調整内容は保存・再現ができるようにします。

・セットアップデータの再構築は1ボタンで行う
ボタンがある程度工程に分かれていると安全装置の代わりになる場合もありますが、ボタンを複数用意する = 手動の工程が複数ある とも取れるので、今回は1ボタンで行えるようにします。

モジュラーリギングシステムについて

今回は所謂モジュラーリギングシステムを構築しようと思います。

モジュラーリギングシステムとは、

・腕のセットアップ
上腕・肘・手首の骨に対してIKコントローラー + FKコントローラー で制御するセットアップを行う

・脚のセットアップ
腿・膝・足首・つま先の骨に対してIKコントローラー(リバースフット) + FKコントローラー で制御するセットアップを行う

・背骨のセットアップ
腰から首にかけての骨に対してIKコントローラー + FKコントローラー で制御するセットアップを行う

という具合に部位毎のセットアップを作成し、それらをリグモジュールとして個別に管理します。

例えばこのようなカニのセットアップを行うとして、

カニのセットアップ

各部位をリグモジュールで考えるとこのようになります。

各部位をリグモジュールで考える

1つのアセットをセットアップする為に、パーツ毎に様々なリグモジュールを適用させて全体をセットアップします。

既存のリグモジュールで対応できない場合は新規のリグモジュールを作成するか、一時的な手動セットアップで対応するかの必要がありますが、それについてはシステム次第です。

リグモジュールの設計

システムの根幹となるリグモジュールをどのように管理・実行させるかを設計します。
骨の生成込みのリグシステムも多数存在しますが、今回は既存の骨格データに対してリグモジュールを紐づけし、セットアップを行うシステムにします。

リグモジュールと骨格データに紐づける方法とその情報の管理方法をまとめます。

・maya上でノードを作成し、必要なアトリビュートを追加して管理する。
プラグインで専用ノードを作成する手もありますが、今回は簡単にtransformノードを使用します。
セットアップ時に必要かつ調整したい値をextraAttrとしてノードに追加し、それを実行時にシステムに渡します。
以後このノードをリグモジュールノードと呼称します。

・リグモジュールノード用のグループを作成し格納する。
transformノードなのでまとめておかないとどこかに行ってしまうので所定の階層に格納します。
今回は rigModule_gp の下に格納するものとします。

・jointの指定が必要な場合は、joint.messageで対応アトリビュートにコネクトする。
joint名の修正や組み換えに対応しやすいので、コネクションによってjoint情報を保持させておきます。

jointの指定が必要な場合は、joint.messageで対応アトリビュートにコネクトする。

・骨格データを保存する際には、ジョイントチェーンと共にリグモジュールノードを保存する。
外部データとして別ファイルに保存する方法もありますが、先述の通りjoint情報をコネクションで保持させたいので骨格データに同梱してしまいます。
その為 root_jnt と rigModule_gp を選択して、exportSelection で書き出します。

・ノードを指定して実行すると、アトリビュートの値をリグモジュールに渡してセットアップが実行される。
リグモジュールによって実行に必要なアトリビュートが異なるので、作成・実行時に参照できるようにします。

・リグモジュールノードの実行順を制御するためのアトリビュートが欲しい。
リグモジュールが実行される順序を制御し、実行順による制約や不具合を回避できるようにします。
transformノードなので、アウトライナー上の並び または 名前 でも順序を制御できますが、アウトライナー上の並びはUIの設定次第では変えられませんし、名前順で実行する場合は頭に数字を入れられないので任意の順にするにはちょっとやりづらいです。
そこで、専用のアトリビュートを用意しそこに数値を入れて制御できるようにします。

これらを考慮しリグモジュールに必要な機能を考えます。

・固有アトリビュートの名前と種類を参照できる。
リグモジュールノードを作成する際、セットアップに必要なアトリビュート名と種類を参照できるようにします。

・リグモジュールのタイプを参照できる。
リグモジュールといっても、
コントローラーを作成するタイプ
内部的なギミックのみを構築するタイプ
など幾つか種類が出て来ると思います。その種類に応じた共通処理を行えるようにします。

・セットアップを実行してくれる。
セットアップの処理についてはリグモジュール内で個別に記述していきます。
階層ルールや命名規則に関しては何処か1か所から参照できるようにします。

ライブラリの設計

pythonで作成していくので、とりあえずライブラリの構造を決めます。
とりあえず決めたものの、きっと後々変わっていくんだろうなと思いつつ

SSRig
・rigProcess.py
 セットアップのメインプロセス
・rigUtil.py
 セットアップの補助色々
・rigSettings.py
 ノード名など共通設定色々
・rigModules
 リグモジュールの格納場所
 - rigModuleA.py
 - rigModuleB.py
 - rigModuleC.py

リグモジュールに関しては1ファイルにまとめてしまった方が色々と効率は良いと思いますが、ひとまずは1モジュール1ファイルに分けて管理しようと思います。

リグモジュール関連実装

先述の設計を基にリグモジュール個別の書式と、運用するためのコマンドを作成してみます。

リグモジュール個別の書式

リグモジュール(python)には

・セットアップの実行部分
今回はdef main(**kwargs)を実行部分として設定します。

・セットアップに必要なパラメーター名と種類
変数として_ATTRS(list)_ATTR_DICT(dict) を用意します。
mayaのシーン内でリグモジュールノードを作成する際に、これらを参照してaddAttrします。
その為、既存のアトリビュート名の使用は避けます。

・モジュールの属性
変数として_MODULETYPEを用意します
コントローラーをセットアップするための物なのか、デフォーマーを追加するための物なのかを設定します。
これを基に必須のアトリビュートを追加したり所定の処理を行います。

を記述しておきます。

rigModules/sampleModule.py

import maya.cmds as cmds
import pprint

##モジュールの種類
_MODULETYPE = "ctrl"

##アトリビュート名リスト
_ATTRS = [
        "sampleJoint",
        "sampleJoints",
        "sampleString",
        "sampleStrings",
        "sampleLong",
        "sampleDouble",
        "sampleEnum",

]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
                "sampleJoint":  {"type":"message",  "multi":False,  "default":""},
                "sampleJoints": {"type":"message",  "multi":True,   "default":""},
                "sampleString": {"type":"string",   "multi":False,  "default":""},
                "sampleStrings":{"type":"string",   "multi":True,   "default":""},
                "sampleLong":   {"type":"long",     "default":0,    "min":0,"max":99},
                "sampleDouble": {"type":"double",   "default":0.0,  "min":0.0,"max":100},
                "sampleEnum":   {"type":"enum",     "enumList":["A","B","C"],   "default":"A"},
}

##セットアップ実行用
def main(**kwargs):
    pprint.pprint(kwargs)
    return

実行部分についてはとりあえず引数をプリントするだけの処理を入れておきます。

リグモジュール運用コマンド

作成したリグモジュールの運用に必要な処理としては

・リグモジュールノードの作成
モジュール名を指定して実行します。

リグモジュールに問い合わせて、固有パラメーター用のアトリビュートとモジュールのタイプに応じた必須アトリビュートを追加します。

必須アトリビュート例
・任意の順序で実行させるための専用のアトリビュート
・コントローラーのサイズアトリビュート

処理のイメージは下図のような感じです。

処理のイメージ
##リグモジュール置き場
RIGMODULELIB = "SSRig.rigModules"

def getModuleInfo(moduleName):
    ##リグモジュールをimport
    rigModule = import_module("."+moduleName ,RIGMODULELIB)
    reload(rigModule)

    ##アトリビュートを問い合わせ
    attrs = list(rigModule._ATTRS)
    attrTypes = dict(rigModule._ATTR_DICT)
    moduleType = str(rigModule._MODULETYPE)
    return moduleType,attrs,attrTypes

##アトリビュート追加
def addAttrs(moduleNode,attr,attrDict,opts):
    if cmds.attributeQuery(attr, node = moduleNode, exists =True):
        return

    if attrDict[attr]["type"] == "string":
        cmds.addAttr(moduleNode,ln = attr, dt = "string", multi = attrDict[attr]["multi"],**opts)

        if attrDict[attr]["multi"] == False:
            cmds.setAttr(moduleNode + "."+attr,attrDict[attr]["default"], type ="string",**opts)

    elif attrDict[attr]["type"] == "double":
        cmds.addAttr(moduleNode,ln = attr, at = "double", dv = attrDict[attr]["default"],min = attrDict[attr]["min"],max = attrDict[attr]["max"],**opts)

    elif attrDict[attr]["type"] == "long":
        cmds.addAttr(moduleNode,ln = attr, at = "long", dv = attrDict[attr]["default"],min = attrDict[attr]["min"],max = attrDict[attr]["max"],**opts)

    elif attrDict[attr]["type"] == "message":
        cmds.addAttr(moduleNode,ln = attr, at = "message", multi = attrDict[attr]["multi"],**opts)

    elif attrDict[attr]["type"] == "enum":
        enumString = ":".join(attrDict[attr]["enumList"])
        cmds.addAttr(moduleNode,ln = attr, at = "enum", enumName = enumString, dv = attrDict[attr]["enumList"].index(attrDict[attr]["default"]),**opts)

    elif attrDict[attr]["type"] == "bool":
        cmds.addAttr(moduleNode,ln = attr, at = "bool", dv = attrDict[attr]["default"],**opts)

    elif attrDict[attr]["type"] == "compound":
        cmds.addAttr(moduleNode, ln = attr, at = "compound", numberOfChildren = len(attr["ch"]), multi = attr["multi"],**opts)

        for chAttr in attr["ch"]:
            addAttrs(moduleNode,chAttr,{"p":attr},attrDict)

##アトリビュートに値をセット
def setValues(moduleNode,attr,parentAttr,attrDict,value):
    if cmds.objExists(moduleNode +"."+ parentAttr + attr) == False:
        return

    if cmds.getAttr(moduleNode +"."+ parentAttr + attr, l =True) == True:
        return

    if attrDict[attr]["type"] == "string":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                cmds.setAttr(moduleNode +"."+ parentAttr + attr + "["+str(i)+"]",value[i],type = "string")
        else:
            cmds.setAttr(moduleNode +"."+ parentAttr + attr, value,type = "string")

    elif attrDict[attr]["type"] == "double" or attrDict[attr]["type"] == "long":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr,value)

    elif attrDict[attr]["type"] == "message":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                if value[i] != "":
                    cmds.connectAttr(value[i] + ".message",moduleNode +"."+ parentAttr + attr + "["+str(i)+"]",f =True)

                else:
                    curInput = cmds.listConnections(moduleNode +"."+ parentAttr + attr, s =True, d=False,p=True)
                    if curInput != None:
                        cmds.disconnectAttr(curInput[0],moduleNode +"."+ parentAttr + attr)
        else:
            if value != "":
                cmds.connectAttr(value + ".message",moduleNode +"."+ parentAttr +attr,f =True)

            else:
                curInput = cmds.listConnections(moduleNode +"."+ parentAttr + attr, s =True, d=False,p=True)
                if curInput != None:
                    cmds.disconnectAttr(curInput[0],moduleNode +"."+ parentAttr + attr)

    elif attrDict[attr]["type"] == "enum":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr,attrDict[attr]["enumList"].index(value))

    elif attrDict[attr]["type"] == "bool":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr, value)

    elif attrDict[attr]["type"] == "compound":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                parentAttr = attr + "["+str(i)+"]."
                for chParam in list(value[i].keys()):
                    setValues(chParam,moduleNode,value[i][chParam],parentAttr,attrDict)
        else:
            parentAttr = attr+"."
            for chParam in list(value.keys()):
                setValues(chParam,moduleNode,value[chParam],parentAttr,attrDict)

def createModuleNode(section,prefix,side,moduleName):
    ##リグモジュールノード名を生成
    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"])

    moduleNode = cmds.createNode("transform",name = moduleNodeName, p = rigSettings.NODENAME_DICT["moduleGP"])

    ##リグモジュールに問い合わせ
    moduleType,attrs,attrDict = getModuleInfo(moduleName)

    ##必須アトリビュートを先に追加
    if moduleType in list(rigSettings.COMMON_ATTR_DICT.keys()):
        commonAttrDict = rigSettings.COMMON_ATTR_DICT[moduleType]

        for attr in list(commonAttrDict.keys()):
            addAttrs(moduleNode,attr,commonAttrDict,{})

        ##必須アトリビュート値設定
        setValues(moduleNode,"moduleName","",commonAttrDict,moduleName)
        setValues(moduleNode,"section","",commonAttrDict,section)
        setValues(moduleNode,"prefix","",commonAttrDict,prefix)
        setValues(moduleNode,"side","",commonAttrDict,side)

    ##リグモジュール固有アトリビュートを追加
    for attr in attrs:
        addAttrs(moduleNode,attr,attrDict,{})

    return moduleNode

・モジュールノード作成テスト

from SSRig import rigProcess
rigProcess.createModuleNode(section = "test",prefix = "arm",side = "left",moduleName = "sampleModule")

実行結果を確認してみます。
sampleModule で定義した固有アトリビュートと、システム上必要な共通アトリビュートがextraAttrに追加されているのが確認できました。

実行結果

リグモジュールの実行
リグモジュールノードから必要なパラメーターを取得し、リグモジュールを実行します。
親グループ階層下のリグモジュールノードを取得し、指定された順番で全て実行するためのコマンドも合わせて作成します。

処理のイメージは下図のような感じです。

処理のイメージ

##-------------------------------------------------------------------------------
## for rigModule command
##-------------------------------------------------------------------------------
##リグモジュール置き場
RIGMODULELIB = "SSRig.rigModules"

def getModuleInfo(moduleName):
    ##リグモジュールをimport
    rigModule = import_module("."+moduleName ,RIGMODULELIB)
    reload(rigModule)

    ##アトリビュートを問い合わせ
    attrs = list(rigModule._ATTRS)
    attrTypes = dict(rigModule._ATTR_DICT)
    moduleType = str(rigModule._MODULETYPE)
    return moduleType,attrs,attrTypes

def addAttrs(moduleNode,attr,attrDict,opts):
    if cmds.attributeQuery(attr, node = moduleNode, exists =True):
        return

    if attrDict[attr]["type"] == "string":
        cmds.addAttr(moduleNode,ln = attr, dt = "string", multi = attrDict[attr]["multi"],**opts)

        if attrDict[attr]["multi"] == False:
            cmds.setAttr(moduleNode + "."+attr,attrDict[attr]["default"], type ="string",**opts)

    elif attrDict[attr]["type"] == "double":
        cmds.addAttr(moduleNode,ln = attr, at = "double", dv = attrDict[attr]["default"],min = attrDict[attr]["min"],max = attrDict[attr]["max"],**opts)

    elif attrDict[attr]["type"] == "long":
        cmds.addAttr(moduleNode,ln = attr, at = "long", dv = attrDict[attr]["default"],min = attrDict[attr]["min"],max = attrDict[attr]["max"],**opts)

    elif attrDict[attr]["type"] == "message":
        cmds.addAttr(moduleNode,ln = attr, at = "message", multi = attrDict[attr]["multi"],**opts)

    elif attrDict[attr]["type"] == "enum":
        enumString = ":".join(attrDict[attr]["enumList"])
        cmds.addAttr(moduleNode,ln = attr, at = "enum", enumName = enumString, dv = attrDict[attr]["enumList"].index(attrDict[attr]["default"]),**opts)

    elif attrDict[attr]["type"] == "bool":
        cmds.addAttr(moduleNode,ln = attr, at = "bool", dv = attrDict[attr]["default"],**opts)

    elif attrDict[attr]["type"] == "compound":
        cmds.addAttr(moduleNode, ln = attr, at = "compound", numberOfChildren = len(attr["ch"]), multi = attr["multi"],**opts)

        for chAttr in attr["ch"]:
            addAttrs(moduleNode,chAttr,{"p":attr},attrDict)

def getValues(moduleNode,attr,parentAttr,attrDict):
    ##アトリビュートが無い場合は設定したデフォルト値を採用
    if cmds.attributeQuery(attr, node = moduleNode, exists =True) ==False:
        return attrDict[attr]["default"]

    ##アトリビュートのタイプによって処理を変更
    if attrDict[attr]["type"] == "message":
        ##message
        if attrDict[attr]["multi"]:
            return cmds.listConnections(moduleNode + "."+ parentAttr + attr,s=True,d=False) or []

        else:
            inputs = cmds.listConnections(moduleNode + "."+ parentAttr + attr,s=True,d=False) or [""]
            return inputs[0]

    elif attrDict[attr]["type"] == "string":
        ##string
        if attrDict[attr]["multi"]:
            inputs = []
            for index in  cmds.getAttr(moduleNode +"."+ parentAttr + attr, mi =True) or []:
                inputs.append(cmds.getAttr(moduleNode +"."+ parentAttr + attr + "["+str(index)+"]") or "")
            return inputs
        else:
            return cmds.getAttr(moduleNode + "."+ parentAttr + attr) or ""

    elif attrDict[attr]["type"] == "enum":
        ##enum
        index = cmds.getAttr(moduleNode +"."+ attr)
        return cmds.attributeQuery(attr, node = moduleNode, listEnum =True)[0].split(":")[index]

    elif attrDict[attr]["type"] == "compound":
        compoundValues = []
        if attrDict[attr]["multi"]:
            indexes = cmds.getAttr(moduleNode +"."+ attr,mi =True) or []
            for i in indexes:
                valueDict = {}
                parentAttr = attr + "["+str(i)+"]."
                for chAttr in attrDict[attr]["ch"]:
                    chValue = getValues(moduleNode,chAttr,parentAttr,attrDict)
                    valueDict[chAttr] = chValue
                compoundValues.append(valueDict)
            return compoundValues

        else:
            valueDict = {}
            for chAttr in attrDict[attr]["ch"]:
                chValue = getValues(moduleNode,chAttr,parentAttr,attrDict)
                valueDict[chAttr] = chValue
            return valueDict
    else:
        ##bool / long / double
        return cmds.getAttr(moduleNode + "." + attr)

def setValues(moduleNode,attr,parentAttr,attrDict,value):
    if cmds.objExists(moduleNode +"."+ parentAttr + attr) == False:
        return

    if cmds.getAttr(moduleNode +"."+ parentAttr + attr, l =True) == True:
        return

    if attrDict[attr]["type"] == "string":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                cmds.setAttr(moduleNode +"."+ parentAttr + attr + "["+str(i)+"]",value[i],type = "string")
        else:
            cmds.setAttr(moduleNode +"."+ parentAttr + attr, value,type = "string")

    elif attrDict[attr]["type"] == "double" or attrDict[attr]["type"] == "long":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr,value)

    elif attrDict[attr]["type"] == "message":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                if value[i] != "":
                    cmds.connectAttr(value[i] + ".message",moduleNode +"."+ parentAttr + attr + "["+str(i)+"]",f =True)

                else:
                    curInput = cmds.listConnections(moduleNode +"."+ parentAttr + attr, s =True, d=False,p=True)
                    if curInput != None:
                        cmds.disconnectAttr(curInput[0],moduleNode +"."+ parentAttr + attr)
        else:
            if value != "":
                cmds.connectAttr(value + ".message",moduleNode +"."+ parentAttr +attr,f =True)

            else:
                curInput = cmds.listConnections(moduleNode +"."+ parentAttr + attr, s =True, d=False,p=True)
                if curInput != None:
                    cmds.disconnectAttr(curInput[0],moduleNode +"."+ parentAttr + attr)


    elif attrDict[attr]["type"] == "enum":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr,attrDict[attr]["enumList"].index(value))

    elif attrDict[attr]["type"] == "bool":
        cmds.setAttr(moduleNode +"."+ parentAttr + attr, value)

    elif attrDict[attr]["type"] == "compound":
        if attrDict[attr]["multi"]:
            for i in range(0,len(value)):
                parentAttr = attr + "["+str(i)+"]."
                for chParam in list(value[i].keys()):
                    setValues(chParam,moduleNode,value[i][chParam],parentAttr,attrDict)
        else:
            parentAttr = attr+"."
            for chParam in list(value.keys()):
                setValues(chParam,moduleNode,value[chParam],parentAttr,attrDict)

##excute------------
def getModuleNodeValues(moduleNode):
    valueDict = {}
    moduleName = cmds.getAttr(moduleNode + ".moduleName")
    moduleType,attrs,attrDict = getModuleInfo(moduleName)

    ##共通値取得
    commonAttrDict = rigSettings.COMMON_ATTR_DICT[moduleType]
    for attr in list(commonAttrDict.keys()):
        valueDict[attr] = getValues(moduleNode,attr,"",commonAttrDict)

    ##固有値取得
    for attr in attrs:
        valueDict[attr] = getValues(moduleNode,attr,"",attrDict)

    return valueDict

def execModuleCmd(moduleName,valueDict):
    ##リグモジュールをimport
    rigModule = import_module("."+moduleName ,RIGMODULELIB)
    reload(rigModule)

    ##セットアップを実行
    result = rigModule.main(**valueDict)
    return result

def excuteModule(moduleNode,baseOpt):
    ##使用するリグモジュール名を取得
    moduleName = cmds.getAttr(moduleNode + ".moduleName")

    ##ノードからリグモジュールに渡す各値を取得
    valueDict = getModuleNodeValues(moduleNode)

    ##リグモジュールによるセットアップを実行
    valueDict.update(**baseOpt)
    execModuleCmd(moduleName,valueDict)

def excuteSceneModules(topNode,baseOpt):
    if cmds.objExists(topNode):
        moduleNodes = cmds.listRelatives(topNode, type = "transform") or []

        ##sortOrder
        moduleSortedDict = {}
        for node in moduleNodes:
            if cmds.attributeQuery("moduleName", node = node, exists =True) == False:
                continue

            orderNum = cmds.getAttr(node + ".orderOfExecution")

            if orderNum not in list(moduleSortedDict.keys()):
                moduleSortedDict[orderNum] = [node]
            else:
                moduleSortedDict[orderNum].append(node)

        for key in sorted(list(moduleSortedDict.keys())):
            for moduleNode in sorted(moduleSortedDict[key]):
                excuteModule(moduleNode,baseOpt)

・モジュールノード実行テスト

from SSRig import rigProcess
rigProcess.excuteModule("testArmSampleModule_L_mod",{})

実行結果を確認します。

実行結果

プリントされるだけなので味気ないですが、モジュールが無事実行されました。

セットアップ構築フローの設計

リグモジュールの仕様が出来たので、それを軸に全体のフローを詰めていきます。

・モデルデータの作成
モデラーさんに作成して頂くなどして準備します。
私はリガーなので基本的には準備しません。

・骨格データの準備
モデルデータをみつつ、アニメーションの訴求を基に必要な機能を想定します。
その上でjointを配置し、リグモジュールを設置します。

・構築コマンド 実行
実行するとこで、セットアップデータが再現されます。

・手動によるデータの編集
構築コマンドの処理内容を考えるとこのようになります。
実行で生成されたデータを使用し、手動による調整を行います。
調整内容を再現する為に外部データとして保存します。
調整内容としては
- smoothBindを行い、skinWeightを編集する。
- コントローラー形状をつかみやすいように調整する。
- コントローラーのデフォルト値を設定する。
などが挙げられます。

・各データを編集し、構築コマンド を再実行
生成されたデータを確認し、要修正箇所がある場合は骨格データ編集するなり手後に再構築を行います。
修正箇所が無くなるまで繰り返します。

処理の流れ

構築コマンドの設計

構築コマンドの処理内容を考えるとこのようになります。

・新規シーン作成
まっさらなシーンを作成します。
この際細かなゴミが残っていると、それによってセットアップの結果に影響してしまう事もあるので、なるべく余分な物は除去しておきます。

余分な物の例としては
- ネームスペース
- unknown plugin
- スクリプトノード

等があげられます。

また、preference設定次第ではノードエディタ・グラフエディタを閉じる処理を入れておかないと構築処理が終わってもmayaが返ってこない事があります。

・モデルデータのインポート
指定したモデルデータをインポートします。

・骨格データのインポート
指定した骨格データ(リグモジュールノード同梱)をインポートします。

・基本構造の作成
先に定義したリグデータの基本構造を作成します。

・各リグモジュールのセットアップ実行
リグモジュールノードを評価します。

・手動調整内容の再現
外部データとして保存した調整内容を読み込みます。

・クリーンナップ
データを綺麗にします。

リグモジュールの実行に関しては処理ができているので、他の部分の処理を設計・実装していきます。
リグモジュール実行タイミングを判り易くする為に、各処理にも番号を振っておきます。

新規シーン作成 ~ 基本構造の作成

まずは基本構造を作成するリグモジュールとして実装します。

コントローラーの見た目のサイズを指定したいですが、個別にモジュールノードを作らないので基本サイズ設定として、globalScale を渡すようにします。

rigModules/createBasicStructure.py

from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil

_MODULETYPE = "ctrl"

##アトリビュート名リスト
_ATTRS = [

]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {

}


##セットアップ実行用
def main(**kwargs):
    ctrlSize = kwargs["globalScale"]

    ##create structure
    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["rootGP"])
    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["ctrlGP"], p = rigSettings.NODENAME_DICT["rootGP"])
    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["jointGP"], p = rigSettings.NODENAME_DICT["rootGP"])
    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["setupGP"], p = rigSettings.NODENAME_DICT["rootGP"])
    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["modelGP"], p = rigSettings.NODENAME_DICT["rootGP"])

    ##全体移動コントローラーの作成
    rigUtil.createCtrlNode(rigSettings.NODENAME_DICT["allCtrl"],"circle_y","joint",     size = ctrlSize*1.4,position = None,rotation =None)
    rigUtil.createCtrlNode(rigSettings.NODENAME_DICT["subCtrl"],"circle_y","joint",     size = ctrlSize*1.2,position = None,rotation =None)
    rigUtil.createCtrlNode(rigSettings.NODENAME_DICT["scaleCtrl"],"circle_y","joint",   size = ctrlSize,position = None,rotation =None)

    cmds.createNode("transform", name = rigSettings.NODENAME_DICT["ctrlWorldSpace"])

    cmds.parent(rigSettings.NODENAME_DICT["allCtrl"],rigSettings.NODENAME_DICT["ctrlGP"],a =True)
    cmds.parent(rigSettings.NODENAME_DICT["subCtrl"],rigSettings.NODENAME_DICT["allCtrl"],a =True)
    cmds.parent(rigSettings.NODENAME_DICT["scaleCtrl"],rigSettings.NODENAME_DICT["subCtrl"],a =True)
    cmds.parent(rigSettings.NODENAME_DICT["ctrlWorldSpace"],rigSettings.NODENAME_DICT["scaleCtrl"],a =True)

    ##allScaleのセットアップ
    cmds.addAttr(rigSettings.NODENAME_DICT["scaleCtrl"], ln = "allScale", at = "double",dv = 1.0,min = 0.0001, k =True)
    cmds.connectAttr(rigSettings.NODENAME_DICT["scaleCtrl"] + ".allScale", rigSettings.NODENAME_DICT["scaleCtrl"] + ".sx")
    cmds.connectAttr(rigSettings.NODENAME_DICT["scaleCtrl"] + ".allScale", rigSettings.NODENAME_DICT["scaleCtrl"] + ".sy")
    cmds.connectAttr(rigSettings.NODENAME_DICT["scaleCtrl"] + ".allScale", rigSettings.NODENAME_DICT["scaleCtrl"] + ".sz")

    ##postTransform_gpのセットアップ
    cmds.addAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"], ln = "postScale", at = "double",dv = 1.0,min = 0.0001, k =False)

    scaleMult = cmds.createNode("floatMath")
    cmds.setAttr(scaleMult + ".operation",2)
    cmds.connectAttr(rigSettings.NODENAME_DICT["scaleCtrl"] + ".allScale", scaleMult + ".floatA")
    cmds.connectAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".postScale", scaleMult + ".floatB")

    cmds.connectAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".postScale", rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".sx")
    cmds.connectAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".postScale", rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".sy")
    cmds.connectAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".postScale", rigSettings.NODENAME_DICT["ctrlWorldSpace"] + ".sz")

    if cmds.objExists(rigSettings.NODENAME_DICT["rootJNT"]):
        cmds.parent(rigSettings.NODENAME_DICT["rootJNT"],rigSettings.NODENAME_DICT["jointGP"],a =True)

        ##connect scaleCtrl to root_jnt
        ##わかりやすくする為に parentConstraint
        cmds.parentConstraint(rigSettings.NODENAME_DICT["ctrlWorldSpace"],rigSettings.NODENAME_DICT["rootJNT"])
        rigUtil.connectAttrZip(scaleMult,rigSettings.NODENAME_DICT["rootJNT"],["outFloat","outFloat","outFloat"],["scaleX","scaleY","scaleZ"],True)

        ##全体スケール対応の為rootJoint の直下のjoint.segmentScaleCompensate をオフにする
        joints = cmds.listRelatives(rigSettings.NODENAME_DICT["rootJNT"],type = "joint") or []
        for joint in joints:
            cmds.setAttr(joint + ".segmentScaleCompensate",0)

    ctrls = [
                rigSettings.NODENAME_DICT["allCtrl"],
                rigSettings.NODENAME_DICT["subCtrl"],
                rigSettings.NODENAME_DICT["scaleCtrl"]
    ]

    ##不要なアトリビュートをロック
    rigUtil.setAttrState(ctrls,["s","v"],True,True,False)
    rigUtil.setAttrState([rigSettings.NODENAME_DICT["rootGP"]],["t","r","s"],True,True,False)

    ##コントローラーの色を設定
    rigUtil.setWireFrameColor([rigSettings.NODENAME_DICT["allCtrl"]],"blue", 0.8)
    rigUtil.setWireFrameColor([rigSettings.NODENAME_DICT["subCtrl"]],"green",0.8)
    rigUtil.setWireFrameColor([rigSettings.NODENAME_DICT["scaleCtrl"]],"yellow",0.8)

    ##コントローラーを setsへ登録
    rigUtil.addToSet(rigSettings.NODENAME_DICT["allCtrlSet"],[rigSettings.NODENAME_DICT["rootGP"]])
    rigUtil.addToSet(rigSettings.NODENAME_DICT["allCtrlSet"],ctrls)

    return

モジュール実行テストを行います。

from SSRig import rigProcess
rigProcess.execModuleCmd("createBasicStructure",{"globalScale":1.0})

問題なく生成出来ているようです。

実行結果

作成した基本構造モジュールはモジュールノードによって管理せず、構築コマンドの中で呼び出してしまいます。
新規作成・データ読み込み部分もあわせて def preProcess() として実装します。

def createNewScene(force = True):
    cmds.file(new=True,f=force)

    ##unknownPlugin 除去
    for plugin in cmds.unknownPlugin(query = True, list =True) or []:
        cmds.unknownPlugin(plugin,r =True)

    ##namespace 除去
    ignorList = ['UI', 'shared']
    allNameSpace = cmds.namespaceInfo(listOnlyNamespaces = True,recurse =True)

    for namespace in allNameSpace:
        if namespace in ignorList:
            continue

        cmds.namespace(removeNamespace = namespace)

def importFile(filePath):
    if os.path.exists(filePath) ==False:
        return []

    ##インポート前の 最上位DAGノード取得
    beforeTopNodes = set(cmds.ls(assemblies =True))

    ##ファイルのインポート
    cmds.file(
            filePath,
            i = True,
            options = 'v=0;',
            renameAll=False,
            loadReferenceDepth="none"
        )

    ##インポート後の 最上位DAGノード取得
    afterTopNodes = set(cmds.ls(assemblies =True))

    ##インポートによって追加された 最上位DAGノード取得
    addedTopNodes = list(afterTopNodes  - beforeTopNodes)

    return addedTopNodes

def preProcess(modelFilePath,skeletonFilePath,globalScale,setupDataPath):
    ##000
    createNewScene(True)

    ##001
    modelNodes = importFile(modelFilePath)
    skeletonNodes = importFile(skeletonFilePath)

    baseOpt = {
                "globalScale":globalScale,
                "setupDataPath":setupDataPath
                }

    ##050
    ##createBasicStructure
    execModuleCmd("createBasicStructure",baseOpt)
    cmds.parent(modelNodes,rigSettings.NODENAME_DICT["modelGP"],a=True)

各リグモジュールのセットアップ実行

リグモジュール実装時に作成したコマンドを使用します。
各モジュールタイプの実行タイミングの目安はとりあえずこんな感じにしておきます。

def setupProcess(globalScale,setupDataPath):
    baseOpt = {
                "globalScale":globalScale,
                "setupDataPath":setupDataPath
                }

    ##100~
    ##200~ setup
    ##300~
    ##400~
    ##500~ deform
    ##600~
    ##700~ ctrl
    ##800~ switch
    excuteSceneModules(rigSettings.NODENAME_DICT["moduleGP"],baseOpt)

手動調整内容の再現

・smoothBindを行い、skinWeightを編集する。
これはリグモジュールにしてしまった方が他のリグモジュールとの評価タイミングを調整しやすいと思うので、別途リグモジュール化します。

・コントローラー形状を調整する。
allCtrl_setsに登録されているノードに対して実行します。
今回コントローラーに使用しているのはnurbsCurveなので、nurbsCurveのCV情報を取得して保存します。
再現時には各CVにsetAttrして再現させます。

・コントローラーのデフォルト値を設定する。
allCtrl_setsに登録されているノードに対して実行します。
コントローラーのチャンネルボックスに表示されているアトリビュートの値を取得して保存します。
再現時には情報を各コントローラーにsetAttrします。

・全体位置のオフセット
postTransform_gpの位置・スケール値を保存・再現します。

各項目の再現はon/offは出来るようにしておきます。
編集内容の書き出しと、読み込みコマンドを作成します。
書き出し形式は何でも良いのですが、jsonが個人的に好きなのでjsonで書き出します。

rigProcess.py

def exportPostTransform(setupDataPath):
    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["transform"]

    postTransformDict = {
                            "translateX":   cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".translateX"),
                            "translateY":   cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".translateY"),
                            "translateZ":   cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".translateZ"),
                            "rotateX":      cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".rotateX"),
                            "rotateY":      cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".rotateY"),
                            "rotateZ":      cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".rotateZ"),
                            "scale":        cmds.getAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+".postScale")
        }

    rigUtil.writeJSON(filePath,postTransformDict,indent = 4)

def exportAllCtrlValues(setupDataPath):
    allCtrls = rigUtil.listSetMembers(rigSettings.NODENAME_DICT["allCtrlSet"],["transform"],True)
    allCtrlValueDict = {}

    for ctrl in allCtrls:
        attrs = rigUtil.listAttrs(ctrl,True,True,True)
        attrsDict = {}

        for attr in attrs:
            curValue = cmds.getAttr(ctrl + "."+attr)
            attrsDict[attr] = round(curValue,5)

        limitDict = rigUtil.getTransformLimit(ctrl)
        attrsDict.update(**limitDict)

        if len(attrsDict) != 0:
            allCtrlValueDict[ctrl] = attrsDict

    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["ctrlValue"]
    rigUtil.writeJSON(filePath,allCtrlValueDict,indent = 4)

def exportAllCtrlCV(setupDataPath):
    allCtrlCVDict = {}
    allCtrls = rigUtil.listSetMembers(rigSettings.NODENAME_DICT["allCtrlSet"],["transform"],True)

    for ctrl in allCtrls:
        cvPositions,degree,form,knots = rigUtil.readCurveShape(ctrl,"object")
        allCtrlCVDict[ctrl] = {"cv":cvPositions}

    ##export for json
    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["ctrlShape"]
    rigUtil.writeJSON(filePath,allCtrlCVDict,indent = 4)

def importAllCtrlCV(setupDataPath):
    ##import from json
    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["ctrlShape"]
    allCtrlCVDict = rigUtil.readJSON(filePath)

    for ctrl in list(allCtrlCVDict.keys()):
        if cmds.objExists(ctrl) ==False:
            continue

        cvPositions = allCtrlCVDict[ctrl]["cv"]
        curCvPositions,degree,form,knots = rigUtil.readCurveShape(ctrl)

        if len(cvPositions) != len(curCvPositions):
            continue

        cvnum = len(cmds.ls(ctrl + ".cv[*]",fl =True))
        for i in range(0,cvnum):
            cmds.xform(ctrl + ".cv["+str(i)+"]",os = True,t = cvPositions[i])

def importAllCtrlValues(setupDataPath):
    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["ctrlValue"]
    allCtrlValueDict = rigUtil.readJSON(filePath)

    for ctrl in list(allCtrlValueDict.keys()):
        attrsDict = allCtrlValueDict[ctrl]

        for attr in attrsDict.keys():
            if cmds.objExists(ctrl + "." + attr) ==False:
                continue
            if cmds.getAttr(ctrl + "." + attr,lock =True):
                print(">>" + ctrl + "."+ attr + "  is locked")
                continue
            try:
                cmds.setAttr(ctrl + "." + attr, attrsDict[attr])
            except:
                print(">>" + ctrl + "."+ attr + "  cant setValue")

def importPostTransform(setupDataPath):
    filePath = setupDataPath + rigSettings.POSTFILENAME_DICT["transform"]
    postTransformDict = rigUtil.readJSON(filePath)

    for attr in list(postTransformDict.keys()):
        value = postTransformDict[attr]

        if attr == "scale":
            attr = rigUtil.addPrefix("post",attr)

        rigUtil.setAttrState([rigSettings.NODENAME_DICT["ctrlWorldSpace"]],[attr],False,False,False)
        cmds.setAttr(rigSettings.NODENAME_DICT["ctrlWorldSpace"]+"." + attr,value)
        rigUtil.setAttrState([rigSettings.NODENAME_DICT["ctrlWorldSpace"]],[attr],True,True,True)

def restoreEditsProcess(setupDataPath,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform):
    if restoreCtrlValue:
        importAllCtrlValues(setupDataPath)

    if restoreCtrlSahpe:
        importAllCtrlCV(setupDataPath)

    if restorePostTransform:
        importPostTransform(setupDataPath)

クリーンナップ処理

アニメーターさんが使用する事を考えつつ、こちらの想定外の挙動を引き起こさないようにするためにクリーンナップ処理を設定します。
アニメーターさんのスタイルによっては合わない内容もあるので、実際にはアニメーターさんと相談して決めます。

・アトリビュートのロック
想定外のノードにキーフレームを打たれたり、数値を変えられてしまうと挙動を保証できなくなってしまうのでallCtrl_setsに格納されていないノードはアトリビュートをロックしてしまいます。
ただし、IKHandleで制御されているjointに関してはrotateをlockしてしまうと動かなくなってしまうので、unkyeableに設定します。

・表示設定
基本階層の表示を設定します。
setup_gp / joint_gp は非表示にします。
pivot/ selectionHandleなどtransformノード毎に設定されている物を非表示にします。
displayOverridesをoffにします。

・デフォルトMatrixの追加
allCtrl_setsに格納されているノードには、初期状態のmatrixとworldMatrixをアトリビュートで追加します。
無くてもいいのですが、あったらそれなりに使い道があります。

・デフォルト値の再設定
addAttrで追加したアトリビュートについては、クリーンナップ時に設定された数値をdefaultValueとして再登録します。
特定のアニメーション補助ツールではdefaultValueを見て数値をリセットしたりするので、設定しておかないと苦情が来ます。(来ました)

クリーンナップしてしまうと調整作業がとてもやりづらいので、処理のon/offは出来るようにしておきます。

最初は厳格なクリーンナップは特に行ってませんでした。
色々と案件を重ねていくうちに、
- topノードを選択し、select -hi; で全選択してからの Sキーで setKeyframe
- コンストレインノードのoffset値を任意に編集
- アウトライナーからではなく、矢印キーで階層を移動して Sキーで setKeyframe
等々こちらの想定外の使用方法を目の当りにすることで、徐々にクリーンナップの内容が厳格化していきました。
mayaの機能もこの先拡張・強化されると思いますが、それに対しても細々対応していく必要があります。

rigProcess.py

def setCtrlDefaultValues(nodes):
    for node in nodes:
        unKeyableAttrs = cmds.listAttr(node,channelBox =True) or []
        keyableAttrs = cmds.listAttr(node,keyable =True,scalar =True) or []
        UDAttrs = cmds.listAttr(node,userDefined =True,scalar =True) or []

        attrs = []
        attrs.extend(unKeyableAttrs)
        attrs.extend(keyableAttrs)

        for attr in attrs:
            if attr in UDAttrs:
                if len(cmds.attributeQuery(attr, node = node, listChildren =True) or []) != 0:
                    continue

                cmds.addAttr(node + "."+attr,edit=True,dv = cmds.getAttr(node+"."+attr))

        matrix = cmds.getAttr(node + ".matrix")
        worldMatrix = cmds.getAttr(node + ".worldMatrix[0]")

        if cmds.attributeQuery("defaultMatrix", node = node, exists =True) == False:
            cmds.addAttr(node, dt = "matrix", ln = "defaultMatrix")

        if cmds.attributeQuery("defaultWorldMatrix", node = node, exists =True) == False:
            cmds.addAttr(node, dt = "matrix", ln = "defaultWorldMatrix")

        cmds.setAttr(node + ".defaultMatrix", matrix, type = "matrix")
        cmds.setAttr(node + ".defaultWorldMatrix", worldMatrix, type = "matrix")

def lockNodeAttrs(nodes,ignorAttrs):
    for node in nodes:
        unKeyableAttrs = cmds.listAttr(node,channelBox =True) or []
        keyableAttrs = cmds.listAttr(node,keyable =True) or []
        enableAttrs = cmds.listAttr(node,settable =True,visible =True,write =True,output =True,scalarAndArray =True) or []
        targetAttrs = list(set(enableAttrs) - (set(keyableAttrs) | set(ignorAttrs) | set(unKeyableAttrs)))
        rigUtil.setAttrState([node],targetAttrs,lock=True,hide=True,keyable=False)
        rigUtil.setAttrState([node],keyableAttrs,lock=True,hide=False,keyable=False)


def cleanUpProcess():
    ##非表示を設定
    cmds.setAttr(rigSettings.NODENAME_DICT["setupGP"] + ".v", False)
    cmds.setAttr(rigSettings.NODENAME_DICT["jointGP"] + ".v", False)

    ##階層下のノードを分類して取得
    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)

構築コマンド

今までの内容をまとめるとこのような形になります。

rigProcess.py

def setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup):

    ##preProcess 001~099
    preProcess(modelFilePath,skeletonFilePath,globalScale,setupDataPath)

    ##setupProcess 100~899
    setupProcess(globalScale,setupDataPath)

    ##900~
    ##998
    ##postProcess
    restoreEditsProcess(setupDataPath,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform)

    #999
    ##cleanUpProcess
    if applyCleanup:
        cleanUpProcess()

一連の処理が完成したので、テスト用モデルと骨格データを用意しテストしてみます。

from SSRig import rigProcess
modelFilePath = "K:/testAsset/testModel.ma"
skeletonFilePath = "K:/testAsset/testSkeleton.ma"
setupDataPath = "K:/testAsset/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = False
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)

実行結果を確認してみます。

実行結果

バインドされていないので、all_ctrl等をつかんで動かすとすっぽ抜けます。

バインドされていないので、all_ctrl等をつかんで動かすとすっぽ抜けます。

次にcleanupをオンにして実行してみます。

from SSRig import rigProcess
modelFilePath = "K:/testAsset/testModel.ma"
skeletonFilePath = "K:/testAsset/testSkeleton.ma"
setupDataPath = "K:/testAsset/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = True
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)

一見違いはありませんが、ノードのロックが実行されているのが確認できました。

cleanupをオンにして実行した結果

次に手動調整内容の書き出しと再現をテストしてみます。

コントローラーの形状を調整します。

コントローラーの形状を調整

コントローラーの初期値を変更します。

コントローラーの初期値を変更

全体オフセットの値を変更します。

全体オフセットの値を変更

これらの調整内容を書き出します。
手動調整内容の書き出しコマンドは

from SSRig import rigProcess
rigProcess.exportPostTransform(setupDataPath)
rigProcess.exportAllCtrlValues(setupDataPath)
rigProcess.exportAllCtrlCV(setupDataPath)

jsonファイルが書き出されました。

jsonファイル

もう一度構築コマンドを実行します。

from SSRig import rigProcess
modelFilePath = "K:/testAsset/testModel.ma"
skeletonFilePath = "K:/testAsset/testSkeleton.ma"
setupDataPath = "K:/testAsset/"
globalScale = 2.0
restoreCtrlValue = True
restoreCtrlSahpe = True
restorePostTransform = True
applyCleanup = True
rigProcess.setupMainProcess(modelFilePath,skeletonFilePath,setupDataPath,globalScale,restoreCtrlValue,restoreCtrlSahpe,restorePostTransform,applyCleanup)

手動の調整内容が再現されていることが確認できました。

構築コマンドを実行

これでリグシステムの構築は一旦これで終了になります。
今後実際のセットアップを行う際にやりづらい部分が出てきたら随時更新していきます。


製品購入に関するお問い合わせ
オートデスク メディア&エンターテインメント 製品のご購入に関してご連絡を希望される場合は、こちらからお問い合わせください。