チュートリアル / Mayaにおける魅惑のプロップリグ~モジュラーリギングシステムの基本と応用~
第3回:リグモジュールの開発とセットアップ その2

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

はじめに

前回に引き続き、サイコロのセットアップを進めていきます。

完成したデータを使用したアニメーションを作成

このような動きを想定し、必要な要素を洗い出しました。
1. 全体移動
2. モデル中心を支点にして回転
3. 接地点を支点にして回転
4. モデル全体のスクワッシュ

前回で3まで終わっているので、今回は4から再開します。

4. モデル全体のスクワッシュ

具体案を詰める

接地させてる場合は接地点・面を支点としてみょいーんとさせたいです。

接地させている場合

角で接地した場合はこのようなイメージで変形させたいです。

角で接地した場合

変形をコントロールする方法は幾つか考えられます。
この手の物も本来はアニメーターさんと相談して決めることが多いです。

1. パラメーターで制御する
 例えば -1 ~ 0 はストレッチで 0 ~ 1 はスクワッシュ という挙動をさせる。

2. コントローラー(1つ)の移動で制御する
 コントローラーをモデル上方に設置する
 コントローラーを上下させることで ストレッチ・スクワッシュさせる

3. コントローラー(2つ)の移動で制御する
 コントローラーをモデルの上下に設置する
 コントローラーそれぞれを上下させて、距離に応じてストレッチ・スクワッシュさせる

3が出来れば1,2も出来そうな気がしますので、とりあえず3. コントローラー(2つ)の移動で制御する で進めます。

次に変形させる方法としては
1. jointを使用してskinCluster
2. FFD(lattice)を使用する
3. clusterを使用する

等々が考えられます。
今回のセットアップで考えると、
オブジェクトの中心で回転した後の状態をスタートとしてそこから更に変形を加えたい ので2か3がやり易そうです。

接地面 または object最下部を支点として変形させるためにデフォーマーの初期位置を動的に変える必要があります。
初期状態のままでも問題ない場合もありますが、赤破線からはみ出た部分の変形に違和感がでそうなので動的に定義したいです。

デフォーマーを動的に定義

その場合 FFDであればbaseオブジェクトがあるので、baseオブジェクトの位置・スケールを動的に調整できれば行ける気がします。
今回は2. FFDを使用する を採用します

試作

とりあえず手作業で試作シーンを作成してみます。
前回もそうですがまずは具体化してみることが目的なので、試作の時はネーミングルールとかはあまり気にせずまずは組んでしまいます。
組んだ後に整理して必要な要素だけを残しつつ整理します。

上部のコントローラーを下に移動させると、潰れて広がります。

上部のコントローラーを下に移動

既存のcenterRot_ctrlで回転させると潰れたままで回転します。

既存のcenterRot_ctrlで回転させる

下部のコントローラーを移動させても潰れて広がります。

下部のコントローラーを移動

この試作を図にするとこのようになります。

試作を図にする

これを要素別に囲んでみるとこうなります。

要素別に囲む

1.BBOXサイズの取得
・回転によって変化したBBOXのサイズに対応する為に、boxPivotSetupで使用しているdice_geoの複製から数値を直接取ってきています。

2.BBOXのスケール変化量の取得
・取得したBBoxSizeは デフォルト値と現在値を使ってscaleの変化量を取得します。
・BBOXのスケール変化量がlatticeShapeとlatticeBaseShapeに反映されるようにします。

3.ストレッチ・スクワッシュ
・squashTopJoint / squashBotJoint のtranslate値を取得して、デフォルト状態からどれくらい伸縮したかを割り出します。
・伸縮スケール値で1を除算し、太さ方向のscale値を取得します。
・太さscaleをsquashMidJointのスケールへ渡します。
・長さ方向のスケール値取得については 2 にも流用できそうです。

4.ジョイントの追従
・squashTopJoint / squashBotJoint の移動にあわせて、squashMidJointが常に中間に来るように移動させています。

5.lattice
・latticeBaseShapeはBBOXのスケール値の影響を受ける階層にparentします。
・latticeShapeはバインドされるので、setup_gp直下などの全体移動の影響を受けない階層にparentします。

勿論これらを1つのリグモジュールとしてしまって、一気にセットアップしてしまっても問題はありません。
ただその場合リグモジュールとしてはかなり使いどころが限定されてしまうものになり、今後の出番はあまり期待できません。
今回でいうとboxPivotSetupが既に使用できる状況の制約が大きくなってしまっているので、もしかしたら今回限りの出番かもしれません。

必要な作業としてはこうなります。

・boxPivotSetupを改修して、BBOXの任意の位置情報をoutputするようにする
・2点の位置情報で、デフォルト状態からの長さの変化をスケール値として出力するセットアップリグモジュール
・ジョイントを追従させるためのリグモジュール
・latticeを追加・適用するリグモジュール

スクリプト化(boxPivotSetupを改修)

boxPivotSetupを改修し、もう少し汎用的で使い勝手が良さそうなリグモジュールを作成します。

・bboxの min/max x/y/z 位置を出力したい。
・複数のノードに対して出力したい。
・出力値の反転するか否かを設定したい。
・出力値をウェイトで制御するか設定したい。

これらを元にboxPivotSetupの改修を試みましたが、もはや別物になってしまったので
新しいリグモジュールとして作成しました。

rigModules/getBBoxPosSetup.py

from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "setup"

