ctypesでアトリビュートのアニメーションをバイナリ出力する

#はじめに
初AdventCalendar参加ということで何を書こうか悩みましたが、最近業務で作成してなかなか役に立っている
「ctypesによるアニメーションバイナリデータエクスポーター」
に関して紹介したいと思います。

ここでは非常に簡単なフォーマットを採用していますが、書き出し部分を変更して別のフォーマットにするなり、
そもそもバイナリではなくてJsonやxmlにすることもできると思います。
（すでにJsonやxml書き出しのフリーツールがあるとか言わないで...）

また、ここではバイナリの解析まではやりません。
UI周りも言及しません。ノードの情報をバイナリデータとして出力する部分がメインです。

*末尾に全コードを載せています*

#目的
ゲーム開発などで、ちょっとした動きのアニメーションをMaya上で作って出力して使いたいということが
あったりします。キャラクターアニメ用のデータ等で出力してもいいでしょうが、
出力用のシーンセットアップが手間だったり機能が過剰だったりで変なところにコストが掛かります。
データに関するルールを都度決めるのも面倒です。

そんな時に、指定したノードのアニメデータがシンプルなフォーマットで出力できて、
プログラム的に個々のアニメーションデータに名前でアクセスできる仕組みがあると検証などに
すぐに取り掛かれて捗ります。

特に名前に関しては、Maya上での「ノード名.アトリビュート名」がそのまま使えると非常に楽です。
例えば攻撃エフェクトのアルファ値によるフェードアウトをアニメーションで制御したいとなった場合に、
「TestAnim.AttackEffectAlpha」
という感じのノード.アトリビュートを作ってキーを打って出力し、プログラマに
「このファイルにTestAnim.AttackEffectAlphaって名前でアニメデータが入ってるんで宜しく」
で済みます。(実運用時には別途命名規則や納品ルールとかは別途決めることになるでしょうけど

まあ当然プログラマに実装負担はあるんですが、専用のアニメデータを用意するとか
それに付随するルール決めなどをやらなくて済むので結果的に楽になるんじゃないかと。

#フォーマット
以下のようなデータを出力したいと思います。

```python
// ----------------------------------------------------
// ヘッダ部
アニメーションデータ数( 4byte )
アニメーションデータ配列の先頭へのオフセット( 4byte )
// ----------------------------------------------------

// ----------------------------------------------------
// アニメーションデータ部
アニメデータ識別名(64byte)
データタイプ識別名(32byte)
キーフレーム数(4byte)
キーフレーム値配列の先頭へのオフセット(4byte)
キーフレーム番号配列の先頭へのオフセット(4byte)
// ----------------------------------------------------
```

バイナリデータ内のイメージとしては以下のような感じになると思います。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/222757/59ae6719-f8df-d884-9523-c0af494b98d4.png)


普通ですね。ヘッダからオフセットでアニメデータ配列の先頭を指示し、
個々のアニメデータからはさらにオフセットでキーフレームの値とフレーム番号の配列を指示してます。
アニメデータ部には一応どんなデータ型なのか名前を入れています。
今回は int と float のアトリビュートに関してそれぞれの型で出力できるようにしています。

#データのクラス
C言語の型が使えるようになる ctypes モジュールを利用して書き出し用のクラスを定義します。

