【Maya】Open Maya2.0でカスタムノードを書く

Mayaでカスタムノードを実装するワークフローを忘れがちなので、備忘録的に書いておきます。

今回はシンプルな入出力のあるノード、配列を扱えるノード、複合アトリビュートを扱えるノードを提示して、最後にそれを使って複数点の中心位置を出すようなノードを作ってみます。

カスタムノード（ディペンデンシーノード、入出力のあるノード）については公式の以下のページなどが参考になります。
[ディペンデンシー グラフ プラグインの基本](https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_GUID_A9070270_9B5D_4511_8012_BC948149884D_htm)
[ディペンデンシー グラフ プラグイン](https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_GUID_4F1D2DC9_D544_432B_A103_7A32D47757C4_htm)

# ディペンデンシーノードを定義するスクリプトに必要な要素
スクリプト内には最低でも

1. Maya Python API 2.0を使用することを知らせる空の関数  
`maya_useNewAPI()`という関数を定義しておくと、Maya側にAPI2.0を使うことを明示できます。
2. プラグインのエントリポイント／エグジットポイント
プライグインの読み込み・終了時に呼び出される`initializePlugin( mobject )`と`uninitializePlugin( mobject )`が必要です。
ここで**ノードの名前**、**ノードID**、**以下の4･5の関数**、**ノードタイプ**、**ノードの分類**を定義します（後述）

3. ノードのインスタンスを返す作成関数
4. ノードのアトリビュートを初期化する関数
5. ノード本体のクラス

が必要になってきます。

## 単純な入出力のあるノード
実際に書いてみたサンプルノードが以下になります。inputアトリビュートに入れたFloat値をsin関数にかけて10倍したものをoutputアトリビュートから出すというノードです。1つずつ説明します。

``` python
# -*- coding: utf-8 -*-
import maya.api.OpenMaya as om
import maya.api.OpenMayaUI as omui
import math, sys

# Maya API 2.0を使用するために必要な関数
def maya_useNewAPI():
    pass

# 実際のクラス
class sampleNode(om.MPxNode):
    id = om.MTypeId(0x7f001) # 一意なID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    input = om.MObject()
    output = om.MObject()

    # インスタンスを返すメソッド
    @staticmethod
    def creator():
        return sampleNode()

    # 初期化時にMayaから呼ばれるメソッド
    # アトリビュートの設定を行う
    @staticmethod
    def initialize():
        # アトリビュートはMFnAttributeクラスのサブクラスのcreateメソッドを使い定義する
        nAttr = om.MFnNumericAttribute()
        sampleNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        nAttr = om.MFnNumericAttribute()
        sampleNode.output = nAttr.create('output', 'o', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        # 定義した後はMPxNodeのaddAttributeを実行する
        sampleNode.addAttribute(sampleNode.input)
        sampleNode.addAttribute(sampleNode.output)
        # また、inputが変更された際にoutputを再計算するように設定する
        sampleNode.attributeAffects( sampleNode.input, sampleNode.output)
        
    # コンストラクタは親のコンストラクタを呼ぶ
    def __init__(self):
        om.MPxNode.__init__(self)

    # アトリビュートの値が計算される際にMayaから呼び出されるメソッド
    def compute(self, plug, dataBlock):
        if(plug == sampleNode.output):
            dataHandle = dataBlock.inputValue(sampleNode.input)
            inputFloat = dataHandle.asFloat()
            result = math.sin(inputFloat) * 10.0
            outputHandle = dataBlock.outputValue(sampleNode.output)
            outputHandle.setFloat(result)
            dataBlock.setClean(plug)
            
    # http://help.autodesk.com/view/MAYAUL/2016/ENU/
    # api1.0では明示的にplugの処理を行わないことを伝えない限りMStatus.kUnknownParameterは返さないとされる
    # api2.0ではそもそもMStatusがないので無視して良さそう

# 新しいノードの登録を行うMayaから呼ばれる関数
def initializePlugin(obj):
    mplugin = om.MFnPlugin(obj)

    try:
        mplugin.registerNode('sampleNode', sampleNode.id, sampleNode.creator,
                             sampleNode.initialize, om.MPxNode.kDependNode)
    except:
        sys.stderr.write('Faled to register node: %s' % 'sampleNode')
        raise

# プラグインを終了する際にMayaから呼ばれる関数
def uninitializePlugin(mobject):
    mplugin = om.MFnPlugin(mobject)
    try:
        mplugin.deregisterNode(sampleNode.id)
    except:
        sys.stderr.write('Faled to uninitialize node: %s' % 'sampleNode')
        raise

```