##アトリビュート名リスト
_ATTRS = [
                ##固有アトリビュート
                "guideJoint",
                
                "useTranslate",
                "useRotate",
                "useScale",
                
                "guideMesh",

                "outputs",

                "useParamCtrl",
                "useParamPrefix"
                
                ##予約済みアトリビュート
                
]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
                "guideJoint":         {"type":"message",  "multi":False,  "default":""},
                "guideMesh":          {"type":"message",  "multi":False,  "default":""},

                ## guideJointの入力attrを設定
                "useTranslate":       {"type":"bool",  "multi":False,  "default":False},
                "useRotate":          {"type":"bool",  "multi":False,  "default":True},
                "useScale":           {"type":"bool",  "multi":False,  "default":False},

                ## compound でmultiにすることで複数出力先個別に設定
                "outputs":              {"type":"compound",  "multi":True,  "ch":["outputTarget","outputPosition","outputX","outputY","outputZ","useOffset","connectWeight"]},
                    
                    ##BBOX値の出力先
                    "outputTarget":         {"type":"message",  "multi":False,  "default":""},  
                    
                    ##BBOX値の出力位置 max or min
                    "outputPosition":       {"type":"enum",     "enumList":["Min","Max"],   "default":"Min"},  
                    
                    #BBOX値の軸出力 + = 通常  - = 反転値 none = 出力無し
                    "outputX":              {"type":"enum",  "enumList":["+","-","none"],   "default":"+"},  
                    "outputY":              {"type":"enum",  "enumList":["+","-","none"],   "default":"+"},
                    "outputZ":              {"type":"enum",  "enumList":["+","-","none"],   "default":"+"},

                    #BBOX値の移動差分のみを出力
                    "useOffset":            {"type":"bool",  "multi":False,  "default":True},  
                    
                    #BBOX値の出力値をweightAttrでコントロールする
                    "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"},
}

##--------------------------------------------------------------------------------------------

def setupBBoxPositions(section,sectionSide,prefix,side,guideJoint,guideMesh,outputs,useParamCtrl,useParamPrefix,useTranslate,useRotate,useScale,**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)

    ##setup guide---------------------------------------------------------
    ##create guideSampleJoint    
    ##セットアップで作成するノード名が固有になるように section / prefix / side から生成
    guideSampleJoint = section + "_" + prefix + "_guide" + sideString + "_jnt"
    cmds.createNode("joint",name = guideSampleJoint)
    rigUtil.snapPosition(guideJoint,guideSampleJoint)
    rigUtil.snapRotation(guideJoint,guideSampleJoint)

    guideParentJoint = cmds.listRelatives(guideJoint,p=True)[0]
    setupGuideSpace = cmds.createNode("joint",name = section + "_" + prefix + "_guide" + sideString + "_space")
    rigUtil.snapPosition(guideParentJoint,setupGuideSpace)
    rigUtil.snapRotation(guideParentJoint,setupGuideSpace)

    cmds.parent(setupGuideSpace,setupSpace,a=True)
    cmds.parent(guideSampleJoint,setupGuideSpace,a=True)

    ##connect guide attrs
    
    if useTranslate:
        rigUtil.connectAttrZip(guideJoint,guideSampleJoint,["t"],["t"])
    if useRotate:
        rigUtil.connectAttrZip(guideJoint,guideSampleJoint,["r"],["r"])
    if useScale:
        rigUtil.connectAttrZip(guideJoint,guideSampleJoint,["s"],["s"])

    ##parent guideMesh
    cmds.parent(guideMesh,setupSpace,a=True)

    ##bind guideSampleJoint guideMesh
    rigUtil.createSkinCluster(guideMesh,[guideSampleJoint])

    ##setup bbox positions
    ##出来れば任意の空間準拠でのbboxSizeを取得したいがー 今回はworld空間準拠で
    guideMeshShape = cmds.listRelatives(guideMesh,type ="shape")[0]    
    
    for outputSetting in outputs:

        outputTarget = outputSetting["outputTarget"]
        outputPosition = outputSetting["outputPosition"] ##Min or Max
        
        outputTargetParent = cmds.listRelatives(outputTarget,p=True)[0]

        BBOXPosNode = outputTarget + "_sourcePos"
        inputPosNode = outputTarget + "_inputPos"
        outputPosSpace = outputTarget + "_outputSpace"
        outputPosNode = outputTarget + "_outputPos"

        cmds.createNode("transform",name = BBOXPosNode)
        cmds.parent(BBOXPosNode,setupSpace,a=True)
        
        ##変化差分だけを抽出(offset不要の場合は0)
        translateValue = cmds.createNode("plusMinusAverage") 
        cmds.setAttr(translateValue + ".operation",2) ##sub
        
        cmds.connectAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition+"X",translateValue + ".input3D[0].input3Dx")
        cmds.connectAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition+"Y",translateValue + ".input3D[0].input3Dy")
        cmds.connectAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition+"Z",translateValue + ".input3D[0].input3Dz")
        
        if outputSetting["useOffset"]:
            cmds.setAttr(translateValue + ".input3D[1].input3Dx",cmds.getAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition + "X"))
            cmds.setAttr(translateValue + ".input3D[1].input3Dy",cmds.getAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition + "Y"))
            cmds.setAttr(translateValue + ".input3D[1].input3Dz",cmds.getAttr(guideMeshShape + ".boundingBox.boundingBox"+outputPosition+".boundingBox"+outputPosition + "Z"))

        ##変化差分を *weight でon/off
        translateWeight = cmds.createNode("multiplyDivide") 
        cmds.setAttr(translateWeight + ".operation",1) ##multi
        cmds.connectAttr(translateValue + ".output3Dx",translateWeight + ".input1X")
        cmds.connectAttr(translateValue + ".output3Dy",translateWeight + ".input1Y")
        cmds.connectAttr(translateValue + ".output3Dz",translateWeight + ".input1Z")

        cmds.setAttr(translateWeight + ".input2X",1)
        cmds.setAttr(translateWeight + ".input2Y",1)
        cmds.setAttr(translateWeight + ".input2Z",1)

        if outputSetting["connectWeight"]:
            # ##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 = 1, k =True, min = 0, max =1)
            
            cmds.connectAttr(paramCtrlnode + "."+switchAttrName ,translateWeight + ".input2X",f=True)
            cmds.connectAttr(paramCtrlnode + "."+switchAttrName ,translateWeight + ".input2Y",f=True)
            cmds.connectAttr(paramCtrlnode + "."+switchAttrName ,translateWeight + ".input2Z",f=True)

        ##出力軸を設定
        ##"outputX","outputY","outputZ"
        translateInv = cmds.createNode("multiplyDivide")  ##multi
        cmds.setAttr(translateInv + ".operation",1)
        
        for axis in ["X","Y","Z"]:
            cmds.connectAttr(translateWeight + ".output"+axis,translateInv + ".input1"+axis)            
            cmds.setAttr(translateInv + ".input2"+axis, 1)

            if outputSetting["output"+ axis] == "-":
                cmds.setAttr(translateInv + ".input2"+axis, -1)
            
            if outputSetting["output"+ axis] != "none":
                cmds.connectAttr(translateInv + ".output"+axis, BBOXPosNode + ".translate"+axis)

        cmds.createNode("joint",name = inputPosNode)
        rigUtil.snapPosition(outputTarget,inputPosNode)
        rigUtil.snapRotation(outputTarget,inputPosNode)
        cmds.parent(inputPosNode,BBOXPosNode,a=True)
        
        cmds.createNode("joint",name = outputPosSpace)
        rigUtil.snapPosition(outputTargetParent,outputPosSpace)
        rigUtil.snapRotation(outputTargetParent,outputPosSpace)
        cmds.parent(outputPosSpace,setupSpace,a=True)

        cmds.createNode("joint",name = outputPosNode)
        rigUtil.snapPosition(outputTarget,outputPosNode)
        rigUtil.snapRotation(outputTarget,outputPosNode)
        cmds.parent(outputPosNode,outputPosSpace,a=True)

        cmds.parentConstraint(BBOXPosNode,outputPosNode,mo = False)
        rigUtil.connectAttrZip(outputPosNode,outputTarget,["t"],["t"])
    
