my funeral week

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

MayaScripts_RotateOrderSwitcher

例によってPymelスクリプトの記事でございます。
アニメーション制作中に必ず1回は遭遇するジンバルロック問題を解決するスクリプトの紹介です。


RotateOrderSwitcher

XYZの回転マニピュレーターの内の2軸が重なってしまう現象。ジンバルロック
こうなるともうめんどくさい。キーフレーム間の補間アニメーションが乱れるし、カーブも崩れて編集しにくい・・・。

世の中にはアニメーションカーブ編集をせず、ジンバルロックも気にしないという恐ろしいアニメーターが居るという伝説も聞きますが、
やっぱり普通はカーブ編集もがっつりしたいもの。

そこで、私は以下のようなスクリプトを使ってジンバルロックの解決とします。

ダウンロード

www.dropbox.com

クリエイティブ・コモンズ・ライセンス


このスクリプトは簡単に言うと、アニメーションを破壊せずに対象ノードのRotateOrderを変更するものです。
※ 処理後のノードのアニメーションはベイクされます。


使い方

ダウンロードしたファイルからRotateOrderSwitherフォルダを下記の場所に配置してください。

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

以下のコードで実行出来ます。

import RotateOrderSwitcher
RotateOrderSwitcher.main()


スクリプトを実行すると、以下のようなウィンドウが表示されます。

  • Order
    • 任意のRotateOrderを選択してください。
    • 基本的には、この項目を任意の値に変更して、下記Switchボタンを押すだけでOKです。
  • Switch
  • Close
    • ウィンドウを閉じます。

以降はオプション機能になります。

  • Auto Reverse
    • With Extract で生成したコントローラに対してSwitchを実行した場合、アニメーションを元のノードにベイクして、コントローラを削除します。
  • With Extract
    • 外部コントローラ(ロケーター)を生成し、元のノードのRotateOrderとアニメーションは編集しません。
  • Follow Position
    • 元のノードに追従する形で、生成される外部コントローラにPointConstraintを接続します。
    • With Extract にチェックを入れていないと、このオプションは利用できません。

適当なOrderの項目を選択して、Switchボタンを押してください。
アトリビュートの編集が許可されていれば、ノードのRotateOrderが変更されて上手く行けばジンバルロックが解消されます。
ベイク処理を行うので、尺の長いアニメーションに対して実行する際は覚悟しておいてください。

使用例として、冒頭の画像の状況にて、RotateOrderを「XYZからXZY」に変更した場合、下記のようになります。


更新履歴

  • ver1.1
    • Maya2022以降でも実行できるように改修。
    • Auto Reverse、Follow Position 機能を追加。
  • ver1.0
    • 配布開始。

余談

アニメーションのベイク処理を挟むようなスクリプトを制作する際は、極力ベイク処理の回数が少なくなるように設計すべきだと思うのです。
対象のノードの数だけ何度もベイクするのではなく、ベイクしたいアトリビュートは一旦リストにまとめて、キリのいいところで一気にベイクする。
これを意識しないと信じられないくらいの時間、処理の完了を待つことになるのも・・・しばしばあります。
皆さんも制作の際は気を付けてください。

では、また。

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)
    • アニメーションレイヤーがあってもベイクできるように修正。




以上。

おつした~。

MayaScripts_FollowCamCreator : ノードを追従するカメラの生成

2022/11/26 Update

こんにちは。

さて、今回もアニメーション制作時の面倒ごとを一つ片づける為の小スクリプトを置いておきます。


FollowCamCreator

アニメーション制作中、キャラクターは 常にカメラに捉えておきたいものです
ただ、走りや大ジャンプ、大きな移動値を持つアニメーションの制作時に、
FIXのカメラではキャラクターがカメラの外に行ってしまいます。

他にも、手足の指のアニメーション制作時なんかは、常に手足をカメラ内に押さえて欲しいでしょう。

いちいちカメラを動かすのも 面倒 なので、今日からはコレを使いましょう。

選択した対象ノードに対して追従するカメラをボタン一つで作成できます。
ちょっとしたオプションもあるよ。


インストール

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

上記の場所に以下のコードをPythonFileとして、保存してください。


# -*- coding:utf-8 -*-