### ノード用のクラスの定義

まずはクラスの定義から

``` python 
class sampleNode(om.MPxNode):
    id = om.MTypeId(0x7f001) # 一意なID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    input = om.MObject()
    output = om.MObject()
```

プラグインのidを決めておきます。詳しくは[オートデスクのサイト](https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_GUID_4F1D2DC9_D544_432B_A103_7A32D47757C4_htm)にもありますが、通常は0x00000～0x7ffffまでの好きな値でいいと思います。
そして入出力のアトリビュートをクラスのフィールド `intput` `output` として用意しました。

### インスタンス生成の関数

``` python 
    # インスタンスを返すメソッド
    @staticmethod
    def creator():
        return sampleNode()
```

ノードのインスタンスを生成する関数です。クラスの外に書いてもいいですが、今回はクラスのスタティックメソッドとして書いておきました。

### アトリビュートを初期化する関数

``` python
# 初期化時にMayaから呼ばれるメソッド
    # アトリビュートの設定を行う
    @staticmethod
    def initialize():
        # アトリビュートはMFnAttributeクラスのサブクラスのcreateメソッドを使い定義する
        nAttr = om.MFnNumericAttribute()
        sampleNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        nAttr = om.MFnNumericAttribute()
        sampleNode.output = nAttr.create('output', 'o', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True

        # 定義した後はMPxNodeのaddAttributeを実行する
        sampleNode.addAttribute(sampleNode.input)
        sampleNode.addAttribute(sampleNode.output)
        # また、inputが変更された際にoutputを再計算するように設定する
        sampleNode.attributeAffects( sampleNode.input, sampleNode.output)
```
先ほどのリストの「4.ノードのアトリビュートを初期化する関数」にあたる部分です。クラスの外に書いてもいいですが、散らかるのでスタティックメソッドとして書きました。
アトリビュートは`MFnAttribute`クラスのサブクラスの中から適切なものを使い定義します。今回はFloat値なので`MFnNumericAttribute`を使っています。3次元のfloat値（座標など）やbool値などもこの`MFnNumericAttribute`です。角度や距離・時間は`MFnUnitAttribute`、行列は`MFnMatrixAttribute`、その他は以下のリファレンスから探せるかと思います。
[MFnAttribute Class Reference](https://download.autodesk.com/us/maya/2011help/API/class_m_fn_attribute.html)
[OpenMaya.MFnAttribute Class Reference](https://help.autodesk.com/view/MAYAUL/2016/ENU/?guid=__py_ref_class_open_maya_1_1_m_fn_attribute_html)

`nAttr.create`でアトリビュートの名前、省略名、型、初期値を指定します。
`nAttr.storable`は保存時ファイルにアトリビュートの値を書き込むかどうか。そのほかにも`writable`や`readable`などのプロパティがあるので適宜設定してください。

`sampleNode.addAttribute(sampleNode.input)` 作成したアトリビュートをノードに追加します。
`sampleNode.attributeAffects( sampleNode.input, sampleNode.output )` inputアトリビュートの値が変わったとき、outputアトリビュートを更新するようにします。

### 計算本体のメソッド

``` python
    # アトリビュートの値が計算される際にMayaから呼び出されるメソッド
    def compute(self, plug, dataBlock):
        if(plug == sampleNode.output):
            dataHandle = dataBlock.inputValue(sampleNode.input)
            inputFloat = dataHandle.asFloat()
            result = math.sin(inputFloat) * 10.0
            outputHandle = dataBlock.outputValue(sampleNode.output)
            outputHandle.setFloat(result)
            dataBlock.setClean(plug)
            
```

computeメソッドは計算が行われるときに呼び出されるメソッドです。このノードではSinの計算をしています。
プラグという形で値が渡されます。プラグについて詳しくは[Autodeskのヘルプ](https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_Dependency_graph_plugins_Attributes_and_plugs_htm)で。
入力を得たり出力に割当てたりするのにDataBlockからハンドルを介する形で行うので長くなっていますが、実際にはSin関数を計算しているだけです。

### エントリポイント

``` python
# 新しいノードの登録を行うMayaから呼ばれる関数
def initializePlugin(obj):
    mplugin = om.MFnPlugin(obj)

    try:
        mplugin.registerNode('sampleNode', sampleNode.id, sampleNode.creator,
                             sampleNode.initialize, om.MPxNode.kDependNode)
    except:
        sys.stderr.write('Faled to register node: %s' % 'sampleNode')
        raise

```
クラスの外にあるエントリポイントです。
ノード名とID、クラス内に定義してきたインスタンスメソッド、ノードの種類を指定します。

# 実行
作成したスクリプトをMayaのプラグインマネージャからロードします。
Mayaのコマンドラインかスクリプトエディタで`cmds.createNode( 'sampleNode' )`または `cmds.shadingNode( 'sampleNode', asUtility=True )`でノードを作成します。後者で作成した場合、ハイパーシェードウィンドウの中の「ユーティリティ」というタブに自作したノードが表示されます。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/2377d672-2ce5-b2a6-dcc2-41e309911d07.png)

# 配列のアトリビュート
配列データを一つのアトリビュートとしてプラグに接続する方法と、プラグ自体が配列になっている**配列プラグ**を用いる方法があります。

## 配列プラグ
先ほどの`initialize`メソッド内でのアトリビュートの定義を以下の様に変更します。`nAttr.array = True`は見たまんまですが、`nAttr.indexMatters = False`はFalseにすることで`connectAttr`コマンドで`-nextAvailable`が使えるようになります。逆にTrueの場合は挿入するインデックスを必ず指定しなければならないらしい。


``` python
    @staticmethod
    def initialize():
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i', om.MFnNumericData.kFloat, 0.0)
        nAttr.storable = True
        nAttr.writable = True
        nAttr.readable = True
        nAttr.array = True  # 追加
        nAttr.indexMatters = False  # 追加
```

次に実際の計算を担う`compute`メソッド。今回はinputの配列の合計値を出すという処理にしています。

``` python
    def compute(self, plug, dataBlock):
        arrayDataHandle = dataBlock.inputArrayValue(
            sampleArrayNode.input
        )
        sum = 0
        while not arrayDataHandle.isDone():
            handle = arrayDataHandle.inputValue()
            v = handle.asFloat()
            sum += v
            arrayDataHandle.next()

        outhandle = dataBlock.outputValue( sampleArrayNode.output )
        outhandle.setFloat(sum)
        dataBlock.setClean(plug)
```

配列プラグを使うときは、`inputValue`でなく一度`inputArrayValue`で`MArrayDataHandle`を取得します。
これはイテレータになっているので、`next()`や`jumpToLogicalElement()`などでイテレータを進めて`arrayDataHandle.inputValue()`で配列の要素の値を取得します。あとは通常のプラグと同じように数値に変換して計算をします。


![01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/add46fe5-3cb0-20b7-6216-31322d65e329.jpeg)
↑定数 1+2+3+4 = 10 と正しく計算されました。

# 複合アトリビュート
複数のアトリビュートをひとまとめにしたものが複合アトリビュートです。
[複雑なダイナミック アトリビュート
](https://help.autodesk.com/view/MAYAUL/2016/JPN/?guid=__files_Dependency_graph_plugins_Complex_Attributes_htm)
今回の例では「座標とウェイト」を複合アトリビュートとし、それを配列プラグにして使っています。

ノードエディタ上では以下の画像のような感じになります。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/44b95395-e448-3a7d-5b05-d3e684a0c8f4.png)
クラス部分の実装は以下の通り。エントリポイントなどは前のコードのままです（省略）。

``` python
class sampleArrayNode(om.MPxNode):
    # 一意なID https://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html
    id = om.MTypeId(0x7f011)
    input = om.MObject()
    output = om.MObject()
    # 子アトリビュート
    position = om.MObject()
    weight = om.MObject()

    # インスタンスを返すメソッド
    @staticmethod
    def creator():
        return sampleArrayNode()

    # 初期化時にMayaから呼ばれるメソッド
    # アトリビュートの設定を行う
    @staticmethod
    def initialize():
        # 子のアトリビュート
        # 座標
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.position = nAttr.create(
            'position', 'pos', om.MFnNumericData.k3Float, 0
        )
        nAttr.readable = True
        # ウェイト
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.weight = nAttr.create(
            'weight', 'w', om.MFnNumericData.kFloat, 1
        )
        nAttr.readable = True
        nAttr.setMax(1)  # Min, Maxも指定可能
        nAttr.setMin(0)

        # 複合アトリビュート
        nAttr = om.MFnCompoundAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i')
        nAttr.readable = True
        nAttr.array = True
        nAttr.indexMatters = False
        nAttr.addChild(sampleArrayNode.position)
        nAttr.addChild(sampleArrayNode.weight)

        # 出力は今回は座標（3次元Float）
        nAttr = om.MFnNumericAttribute()
        sampleArrayNode.output = nAttr.create(
            'output', 'o', om.MFnNumericData.k3Float)
        nAttr.storable = True
        nAttr.writable = True
        nAttr.readable = True

        # 定義した後はMPxNodeのaddAttributeを実行する
        sampleArrayNode.addAttribute(sampleArrayNode.input)
        sampleArrayNode.addAttribute(sampleArrayNode.output)
        # また、inputが変更された際にoutputを再計算するように設定する
        sampleArrayNode.attributeAffects(
            sampleArrayNode.input, sampleArrayNode.output)

    # コンストラクタは親のコンストラクタを呼ぶ
    def __init__(self):
        om.MPxNode.__init__(self)

    # アトリビュートの値が計算される際にMayaから呼び出されるメソッド
    def compute(self, plug, dataBlock):
        arrayDataHandle = dataBlock.inputArrayValue(
            sampleArrayNode.input
        )
        sumX = 0
        sumY = 0
        sumZ = 0
        num = len(arrayDataHandle)
        while not arrayDataHandle.isDone():
            # 複合アトリビュートのデータハンドル
            dataHandle = arrayDataHandle.inputValue()
            # .childで子アトリビュートを取得できる
            childHandle = dataHandle.child(
                sampleArrayNode.position
            )
            pos = childHandle.asFloat3()
            childHandle = dataHandle.child(
                sampleArrayNode.weight
            )
            w = childHandle.asFloat()
            sumX += pos[0] * w
            sumY += pos[1] * w
            sumZ += pos[2] * w
            arrayDataHandle.next()

        outhandle = dataBlock.outputValue(sampleArrayNode.output)
        if(num != 0):
            outhandle.set3Float(sumX / num, sumY / num, sumZ / num)
        else:
            outhandle.set3Float(0, 0, 0)
        dataBlock.setClean(plug)

    # http://help.autodesk.com/view/MAYAUL/2016/ENU/
    # api1.0では明示的にplugの処理を行わないことを伝えない限りMStatus.kUnknownParameterは返さないとされる
    # api2.0ではそもそもMStatusがないので無視して良さそう

# 新しいノードの登録を行うMayaから呼ばれる関数

```

変わっているのはまずクラスのフィールドとして`position`、`weight`という変数を用意しています。これらは複合アトリビュートの子アトリビュートとして利用するため、`initialize`メソッド内で通常のアトリビュート同様定義します。これらをひとまとめにした複合アトリビュートが`input`です。

``` python 
# 複合アトリビュート
        nAttr = om.MFnCompoundAttribute()
        sampleArrayNode.input = nAttr.create(
            'input', 'i')
        nAttr.readable = True
        nAttr.array = True
        nAttr.indexMatters = False
        nAttr.addChild(sampleArrayNode.position) # ←ここがポイント
        nAttr.addChild(sampleArrayNode.weight)
```

通常のアトリビュートと異なるところはアトリビュートのクラスが`MFnCompoundAttribute`になっていること、`.addChild`で上に定義した子アトリビュートを追加している点です。

複合アトリビュートをcomputeメソッド内で使用するためには

``` python
            # 複合アトリビュートのデータハンドル
            dataHandle = arrayDataHandle.inputValue()
            # .childで子アトリビュートを取得できる
            childHandle = dataHandle.child(
                sampleArrayNode.position
            )
            pos = childHandle.asFloat3()
            childHandle = dataHandle.child(
                sampleArrayNode.weight
            )
            w = childHandle.asFloat()
```
複合アトリビュートのデータハンドルから`.child`メソッドを使って子のアトリビュートのデータハンドルを取得してアクセスします。

## 実際の使用
作成したノードを実際に使用するとこんな感じ。
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/af648fd7-cc3a-ff97-7f5a-7eed6337b0b5.png)
複数の物体の位置をノードに接続。出力をロケータにつないでみます。
![03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/c234ca03-053e-27d7-0ad1-a0397c9d61d7.jpeg)
球体・円錐・立方体の中心位置にロケータが移動しました。
今回は複合アトリビュートとして座標の他にウェイト値を付けてみたので使ってみましょう。
![05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/be039e9e-9167-0d41-00c8-6cbc8e503589.jpeg)
球体のウェイトを下げると…
![06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47220/890534f3-bae2-771a-4e9f-0d9825190a6d.jpeg)
球体の影響がなくなり、円錐と立方体の中心にロケータが移動しました。