##セットアップ実行用
def main(**kwargs):    
    setupBBoxPositions(**kwargs)
    return

skeleton_v05.ma データを開いて編集を行います。

まず getBBoxPosSetup のモジュールノードを作成します。

from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "point",side = "center",moduleName = "getBBoxPosSetup")

次に出力用jointを追加し、モジュールノードへ接続します。
pivotPoint_jnt に関してはboxPivotSetupと同じ動きをさせたいので、outputY = - に設定します。

bodyPointBoxPivotSetup_modで使用していたガイドメッシュも移植します。

最後に不要になったbodyPointBoxPivotSetup_modを削除します。

不要になったbodyPointBoxPivotSetup_modを削除

これをdiceSkeleton_v06.maとして保存し、リグの構築を実行します。

from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v06.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)
diceSkeleton_v06.maとして保存
diceSkeleton_v06.maとして保存

boxPivotSetupの動きを踏襲しつつ、拡張させることが出来ました。
まだ状況に制約はありますが、boxPivotSetupよりは汎用的になりすこし使いやすくなりました。

再度改修の必要が出てきた場合には、改修内容を精査しなるべく現在の処理結果が変わらないように調整を加えます。
処理結果 = 処理内容ではなく出力内容(今回で言えば outputTargetの挙動)なので、出力内容が保証されるのであれば処理内容(組み方)は適宜調整します。

稀に、組み方にミスがあり処理結果が想定通りではなかった が リグデータはリリース済みで想定外の処理結果が正式な状態として認識されてしまってる という場合があります。稀に。

うっかり処理内容を調整してしまうと、想定外の部分が是正され進行中のアニメーション作業等に著しい影響が出てしまいます。
その為リグモジュールをアップデートする場合には処理結果にズレが無いかを 実際に運用されてるシーンデータサンプルとして確認する事にしています。
良くも悪くもこちらの想定外の運用をされているケースがあることが多いので、とても重要なサンプルになります。

スクリプト化(lattice)

latticeを追加・適用するリグモジュールに必要な情報としては

・latticeを適用するターゲット(複数可)
・latticeのサイズ(自動 or 手動)
・latticeのパラメーター (div / localInf / outside / falloff / componentTag)
・latticeShape / baseShape それぞれのparent

他細かいオプションもありますが、個人的に割とよく使用する設定で決め打ちしてしまいます。

latticeのサイズを手動で指定する方法ですが、translate/rotate/scaleを数値で打ち込むのは面倒なので、
cubeか何かを作成しそこから数値をそのまま流用してしまおうかと思います。

rigModules/latticeDeformer.py

from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigUtil,rigSettings
##モジュールの種類
_MODULETYPE = "deform"

