チュートリアル / カットシーンのデータ管理
第4回:TimeEditorを⼯夫して管理を効率化してみよう④

  • Maya
  • UI・ビューポート
  • ゲーム
  • コラム
  • スクリプト・API
  • チュートリアル
  • 学生・初心者
「TimeEditorからFBXやcsvを出力する」解説記事のキービジュアル

みなさんこんにちは。このコラムでは、ゲーム制作におけるカットシーンのデータ管理について、変化球的な効率化の事例を紹介しています。

ここまでで…、

第⼀回⽬「TimeEditorのデータ構造に関して理解を深める」
第⼆回⽬「TimeEditorをスクリプトで操作してみる」
第三回⽬「TimeEditorの変則的な使い⽅とカスタマイズ」

…といったお話を展開してまいりました。

今回はカットシーンのデータ管理「TimeEditor編」の最後として、拡張したTimeEditorを使ったファイル連携…

登録したクリップからFBXアニメーションの出⼒する
登録クリップからアニメーションの情報をcsvで出⼒する

…についてご紹介します。

この記事では最終的に…

任意のクリップを選択してFBXアニメーションを一括出力するTimeEditorの操作例アニメーション

…このように任意のクリップを選択し、メニューからFBXアニメーションを出⼒させたり。 TimeEditorに登録されているすべてのクリップ情報を対象にして、FBXアニメーションを⼀括出⼒させられるようにします。

また開発現場では様々な情報の整理やとりまとめのため、古来よりエクセルファイルをヘビーユースしているケースが少なくありません。

TimeEditorのクリップ情報からエクセル/csvファイルを出力する処理のイメージ図

そのため、これについても選択クリップや登録された全クリップ情報から、エクセルファイルやcsvファイルを出⼒できるようにします。

サンプルをBoothからダウンロードできます

なお今回の記事までの内容をまとめて、以下BoothサイトにてサンプルToolとして無料配布しております。
https://coyotec.booth.pm/items/7998260
ぜひダウンロードいただき、ご参考いただけましたら⼤変幸いです。

クリップ情報からFBXアニメーションファイルを出⼒させよう

前回の記事までで、FBXに出⼒したいノードをクリップに登録するところまできました。あとはシンプルにクリップの接続をたどって対象ノードを取得し、アニメーションレンジはクリップ情報を参照すればいいだけなので、簡単ですね。

選択した対象クリップから、出⼒対象をたどる

前回の記事で、作成するクリップと対象ノードはmessageアトリビュート「COYOTE_ExportTargetNode」と「ExportSourceClip」で接続させました。

この接続を参照して、以下のコードで対象のクリップから出⼒対象ノードのリストが取得できます。

(注︕)from maya import cmds, mel 記述は省略してあります。

def getTargetNodeFromClip(clip, *args, **kwargs):
    # もしも「COYOTE_ExportTargetNode」アトリビュートがなかったら対象外クリップのため、処理スキップ
    if not cmds.attributeQuery('COYOTE_ExportTargetNode', n=clip, ex=True):
        return ''
    targetNodes = cmds.listConnections(
                                    '{}.COYOTE_ExportTargetNode'.format(clip),
                                    s=True,
                                    d=False
                                )

    # 接続元がなかったら空を返す
    if not targetNodes:
        return ''

    # ソース接続は必ず単一なので、0番目を返す
    return targetNodes[0]

(ポイント)選択クリップがカスタムTimeEditorの機能で作られたものでないケースを想定して、最初に対象アトリビュート「COYOTE_ExportTargetNode」アトリビュートが存在するかどうかチェックしておきます。

TimeEditorで選択されているクリップノードの取得は、ちょっとひとクセ

TimeEditor上で選択されているクリップを取得するのはちょっと厄介。
よく⽤いられる例ですが、ls -slコマンドでtimeEditorClipノードを指定して取得する以下のようなスクリプト…

cmds.ls(sl=True, type='timeEditorClip')

…では取得できません。

試しにTimeEditor上でクリップノードを選択した状態で、以下のスクリプトを実⾏してみると、理由は明らかです。