import pymel.core as pm

VERSION = "1.2"

print ("-*- import FollowCamCreator.{0} -*-".format(VERSION))

WINDOWNAME = "FollowCam"
WINDOWSIZE = [150,196]
TRANSLATE_ATTR = ["tx","ty","tz"]
SWITCH_CAMERA = True

def __bakeAnim (target_node, target_attribute):
    # type:(pm.PyNode, list[str]) -> None
    
    framerange = [
        pm.playbackOptions(q = True, min = True),
        pm.playbackOptions(q = True, max = True),
        ]
    
    attributes = [target_node.attr(a) for a in target_attribute]
    
    pm.refresh(su = True)
    pm.bakeResults(attributes, t = framerange, sm = True, pok = True)
    pm.refresh(su = False)

def __setPointContraints (src_node, dst_node, target_attribute):
    # type:(pm.PyNode, pm.PyNode, list[str]) -> list[pm.PyNode]
    
    skip_attribute = list()
    
    for a in TRANSLATE_ATTR:
        if a in target_attribute : continue
        skip_attribute.append(a.replace("t",""))
    
    return pm.pointConstraint(src_node, dst_node, skip = skip_attribute, mo = False)
    
def __getActiveModelEditor ():
    # type:() -> pm.uitypes.ModelEditor
    
    for e in pm.lsUI(ed = True):
        if type(e) == pm.uitypes.ModelEditor and e.getActiveView():
            return e
    
    return None 
            

def __createFollowCam (target_attribute, withRotation, withBake):
    # type:(list[str], bool, bool) -> list[pm.PyNode]
    
    selection = pm.selected()
    if not selection or (not target_attribute and not withRotation):
        print ("\nCanceled.")
        return None
    
    __deleteExitWindow()
    
    src_node = selection[0]
    name = "FollowCam_{0}".format(src_node.name(stripNamespace = True))
    
    print ("\nsrc_node:\t{0}\nattribute:\t{1}\nrotation:\t{2}\nanim_bake:\t{3}".format(src_node.name(), target_attribute, withRotation, withBake))
    
    cam_root = pm.spaceLocator(n = name)
    cam_nodes = pm.camera(name = name + "_CAM")
    cam_transform = cam_nodes[0]
    cam_transform.setParent(cam_root)
    
    if withRotation:
        pm.orientConstraint(src_node, cam_root)
    
    constraint_nodes = __setPointContraints(src_node, cam_root, target_attribute)
    
    if withBake:
        __bakeAnim(cam_root, target_attribute)
        pm.delete(constraint_nodes)
    
    if SWITCH_CAMERA:
        active_viewport = __getActiveModelEditor()
        active_viewport.setCamera(cam_nodes[1])
    
    result = [cam_root] + cam_nodes
    
    pm.select(result)
    pm.headsUpMessage("-*- FollowCamCreate done. : {0} -*-".format(cam_root))
    
    return result
    

def __getCheckedAttr (checkboxes):
    # type:(list[pm.uitypes.CheckBox]) -> list[str]
    
    attributes = list()
    
    for i in list(range(0, len(TRANSLATE_ATTR))):
        if checkboxes[i].getValue():
            attributes.append(TRANSLATE_ATTR[i])
    
    return attributes
    
    
def __deleteExitWindow (*arg):

    if pm.window(WINDOWNAME, q = True, ex = True):
        pm.deleteUI(WINDOWNAME)


def __checkAll (checkboxes) :
    # type:(list[pm.uitypes.CheckBox]) -> None
    
    if False not in [c.getValue() for c in checkboxes]:
        checkboxes[0].setValue(False)
        checkboxes[1].setValue(False)
        return
    
    for c in checkboxes:
        c.setValue(True)
    
    