##アトリビュート名リスト
_ATTRS = [
            ##固有アトリビュート
            "sDivisions",
            "tDivisions",
            "uDivisions",
            
            "sLocalInfluence",
            "tLocalInfluence",
            "uLocalInfluence",
            
            "outsideLattice",

            "latticeGuide",
            "latticeParent",
            "baseParent",

            "targets"
            ]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
                "sDivisions":           {"type":"long","default":2,"max":9999,"min":2},
                "tDivisions":           {"type":"long","default":2,"max":9999,"min":2},
                "uDivisions":           {"type":"long","default":2,"max":9999,"min":2},

                "sLocalInfluence":      {"type":"long","default":2,"max":9999,"min":2},
                "tLocalInfluence":      {"type":"long","default":2,"max":9999,"min":2},
                "uLocalInfluence":      {"type":"long","default":2,"max":9999,"min":2},

                "outsideLattice":       {"type":"enum","enumList":["inside","all","fallOff"],"default":"all"},

                "latticeGuide":         {"type":"message","default":"","multi":False},

                "latticeParent":        {"type":"string","default":"","multi":False},
                "targetPrefix":         {"type":"string","default":"","multi":False},
                "baseParent":           {"type":"string","default":"","multi":False},

                "targets":              {"type":"string","default":"","multi":True},
                }

##------------------------------------------------------------------------------------------------
def createLattice(targets,latticeName,outsideLattice,divisions,localDiv,position = None,rotation = None,scale = None):
    ffdNode,lattice,base = cmds.lattice(
                                        targets,
                                        name = latticeName + "Ffd",
                                        divisions = divisions,
                                        ldivisions = localDiv,
                                        commonParent = False,
                                        objectCentered = True,
                                        outsideLattice = outsideLattice
                                        )

    if position != None:
        cmds.setAttr(lattice + ".t",*position)
        cmds.setAttr(base + ".t",*position)

    if rotation != None:
        cmds.setAttr(lattice + ".r",*rotation)
        cmds.setAttr(base + ".r",*rotation)

    if scale != None:
        cmds.setAttr(lattice + ".s",*scale)
        cmds.setAttr(base + ".s",*scale)

    return ffdNode,lattice,base

##---------------------------------------------------------------------------------------------
def setup(section,prefix,side,targets,divisions,localDiv,position,rotation,scale,outsideLattice,latticeParent,baseParent,**kwargs):

    deformerPrefix = rigUtil.addPrefix(section, prefix)
    sideString = rigUtil.getSideString(side)
    
    outsideOpt = {"inside":0,"all":1,"fallOff":2}
    
    deformTargets = []
    for target in targets:
        if cmds.objExists(target):
            deformTargets.append(target)
        
        else:
            ##ターゲットが見つからない場合はエラーにしてreturnするのも場合によっては有
            print("do not exists " + target)

    ##  deformerPrefix + sideString + _Ffd  をノード名にする
    ffdNode,lattice,base = createLattice(deformTargets,deformerPrefix + sideString ,outsideOpt[outsideLattice],divisions,localDiv,position,rotation,scale)

    ##指定が無い場合は setup_gpに格納する
    if latticeParent == "":
        latticeParent = rigSettings.NODENAME_DICT["setupGP"]

    if cmds.objExists(latticeParent) == False:
        latticeParent = rigSettings.NODENAME_DICT["setupGP"]

    ## root_jnt 内を指定していた場合は proxyを作成してそちらに変更
    latticeParentHie = rigUtil.getCurHierarchy(latticeParent)
    if rigSettings.NODENAME_DICT["rootJNT"] in latticeParentHie:
        latticeParent = rigUtil.createParentProxy(latticeParent,rigSettings.NODENAME_DICT["ctrlWorldSpace"])

    if baseParent == "":
        baseParent = latticeParent

    if cmds.objExists(baseParent) == False:
        baseParent = latticeParent

    ## root_jnt 内を指定していた場合は proxyを作成してそちらに変更
    baseParentHie = rigUtil.getCurHierarchy(baseParent)
    if rigSettings.NODENAME_DICT["rootJNT"] in baseParentHie:
        baseParent = rigUtil.createParentProxy(baseParent,rigSettings.NODENAME_DICT["ctrlWorldSpace"])

    cmds.parent(lattice,latticeParent,a=True)
    cmds.parent(base,baseParent,a=True)

    cmds.setAttr(lattice + ".v", False)
    cmds.setAttr(base + ".v", False)

def main(**kwargs):

    setupParam = {
                    "section":          kwargs["section"],
                    "prefix":           kwargs["prefix"],
                    "side":             kwargs["side"],
                    
                    "targets":          kwargs["targets"],
                    "outsideLattice":   kwargs["outsideLattice"],
                    "latticeParent":    kwargs["latticeParent"],
                    "baseParent":       kwargs["baseParent"],
                    
                    "divisions":    [kwargs["sDivisions"],kwargs["tDivisions"],kwargs["uDivisions"]],
                    "localDiv":     [kwargs["sLocalInfluence"],kwargs["tLocalInfluence"],kwargs["uLocalInfluence"]]
                }

    if kwargs["latticeGuide"] != "":
        setupParam["position"] = cmds.getAttr(kwargs["latticeGuide"] + ".t")[0]
        setupParam["rotation"] = cmds.getAttr(kwargs["latticeGuide"] + ".r")[0]
        setupParam["scale"] = cmds.getAttr(kwargs["latticeGuide"] + ".s")[0]
    else:
        setupParam["position"] = None
        setupParam["rotation"] = None
        setupParam["scale"] = None
            
    setup(**setupParam)