for sel in cmds.ls(sl=True):
    print(sel)
ls -slコマンドでTimeEditorクリップのマルチアトリビュートが列挙されているMayaスクリプトエディタの画面

このように、ls -slコマンドで列挙されるのはノード名ではなく、クリップのマルチアトリビュートになっています。
そのため、シンプルにlsコマンドでtypeを指定するだけではダメなんですね。

そういう場合は以下のようなコードにして、明⽰的にオブジェクトを返すように指定します。

clips = cmds.ls(sl=True, type='timeEditorClip', o=True)

こうしてスクリプトを再度実⾏してみると…

修正したスクリプトでTimeEditor上の選択からクリップノードを正しく取得できている様子

このようにTimeEditor上の選択からクリップを取得することができました。

出⼒に必要な情報をクリップから取得

次にFBXアニメーションの出⼒に必要な、アニメーションレンジの情報をクリップから取得しましょう。クリップの情報には、クリップの開始時間クリップの⻑さが記載されています。しかし終了フレームの情報は記載がありません。

TimeEditorクリップの開始時間と長さなどが表示された属性情報のスクリーンショット

そのため、開始時間とクリップの⻑さを⾜し算して、アニメーションの終了フレームを算出する必要があります。

対象クリップからアニメーションレンジを取得する処理は以下の通りです。

def getAmimRangeFromClip(clip='', *args, **kwargs):
    startTime = cmds.getAttr('{}.clip[0].clipStart'.format(clip))
    endTime = startTime + cmds.getAttr('{}.clip[0].clipDuration'.format(clip))
    
    return (startTime, endTime)

ポイントは、「.clip[0].~~」から値を取得してくる点です。

前述のTimeEditor上でクリップを取得した際、選択されたアトリビュートが

・null1_clip.clip[0]
・null2_clip.clip[0]
・null3_clip.clip[0]…

…であった点を⾒てもわかるように、ここでのクリップ情報はマルチアトリビュートの.clip[0]上に記載されています。
カットシーンの情報管理⽬的での運⽤では、.clip[1]…以降の運⽤はしないこととし、.clip[0]から取得するようにして、処理内容をシンプルにします。

登録されているクリップをリストアップして、FBX⼀括出⼒処理

さて、ここまでは「選択したクリップに対しての処理」でしたが、今度は「登録されているすべてのクリップ」を⼀括処理させてみます。
といっても、カスタムTimeEditorに登録されたすべてのクリップを対象とするのではなく、「現在表⽰されているCompositionに登録されたクリップ」を対象にすると使いやすそうです。

現在表⽰されているCompositonを取得する

現在TimeEditorで表⽰されているCompositionを取得するには、以下のスクリプトを実⾏します。

comp = cmds.timeEditorComposition(q=True, act=True)

Compositionに含まれているクリップを取得する

対象のCompositionが取得できたところで、今度はそのCompositonに登録されているクリップを取得します。
クリップノードはCompositionノードにつながっているので、対象のCompositionノードから接続をたどれば簡単です。

Compositionノードと接続された複数のクリップノードの関係を示すTimeEditorの画面

接続をたどる処理を関数にまとめてみましょう。

def getTargetClipFromCurrentComposition(*args, **kwargs):
    # 現在のcompositonを取得
    comp = cmds.timeEditorComposition(q=True, act=True)
    # compositionに接続しているclipノードを探す
    clipNodes = cmds.listConnections(comp, s=False, d=True, type='timeEditorClip')  # 重複の削除
    clipNodes = list(set(clipNodes))

    return clipNodes

これで「登録されているすべてのクリップ」に対してもFBX出⼒処理を⾛らせることが可能になりました。

FBX出⼒機能をカスタムTimeEditorのメニューに実装する

では、ここまででまとめたFBX出⼒処理をまとめて、カスタムTimeEditorのメニューから実⾏できるようにしてみます。