def __showUI ():
    # type:() -> None
    
    __deleteExitWindow()

    with pm.window(WINDOWNAME, mnb = False, mxb = False, s = False) as w:

        w.setWidthHeight(WINDOWSIZE)

        with pm.columnLayout(adj = True, rs = 5):

            pm.text(l = "Select Follow Axis", bgc = [0.9,0.5,0.3])

        with pm.columnLayout(adj = True, rs = 5, co = ["both", 10]):
            
            pm.separator(h = 6) # ---------------------------------------

            with pm.rowLayout(nc = 4):
                
                attr_checkboxes = [
                    pm.checkBox(l = "X", w = 48),
                    pm.checkBox(l = "Y", w = 48),
                    pm.checkBox(l = "Z", v = True)
                ]
            
            check_all_cmd = lambda *arg : __checkAll(attr_checkboxes)
            pm.button(l = "Check All", h = 17, c = check_all_cmd)
            
            pm.separator(h = 6) # ---------------------------------------

            wr = pm.checkBox(l = "With Rotation", v = False)
            wb = pm.checkBox(l = "With AnimBake", v = False)

            pm.separator(h = 6) # ---------------------------------------

            with pm.rowLayout(nc = 2, h = 36) as r:

                r.rowAttach([(1, "both", 0), (2, "both", 0)])
                
                main_cmd = lambda *arg : __createFollowCam(__getCheckedAttr(attr_checkboxes), wr.getValue(), wb.getValue())
                pm.button(l = "OK", w = 61, c = main_cmd)
                pm.button(l = "Cancel", w = 61, c = __deleteExitWindow)



def main () :

    if pm.getModifiers() == 5:
        __showUI()
        return
    __createFollowCam(TRANSLATE_ATTR, False, False)



その後、MayaのシェルフやHotKeyに以下の2行のスクリプトを保存してください。

import FollowCamCreator
FollowCamCreator.main()


使い方

適当なノードを選択した状態で実行 すると、MayaのPointConstraintにて「選択された対象を追従するカメラノード」を生成します。
生成後はアクティブなビューポートのカメラを生成したカメラに切り替えます。

それと、「 Shift + Ctrl 」 を押した状態で実行すると、下図のようなオプション用のUIが立ち上がります。

  • オプション項目
    • X~Y のチェックボックス
      • 追従する移動軸を選択できます。
    • Check All
      • 上記3軸すべてにチェックを入れます。
      • もう一度押すと、Z軸にのみチェックした状態に戻ります。
    • With Rotation
      • 回転にも追従するようになります。
    • With AnimBake
      • チェックを入れた移動軸に追従するようにカメラの親ノードに対してアニメーションのベイク処理を行います。
      • チェックを外している場合は、コンストレイントのみ行います。
    • OK ボタン
      • カメラを作成します。
    • Cancel ボタン
      • ウィンドウを閉じます。


余談

こういうちょっとしたスクリプトのUIを作るときも、なるべくレイアウトをキレイに整えたいんだけど・・・、MayaのデフォルトのUI作成コマンドだけでやると、正直むちゃくちゃめんどくさい。思った通りのマージンやサイズにならねえ。

やっぱりQtDesignerとか使った方がいいんですかね~。
宗教上の理由であんまり使いたくないから、普段これよりも複雑なUI作るときは、MayaのFormLayoutをある程度簡単に使えるようにした自前のスクリプトで抵抗しています。
とは言え、Qtはレイアウトだけじゃなく、ボタンからのコマンドとかユーザーからのアクションに対しても柔軟に対応できたりと、勉強するだけのメリットがあるのはワカル。まぁいつかブログで備忘録紹介できる程度には学習したいと思います。

それでは。

雑記

久しぶりに更新します。
いくらなんでも更新しなさすぎだったので、簡単にですが今後の予定なんかを記述しておきたいと思います。

なにやらいつの間にかMaya2022あたりからPython3がデフォルトになってしまったようで、このブログで公開しているスクリプトが動かないという報告を受けました。
ちょっと対応する時間がありませんので、この辺りは後々修正させて貰います。
たぶんやり始めるとコードの修正とか整理にも繋がっていく気がするので、かなり先になりそうですが・・・。

またUnity関連の記事にて足滑りを解消する実験記録を公開していました。

こちら、どうやら思った以上にアクセス数があるようなのですが、古い記事なので中々酷い内容になっています。非効率だったり無駄に難解だったり・・・。
恐らく読み解けた人はいないでしょう。こちらも今制作中のゲームで実装している簡潔なものに修正したいと考えていますので、一時的に非公開にしました。

