Maya の TimeEditor クリップ とレファレンスデータ

皆様ごきげんよう。この記事は [Maya Advent Calender 2018](https://qiita.com/advent-calendar/2018/maya) のX日目の記事です。この記事では Maya 2017 より搭載された `TimeEditor` の紹介、および レファレンスファイルからクリップを作成する方法、スクリプトから触る際の（灰色な） Tips について記述する。

# はじめに
## TimeEditor とは
MotionBuilder での `Story mode`,  XSI での `Animation Mixer` に相当する機能だ。 (*Trax Editor? なんでしたっけそれ* ) 概要は[レファレンスを参照](https://help.autodesk.com/view/MAYAUL/2017/JPN/?guid=GUID-B90726CA-6C86-4E9B-80F3-1F65745C3B3F) してほしい。またすでにとても良い紹介記事があるので Ritaroさんの SI User Notes もぜひ参照。 https://www.comtec.daikin.co.jp/DC/UsersNotes/Ritaro/tutorial/maya_10/ この記事では TimeEditorの基礎事項についての解説は行わないので上記紹介記事をまずは読んでいただきたい。

## できること
では実際にどのようなことに使えるかというと、たとえば

* セカンダリアニメーションのデータ分離
* ループモーションの作成支援
* 待機モーション（から|への）つながりブレンド作成支援
* カットシーン作成支援

といった用途につかえる。アイデア次第でさらに活用可能だ。（筆者の場合ゲーム向けのアクションアニメの作成が多いためこのような例となるが他分野でも十分有用だろう）


## 使用に当たっての注意点
Maya の(新)機能のご多分に漏れず、当然のように落ちる。よく落ちる（ *とはいえ Camera Sequencerほどではない* ）。とくにファイル読み込みまわりで落ちることが多い。回避策は今のところ見つかっていないため使用には覚悟を決めよう。


# 外部ファイルをソースに TimeEditor のクリップを作成する
非破壊フローでアニメーションを合成できるとなると、ソースに外部ファイルを指定したくなるのも当然と言える。TimeEditor ウィンドにも `外部ファイルからコンテンツを読み込む` てあるし。実際このメニューは機能する。ただし以下の制約がある。

1. `読み込むクリップが、シーン中のオブジェクトにマップ可能であること` 
1. `読み込んだクリップは、レファレンスではなくシーン固有データとなる`

1点目は
つまり読み込み先がみつからない状態では外部ファイルを TimeEditor のクリップとして読み込めない。この制約は妥当なように思えるが以下の状況で問題となる。
『maya でのアニメーション製作時にはネームスペースを付与して作業するが、アニメーションを保存する際にはネームスペースをはずしている』読み込み前に変更かければよさそうだがなかなか微妙な対応といえる

2点目は致命的だ
つまり、ソースファイルに変更があっても、こちらには伝搬しない。なんのために外部ファイルから読み込みたいのか・・・たしかにただ合成して結果をつかうだけだったらそれでも良い。しかしソースクリップをレファレンスのままにしておけるのであれば用途が広がるはずだ。（念のためかくとアニメーションソースをレファレンス化することは可能。のだがいろいろあれやこれやがそうじゃないだろと隔靴掻痒の絶）

次項では以上2点への対応を考察する


# 外部ファイルをレファレンスし、 TimeEditor のクリップを作成する
上記2点の解決のため、ここではレファレンスの使用を考える。いったんシーン中にレファレンスとしてソースを読み込み(ma|mb, fbxいずれでもよい）、そのノードをもとにクリップを作成（`選択したコンテンツをシーンから追加`）する。まずこれで `２` を解消できる。つぎに作成したクリップの `AnimSource` をリマップしてやる。操作は https://www.comtec.daikin.co.jp/DC/UsersNotes/Ritaro/tutorial/maya_10/#Composition や、https://help.autodesk.com/view/MAYAUL/2017/JPN/?guid=GUID-0E71C812-1E06-4F4A-AD19-812FF0692976  これで `1` を解決だ


# スクリプトから操作する方法
いい感じに使うと有用であることはわかった。一連の操作をスクリプトにまとめたいと考えるのは道理である。ではレファレンスにあたろう・・・ろくに整備されていないことがわかる。大概 `cmds.timeEditor***` コマンドから操作ができるのだが、まともなサンプルもない状態ではどうつかったものか皆目見当もつかないだろう。そこでここでは以下に要所を抜粋する。サンプルの全文は https://gist.github.com/yamahigashi/3eba581df545d861088796c32e4960c4 を参照されたい。検証時間が取れなかったため maya2017update4 のみでの動作確認、およびいくらか不備がのこっている（remap_to指定しない場合の動作）予めご了承いただきたい。


```python

def add_fbx_clip(
        fbx_path,
        track_container_name=None,
        track_name=None,
        remap_to=None
):
    # type: (Text, Text, Text, Text) -> Int, Int
    """FBX を レファレンスしTrackEditor のクリップとして読み込む。
    読み込んだ FBXは remap_to で指定したネームスペースへとリマップされる
    """

    if not track_container_name:
        track_container_name = DEFAULT_TRACK_CONTAINER_NAME

    create_composition_track(track_container_name)

    # fbx をネームスペース付きで読み込む。ネームスペースはファイル名からとる
    fbx_path = fbx_path.replace(os.sep, "/")
    clip_name = os.path.basename(fbx_path).split(".")[0]
    nodes = cmds.file(fbx_path, r=True, f=True, namespace=clip_name, returnNewNodes=True)

    # unsafeなファイル名な場合勝手に変更かkるので実際に読み込まれたネームスペースを取得
    animation_ns = next(x for x in nodes if (":" in x and x.startswith("|")))
    animation_ns = animation_ns.split(":")[0]

    # よみこんだレファレンスノードからクリップを作り animation source を作成しておく
    track_path = "{}:-1".format(track_container_name)
    track_index, clip_index = create_clip(track_path, nodes, clip_name)

    # 作成したもののネームスペースをリマップする
    if not remap_to:
        remap_to = ":(root)"
    remapped_clip_index = remap_clip(track_container_name, track_index, clip_index, remap_to)

    # 新しく作られたインデックスを取得する
    remapped_track_index = cmds.timeEditorClip(remapped_clip_index, q=True, track=True)
    remapped_track_index = int(remapped_track_index.split(":")[-1])

    # rename the track
    if track_name is not None:
        cmds.timeEditorTracks(
            track_container_name,
            e=True,
            trackIndex=remapped_track_index,
            trackName=track_name
        )

    return remapped_track_index, remapped_clip_index


def create_clip(track_path, nodes, clip_name):
    # type: (Text, List[Text], Text) -> Tuple[Int, Int]

    kwargs = {
        "type": [
            "animCurveTL",
            "animCurveTA",
            "animCurveTT",
            "animCurveTU"
        ],
        "addRelatedKG": True,
        "recursively": True,
        "includeRoot": True,
        "rsa":  1,
        "rootClipId": 1,
        "aso": True,
        "track": track_path
    }

    cmds.select(nodes)
    clip_index = cmds.timeEditorClip(clip_name, **kwargs)
    track_index = cmds.timeEditorClip(clip_index, q=True, track=True)
    track_index = int(track_index.split(":")[-1])

    return track_index, clip_index


def remap_clip(track_container_name, track_index, clip_index, remap_to):
    # type:(Text, Int, Int, Text) -> Int
    # TODO: get confirmation working on maya 2018

    anim_source_name = cmds.timeEditorClip(clip_index, q=True, animSource=True)

    try:
        # コールバックでよばれるUI ポップ関数を強制上書き
        # なにも実行しない関数にしてしまう
        overwrite_cmd = """global proc teRosterMappingWindow(string $animSource, int $clipId, string $continueCmd, string $remapNS){{}}"""
        mel.eval(overwrite_cmd)

        remap_cmd = """teRemapAnimSourceToNamespace {} :{};""".format(anim_source_name, remap_to)
        start_time = cmds.getAttr("{}.start".format(anim_source_name))
        duration = cmds.getAttr("{}.duration".format(anim_source_name))
        remap_cmd = """teRemapToNamespace {} :{} {} {} {};""".format(
            anim_source_name,
            remap_to,
            start_time,
            duration,
            "{}_{}".format(anim_source_name, remap_to)
        )
        mel.eval(remap_cmd)
        cmds.timeEditorTracks(track_container_name, e=True, removeTrack=track_index)

        remapped_clip_index = clip_index + 1

        # 本来 UI経由で呼ばれる処理をここで行う
        sources = cmds.timeEditorAnimSource(anim_source_name, q=True, targets=True)
        for s in sources:
            src = s
            dst = "{}:{}".format(remap_to, s.split(":")[-1])
            cmds.timeEditorClip(e=True, remapSource=(dst, src), clipId=remapped_clip_index)

        cmds.timeEditorClip(e=True, animSource=anim_source_name, existingOnly=True, clipId=remapped_clip_index)
        for s in sources:
            src = s
            dst = "{}:{}".format(remap_to, s.split(":")[-1])
            cmds.timeEditorClip(e=True, remap=(dst, src), clipId=clip_index + 1)

    except Exception:
        import traceback
        traceback.print_exc()

        # 強制上書きした proc をもとに戻す
        source_cmd = """source teRemapToNamespace.mel;"""
        mel.eval(source_cmd)
        raise

    return remapped_clip_index
```


コードを読めばわかるが、使用方法は `add_fbx_clip()` を呼ぶと FBX を レファレンスしTrackEditor のクリップとして読み込む。読み込んだ FBXは remap_to で指定したネームスペースへとリマップされる。上記の抜粋の見どころは `remap_clip` だ。素直な書き方ではどうしても無理だったためこのような記述となっている。行儀のわるい手段なので参考にしないほうがよいといえる。は通常 ui から実行される mel 関数を上書きし、スクリプト実行中邪魔なUIが表示されないようにしてしまい、同等の処理を自前で行い、melをもとに戻すということを行っている

```py

    try:
        # コールバックでよばれるUI ポップ関数を強制上書き
        # なにも実行しない関数にしてしまう
        overwrite_cmd = """global proc teRosterMappingWindow(string $animSource, int $clipId, string $continueCmd, string $remapNS){{}}"""
        mel.eval(overwrite_cmd)
```

どうしても行儀の悪いコードを書くときはせめて後始末をきちんとするようにしよう。

```py

    except Exception:
        import traceback
        traceback.print_exc()

        # 強制上書きした proc をもとに戻す
        source_cmd = """source teRemapToNamespace.mel;"""
        mel.eval(source_cmd)
        raise
```
このへん。


# おわりに
いかがだったろうか。以上 TimeEditor の紹介とそれを操作するスクリプトの紹介を行った。実用に当たっての操作は各自どのようなものか実際試していただきたい。細かくオプションにより動作が変わるので最適な手法・組み合わせを見つけてほしい。もっと洗練されたやりかた、書き方があるとおもう。そのようなものをご存知な方はぜひコメント欄へお願いします