diceSkeleton_v06.maを開き、モジュールノードを生成し各種設定を行います。

from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "",side = "center",moduleName = "latticeDeformer")

latticeのサイズ取り用にcubeを作成し、モジュールノードにコネクトします。
translate/rotate/scaleの値を取得するので、フリーズしてしまうと値が取れなくなってしまうので注意してください。

translate/rotate/scaleの値を取得

これをdiceSkeleton_v07.maとして保存し、リグを構築してみます。

diceSkeleton_v07.maとして保存

latticeが生成され、オブジェクトにも適用されました。

ただ、今の状態ですと
lattice -> skinCluster の順番にデフォーマーが掛かっています。
これだと laticeで変形を加えたものをオブジェクトの中心で回転  する事になってしまいます。

laticeで変形を加えたものをオブジェクトの中心で回

本来行いたいのは、オブジェクトの中心で回転した後の状態をスタートとしてそこから更に変形を加える なので、
skinCluster -> lattice の順番にデフォーマーが掛かるようにリグモジュールノードのorderOfExecutionを調整します。
今回は bodyImportSkinWeight_mod を 400 に設定し、diceSkeleton_v07b.maとして保存後実行してみます。

skinCluster -> lattice の状態になり、オブジェクトの中心で回転した後の状態をスタートとしてそこから更に変形を加える  事が出来ました。

オブジェクトの中心で回転した後の状態をスタートとしてそこから更に変形を加える

スクリプト化(stretch / squash)

次にストレッチ・スクワッシュのリグモジュールを作成します。


このモジュールの機能としては、

・2つのノードの距離を計測して長さ方向のスケールと太さ方向のスケールに変換して出力する
 ものを想定しています。
この機能に加えて
・計測するjointに対してコントローラーを設置する
 を追加するかが悩みどころではあります。

計測に使う2つのノードへのコントローラーからの入力(translate/rotate/scale)に制限を設けないと誤動作を起こす可能性がるならば
コントローラーの設置機能まで盛り込んでしまった方が安全になります。
基本的には距離を計測しての処理なので、コントローラーの入力は特に制限する必要は無さそうです。
今回は別途pointCtrlモジュールを使用してコントローラーを設置します。

lengthToScaleSetup.py

from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "setup"

##アトリビュート名リスト
_ATTRS = [
                ##固有アトリビュート
                "startJoint",
                "endJoint",
                "parentSpace",

                "outputs",

                "useParamCtrl",
                "useParamPrefix",

                ##予約済みアトリビュート
                
]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
                "startJoint":       {"type":"message",  "multi":False,  "default":""},
                "endJoint":         {"type":"message",  "multi":False,  "default":""},
                "parentSpace":         {"type":"message",  "multi":False,  "default":""},

                "outputs":              {"type":"compound",  "multi":True,  "ch":["targetJoint","directionAxis","useWidthScale","useWidthScaleLimit","connectWeight"]},
                    "targetJoint":        {"type":"message",  "multi":False,  "default":""},
                    "directionAxis":      {"type":"enum",     "enumList":["x","y","z"],   "default":"x"},
                    "useWidthScale":        {"type":"bool",  "multi":False,  "default":False},
                    "useWidthScaleLimit":   {"type":"bool",  "multi":False,  "default":False},
                    "connectWeight":        {"type":"bool",  "multi":False,  "default":False},

                "useParamCtrl":     {"type":"enum",     "enumList":["root","sectionParamCtrl"],   "default":"sectionParamCtrl"},
                "useParamPrefix":   {"type":"enum","enumList":["none","section","section+side","prefix","prefix+side"],"default":"none"},
}

##--------------------------------------------------------------------------------------------