```python
from ctypes import *

class FileHeader( Structure ):
    _fields_ = [
    # アニメーションの個数
    ('numAnim', c_uint32),
    # アニメーションデータ配列先頭へのオフセット
    ('animDataOffset', c_uint32),
    ]

class AnimData( Structure ):
    _fields_ = [
    # アニメーションデータの識別名
    ('name', c_char *64),
    # タイプ名 float とか
    ('typeName', c_char *32),
    # キーフレームデータの個数
    ('numFrame', c_uint32),
    # numFrame 個の要素の値配列先頭へのオフセット
    ('valueArrayOffset', c_uint32),
    # numFrame 個の要素のキーフレーム番号配列先頭へのオフセット
    ('keyNumberArrayOffset', c_uint32),
    ]
    def __init__(self):
        # 一時データ用  シーンから取得したアニメデータは一旦ここに
        self.tmpValueArray = []
        self.tmpKeyNumberArray = []
        
        # 書き出し用のキーフレームデータ, 一時データから実際に書き出すための変換をかけてこちらにコピーされる
        self.valueArray = None
        self.keyNumberArray = None

class FileObj:
    def __init__(self):
        self.fileHeader = FileHeader()
        self.animDataList = []
        #　オフセットは後で適切に書き換える
        self.fileHeader.animDataOffset = 0
        self.fileHeader.numAnim = 0
        # ファイルサイズ
        self.fileSize = 0
        pass
    
    # 配列から書き出し用のc_charバッファを作って返す
    @staticmethod
    def buildDataFromArray( src  ):
        elemByteSize = sizeof( src[0] )
        byteSize = len(src) * elemByteSize
        dst = ( c_char * byteSize )()
        # コピー
        for i in range(len(src)):
            memmove( byref(dst, i * elemByteSize), byref(src[i]), elemByteSize )
            pass
        return dst
        
    # 色々データを詰めた後にオフセットとかの計算をする
    def calc(self):
        offsetCnt = 0
        offsetCnt += sizeof( self.fileHeader )
        
        # ここからアニメデータ構造体配列が並ぶ
        self.fileHeader.animDataOffset = 0
        if 0 < self.fileHeader.numAnim:
            # アニメデータがある場合だけ
            # アニメデータ配列の先頭オフセット
            self.fileHeader.animDataOffset = offsetCnt
            offsetCnt += self.fileHeader.numAnim * sizeof(self.animDataList[0])
        else:
            # アニメデータが無い場合はオフセットゼロ
            pass
            
        # ここから一つ一つのアニメの配列データを敷き詰める
        for i in range(self.fileHeader.numAnim):
            ##############################
            # 値配列の先頭オフセット
            self.animDataList[i].valueArrayOffset = offsetCnt
            # 書き出し用データを構築
            self.animDataList[i].valueArray = FileObj.buildDataFromArray( self.animDataList[i].tmpValueArray )
            # 値配列分進める
            offsetCnt += sizeof( self.animDataList[i].valueArray )
            
            ##############################
            # キー番号配列の先頭オフセット
            self.animDataList[i].keyNumberArrayOffset = offsetCnt
            # 書き出し用データを構築
            self.animDataList[i].keyNumberArray = FileObj.buildDataFromArray( self.animDataList[i].tmpKeyNumberArray )
            # キー番号配列分進める
            offsetCnt += sizeof( self.animDataList[i].keyNumberArray )
        
        # ファイルサイズ確定
        self.fileSize = offsetCnt

    # ファイル書き出し
    def write(self, filePath):
        # オープン
        with open(filePath, "wb") as fout:
            # ヘッダ書き出し
            fout.write( self.fileHeader )
            
            # 先にアニメの固定部を連続書き出し
            for i in range( len( self.animDataList ) ):
                fout.write( self.animDataList[i] )
                
            # その後にアニメデータ部を連続書き出し
            for i in range( len( self.animDataList ) ):
                fout.write( self.animDataList[i].valueArray )
                fout.write( self.animDataList[i].keyNumberArray )
        pass
```

FileObjがファイルに書き出すためのクラスになり、ヘッダ情報のFileHeaderとアニメデータ部のAnimDataを持ちます。
FileObjのメンバに適切に情報を設定してから calc()でオフセット情報を計算、write()で書き出しとなります。


注意点として書き出すためのキーフレームの値配列等はctypes型の固定サイズ配列にする必要があるので、
データ収集の段階では適当に作業用listにaddしていき、最後にbuildDataFromArray()で適切なサイズのバッファを生成して
そこにコピーしています。
( tmpValueArray,tmpKeyNumberArray が作業用listで valueArray,keyNumberArray が書き出し用バッファ )

#データの収集と出力
エクスポートを実行する際の関数は以下のように対象とするアトリビュートとその出力名を複数指定できるようにしています。

``` python
# 出力情報登録
exportTargetInfo = []
# 出力対象の ノード名.アトリビュート名  とそれに付ける出力名
exportTargetInfo.append(['pCube1.tx', 'CubeTransX'])
# 出力
export( 出力パス, exportTargetInfo, 開始フレーム, 終了フレーム )
```

exportの実装は以下になります。
ノード名からOpenMaya2.0のオブジェクトを取得するユーティリティ関数がいくつか定義してあります。