クリップから対象ノードやアニメーションレンジを取得する処理や、対象のクリップを取得する処理はできているので、これらを順番に実⾏してFBX出⼒処理につなげるだけでOKです。
FBXに関するスクリプトについては以下公式ドキュメントに解説があるので、ご参照ください。
公式ドキュメント︓「FBX MEL スクリプティング」

また処理を作成する際、関数の引数にselection=True/Falseなどを指定できるようにして、「全出⼒/選択出⼒」を切り替えられるようにしたら、使いやすそうです。

FBXの出⼒処理については、そのテーマだけで結構深掘りできてしまうため、今回の記事では解説を割愛いたします。
また別の機会に記事にできれば幸いです。

クリップ上で右クリック…のポップアップメニューから実⾏できたら最⾼

TimeEditor上でクリップを右クリックしFBX出力メニューを表示しているカスタムポップアップメニューの画面

では、作成したFBX出⼒処理をカスタムTimeEditorのメニューから実⾏できるようにします。

しかし…、前回の記事で追加したOutliner上のポップアップメニューに追加するのは…なんかちょっとイメージ違うなぁ…という感じ。
やっぱりTool操作をイメージすると、FBX出⼒したいクリップを選択して右クリック︕…という、この流れを崩したくないわけです。

そしたら、TimeEditor上で右クリックして出現するPopupMenuを少し調べてみましょう。前回同様、スクリプトエディタをEcho allにしてTimeEditorを右クリックしてみます。すると…

TimeEditor右クリック時にスクリプトエディタへ出力されるteContextMenusとポップアップメニュー名のログ表示
teContextMenus timeEditorPanel1TimeEd timeEditorPanel1TimeEdPopupMenu Popup;

このようなプロシージャが実⾏されていることがわかります。
どうやらプロシージャ「teContextMenus」に対して、TimeEditorPanelの名前とポップアップメニューの名前、定数の⽂字列「Popup」を渡して実⾏しているようです。

さらにこれをWhatIsコマンドでたどりソースコードを⾒てみると、

・指定されたポップアップメニューをリフレッシュ
・その時の右クリックした状況に応じて、popupMenuの内容を変えてビルド

…というようなことをしていることがわかります。

ここまでわかればもうあとは簡単で、右クリックした際に実⾏される「teContextMenus」をこちらで⽤意した関数でオーバーライドしてしまえばよさそうです。

しかし、前回同様にこちらの勝⼿な拡張によって、元々のTimeEditorの機能に不具合が出てはいけません。なので、オーバーライド⽤に⽤意する関数内で元々の処理「teContextMenus」を先に呼んでおき、最後にカスタムメニューの追加処理が⾛るようにしておけばよさそうです。

ではちょっと書いてみましょう。
まずは、ポップアップメニューにカスタムメニューを追加する処理です。

def addmenuItemToTimeEditor(menuName, *args, **kwargs): 
    """TimeEditorのポップアップメニューにmenuItemを追加する処理です"""
    # メニューをわかりやすくするために罫線を入れて区切ります。 
    cmds.menuItem( 
            l='COYOTE FBXExport', 
            d=True, 
            p=menuName 
        ) 
    cmds.menuItem( 
            l='Export All', 
            p=menuName, 
            c=lambda *args: FBXExport(selection=False) 
        ) 
    cmds.menuItem( 
            l='Expirt Selection', 
            p=menuName, 
            c=lambda *args: FBXExport(selection=True) 
        ) 

次に、プロシージャ「teContextMenus」をオーバーライドする関数です。

def teContextMenusOverride(*args, **kwargs): 
    """teContextMenusプロシージャのオーバーライド関数です 
    TimeEditorのポップアップメニューが呼ばれた直後に実⾏される関数です。 
    """ 
    # 引数からメニューの名前とTimeEditorPanelの名前を取得します。 
    menuName = args[0].split('|')[-1] 
    panelName = args[1].split('|')[-1] 
    try: 
        # 念のためTry文でエラー回避させ、後の処理が必ず実⾏されるようにします。 
        mel.eval('teContextMenus {} {} Popup;'.format(panelName, menuName)) 
    except: 
        pass 
    # カスタムメニューを追加します。 
    addmenuItemToTimeEditor(menuName) 