また、ちょくちょくTwitterのDMにて質問的な連絡を頂くことがあるのですが、TwitterのDMは怪しいスパムが多いせいで、せっかく連絡頂いても高頻度で見落としてしまっています。すみません。
もし何か連絡したいことがある場合は、ブログのコメント欄に書き込んでくれた方がメールでも通知が来るように設定している分、気が付きやすいです。たぶん。

そんな感じでございます。それでは。

MayaScripts_OpenSpecifiedFile:パスを明示してシーンを開く

寒すぎる。

今日もちょっとしたスクリプトの紹介になります。

OpenSpecifiedFile

Mayaの「シーンを開く」で使われているファイルブラウザ、使いにくすぎ問題。

マウスの進む戻るボタンも反応しないし、ファイルのブックマークにショートカットを指定すると、ショートカット名ではなくリンク先のディレクトリ名が表示されるとか・・・etc

いろいろ不満はあるんですが、なによりファイルパスを直接指定してシーンを開くことが出来ず、使いにくいファイルブラウザからシーンの選択を強制されること。今日はこれを解決します。


コード

# encoding=utf8
# -*- coding:utf-8 -*-

import os
import pymel.core as pm

BUTTON = ["Open", "Cancel"]

def main ():
    
    dialog = pm.promptDialog(
        t = "Open Specified Path",
        m = "Type File Path",
        b = BUTTON,
        db = BUTTON[0],
        cb = BUTTON[1],
        ds = BUTTON[1]
        )
    
    # Cancel
    if dialog == BUTTON[1] : return
    
    filePath = pm.promptDialog(q = True, text = True)
    
    # Windowsの「パスのコピーに対応」する
    if filePath[0] == '"' and filePath[-1] == '"':
        filePath = filePath[1:-1]
    
    # Fileが存在しない
    if not os.path.exists(filePath):
        pm.confirmDialog(t = "Error", m = "File does not open.\n"+filePath)
        return
    
    # シーンの開く(強制的に)
    pm.openFile(filePath, force = True)


上記のコードを「OpenSpecifiedFile」という名前のPythonファイルに保存し、以下の場所に配置。

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


使い方

以下の2行で実行できます。

import OpenSpecifiedFile
OpenSpecifiedFile.main()


実行すると以下のダイアログが表示されるので、開きたいシーンのフルパスを入れてください。

f:id:garysfirearms108:20211227234628j:plain

ちょっとダイアログ自体が小さいんですけど、それはすんません。

入力したら「Open」を押すか、Enterキーを叩いてください。
現在のシーンはセーブしておいてください。普通に開くのと違って未保存の内容確認を行いませんので。

入力したパスが存在するなら、そのシーンが開かれているはずです。

まぁそれだけです。
これだけなんですけど、標準の機能でこれってないんですかね?

MayaScripts_FrameStepPlus:フレーム送り置き換え

こんにちわ。 最近えらく寒くなりましたね。

・・・忙しいんで前置きはいいですわ。

今回は小スクリプトの紹介になります。


FrameStepPlus

Maya標準のフレーム送り機能あるじゃないですか。1フレームづつ送ることができるアレです。 アレでも全然問題ないんですけど、Mayaのバージョンによってフレーム端まで送った時の挙動が異なる ように感じるのですけど・・・そんなことないですか?

例えば、Maya2018でフレーム送りをすると・・・

f:id:garysfirearms108:20211219005209j:plain

こんな感じにタイムスライダーがアニメーションの再生範囲外まで行ってしまう。

f:id:garysfirearms108:20211219005219j:plain

ところがMaya2020だと、アニメーションの再生範囲の端まで行くと、反対側の端までスライダーが飛ぶようになってたり、Maya2018でもUI上のPlaybackControlのボタンをクリックしてフレーム送りをするとMaya2020と同じ挙動で動いたりと・・・。

自分が良く知らないだけかとも 強く思います が、仕事の都合で使用するMayaのバージョンは行ったり来たりと頻繁に切り替える必要があり、その都度調べるのも面倒なので例によってスクリプトで解決することにしました。


コード

# encoding=utf8
# -*- coding:utf-8 -*-

import pymel.core as pm

NAME = "FrameStepPlus"
VERSION = "1.0"

