my funeral week

少しでも日々の生活に変化を。

MayaScripts_EasyBakeAnimations : アニメーションの焼き付け(ベイク)補助ツール

アニメーション制作時にもっとも多く行う面倒なことってなんでしょうか?
私にとってアニメーションのベイクがそれに当たります。

2022/11/29 Update

EasyBakeAnimations

Mayaのアニメーションのベイクってなんであんなに面倒なんでしょうか?
チャンネルボックスからいちいちベイクしたいアトリビュートを選択して、フレーム数を入力して・・・。
作業中に何度もこれを操作する時代はもう終わりです。


今日からは以下のコードを使ってください。

# encoding:utf-8

import pymel.core as pm

VERSION = "1.3"

print ("\n-*- import EasyBakeAnimations.{0} -*-".format(VERSION))

WINDOWNAME = "EasyBakeAnimations"
WINDOWSIZE = [200,100]
DEFAULT_CHANNELBOX = "mainChannelBox"
PRESERVE_OUTSIDE_KEYS = True
REMOVE_ATTR_FROM_LAYER = True
TARGET_NODE_TYPES = [
    "animCurve",
    "pairBlend",
    "pointConstraint",
    "orientConstraint",
    "scaleConstraint",
    "aimConstraint",
    "parentConstraint",
    "animBlendNodeBase"
]
BAKE_TARGET_ATTRS = [
    "translateX",
    "translateY",
    "translateZ",
    "rotateX",
    "rotateY",
    "rotateZ",
    "scaleX",
    "scaleY",
    "scaleZ"
]


def __getAttrFromChannelBox (target_node, channelBox_name):
    """任意のノードとチャンネルボックスからアトリビュートを返す"""
    # type:(pm.PyNode, str) -> list[pm.general.Attribute]
    
    attributes = list()
    
    for a in pm.channelBox(channelBox_name, q = True, sma = True):
        if not pm.attributeQuery(a, n = target_node, ex = True) : continue
        attr = target_node.attr(a)
        if attr.isLocked() or not attr.isKeyable() : continue
        attributes.append(attr)
        
    return attributes


def __getAnimatableAttrFromNode (target_node):
    """任意のノードからアニメーション可能なアトリビュートを返す"""
    # type:(pm.PyNode) -> list[pm.general.Attribute]
    
    animatable_attrs = list()
    
    for a in target_node.listAnimatable():
        if a.attrName(longName = True) in BAKE_TARGET_ATTRS:
            animatable_attrs.append(a)
            
    return animatable_attrs


def __popAnimatedAttrs (target_attributes):
    """アトリビュートリストからAnimatedなアトリビュートをPopする"""
    # type:(list[pm.general.Attribute]) -> list[pm.general.Attribute]
    
    animated_attrs = list()
    
    for a in target_attributes:
        # AttrにTARGET_NODE_TYPESの接続があればAnimatedとする。
        if a.connections(t = TARGET_NODE_TYPES, d = False):
            animated_attrs.append(a)
    
    return animated_attrs


def __bakeAnimations (target_attributes, framerange):
    """任意のアトリビュートのアニメーションをベイクする"""
    # type:(list[pm.general.Attribute, list[int]]) -> bool
    
    bake_result = False
    pm.refresh(su = True)
    
    try:
        pm.bakeResults(
            target_attributes,
            t = framerange,
            sm = True,
            pok = PRESERVE_OUTSIDE_KEYS,
            ral = REMOVE_ATTR_FROM_LAYER
            )
        bake_result = True
    except:
        bake_result = False
    
    pm.refresh(su = False)
    return bake_result

def __getCurrentFramerange ():
    """現在のframerangeを返す"""
    # type:() -> list[int]
    
    return [
        pm.playbackOptions(q = True, min = True),
        pm.playbackOptions(q = True, max = True)
    ]

def bakeTargetNodes (framerage = [], target_nodes = [], fromChannelBox = False):
    """任意のノードのアニメーションされているアトリビュートをベイクする"""
    # type:(list[int], list[pm.PyNode], bool) -> bool
    
    bake_attributes = list()
    target_nodes = target_nodes if target_nodes else pm.selected()
    
    for n in target_nodes:
        
        target_attrs = list()# type:list[pm.general.Attribute]
        
        if fromChannelBox:
            target_attrs = __getAttrFromChannelBox(n, DEFAULT_CHANNELBOX)
        else:
            animatable_attrs = __getAnimatableAttrFromNode(n)
            target_attrs = __popAnimatedAttrs(animatable_attrs)
            
        bake_attributes.extend(target_attrs)
        print ("\nAdd Bake Target : {0} {1}".format(n,[a.attrName() for a in target_attrs]))
        
    if not framerage or len(framerage) != 2:
        framerage = __getCurrentFramerange()
        
    bake_result = __bakeAnimations(bake_attributes, framerage)
    pm.headsUpMessage("---------- Complete Bake Animation : {0} ----------".format(bake_result))
    
    return bake_result


def __deleteExitsUI ():
    """既存のUIを削除"""
    # type:() -> None
    
    if pm.window(WINDOWNAME, q = True, ex = True):
        pm.deleteUI(WINDOWNAME)


def __buildUI ():
    """framerange, channelbox flag を設定できるUIを表示する"""
    # type:() -> None
    
    __deleteExitsUI()
    
    with pm.window(WINDOWNAME, mnb = False, mxb = False) as w:
        w.setWidthHeight(WINDOWSIZE)
        
        with pm.columnLayout(adj = True, rs = 5):
            pm.text(l = "Bake Options", bgc = [0,0.1,0.1])
            cf = pm.checkBox(l = "From Channelbox", v = False)
            
            with pm.rowLayout(nc = 3):
                currentrange = __getCurrentFramerange()
                sff = pm.floatField(v = currentrange[0], pre = 2, w = WINDOWSIZE[0]*0.4)
                pm.text(l = " - ", w = WINDOWSIZE[0]*0.15)
                eff = pm.floatField(v = currentrange[1], pre = 2, w = WINDOWSIZE[0]*0.4)
                
            command = lambda *arg : bakeTargetNodes([sff.getValue(), eff.getValue()], [], cf.getValue())
            pm.button(l = "Start Bake", c = command)
            
def main ():
    
    if pm.getModifiers() == 5:
        __buildUI()
        return
    
    bakeTargetNodes()


これで面倒なことが一個なくなりました。


概要

こいつのポイントは、アニメートされているアトリビュートを自動で判断でき、
ベイクするフレーム範囲もシーンの再生範囲が使われます。

つまり、ノードを選択してこいつを実行するだけでベイクは完了です。
なにも難しいことはありません。


使い方

このコードを適当なPythonファイルに保存して、

C:\Users\ユーザー名\Documents\maya\scripts

の中に配置して、
以下のようなコードをシェルフなりホットキーなりに登録するだけです。

import EasyBakeAnimations
EasyBakeAnimations.main()


一応、オプションとして簡単なUI表示もあります。
「Shift+Ctrl」を押しながら実行すると以下のような小さなウィンドウが表示されます。

レイアウトが若干乱れているのは気にしないでおくれ・・・。


オプション

これ使うくらいなら、デフォルトのMayaのベイク機能を使うだろうから、
こいつのUI機能は多分誰も使わないと思うけど。一応ね。一応。


更新履歴

  • ver 1.3 (22/11/29)
    • Maya2022以降にて実行できるように修正。
    • main関数からモディファイヤを使ってUIを表示できるように変更。
  • ver 1.2 (21/05/01)
    • アニメーションレイヤーがあってもベイクできるように修正。




以上。

おつした~。