Mayaでカスタムノードを実装するワークフローを忘れがちなので、備忘録的に書いておきます。
今回はシンプルな入出力のあるノード、配列を扱えるノード、複合アトリビュートを扱えるノードを提示して、最後にそれを使って複数点の中心位置を出すようなノードを作ってみます。
カスタムノード(ディペンデンシーノード、入出力のあるノード)については公式の以下のページなどが参考になります。
ディペンデンシー グラフ プラグインの基本
ディペンデンシー グラフ プラグイン
ディペンデンシーノードを定義するスクリプトに必要な要素
スクリプト内には最低でも
- Maya Python API 2.0を使用することを知らせる空の関数
maya_useNewAPI()
という関数を定義しておくと、Maya側にAPI2.0を使うことを明示できます。 プラグインのエントリポイント/エグジットポイント
プライグインの読み込み・終了時に呼び出されるinitializePlugin( mobject )
とuninitializePlugin( mobject )
が必要です。
ここでノードの名前、ノードID、以下の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
は保存時ファイルにアトリビュートの値を書き込むかどうか。そのほかにもwritable
やreadable
などのプロパティがあるので適宜設定してください。
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 )
でノードを作成します。後者で作成した場合、ハイパーシェードウィンドウの中の「ユーティリティ」というタブに自作したノードが表示されます。
配列のアトリビュート
配列データを一つのアトリビュートとしてプラグに接続する方法と、プラグ自体が配列になっている配列プラグを用いる方法があります。
配列プラグ
先ほどの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
でなく一度inputArrayValue
でMArrayDataHandle
を取得します。
これはイテレータになっているので、next()
やjumpToLogicalElement()
などでイテレータを進めてarrayDataHandle.inputValue()
で配列の要素の値を取得します。あとは通常のプラグと同じように数値に変換して計算をします。
複合アトリビュート
複数のアトリビュートをひとまとめにしたものが複合アトリビュートです。
複雑なダイナミック アトリビュート
今回の例では「座標とウェイト」を複合アトリビュートとし、それを配列プラグにして使っています。
ノードエディタ上では以下の画像のような感じになります。
クラス部分の実装は以下の通り。エントリポイントなどは前のコードのままです(省略)。
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
です。
# 複合アトリビュート
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
メソッドを使って子のアトリビュートのデータハンドルを取得してアクセスします。
実際の使用
作成したノードを実際に使用するとこんな感じ。
複数の物体の位置をノードに接続。出力をロケータにつないでみます。
球体・円錐・立方体の中心位置にロケータが移動しました。
今回は複合アトリビュートとして座標の他にウェイト値を付けてみたので使ってみましょう。
球体のウェイトを下げると…
球体の影響がなくなり、円錐と立方体の中心にロケータが移動しました。