最後に、上記で作成した関数をTimeEditorのポップアップメニューから呼ばれる関数に登録する処理です。

def buildTimeEditorPopipMenu(*args, **kwargs): 
    """TimeEditorのポップアップメニューにオーバーライド関数をセットする処理 
    カスタムTimeEditorが呼ばれたときに実⾏する関数です。 
    """ 
    # teContextMenusをオーバーライドする関数「teContextMenusOverride」をpopupMenuに設定します。
    cmds.popupMenu( 
        'timeEditorPanel1TimeEdPopupMenu', 
        e=True, 
        pmc=teContextMenusOverride 
        )

カスタムTimeEditorを⽴ち上げる際に上記関数が実⾏されるようにしておけば、TimeEditor上のPopUpMenu の拡張もOKです。

実際に拡張した例はこんな感じです。

FBX出力やcsv出力などを追加した拡張TimeEditorポップアップメニューの画面

うん、やっぱこうでしょ︕
オペレーションとToolのデザインがうまくかみ合うと、とても気持ちがよくていいですね。

【ポイント】popupMenuコマンドのpostMenuCommandでスクリプトを実⾏させる

ここでのポイントは…

・popupMenuコマンドのpmc(postMenuCommand)フラグ(公式ヘルプ参照)
・そこで指定した関数「def teContextMenusOverride」

…です。

処理の中で、

menuName = args[0].split('|')[-1]
panelName = args[1].split('|')[-1]

mel.eval('teContextMenus {} {} Popup;'.format(panelName, menuName))

こんなふうにargsから値をとって、元々のプロシージャに値を渡しています。
実は、popupMenuのフラグpmc(postMenuCommand)で関数を実⾏すると…

・ポップアップメニューを出したGUIのパス
・対象のポップアップメニューの名前

…が関数に渡されます。

試しに…

def testOverride(*args, **kwargs):
    for arg in args:
    print(arg)

cmds.popupMenu('timeEditorPanel1TimeEdPopupMenu', e=True, pmc=testOverride)

このような関数を書いて、引数のargsの中⾝をプリントさせてみます。

TimeEditor上で右クリックしてみると…

popupMenuのpostMenuCommandで渡される対象メニュー名と親メニュー名のフルパスをプリントしているスクリプトエディタの画面

このように、「対象メニューの名前」と「対象メニューの親の名前」のフルパスがプリントされるんですね。

TimeEditorPanelEditorの名前とポップアップメニューの名前は固有な定数なので…

・timeEditorPanel1TimeEd
・timeEditorPanel1TimeEdPopupMenu

…と直に書き込んでしまってもよかったのですが…。
しかしせっかくなら、取得して使ったほうがエレガント(?)な気がしますね。(個⼈の⾒解)

カットシーンの情報をエクセルに出⼒しよう

前述したように、開発現場では情報管理や様々なやり取りのため、エクセルをヘビーユースしているケースが少なくありません。
最初の記事でも述べたように、しばしば開発現場ではこのエクセル管理とMayaシーン管理がダブルスタンダードになって、管理コストをいたずらに上げてしまうケースに陥ることがあります。

カスタムTimeEditorでは

・出⼒対象
・アニメーションレンジ

…が、すでにクリップに登録されているため、カスタムTimeEditorを上⼿に運⽤すればMayaシーン上で管理が完結することができそうです。
とはいえ、他部署との連携にどうしてもエクセルファイルで⾒たい、というケースもあるでしょう。 (他部署はMayaが使えなかったりして。)

そこで、カスタムTimeEditorからエクセルファイルを出⼒できるようにしてみます。

対象クリップから必要な情報を取得

選択クリップや、compositionに含まれる全クリップ情報の取得は、FBX出⼒処理の項で記載した通りですので、同じ関数が応⽤できます。
また開発プロジェクトに応じて、クリップに登録する/接続する情報をさらにカスタマイズしてもよいでしょう。

対象クリップの情報をもとにcsv出⼒させる