```python
#   OpenMayaによるAPIを使ったノード操作関連
#   ノード名からOpenMayaのノードオブジェクト(MObject)を取得
def nameToNode( name ):
    selectionList = nom.MSelectionList()
    try:
        selectionList.add( name )
    except:
        return None
    return selectionList.getDependNode( 0 )
 
#   ノード名＋アトリビュート名からOpenMayaのノードアトリビュートオブジェクト(MPlug)を取得
def nameToNodePlug( attrName, nodeObject ):
    depNodeFn = None
    if isinstance( nodeObject, nom.MObject ):
        depNodeFn = nom.MFnDependencyNode( nodeObject )
    else:
        mobj = nameToNode(nodeObject)
        if not mobj:
            return None
        else:
            depNodeFn = nom.MFnDependencyNode( nameToNode(nodeObject) )
        
    if not depNodeFn.hasAttribute(attrName):
        return None
    attrObject = depNodeFn.attribute( attrName )
    plug = nom.MPlug( depNodeFn.object(), attrObject )
    return plug
    
# NodeName.AttrName からMPlug取得
def nodeAttrNameToNodePlug( nodeAttrName ):
    searchObj = re.search( '[.][^.]+$', nodeAttrName )
    if not searchObj:
        return None
    else:
        dotAttrName = searchObj.group(0)
        nodeName = nodeAttrName.replace( dotAttrName, '' )
        attrName = dotAttrName.replace( '.', '' )
        return nameToNodePlug( attrName, nodeName )


# アトリビュートから値を取り出して適切なctypes型でリストに積むためのコールバック関数
# アトリビュートの値をfloat型で返す
def getAttrValCallback_float( plug ):
    return c_float( plug.asFloat() )

# アトリビュートの値をintとして返す
def getAttrValCallback_int( plug ):
    return c_int32( plug.asInt() )


"""
# Input Args
# 出力したいアトリビュートの情報
# [ ノード.アトリビュート名, 出力名 ] のリスト, 開始フレーム番号, 終了フレーム番号
# 出力名が指定されなかった場合は　ノード.アトリビュート名が使われる
"""
def export( outputFilePath, targetAttrNameList, startFrame, endFrame ):
    #念のため
    if startFrame > endFrame:
        endFrame = startFrame
    

    # 入力情報から出力可能なPlugを選定
    exportablePlugs = []
    exportablePlugLabelNames = []
    exportablePlugTypeNames = []
    exportablePlugGetValueCallbacks = []
    
    for target in targetAttrNameList:
        
        exportName = target[0]
        targetPlug = nodeAttrNameToNodePlug( target[0] )
        if not targetPlug:
            continue
            
        # プラグへの入力を探す 何らかの入力があれば出力対象とする
        inConnectArray = targetPlug.connectedTo( True, False )
        if 0 >= len(inConnectArray):
            # このアトリビュートが子であり,親アトリビュートにコネクションがある場合もチェックする
            if not targetPlug.isChild:
                continue
            targetParentPlug = targetPlug.parent()
            inConnectArray = targetParentPlug.connectedTo( True, False )
            if 0 >= len( inConnectArray ):
                continue
        
        # 出力用の名前が指定されている場合
        if 2 <= len(target) and 0 < len(target[1]):
            exportName = target[1]

        # 出力タイプ名
        plugTypeName = ''
        getterMethod = None
        attrObj = targetPlug.attribute()
        apiType = attrObj.apiType()
        if nom.MFn.kNumericAttribute == apiType:
            # 数値アトリビュート
            numericAttr = nom.MFnNumericAttribute(attrObj)
            numericType = numericAttr.numericType()
            if nom.MFnNumericData.kBoolean == numericType or nom.MFnNumericData.kShort == numericType or nom.MFnNumericData.kLong == numericType:
                # 型名の登録とデータ取得コールバック登録
                plugTypeName = 'int'
                getterMethod = getAttrValCallback_int
                pass
            elif nom.MFnNumericData.kFloat == numericType or nom.MFnNumericData.kDouble == numericType:
                # 型名の登録とデータ取得コールバック登録
                plugTypeName = 'float'
                getterMethod = getAttrValCallback_float
                pass
            else:
                print '[Unsupported] ' + str(target[0])
                pass
            pass
        
        # タイプがわからなかったらとりあえずfloatとしてしまう
        if '' == plugTypeName:
            # それ以外は全部float扱いするか... transformノードのtranslateXとかはkNumericAttributeではなくkDoubleLinearAttributeだったりするので
            plugTypeName = 'float'
            getterMethod = getAttrValCallback_float

        # コールバックが無かったらだめ
        if not getterMethod:
            continue
        
        #exportablePlugTypeNames

        exportablePlugs.append( targetPlug )
        exportablePlugLabelNames.append( exportName )
        exportablePlugTypeNames.append( plugTypeName )
        exportablePlugGetValueCallbacks.append( getterMethod )

        
    #####################################################
    # 書き出し様オブジェクト
    fileObj = FileObj()
    #####################################################
    
    # アニメオブジェクト数
    fileObj.fileHeader.numAnim = len(exportablePlugs)
    
    # アニメデータを必要分用意
    for ti in xrange( len( exportablePlugs ) ):
        fileObj.animDataList.append(AnimData())
        fileObj.animDataList[ti].name = exportablePlugLabelNames[ti]
        # フレーム数は最大フレームの番号ではなく、キーフレームデータが何個あるかなのであとで計算する
        fileObj.animDataList[ti].numFrame = 0
        
        # タイプ
        fileObj.animDataList[ti].typeName = exportablePlugTypeNames[ti]
    
    # アニメ情報を取り出していく
    for i in xrange( startFrame, endFrame+1 ):

        # フレームを動かしつつデータを取得
        #noma.MAnimControl.setCurrentTime( nom.MTime(i) )
        # APIでのフレーム移動では更新されないアトリビュートがあるようなのでcmdsで代用
        cmds.currentTime( i )

        frameIndex = i - startFrame
        for ti in xrange( len( exportablePlugs ) ):
            # 値の取得
            animKeyNumber = frameIndex
            
            # コールバックでアトリビュートから値を取り出してc言語型にキャストして積む
            fileObj.animDataList[ti].tmpValueArray.append( exportablePlugGetValueCallbacks[ti]( exportablePlugs[ti] ) )

            # キー番号とオプション
            fileObj.animDataList[ti].tmpKeyNumberArray.append( c_uint32( animKeyNumber ))
                
    # データの取得が終わったので要素のバイト数設定や不要なデータの削除とかをする
    # 同一値が連続するフレームを除去する等の処理はこのあたりで
    
    # デバッグ表示
    for ti in xrange( len( exportablePlugs ) ):
        print '------------------------------------------------'
        print '[ ' + fileObj.animDataList[ti].name + ' ]'
        print fileObj.animDataList[ti].tmpValueArray
        print fileObj.animDataList[ti].tmpKeyNumberArray
        print '------------------------------------------------'
        pass
    
    
    # キーの削除などが終わったのでフレーム数を確定する
    for ti in xrange( len( exportablePlugs ) ):
        fileObj.animDataList[ti].numFrame = len(fileObj.animDataList[ti].tmpKeyNumberArray)
    
    # オフセットなどを計算
    fileObj.calc()
        
    fileObj.write( outputFilePath )
 
####################################################################
```

