コントローラーの一時的なワールド挙動化

゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o++゚+o。。o+゚+o。。o+゚

今回はアニメーション制作で、揺れモノなどでよく使用される
”一時的なワールド挙動化”についてお話いたします。

末端の子になりやすい揺れモノは、親の動きを拾ってしまい、意図した動きを作成しづらくなりがちです。
そこで、”一時的なワールド挙動化”をロケーターなどのトランスフォームノードを使って
比較的アニメーションをつけやすくする方法の紹介になります。

手作業で行う場合
・ワールド挙動化したいコントローラー→ロケーター でコンストレイントを行い
・ロケーターをベイクしてアニメーションカーブを取得
・ロケーター→コントローラーにコンストレイントを行うことで完成

_

ちなみに

”一時的なワールド挙動化”については特に名称が存在しません
弊社アニメーター間では「ロケーターで外に出す」が共通言語になっております。

今回はMel/maya.cmdsではなく、PyMelを使用して解決したいと思います。

————————————

まず完成形をどうぞ

スクリプト
# !python2.7
# -*- coding: utf-8 -*-
'''。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。'''
from __future__      import absolute_import, division, print_function , unicode_literals ,generators
from future_builtins import *
'''。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。'''
import pymel.core as   pm

'''゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+'''
def Locator_Constraint(_Time_flag=None):
    _source_List = pm.selected() or None
    _Locator_List = []
    _Tmp = []
    Bake_Range = []

    if _source_List:

        if not pm.objExists('Locator_Grp'):
            pm.group(em=1,n='Locator_Grp')

        for _i,_source in enumerate(_source_List):
            _Locator_List.append(pm.spaceLocator(n=_source+'_Tmp'))
            pm.parent(_Locator_List[-1],'Locator_Grp')

            pm.addAttr(_Locator_List[-1],dt= "string",sn='Src_Node', ln='source_Node')
            pm.setAttr(_Locator_List[-1].name()+'.source_Node',_source.name())

            pm.setAttr(_Locator_List[-1].name()+'.localScale',[60,60,60])
            pm.setAttr(_Locator_List[-1].name()+'.overrideEnabled',1)
            pm.setAttr(_Locator_List[-1].name()+'.overrideColor',31)

            _Tmp.append(pm.parentConstraint(_source,_Locator_List[-1]))

            if _i == 0:
                Bake_Range.append(pm.findKeyframe(_source,w='first'))
                Bake_Range.append(pm.findKeyframe(_source,w='last'))
            else:
                if pm.findKeyframe(_source,w='first') < Bake_Range[0]:
                    Bake_Range[0] = pm.findKeyframe(_source,w='first')

                if pm.findKeyframe(_source,w='first') < Bake_Range[1]:
                    Bake_Range[1] = pm.findKeyframe(_source,w='last')

        if _Time_flag == 0:
            pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (pm.playbackOptions(q=1, min=1),pm.playbackOptions(q=1, max=1)))
        elif _Time_flag == 1:
            pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (Bake_Range[0],Bake_Range[1]))

        pm.delete(_Tmp)

        for _source,_Locator in zip(_source_List,_Locator_List):
            pm.addAttr(_Locator,dt= "string",sn='Cnst_Node', ln='ConstraintNode')
            pm.setAttr(_Locator.name()+'.ConstraintNode',pm.parentConstraint(_Locator,_source))


Locator_Constraint(_Time_flag=1)
結果

また、これを使うことでアニメーションカーブの
親の動きの含めた具合も視覚化できます。
妙にガタガタしていたり、意図した回転をしているか見やすいですね

------------------------------------

ではどんなことをやっているか解説します

------------------------------------



+:。:+モジュールのインポート+:。:+

# !python2.7
# -*- coding: utf-8 -*-
'''。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。'''
from __future__      import absolute_import, division, print_function , unicode_literals ,generators
from future_builtins import *
'''。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。:+* ゚ ゜゚ *+:。'''
import pymel.core as pm