MayaのPython環境で簡単に出⼒できるファイル形式で⾔うと、csv形式がよさそうです。
Pythonの標準ライブラリにはcsvモジュールもあるので、⽐較的扱いやすいのもポイントです。

csvファイルはエクセルファイルではないですが、エクセルで開くことで表として表⽰されます。
または、エクセル側にマクロやVBAを仕込んでエクセルファイルにコンバートさせてもよさそうです。

では、まずはクリップから取得した情報をcsvファイルに出⼒する処理を書いてみましょう。

(注︕)import csv 記述は省略してあります。

まずはクリップからcsv向けに必要な情報を取得します。

def getClipsInfoByRow(clips=[], *args, **kwargs): 
    """クリップからcsv向けの情報を取得します。 
    csvの⾏ごとにclipの情報をまとめて、リストで返します。 
    """ 
    Rows_clipInfo = [] 
    
    # csvのヘッダー⾏ 
    headder = ['Clip name', 'Character name', 'Start time', 'End time', 'Anim length']
    Rows_clipInfo.append(headder) 
    
    clips.sort() 
    for clip in clips: 
        clipNames = clip 
        chrNames = getTargetNodeFromClip(clip) 
        animRanges = getAmimRangeFromClip(clip) 
    
        # アニメーションレンジを開始・終了・アニメーション⻑にします。 
        startTime = animRanges[0] 
        endTime = animRanges[1] 
        animLength = endTime - startTime 
    
        Rows_clipInfo.append([clipNames, chrNames, animLength, endTime, animLength]) 
    
    # ⾏ごとに情報をまとめた状態のリストを返します。 
    return Rows_clipInfo 

次に、上記の処理に対象クリップの名前を渡しつつ、csvファイルを実際に書き出す処理を書いてみます。

def exportCSV(selection=False, *args, **kwargs): 
    # ファイルダイアログで書き出すcsvのファイルパスを決める。  
    exportCSVPath = cmds.fileDialog2( 
                            fm=0, 
                            ff='CSV(*.csv);;All(*.*)' 
        ) 
    # ファイルダイアログがキャンセルされるなどして、パスが決まらなかった場合は何もしない
    if not exportCSVPath: 
        return 
    
    # 対象のクリップを取得します。 
    clips = [] 
    if selection: 
        # selection = Trueの場合は選択クリップを取得 
        clips = cmds.ls(sl=True, type='timeEditorClip', o=True) 
    else: 
        # そうでなければcompositionに含まれる全クリップを対象にする 
        clips = getTargetClipFromCurrentComposition() 
    
    # csvに書き出す情報を取得(⾏ごとのリスト) 
    rowValues = getClipsInfoByRow(clips) 
    
    # csvを書き出す処理 
    with open(exportCSVPath[0], 'w', newline='') as f: 
        writer = csv.writer(f) 
    
        # データをファイルに書き込む 
        for row in rowValues: 
            writer.writerow(row)

さて、これで処理ができました。
あとは前述の要領でTimeEditorのポップアップメニューに追加して、実⾏できるようにします。

実際の挙動はこんな感じです。

TimeEditorのクリップ情報をcsvに出力しエクセルで表として表示している挙動のアニメーション

このようにクリップの情報をcsvに出⼒することができました。
エクセルでcsvを開いて保存すれば、エクセルファイルとして取り扱うことも可能です。ヤッタネ︕

csvじゃなくてエクセルファイルを直に出⼒したいんじゃよ。

しかし、そうはいってもcsvかぁ。
やっぱりエクセルファイルを直に出⼒したいんだよな…ですって?ええ、わかります。
ぼくも同じきもちです。

そういう場合は、Pythonでエクセルを作成・編集できる外部モジュールを導⼊することで対応が可能です。
例として…

・openpyxl
・xlwings

…などがまず挙がります。
状況に合わせご検討ください。

これらのpythonモジュールの話はMaya機能から⼤きく逸脱するので、こちらの記事では割愛いたします。

まとめ

今回は「TimeEditorを使った効率化」編のまとめとして、