最初に引数で指定された出力対象のアトリビュート群に関して必要な情報を収集しています。

```python
exportablePlugs.append( targetPlug )
exportablePlugLabelNames.append( exportName )
exportablePlugTypeNames.append( plugTypeName )
exportablePlugGetValueCallbacks.append( getterMethod )
```

exportablePlugs にはアトリビュートにアクセスするためのOpenMaya2.0のMPlugオブジェクトを、
exportablePlugLabelNames にはアトリビュートの出力名を、
exportablePlugTypeNames にはアトリビュートの出力型('int'や'float')を、
exportablePlugGetValueCallbacks にはアトリビュートの値を出力用に取得変換する関数オブジェクトを
登録しています。

exportablePlugGetValueCallbacks は少しわかりにくいかもしれないので説明します。

今回はアトリビュートの型に応じて　int(32bit)　か float(32bit) でバイナリ出力しようとしています。
そのためにはアトリビュートの値をctypesの c_int などの型に変換する必要がありますが、
値を取得するたびに必要なキャストをするのは面倒です。
情報収集の時点で exportablePlugGetValueCallbacks に、アトリビュートの値を取得して適切な
ctypes型にキャストして返してくれる関数オブジェクトを登録しておけば、あとはその関数オブジェクトを
コールするだけで適切な型で値が取得できます。