'''゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+'''

pymel.coreをインポートすることでPyMelが使えるようになります。
また、PythonでPython3系になるべく対応できるようにfutureモジュールをインポートしています。
futureモジュールは任意ですが、弊社では記述を統一しております。
いつ対応するのでしょうか…

------------------------------------



+:。:+選択しているノードの取得+:。:+

_source_List = pm.selected() or None

選択しているものが何か"_source_List"に情報を格納します。
or Noneとは何もなかったら"None"を格納します。
これによってif文にかけるとき、Listの個数が0ではなかったら…という条件式を書く必要がなくなります。
ちなみに、None と '' と [ ] は異なります。

------------------------------------



+:。:+選択しているノードの取得+:。:+

if not pm.objExists('Locator_Grp'):
    pm.group(em=1,n='Locator_Grp')

Locator_Grpというグループトランスフォームノードを作ります。
もしも存在しなかったら。という条件式です。
以降、ロケーターはLocator_Grpの子供にまとめられるように準備しています。

------------------------------------



+:。:+for文でロケーターを処理+:。:+


for _i,_source in enumerate(_source_List):
    _Locator_List.append(pm.spaceLocator(n=_source+'_Tmp'))
    pm.parent(_Locator_List[-1],'Locator_Grp')

    pm.addAttr(_Locator_List[-1],dt= "string",sn='Src_Node', ln='source_Node')
    pm.setAttr(_Locator_List[-1].name()+'.source_Node',_source.name())


    pm.setAttr(_Locator_List[-1].name()+'.localScale',[60,60,60])
    pm.setAttr(_Locator_List[-1].name()+'.overrideEnabled',1)
    pm.setAttr(_Locator_List[-1].name()+'.overrideColor',31)

    _Tmp.append(pm.parentConstraint(_source,_Locator_List[-1]))

    if _i == 0:
        Bake_Range.append(pm.findKeyframe(_source,w='first'))
        Bake_Range.append(pm.findKeyframe(_source,w='last'))
    else:
        if pm.findKeyframe(_source,w='first') < Bake_Range[0]:
            Bake_Range[0] = pm.findKeyframe(_source,w='first')

        if pm.findKeyframe(_source,w='first') < Bake_Range[1]:
            Bake_Range[1] = pm.findKeyframe(_source,w='last')

for文にenumerateを使って配列のインデックスと配列の要素を1つ1つ取得して処理をします。
(enumerateは使わなくても良いのですが、便利なのでこちらで紹介いたします。)


1.ロケーターの作成とLocator_Grpの子供へ
以降、_Locator_List[-1]として配列の一番うしろを指定することで
for文によるサイクル中、常に新しいロケーターを指定します。

_Locator_List.append(pm.spaceLocator(n=_source+'_Tmp'))
pm.parent(_Locator_List[-1],'Locator_Grp')

2.文字列データタイプをaddAttrします
選択したノードをsource_Nodeアトリビュートにメモすることで、後々処理の追加をしやすくしています。

pm.addAttr(_Locator_List[-1],dt= "string",sn='Src_Node', ln='source_Node')
pm.setAttr(_Locator_List[-1].name()+'.source_Node',_source.name())

3.各種値をsetAttr(見やすくするためだけなので記述しなくても大丈夫です。)

pm.setAttr(_Locator_List[-1].name()+'.localScale',[60,60,60])
pm.setAttr(_Locator_List[-1].name()+'.overrideEnabled',1)
pm.setAttr(_Locator_List[-1].name()+'.overrideColor',31)

4.ペアレントコンストレイント

_Tmp.append(pm.parentConstraint(_source,_Locator_List[-1]))

5.ベイクする尺を取得
1回目は選択したコントローラーの開始と終了のキーフレームを取得します。
2回目以降は比較します。

if _i == 0:
    Bake_Range.append(pm.findKeyframe(_source,w='first'))
    Bake_Range.append(pm.findKeyframe(_source,w='last'))
