Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

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

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

カスタムノード(ディペンデンシーノード、入出力のあるノード)については公式の以下のページなどが参考になります。
ディペンデンシー グラフ プラグインの基本
ディペンデンシー グラフ プラグイン

ディペンデンシーノードを定義するスクリプトに必要な要素

スクリプト内には最低でも

  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つずつ説明します。

# -*- 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

ノード用のクラスの定義

まずはクラスの定義から

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を決めておきます。詳しくはオートデスクのサイトにもありますが、通常は0x00000~0x7ffffまでの好きな値でいいと思います。
そして入出力のアトリビュートをクラスのフィールド intput output として用意しました。

インスタンス生成の関数

    # インスタンスを返すメソッド
    @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)

先ほどのリストの「4.ノードのアトリビュートを初期化する関数」にあたる部分です。クラスの外に書いてもいいですが、散らかるのでスタティックメソッドとして書きました。
アトリビュートはMFnAttributeクラスのサブクラスの中から適切なものを使い定義します。今回はFloat値なのでMFnNumericAttributeを使っています。3次元のfloat値(座標など)やbool値などもこのMFnNumericAttributeです。角度や距離・時間はMFnUnitAttribute、行列はMFnMatrixAttribute、その他は以下のリファレンスから探せるかと思います。
MFnAttribute Class Reference
OpenMaya.MFnAttribute Class Reference

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

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

計算本体のメソッド

    # アトリビュートの値が計算される際に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のヘルプで。
入力を得たり出力に割当てたりするのにDataBlockからハンドルを介する形で行うので長くなっていますが、実際にはSin関数を計算しているだけです。

エントリポイント

# 新しいノードの登録を行う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

配列のアトリビュート

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

配列プラグ

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

    @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の配列の合計値を出すという処理にしています。

    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でなく一度inputArrayValueMArrayDataHandleを取得します。
これはイテレータになっているので、next()jumpToLogicalElement()などでイテレータを進めてarrayDataHandle.inputValue()で配列の要素の値を取得します。あとは通常のプラグと同じように数値に変換して計算をします。

01.jpg
↑定数 1+2+3+4 = 10 と正しく計算されました。

複合アトリビュート

複数のアトリビュートをひとまとめにしたものが複合アトリビュートです。
複雑なダイナミック アトリビュート

今回の例では「座標とウェイト」を複合アトリビュートとし、それを配列プラグにして使っています。

ノードエディタ上では以下の画像のような感じになります。
02.png
クラス部分の実装は以下の通り。エントリポイントなどは前のコードのままです(省略)。

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から呼ばれる関数

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

# 複合アトリビュート
        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メソッド内で使用するためには

            # 複合アトリビュートのデータハンドル
            dataHandle = arrayDataHandle.inputValue()
            # .childで子アトリビュートを取得できる
            childHandle = dataHandle.child(
                sampleArrayNode.position
            )
            pos = childHandle.asFloat3()
            childHandle = dataHandle.child(
                sampleArrayNode.weight
            )
            w = childHandle.asFloat()

複合アトリビュートのデータハンドルから.childメソッドを使って子のアトリビュートのデータハンドルを取得してアクセスします。

実際の使用

作成したノードを実際に使用するとこんな感じ。
04.png
複数の物体の位置をノードに接続。出力をロケータにつないでみます。
03.jpg
球体・円錐・立方体の中心位置にロケータが移動しました。
今回は複合アトリビュートとして座標の他にウェイト値を付けてみたので使ってみましょう。
05.jpg
球体のウェイトを下げると…
06.jpg
球体の影響がなくなり、円錐と立方体の中心にロケータが移動しました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?