関数オブジェクトとして登録しているのは getAttrValCallback_float と getAttrValCallback_int になります。
中身はシンプルですね。引数にOpenMaya2.0のMPlugをとって値を取り出し、キャストして返しているだけです。

実際に関数オブジェクトをコールして値を取得、リストにaddしているのは

```python
fileObj.animDataList[ti].tmpValueArray.append( exportablePlugGetValueCallbacks[ti]( exportablePlugs[ti] ) )
```
の部分です。

エラーチェックやそのそもpython的な書き方ができていない部分があると思いますが以上のような感じです。

#全コード
Mayaのスクリプトエディタ(python)に張り付けて実行できると思います。
一番下にテスト出力のコードがあるのでアトリビュート指定を変えて試してみてください。


```python
import sys
import io
import os.path
import re
import maya.cmds as cmds
import maya.api.OpenMaya as nom
from ctypes import *

##################################################################################
# ヘッダ
# アニメデータ配列の先頭へのオフセットを持つ
class FileHeader( Structure ):
    _fields_ = [
    # アニメーションの個数
    ('numAnim', c_uint32),
    # アニメーションデータ配列先頭へのオフセット
    ('animDataOffset', c_uint32),
    ]

        
# アトリビュート一つのアニメデータ　実際のキーフレームデータへのオフセットを持つ
class AnimData( Structure ):
    _fields_ = [
    # アニメーションデータの識別名
    ('name', c_char *64),
    # タイプ名 float とか
    ('typeName', c_char *32),
    # キーフレームデータの個数
    ('numFrame', c_uint32),
    # numFrame 個の値配列先頭へのオフセット
    ('valueArrayOffset', c_uint32),
    # numFrame 個のキーフレーム番号配列先頭へのオフセット
    ('keyNumberArrayOffset', c_uint32),
    ]
    
    def __init__(self):
        # 一時データ用  シーンから取得したアニメデータは一旦ここに
        self.tmpValueArray = []
        self.tmpKeyNumberArray = []
        
        # 書き出し用のキーフレームデータ, 一時データから実際に書き出すための変換をかけてこちらにコピーされる
        self.valueArray = None
        self.keyNumberArray = None
        
#   OpenMayaによるAPIを使ったノード操作関連
#   ノード名からOpenMayaのノードオブジェクト(MObject)を取得
def nameToNode( name ):
    selectionList = nom.MSelectionList()
    try:
        selectionList.add( name )
    except:
        return None
    return selectionList.getDependNode( 0 )
 
#   ノード名＋アトリビュート名からOpenMayaのノードアトリビュートオブジェクト(MPlug)を取得
def nameToNodePlug( attrName, nodeObject ):
    depNodeFn = None
    if isinstance( nodeObject, nom.MObject ):
        depNodeFn = nom.MFnDependencyNode( nodeObject )
    else:
        mobj = nameToNode(nodeObject)
        if not mobj:
            return None
        else:
            depNodeFn = nom.MFnDependencyNode( nameToNode(nodeObject) )
        
    if not depNodeFn.hasAttribute(attrName):
        return None
    attrObject = depNodeFn.attribute( attrName )
    plug = nom.MPlug( depNodeFn.object(), attrObject )
    return plug
    
# NodeName.AttrName からMPlug取得
def nodeAttrNameToNodePlug( nodeAttrName ):
    searchObj = re.search( '[.][^.]+$', nodeAttrName )
    if not searchObj:
        return None
    else:
        dotAttrName = searchObj.group(0)
        nodeName = nodeAttrName.replace( dotAttrName, '' )
        attrName = dotAttrName.replace( '.', '' )
        return nameToNodePlug( attrName, nodeName )


# 書き出し用
class FileObj:
    def __init__(self):
        self.fileHeader = FileHeader()
        self.animDataList = []
        
        #　オフセットは後で適切に書き換える
        self.fileHeader.animDataOffset = 0
        self.fileHeader.numAnim = 0
        
        
        # ファイルサイズ
        self.fileSize = 0
        pass
    
    @staticmethod
    def buildDataFromArray( src  ):
        elemByteSize = sizeof( src[0] )
        byteSize = len(src) * elemByteSize
        dst = ( c_char * byteSize )()
        # コピー
        for i in range(len(src)):
            memmove( byref(dst, i * elemByteSize), byref(src[i]), elemByteSize )
            pass
        return dst
        
    # 色々データを詰めた後にオフセットとかの計算をする
    def calc(self):
        offsetCnt = 0
        offsetCnt += sizeof( self.fileHeader )
        
        # ここからアニメデータ構造体配列が並ぶ
        self.fileHeader.animDataOffset = 0
        if 0 < self.fileHeader.numAnim:
            # アニメデータがある場合だけ
            # アニメデータ配列の先頭オフセット
            self.fileHeader.animDataOffset = offsetCnt
            offsetCnt += self.fileHeader.numAnim * sizeof(self.animDataList[0])
        else:
            # アニメデータが無い場合はオフセットゼロ
            pass
            
        # ここから一つ一つのアニメの配列データを敷き詰める
        for i in range(self.fileHeader.numAnim):
            ##############################
            # 値配列の先頭オフセット
            self.animDataList[i].valueArrayOffset = offsetCnt
            # 書き出し用データを構築
            self.animDataList[i].valueArray = FileObj.buildDataFromArray( self.animDataList[i].tmpValueArray )
            # 値配列分進める
            offsetCnt += sizeof( self.animDataList[i].valueArray )
            
            ##############################
            # キー番号配列の先頭オフセット
            self.animDataList[i].keyNumberArrayOffset = offsetCnt
            # 書き出し用データを構築
            self.animDataList[i].keyNumberArray = FileObj.buildDataFromArray( self.animDataList[i].tmpKeyNumberArray )
            # キー番号配列分進める
            offsetCnt += sizeof( self.animDataList[i].keyNumberArray )
        
        # ファイルサイズ確定
        self.fileSize = offsetCnt

    # ファイル書き出し
    def write(self, filePath):
        # オープン
        with open(filePath, "wb") as fout:
            # ヘッダ書き出し
            fout.write( self.fileHeader )
            
            # 先にアニメの固定部を連続書き出し
            for i in range( len( self.animDataList ) ):
                fout.write( self.animDataList[i] )
                
            # その後にアニメデータ部を連続書き出し
            for i in range( len( self.animDataList ) ):
                fout.write( self.animDataList[i].valueArray )
                fout.write( self.animDataList[i].keyNumberArray )
        pass
##################################################################################

# アトリビュートから値を取り出して適切なctypes型でリストに積むためのコールバック関数
# アトリビュートの値をfloat型で返す
def getAttrValCallback_float( plug ):
    return c_float( plug.asFloat() )

# アトリビュートの値をintとして返す
def getAttrValCallback_int( plug ):
    return c_int32( plug.asInt() )

"""
# Input Args
# 出力したいアトリビュートの情報
# [ ノード.アトリビュート名, 出力名 ] のリスト, 開始フレーム番号, 終了フレーム番号
# 出力名が指定されなかった場合は　ノード.アトリビュート名が使われる

# 出力情報登録
exportTargetInfo = []
exportTargetInfo.append(['pCube1.tx', 'CubeTransX'])
# 出力
export( 'C:/Users/Documents/testAnim.anm', exportTargetInfo, 0, 9 )
"""
def export( outputFilePath, targetAttrNameList, startFrame, endFrame ):
    #念のため
    if startFrame > endFrame:
        endFrame = startFrame
    

    # 入力情報から出力可能なPlugを選定
    exportablePlugs = []
    exportablePlugLabelNames = []
    exportablePlugTypeNames = []
    exportablePlugGetValueCallbacks = []
    
    for target in targetAttrNameList:
        
        exportName = target[0]
        targetPlug = nodeAttrNameToNodePlug( target[0] )
        if not targetPlug:
            continue
            
        # プラグへの入力を探す 何らかの入力があれば出力対象とする
        inConnectArray = targetPlug.connectedTo( True, False )
        if 0 >= len(inConnectArray):
            # このアトリビュートが子であり,親アトリビュートにコネクションがある場合もチェックする
            if not targetPlug.isChild:
                continue
            targetParentPlug = targetPlug.parent()
            inConnectArray = targetParentPlug.connectedTo( True, False )
            if 0 >= len( inConnectArray ):
                continue
        
        # 出力用の名前が指定されている場合
        if 2 <= len(target) and 0 < len(target[1]):
            exportName = target[1]

        # 出力タイプ名
        plugTypeName = ''
        getterMethod = None
        attrObj = targetPlug.attribute()
        apiType = attrObj.apiType()
        if nom.MFn.kNumericAttribute == apiType:
            # 数値アトリビュート
            numericAttr = nom.MFnNumericAttribute(attrObj)
            numericType = numericAttr.numericType()
            if nom.MFnNumericData.kBoolean == numericType or nom.MFnNumericData.kShort == numericType or nom.MFnNumericData.kLong == numericType:
                # 型名の登録とデータ取得コールバック登録
                plugTypeName = 'int'
                getterMethod = getAttrValCallback_int
                pass
            elif nom.MFnNumericData.kFloat == numericType or nom.MFnNumericData.kDouble == numericType:
                # 型名の登録とデータ取得コールバック登録
                plugTypeName = 'float'
                getterMethod = getAttrValCallback_float
                pass
            else:
                print '[Unsupported] ' + str(target[0])
                pass
            pass
        
        # タイプがわからなかったらとりあえずfloatとしてしまう
        if '' == plugTypeName:
            # それ以外は全部float扱いするか... transformノードのtranslateXとかはkNumericAttributeではなくkDoubleLinearAttributeだったりするので
            plugTypeName = 'float'
            getterMethod = getAttrValCallback_float

        # コールバックが無かったらだめ
        if not getterMethod:
            continue
        
        #exportablePlugTypeNames

        exportablePlugs.append( targetPlug )
        exportablePlugLabelNames.append( exportName )
        exportablePlugTypeNames.append( plugTypeName )
        exportablePlugGetValueCallbacks.append( getterMethod )

        
    #####################################################
    # 書き出し様オブジェクト
    fileObj = FileObj()
    #####################################################
    
    # アニメオブジェクト数
    fileObj.fileHeader.numAnim = len(exportablePlugs)
    
    # アニメデータを必要分用意
    for ti in xrange( len( exportablePlugs ) ):
        fileObj.animDataList.append(AnimData())
        fileObj.animDataList[ti].name = exportablePlugLabelNames[ti]
        # フレーム数は最大フレームの番号ではなく、キーフレームデータが何個あるかなのであとで計算する
        fileObj.animDataList[ti].numFrame = 0
        
        # タイプ
        fileObj.animDataList[ti].typeName = exportablePlugTypeNames[ti]
    
    # アニメ情報を取り出していく
    for i in xrange( startFrame, endFrame+1 ):

        # フレームを動かしつつデータを取得
        #noma.MAnimControl.setCurrentTime( nom.MTime(i) )
        # APIでのフレーム移動では更新されないアトリビュートがあるようなのでcmdsで代用
        cmds.currentTime( i )

        frameIndex = i - startFrame
        for ti in xrange( len( exportablePlugs ) ):
            # 値の取得
            animKeyNumber = frameIndex
            
            # コールバックでアトリビュートから値を取り出してc言語型にキャストして積む
            fileObj.animDataList[ti].tmpValueArray.append( exportablePlugGetValueCallbacks[ti]( exportablePlugs[ti] ) )

            # キー番号とオプション
            fileObj.animDataList[ti].tmpKeyNumberArray.append( c_uint32( animKeyNumber ))
                
    # データの取得が終わったので、同一値が連続するフレームを除去する等の処理はこのあたりでできるはず
    
    # デバッグ表示
    #for ti in xrange( len( exportablePlugs ) ):
    #    print '------------------------------------------------'
    #    print '[ ' + fileObj.animDataList[ti].name + ' ]'
    #    print fileObj.animDataList[ti].tmpValueArray
    #    print fileObj.animDataList[ti].tmpKeyNumberArray
    #    print '------------------------------------------------'
    #    pass
    
    
    # フレーム数を確定する
    for ti in xrange( len( exportablePlugs ) ):
        fileObj.animDataList[ti].numFrame = len(fileObj.animDataList[ti].tmpKeyNumberArray)
    
    # オフセットなどを計算
    fileObj.calc()
        
    fileObj.write( outputFilePath )
 
####################################################################


# 出力テスト
exportTargetInfo = []
# 登録
# pCube1のY移動アトリビュートに関して「pCube1_transY」という名前を付けて出力する
exportTargetInfo.append(['pCube1.ty','pCube1_transY'])
# 出力
export( 'C:/Users/Documents/testNodeAnim.anm', exportTargetInfo, 0, 9 )

```