print "-----import", NAME + VERSION, "-----"

MODE = ["cycle", "continuous", "jump"]
currentMode = MODE[0]

def __stepTo (vectorInt):
    current = pm.currentTime()
    pm.currentTime(current + vectorInt)

def __jumpTo (vectorStr):
    time = pm.findKeyframe(ts = True, w = vectorStr)
    pm.currentTime(time)

def __stepToCycleFrame (vectorInt = 1):
    minTime = pm.playbackOptions(q = True, min = True)
    maxTime = pm.playbackOptions(q = True, max = True)
    current = pm.currentTime()
    
    if current <= minTime and vectorInt < 0:
        pm.currentTime(maxTime)
        return
        
    if current >= maxTime and vectorInt > 0:
        pm.currentTime(minTime)
        return
    
    __stepTo(vectorInt)

def stepToNextFrame ():
    """次のフレームに送る"""
    if currentMode == MODE[0]:
        __stepToCycleFrame(1)
        return
    if currentMode == MODE[1]:
        __stepTo(1)
        return
    __jumpTo("next")
    
def stepToPrevFrame ():
    """前のフレームに送る"""
    if currentMode == MODE[0]:
        __stepToCycleFrame(-1)
        return
    if currentMode == MODE[1]:
        __stepTo(-1)
        return
    __jumpTo("previous") 

def switchMode ():
    """フレーム送りタイプを切り替える"""
    global currentMode
    
    index = MODE.index(currentMode)
    index = index + 1
    if index >= len(MODE):
        index = 0
    currentMode = MODE[index]
    
    line = "-----------------------"
    pm.headsUpMessage(line+"FrameStepPlus.switchMode : "+currentMode+line)


上記のコードを「FrameStepPlus」という名前のPythonファイルに保存し、以下の場所に配置。

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


各機能を実行するには以下のコードを入力してください。

次のフレームに送る

import FrameStepPlus
FrameStepPlus.stepToNextFrame()

前のフレームに送る

import FrameStepPlus
FrameStepPlus.stepToPrevFrame()

基本的には上記のコードを任意のホットキーに登録して利用することを想定しています。
過去にXSIを使っていた経験から、なんとなくフレーム送り機能のホットキーは矢印である方が嬉しいので、自分は「左右の矢印キー」にそれぞれ登録しています。

動作の挙動としては再生範囲の前後端で折り返すようにフレーム送りを行うものになっていますが、どのようにフレームを送るのかタイプを切り替えることが出来ます。


フレーム送りタイプを切り替える

import FrameStepPlus
FrameStepPlus.switchMode()

このコードを実行すると、以下の3タイプのモードで切り替えます。

  • cycle:再生範囲の端で折り返す。
  • continuous:再生範囲の端で折り返さない。
  • jump:次または前のキーフレームまで飛ぶ

好みに併せて、これも適当なホットキーに登録するといいかもしれません。

つっても最後の「jump」は既存のホットキーにあるものですけども。



以上。今回はこんな感じです。
なんともまぁ標準の機能にありそうなものをわざわざスクリプトに置き換えているだけ感が漂っていますが、とにかくソフトのバージョンが違っても必ずこちらが想定している動作をしてくれる保障がある方が精神的に安心なので、これでいいんです。これで。

MayaScriptMemo:オイラーフィルターについて

アニメーションの作業中に回転オーダーの変更等々によってアニメーションカーブが乱れてしまったとき、ひとまずはオイラーフィルターを実行するのですが、いちいちグラフエディタのカーブメニューから選択するのが面倒なので以下のスクリプトをホットキーに登録しました。

# encoding=utf8
# -*- coding:utf-8 -*-
import pymel.core as pm

def doEulerFilter ():
    """選択しているノードにEulerFilterを実行"""
    selection = pm.selected()
    pm.filterCurve(selection, f = "Euler")

私はこれを「Alt + W」に登録してます。

前から思っていたんですけど、MayaってXSIに比べてデフォルトだとアニメーション作業周りのホットキー登録が少なくないですか?
キーフレームのスロープ角度を「Auto」に変更するホットキーもないから自分で設定しなきゃならんし・・・。XSIだとありましたよね?