・FBXアニメーションの出⼒
・エクセルファイル(csv)の出⼒

…を紹介しました。
これらの話は、実際に現場での運⽤のケースに近いケースだと思います。
しかしFBXの出⼒仕様やエクセルに記載する内容など、細かい部分はプロジェクトやスタジオごとに異なるはずです。
実際には、適宜現場で運⽤される⽅々と相談しながら、そういった細かい箇所を詰めながら機能拡張・取捨選択していくことが肝要と思います。

ぜひ皆様の⽇々の業務にお役⽴ていただけますと幸いです。

サンプルToolをBoothからダウンロードできます

また最初に記載した通り、ここまでの内容をサンプルToolにまとめました。
以下BoothのURLからダウンロード可能です。
https://coyotec.booth.pm/items/7998260

…でもこれ、TimeEditorへの登録オペレーションが⾯倒︖

ところが実際にプロジェクトで運⽤したところ、この「TimeEditorにクリップ登録していく⼿順」が割と⾯倒になりました。
登場するキャラクターやオブジェクト、カメラが多い場合などでは、特にこの問題は顕著です。ここへ来て、さらに代替えのプランを考える必要が出てきたわけです。 (ここまでTimeEditorを拡張した⼿法を提案して何ですが…。。)

やっぱり開発現場ではなかなか⼀筋縄には上⼿くいきませんね。「事件は会議室ではなく現場で起こっている」というわけです。
しかしそれが⾯⽩いところです。

【解決策(次回︕)】カメラに映るものを出⼒対象にする、「カメラドリブン」での運⽤を検討

それならもう、いっそTimeEditorにもエクセルにも登録しないで「カメラに映ったものを対象にしてしまえ」と考えて、いわゆる「カメラドリブン」な⼿法を構築することにしました。

次回と、その次の最終回では、この「カメラドリブン」でのカットシーンのデータ管理⼿法についてご紹介したいと思います。

ではまた次回︕

TA応援隊の⼩話

【第4回】

みなさん你好︕COYOTE TAチームの⼩澤です。

すっかり冬になったと思ったら、気が付けば冬季オリンピックが始まっていました。もしかしたら掲載のころにはそれすら終わっているかもしれません。時の流れが早くて怖いです…。

さて、前回は弊チームの「仕事」についてご紹介いたしましたが、今回はそれ以外の各種「活動」についてご紹介したいと思います。メインはもちろん各社様へテクニカルサポートを提供するお仕事なのですが、それ以外にも社内プロジェクトをいくつか設けていたり、「TA業界を盛り上げたい」という思いのもとTAの啓蒙活動なども⾏っていたりします。

まず社内プロジェクトとしては、まだ詳細をお伝えできないものも多いのですが、内製システム・ツールの開発や、特定分野のR&D、ノウハウ蓄積⽬的としたミニゲーム開発…などなど多岐にわたります。内製ツールに関しては⼤⼩ありますが、ライトなものに関してはBOOTH(本記事内のツールもそちらにございます)で頒布させていただいておりまして、ありがたいことに弊チームを知っていただくきっかけにもなっています。

社内プロジェクトに関しては、「チームメンバーのやりたいこと」「市場で求められていること」「チームとして挑戦していきたいこと」などをベースに取り組んでいます。

そして”TAの啓蒙活動”については、技術ブログの運営や各種セミナーへの登壇、さらに「TA Night」の主催に⼒を⼊れています。TA NightはTA業界における情報発信・コミュニティ形成を⽬的としたイベントで、主にセミナー+懇親会形式で定期的に開催しています。

>>TA Night過去実施回

⼤変ありがたいことに多くのTAの⽅、CG関係者、学⽣さん…etc.にご参加いただいて、どんどん輪が広がってきています。我々としてもそこからお仕事や採⽤につながることも増えてきて、主催という⽴場でありながらも多くのことを学ばせていただき、そしてそれが具体的な実りとなってきていることを実感しています。

これからも趣向を凝らした企画に努めてまいりますので、ぜひご参加いただけたら幸いです︕それではまた次回お会いしましょう︕

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