def setup(section,sectionSide,prefix,side,startJoint,endJoint,parentSpace,useParamCtrl,useParamPrefix,outputs,**kwargs):
    
    ##parentSpace指定が無い場合は、startJointの親をparentSapceとして採用
    if parentSpace == None or parentSpace == "":
        parentSpace = cmds.listRelatives(startJoint,p=True)[0]

    startMultmatrix = cmds.createNode("multMatrix")
    cmds.connectAttr(startJoint + ".worldMatrix[0]",startMultmatrix + ".matrixIn[0]")    
    cmds.connectAttr(parentSpace + ".worldInverseMatrix[0]",startMultmatrix + ".matrixIn[1]")

    endMultmatrix = cmds.createNode("multMatrix")
    cmds.connectAttr(endJoint + ".worldMatrix[0]",endMultmatrix + ".matrixIn[0]")    
    cmds.connectAttr(parentSpace + ".worldInverseMatrix[0]",endMultmatrix + ".matrixIn[1]")

    ##setup squash
    distance = cmds.createNode("distanceBetween")
    cmds.connectAttr(startMultmatrix + ".matrixSum", distance + ".inMatrix1")
    cmds.connectAttr(endMultmatrix + ".matrixSum", distance + ".inMatrix2")
    
    distToLeghthScale = cmds.createNode("floatMath")
    cmds.setAttr(distToLeghthScale + ".operation",3)
    cmds.connectAttr(distance + ".distance", distToLeghthScale + ".floatA")
    cmds.setAttr(distToLeghthScale + ".floatB", cmds.getAttr(distance + ".distance"))

    ## 太さ方向スケールのwight(全体)
    switchAttrName = rigUtil.geneParamName(useParamPrefix,"widthScaleWeight",section,side,prefix)
    paramCtrlnode = rigSettings.NODENAME_DICT["rootGP"]

    if useParamCtrl == "sectionParamCtrl":
        sectionSideString = rigUtil.getSideString(sectionSide)        
        paramCtrlnode = "PM_" + section + sectionSideString + rigSettings.SUFFIX_DICT["ctrl"]

    for output in outputs:
        #"targetJoint","directionAxis","useWidthScale","connectWeight"
        targetJoint = output["targetJoint"]
        jointNamePrefix = targetJoint.replace(rigSettings.SUFFIX_DICT["jnt"],"")

        rigUtil.connectAttrMerge(distToLeghthScale + ".outFloat",targetJoint , "s" + output["directionAxis"])

        if output["useWidthScale"]:
            
            scaleToWidth = cmds.createNode("floatMath")
            cmds.setAttr(scaleToWidth + ".operation",3)
            cmds.setAttr(scaleToWidth +".floatA",1.0)
            cmds.connectAttr(distToLeghthScale + ".outFloat", scaleToWidth + ".floatB")

            blendAttr = cmds.createNode("blendTwoAttr")
            cmds.setAttr(blendAttr + ".input[0]",1.0)

            if output["useWidthScaleLimit"]:
            ## 太さのスケール値リミット
                limitMaxName = rigUtil.geneParamName(useParamPrefix,"widthScaleMax",section,side,prefix)
                if cmds.attributeQuery(limitMaxName, node = paramCtrlnode, exists =True) == False:
                    cmds.addAttr(paramCtrlnode, ln = limitMaxName, at = "double", dv = 10, k =True, min = 0.001)

                limitMinName = rigUtil.geneParamName(useParamPrefix,"widthScaleMin",section,side,prefix)
                if cmds.attributeQuery(limitMinName, node = paramCtrlnode, exists =True) == False:
                    cmds.addAttr(paramCtrlnode, ln = limitMinName, at = "double", dv = 0.1, k =True, min = 0.001)

                clamp = cmds.createNode("clamp")
                cmds.connectAttr(scaleToWidth + ".outFloat",clamp + ".inputR")
                cmds.connectAttr(paramCtrlnode + "."+limitMaxName,clamp + ".maxR")
                cmds.connectAttr(paramCtrlnode + "."+limitMinName,clamp + ".minR")

                cmds.connectAttr(clamp + ".outputR", blendAttr + ".input[1]")

            if output["connectWeight"]:
                if cmds.attributeQuery(switchAttrName, node = paramCtrlnode, exists =True) == False:
                    cmds.addAttr(paramCtrlnode, ln = switchAttrName, at = "double", dv = 1.0, k =True, min = 0, max =1)

                ## 太さ方向スケールのwight(個別)
                partWeightAttrName = rigUtil.geneParamName(useParamPrefix, jointNamePrefix + "widthScaleWeight",section,"",prefix)
                if cmds.attributeQuery(partWeightAttrName, node = paramCtrlnode, exists =True) == False:
                    cmds.addAttr(paramCtrlnode, ln = partWeightAttrName, at = "double", dv = 1.0, k =True, min = 0, max =1)

                multiWeight = cmds.createNode("floatMath")
                cmds.setAttr(multiWeight + ".operation",2)
                cmds.connectAttr(paramCtrlnode + "."+switchAttrName, multiWeight + ".floatA")
                cmds.connectAttr(paramCtrlnode + "."+partWeightAttrName, multiWeight + ".floatB")
                cmds.connectAttr(multiWeight + ".outFloat",blendAttr + ".attributesBlender")

            for axis in ["x","y","z"]:
                if axis != output["directionAxis"]:
                    rigUtil.connectAttrMerge(blendAttr + ".output",targetJoint , "s" + axis)
        
##セットアップ実行用
def main(**kwargs):
    setup(**kwargs)
    return

リグモジュールが揃ってきたので、一度骨格データを整理します。
diceSkeleton_v07b.maを基にし下記の調整を行った diceSkeleton_v08.maを作成しました。

・stretch / squash 制御用のjoint を追加
・BBOXサイズ出力用jointを追加
・latticeモジュールのbaseParent 先を変更
・boxPivotSetupを BBoxPosSetupに変更

diceSkeleton_v08.maを作成

試作時に作成した図も整理し、リグモジュールがどの部分を担当しているかを追加してみるとこのようになります。

リグモジュールがどの部分を担当しているかを追加

diceSkeleton_v08.maを使用してリグを構築してみます。
構築出来たデータでcenterRot_ctrlを回転させてみると、なんだか変な感じに歪みます。

構築出来たデータでcenterRot_ctrlを回転

latticeShapeをバインドしていませんが、lengthToScaleの影響でlatticeBaseの方が変形しているので
相対的にlatticeの変形が発生してこのようになってます。

ひとまず手作業で squashTop / squashMid / squashBot_joint で latticeShapeをsmoothBindし、skinWeightを編集します。

skinWeightを編集

この状態で改めて回転させてみると、形状が崩れず回転できました。

skinWeightを編集して回転

コントローラーがまだないので、squashTop_jntを直接つかんで動かしてみます。

下方向に移動させると、潰れて横方向に膨らみ

squashTop_jntを直接つかんで下方向に移動

上方向に移動させると、伸びて細くなります。

squashTop_jntを直接つかんで上方向に移動

ここまでを再現できるようにlatticeShapeのskinWeightを保存しておきます。
ただし、dice_geoのskinWeightを保存した場所と同じ場所にすると問題が出てきます。

現状ではこの順で構築されます。

latticeShapeのskinWeightを保存