else:
    if pm.findKeyframe(_source,w='first') < Bake_Range[0]:
    Bake_Range[0] = pm.findKeyframe(_source,w='first')

    if pm.findKeyframe(_source,w='first') < Bake_Range[1]:
    Bake_Range[1] = pm.findKeyframe(_source,w='last')

------------------------------------



+:。:+ベイク処理+:。:+


ロケーターにコントローラーの動きを焼き付けます
0の場合は現在の開始と終了尺、1の場合はコントローラーの開始と終了尺
以降処理の分岐を追加することも可能です。(_Time_flagは後で解説します)
最期はコンストレイントを切りたいのでコンストレイントノードをまるごと削除します。

if _Time_flag == 0:
    pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (pm.playbackOptions(q=1, min=1),pm.playbackOptions(q=1, max=1)))
elif _Time_flag == 1:
        pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (Bake_Range[0],Bake_Range[1]))
pm.delete(_Tmp)

------------------------------------



+:。:+コンストレイント処理+:。:+

ロケーター→コントローラーにコンストレイントを適用します。
赤文字の部分でコンストレイントを行いつつ、setAttrでついでに登録します。
pm.setAttr(_Locator.name()+'.ConstraintNode',pm.parentConstraint(_Locator,_source))

for _source,_Locator in zip(_source_List,_Locator_List):
    pm.addAttr(_Locator,dt= "string",sn='Cnst_Node', ln='ConstraintNode')
    pm.setAttr(_Locator.name()+'.ConstraintNode',pm.parentConstraint(_Locator,_source))

------------------------------------



+:。:+実行+:。:+

_Time_flagは"def Locator_Constraint(_Time_flag=None):"に引数として設置しています
実行するときに"Locator_Constraint(_Time_flag=1)"といったように記述して処理を分岐しています。

Locator_Constraint(_Time_flag=1)

------------------------------------

以上になりますが、実は色々後でいじりやすいようにしておく設定など
省いて記述することでここまでスマートになります。

必要最低限な記述

import pymel.core as   pm

def Locator_Constraint(_Time=None):
    _source_List = pm.selected() or None
    _Locator_List = []
    _Tmp = []
    Bake_Range = []

if _source_List:
    if not pm.objExists('Locator_Grp'):
        pm.group(em=1,n='Locator_Grp')

    for _i,_source in enumerate(_source_List):
        _Locator_List.append(pm.spaceLocator(n=_source+'_Tmp'))
        pm.parent(_Locator_List[-1],'Locator_Grp')

        _Tmp.append(pm.parentConstraint(_source,_Locator_List[-1]))

        if _i == 0:
            Bake_Range.append(pm.findKeyframe(_source,w='first'))
            Bake_Range.append(pm.findKeyframe(_source,w='last'))
        else:
            if pm.findKeyframe(_source,w='first') < Bake_Range[0]:
                Bake_Range[0] = pm.findKeyframe(_source,w='first')

            if pm.findKeyframe(_source,w='first') < Bake_Range[1]:
                Bake_Range[1] = pm.findKeyframe(_source,w='last')

        if _Time == 0:
            pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (pm.playbackOptions(q=1, min=1),pm.playbackOptions(q=1, max=1)))
        elif _Time == 1:
            pm.bakeResults(_Locator_List,sm=1,t='%s:%s' % (Bake_Range[0],Bake_Range[1]))

        pm.delete(_Tmp)

        [pm.parentConstraint(_Locator,_source) for _source,_Locator in zip(_source_List,_Locator_List)]

実行分:Locator_Constraint(_Time=1)

゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o+゚+o。。o++゚+o。。o+゚+o。。o+゚

意外とどこを探してもロケーターでワールド挙動について記述されている所が見当たらなかったので
良い機会なのでまとめてみました。
お役に立てれば幸いです。

記述されているスクリプトを実行したことで起こったトラブルは対応しかねますのでご了承ください。

Author: kento.yoshishiba