latticeShapeのskinWeightを dice_geo のskinWeightと同じ場所に保存してしまうと

latticeShapeのskinWeightを dice_geo のskinWeightと同じ場所に保存

latticeShapeが構築される前にskinWeightを読み込もうとしてしまうのでエラーになってしまいます。
なので、latticeの構築後にskinWeightが読まれるように調整をします。

latticeの構築後にskinWeightが読まれるように調整

lattice skinWeightの保存先を weightAfter とし、書き出します。

from SSRig import rigUtil
setupDataPath = "K:/dice/setupData_v01/weightAfter/"
nodes = cmds.ls(sl =True) or []
for node in nodes:
    rigUtil.exportSkinWeight(setupDataPath,node,ext = "xml",cleanup = True,makeDir = True)

diceSkeleton_v08.ma に新しく importSkinWeight のモジュールを追加します。
ついでに、squashTop/Botのコントローラーも追加します。

from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "after",side = "center",moduleName = "importSkinWeight")

評価順はデフォルトの600のままにし、読み込みフォルダを weightAfter/ に指定します。

読み込みフォルダを weightAfter/ に指定

これをdiceSkeleton_v09.ma として保存して、また再構築してみますと

diceSkeleton_v09.ma として保存

ここまでの内容を再現できるようになりました。

スクリプト化(ジョイントの追従)

最後の要素となる ジョイントの追従について進めます。
現状では squashTop / Bot を動かしても、squashMidの位置が変わらないので偏った感じになります。
squashMidにもコントローラーを設置して、手動で調整できるようにしてしまうというのも解決策の1つです。

追従させる方法を思いつく限り羅列してみますと

1. top/bot から mid に対してparentCosntraintを掛ける
 一見シンプルですが、midの元の位置をキープしつつparentConstraintを掛けるとなると、
 weightを良い感じの比率に設定するか、maintainOffsetをTrueにするか、ちょっと工夫が必要そうです。

2. top/bot のtranslate値の平均値をmidに渡す
 translate値の平均となるので、top/bot/mid が同じ空間(階層)にあることが前提となります。

3. top/bot でnurbsCurveを拘束し、midをnurbsCurve上に追従させる
 nurbsCurveへの追従は使い勝手が良いので、リグモジュールとして作成しておくと重宝します。

4. bot位置からtopをターゲットとしたaimConstraintをするjointを追加しmidの親とし、lengthToScaleを掛けてmidが中央に来るようにする
 aimConstarintで向きを制御し、scaleで位置を制御します。
 aimConstarintではなくSingleChainIKを使う手も考えられます。

今回はシンプルな4で進めようと思います。

aimConstraintはそれなりにオプションが存在します。
ただ、それらすべてを網羅するとリグモジュールとしては少々使いづらい場合もあります。
そこで割と個人的に使用頻度の高いオプションを軸として組んでみます。

rigModules/aimConstraintSetup.py

from __future__ import (absolute_import, division,print_function)
import maya.cmds as cmds
import maya.api.OpenMaya as om
from .. import rigSettings,rigUtil
##モジュールの種類
_MODULETYPE = "setup"

##アトリビュート名リスト
_ATTRS = [
                ##固有アトリビュート
                "targets",
                
                ##予約済みアトリビュート
                
]

##アトリビュートのタイプ デフォルト値などオプションを定義
_ATTR_DICT = {
                "targets":          {"type":"compound","ch":["startJoint","endJoint","aimParent","upParent","upAxis","upPointDistance"],"multi":True},
                    "startJoint":       {"type":"message",  "multi":False,  "default":""},
                    ## aimConstraintで制御されるjoint
                    
                    "endJoint":         {"type":"message",  "multi":False,  "default":""},
                    ## aimtConstraintのtargetの位置参照元. startJoint -> endjointで aimVectorを定義する。
                    
                    "aimParent":        {"type":"message",  "multi":False,  "default":""},
                    ## endJoint位置に作成した aimTargetのparent先
                    
                    "upParent":         {"type":"message",  "multi":False,  "default":""},
                    ## worldUpObject のparent先

                    "upAxis":           {"type":"enum",     "enumList":["x","y","z","-x","-y","-z"],   "default":"y"},
                    "upPointDistance":  {"type":"double",     "multi":False,   "default":0.0, "min":0.0, "max":100},
                    ## startJoint位置にworldUpObjectを作成し、upAxis方向に移動させる数値。 
                    ## 0 の場合にはworldUpType を objectrotation に設定する。
}

##--------------------------------------------------------------------------------------------
def setup(startJoint,endJoint,aimParent,upParent,upAxis,upPointDistance,**kwargs):

    ##startJointの親を取得
    parentJoint = cmds.listRelatives(startJoint,p=True)[0]

    ## start / end / parnt のworldMatrixを取得し、aimVectorを導き出す。
    parentMatrix = om.MMatrix(cmds.getAttr(parentJoint + ".worldMatrix[0]"))
    parentInvMatrix = parentMatrix.inverse()
    startPointMatrix = om.MMatrix(cmds.getAttr(startJoint + ".worldMatrix[0]")) * parentInvMatrix
    endPointMatrix = om.MMatrix(cmds.getAttr(endJoint + ".worldMatrix[0]")) * parentInvMatrix
    startPosition = om.MTransformationMatrix(startPointMatrix).translation(om.MSpace.kObject)
    endPosition = om.MTransformationMatrix(endPointMatrix).translation(om.MSpace.kObject)
    vectorA = om.MVector(startPosition)
    vectorB = om.MVector(endPosition)
    aimVector = vectorB - vectorA
    aimVector = aimVector.normal()
    
    ##endJointの位置に aimTarget となるjointを作成する。
    aimTarget = cmds.createNode("joint",name = startJoint + "_aim")
    rigUtil.snapPosition(endJoint,aimTarget)
    rigUtil.snapRotation(endJoint,aimTarget)
    cmds.parent(aimTarget,aimParent)

    ##startJointの位置に upTarget となるjointを作成する。
    upTarget = cmds.createNode("joint",name = startJoint + "_up")
    rigUtil.snapPosition(startJoint,upTarget)
    rigUtil.snapRotation(startJoint,upTarget)

    if upPointDistance == 0:
        ##upPointDistance が0の場合は worldUpType を "objectrotation"に設定
        worldUpType = "objectrotation"
    else:
        ##upAxis方向にupTargetを移動
        worldUpType = "object"
        cmds.parent(upTarget,startJoint,a=True)

        if upAxis[0] == "-":
            upPointDistance = upPointDistance * -1.0

        cmds.setAttr(upTarget + ".t"+upAxis[-1],upPointDistance)

    ##upParentの指定が無い場合は、startJointと同階層にparent
    if upParent == "":
        upParent = parentJoint

    cmds.parent(upTarget,upParent,a=True)

    ##aimConstraintを適用
    cmds.aimConstraint(
                            aimTarget,startJoint,
                            aim = aimVector,
                            upVector = rigUtil.stringToVector(upAxis),
                            worldUpType = worldUpType,
                            worldUpObject = upTarget,
                            worldUpVector = rigUtil.stringToVector(upAxis)
                        )

##セットアップ実行用
def main(**kwargs):
    for targets in kwargs["targets"]:
        setup(**targets)

    return

モジュールノードを作成し、必要なjointを追加し設定を行います。

from SSRig import rigProcess
rigProcess.createModuleNode(section = "body",sectionSide = "center",prefix = "squash",side = "center",moduleName = "aimConstraintSetup")
モジュールノードを作成し、必要なjointを追加し設定

diceSkeleton_v10.ma として保存し、セットアップの構築を行います。

縦方向が均等に伸縮するようになりました。

diceSkeleton_v10.ma として保存し、セットアップの構築
diceSkeleton_v10.ma として保存し、セットアップの構築

squashBot_ctrlを動かしてみると・・・・・

squashBot_ctrlを動かす

均等になってませんね。
jointの位置がどうなっているかを実際に確認してみると判りますが、squashBot_ctrlの動きに対しては追従しない階層にいるのでこの結果は正しくはあります。

jointの位置がどうなっているかを実際に確認

ただ、squashBot_ctrlの動きに対しても追従して欲しいので squashMidParent_jnt を squashBot_jntの子供にし diceSkeleton_v11.ma として保存します。
これで構築してみると

squashMidParent_jnt を squashBot_jntの子供にし diceSkeleton_v11.ma として保存

理想的な結果になりました。

念のためにcenterRot_ctrlを回転させると・・・・
おや?squashTop/Bot_ctrl を動かしていないのに歪んでしまっています。
BBOXサイズの変動に対して squashMidParent_jnt のスケールが追従できていない為ですね。

squashTop/Bot_ctrl を動かしていないのに歪む

BBOXのサイズに追従できるようにモジュールノードの設定を調整します。

BBOXのサイズに追従できるようにモジュールノードの設定を調整

diceSkeleton_v12.ma を使用して構築すると、歪みは無事取れました。

diceSkeleton_v12.ma を使用して構築

完成

さて、ようやく機能が一通り揃ったので完成させます。

・コントローラーの形状調整
・デフォルト値の設定

を行い外部ファイルとして保存、applyCleanupをTrueにして構築を実行します。

import importlib
from SSRig import rigProcess
modelFilePath = "K:/dice/diceModel_v01.ma"
skeletonFilePath = "K:/dice/diceSkeleton_v12.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)
pplyCleanupをTrueにして構築を実行

root_gp と allCtrl_sets を選択し、 export selectionから保存します。
save as でも良いのですが、export selection の方がより不要なノードが付随しにくくなります。
ただし必要なノードもコネクション次第ではexportされない場合もあるので、保存したデータは必ず開き直して挙動を確認するようにしています。

今回は diceAsset_v02.ma として保存しました。
これでひとまず完成となります。

テスト

完成したデータを使用してアニメーションを作成してみます。
今回も自身でアニメーションを付けてみます。

機能としては全てそろったので、この動きの想定に沿ってアニメーションを付けてみます。

完成したデータを使用してアニメーションを作成

はい、このくらいが精一杯です。

実際に使用してみて思ったのは、

接地位置をキープしての着地スクワッシュはやり易いです。

接地位置をキープするモードにした際の横方向の位置合わせが結構手間かかるので、
これについてはリグで対応するか位置を調整するスクリプトがあったほうがやり易いかなと思います。

おわりに

たかがサイコロ1つのセットアップでも、やりたい事を色々盛り込むとそれなりに手間はかかります。
特に今回はゼロからリグモジュールを作成しつつだったので、結構な分量になってしまいました。
ただ、今回作成したリグモジュールは割と汎用性が高い物が多いので、別のセットアップを行うときに あ、あれ使える となればセットアップの工数が多少軽減されます。
モジュラーリギングの面白いところは、リグモジュールを育てると大枠のシステムがあったとしてもそれなりに個性が出てくるところでしょうかね。


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