株式会社デジタル・フロンティア-Digital Frontier

Header

Main

  • TOP
  • DF TALK
  • はじめてPythonで書くMayaデフォーマ

はじめてPythonで書くMayaデフォーマ

2014/4/30

Tag: ,,

もう新年度始まって1ヶ月。いろいろと新しいことがはじまっている時期ですね。
開発部の齊藤です

過去に書いた、はじめてC++で書くMayaプラグインを覚えてる方もいらっしゃるでしょうか。
もう1年半前ですね。早いです。
今回は、C++ではなくPythonを用いてプラグインを書いていこうと思います。
必要なのはMayaとコードを書くためのテキストエディタ。身軽ですね。
何かの規則に基づいてオブジェクトを変形させるのがデフォーマ。
今回は、下記2つのデフォーマを実装を通して、デフォーマ入門してみましょう

  • noiseDeformer: 頂点のY座標に対してランダムな値を足しこむデフォーマ
  • spinDeformer: polyPlaneを円を描いて回転させるデフォーマ

 

最初にnoiseDeformerを通して、Pythonのプラグインの作り方、ノードの作り方、デフォーマの作り方を説明し
spinDeformerで2,3歩踏み込んだデフォーマ作成を説明したいと思います
一気に2つのデフォーマ。がんばっていきましょう。

Pythonでプラグイン

前回はC++でプラグインを実装しましたが、今回はPythonでプラグインを書くので
まずは、Pythonで書く場合のプラグイン書き方について説明します

小さなデフォーマプラグイン

Pythonを使うことで、Maya上のScriptEditorで検証用のコードを実行して確認も出来るし
バージョンごとにコンパイルが要らない、WindowsならVisualStudioを用意しなくていいなどのメリットがあります

どうやればMayaがPythonのコードをプラグインとして認識されるのか?
まずは、最小限のデフォーマノードのコードから書いていきましょう
下記のコードを”noiseDeformer.py”というファイルで保存して下さい

# -*- coding: utf-8 -*-
import traceback
from maya import OpenMaya
from maya import OpenMayaMPx
 
class NoiseDeformerNode(OpenMayaMPx.MPxDeformerNode):
    """ 頂点のY座標に対してランダムな値を足しこむデフォーマ
    """
    type_name = "noiseDeformer"
    node_id = OpenMaya.MTypeId(0x70100)
 
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)
 
    @staticmethod
    def creator():
        """ ノード作成
        """
        return OpenMayaMPx.asMPxPtr(NoiseDeformerNode())
 
    @staticmethod
    def initializer():
        """ Nodeの初期化処理を書く
        """
        pass
 
    def deform(self, data_block, geometry_iter, matrix, multi_index):
        """ 変形処理
        """
        pass
 
def initializePlugin(MObj_mobject):
    """ プラグインロード時に呼ばれる
    """
    MPlg_plugin = OpenMayaMPx.MFnPlugin(MObj_mobject)
    try:
        MPlg_plugin.registerNode(NoiseDeformerNode.type_name,
                                 NoiseDeformerNode.node_id,
                                 NoiseDeformerNode.creator,
                                 NoiseDeformerNode.initializer,
                                 OpenMayaMPx.MPxNode.kDeformerNode)
    except:
        msg = "Failed to register node\n{0}"
        sys.stderr.write(msg.format(traceback.format_exc()))
 
def uninitializePlugin(MObj_mobject):
    """ プラグインアンロード時に呼ばれる
    """
    MPlg_plugin = OpenMayaMPx.MFnPlugin(MObj_mobject)
    try:
        MPlg_plugin.deregisterNode(NoiseDeformerNode.node_id)
    except:
        msg = "Failed to unregister node\n{0}"
        sys.stderr.write(msg.format(traceback.format_exc()))

ファイルを保存した場所に、環境変数:MAYA_PLUG_IN_PATHを通してMAYAを起動しましょう
例えば、F:\dftalkにnoiseDeformer.pyに保存したら、コマンドプロンプトから下記のように起動します

set MAYA_PLUG_IN_PATH=F:\dftalk
"C:\Program Files\Autodesk\Maya2014\bin\maya.exe"

プラグインマネージャを開くと、下記のようにnoiseDeformerが認識されていると思います

ファイル内にinitializePlugin関数とuninitializePlugin関数が書いてあれば、Mayaはプラグインとして認識します
これで認識されるのだからC++よりも簡単です。さてここで、プラグインがロードされた時の呼び出しの流れを見てみましょう

プラグインをロードしたときにinitializePlugin関数が呼ばれ、アンロードした時はuninitializePlugin関数が呼ばれる流れは、Pythonで記述しても変わりません。
コマンドを登録した前回との違いとして、コマンド登録にはregisterCommand関数を用いましたが、今回ノードなのでregisterNode関数を用います
initializer関数は登録時に呼出され、これは何をするのかと言えば、アトリビュートの定義を行います。ここについては後述します

ノードを作る上で重要なのが、ノード名とノードIDの存在です。
過去の記事にも登場していますがノード名も重要ですが、ノードIDはかなり重要です
IDは0x00000000 ~ 0x0007ffffまでは自由にテストで使えます。バッティングしても、文句は言えません。
ただし、プラグインを配布する場合には使ってはいけません。配布する場合には、Autodeskに申請してユニークなIDを取得して配布することになります
本番で使用している状況でノードIDを途中で変更した場合、ID変更後は同じノードとして認識されなくなります。
ですから、ID変更後にシーンを開くとUnknown Nodeになります。こういう事態は避けたいです。

デフォーマノードのクラスは次のように書きます

class NoiseDeformerNode(OpenMayaMPx.MPxDeformerNode)

1ノード1クラスです。継承するクラスは、DeformerなのかLocatorなのか目的に応じて変更します
今回はデフォーマなのでMPxDeformerNodeを継承し、必要な関数の部分を書いていきます
どんなノードクラスがあるかはドキュメントにも記載があります。興味ある人はドキュメントを参照して下さい

http://docs.autodesk.com/MAYAUL/2014/JPN/Maya-API-Documentation/files/Dependency_graph_plugins_MPxNode_and_its_derived_classes.htm

基本的な部分がわかった所で、早速デフォーマを書いていきましょう

noiseDeformer

どんなものか

noiseDeformerは、頂点のY座標にランダムな値を足して設定するデフォーマです
scaleというアトリビュートを持ち、Y軸の揺れの大きさを変更できます

実装して動かしてみる

コードを解説するまえに、まずは動作させてみましょう
先ほどのnoiseDeformer.pyのNoiseDeformerNodeクラスを下記のコードで上書きします

import random
 
class NoiseDeformerNode(OpenMayaMPx.MPxDeformerNode):
    """ 頂点のY座標に対してランダムな値を足しこむデフォーマ
    """
    type_name = "noiseDeformer"
    node_id = OpenMaya.MTypeId(0x70100)
    # Node Attributes
    scale = OpenMaya.MObject()
 
    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)
 
    @staticmethod
    def creator():
        return OpenMayaMPx.asMPxPtr(NoiseDeformerNode())
 
    @staticmethod
    def initializer():
        """ Nodeの初期化処理を書く
        """
        # scaleアトリビュートを作成する
        n_attr = OpenMaya.MFnNumericAttribute()
        NoiseDeformerNode.scale = n_attr.create("scale", "sc",
                                                OpenMaya.MFnNumericData.kDouble,
                                                1.0)
        n_attr.setKeyable(True)
        try:
            # 各アトリビュートノードに追加し
            # アトリビュートの値が変更した際に影響するアトリビュートを設定する
            NoiseDeformerNode.addAttribute(NoiseDeformerNode.scale)
            output_geometry = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
            NoiseDeformerNode.attributeAffects(NoiseDeformerNode.scale,
                                               output_geometry)
        except:
            msg = "Failed to create attributes of {0} node\n{1}"
            sys.stderr.write(msg.format(NoiseDeformerNode.type_name,
                                        traceback.format_exc()))
 
    def deform(self, data_block, geometry_iter, matrix, multi_index):
        """ 変形処理
        """
        # scaleアトリビュートの値を取得
        DH_scale = data_block.inputValue(self.scale)
        scale = DH_scale.asDouble()
        # envelopeアトリビュートの値を取得
        DH_envelope = data_block.inputValue(self.envelope)
        envelope = DH_envelope.asFloat();
        # deform計算
        while not geometry_iter.isDone():
            # 新しい位置を求める
            P_point = geometry_iter.position()
            P_newpoint = OpenMaya.MPoint()
            P_newpoint.x = P_point.x
            P_newpoint.y = P_point.y + random.random() * scale
            P_newpoint.z = P_point.z
            # 影響度を入れて次の位置にする
            P_point += (P_point - P_newpoint) * envelope
            geometry_iter.setPosition(P_point)
            # 次の点
            geometry_iter.next()

次にMayaを立ち上げて下記のPythonコードをスクリプトエディタで実行します

from maya import cmds
deformer_name = "noiseDeformer"
plguin_settings = cmds.pluginInfo(deformer_name, q=True, settings=True)
if not plguin_settings[0]:
    cmds.loadPlugin(deformer_name + ".py")
cmds.polyPlane(w=50, h=50, sw=100, sh=100)
cmds.deformer(type=deformer_name)

こんな感じの結果が得られると思います

nosieDeformerノードを選択して、Scaleのアトリビュートを変更すれば
ガビガビと動くのが確認出来ると思います

ノードのアトリビュート定義方法

コードの解説に入ります。まずはノードのアトリビュートをどう定義しているのか見て行きましょう。
アトリビュート定義で重要なのが、NoiseDeformerNodeクラスのscale変数とinitializer関数の2つです

class NoiseDeformerNode(OpenMayaMPx.MPxDeformerNode):
    ## 省略
    scale = OpenMaya.MObject()
    ## 省略
 
    @staticmethod
    def initializer():
        """ Nodeの初期化処理を書く
        """
        # scaleアトリビュートを作成する
        n_attr = OpenMaya.MFnNumericAttribute()
        NoiseDeformerNode.scale = n_attr.create("scale", "sc",
                                                OpenMaya.MFnNumericData.kDouble,
                                                1.0)
        n_attr.setKeyable(True)
        try:
            # 各アトリビュートノードに追加し
            # アトリビュートの値が変更した際に影響するアトリビュートを設定する
            NoiseDeformerNode.addAttribute(NoiseDeformerNode.scale)
            output_geometry = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
            NoiseDeformerNode.attributeAffects(NoiseDeformerNode.scale,
                                               output_geometry)
        except:
            msg = "Failed to create attributes of {0} node\n{1}"
            sys.stderr.write(msg.format(NoiseDeformerNode.type_name,
                                        traceback.format_exc()))
     ## 省略

scaleはアトリビュートを保持するための変数です。なぜ関数と同じインデントで定義しているのかというと、
NoiseDeformerをインスタンス化しても共通で同じものを持つようにするためです。
このようなクラス共通で持っている変数をクラス変数と呼んだりします
共通では、複数ノードがあった場合に値が同じなってしまうのでは?という人もいるかもしれません。
重要なのはscaleが持つものは、値ではなくscaleアトリビュートの場所を指しているイメージです。
ここややこしいですが、後述のアトリビュートの値取得方法でまたお話します

scaleアトリビュートは、Double型でデフォルト値は1、ロングネームをscale、ショートネームをscとします
ではこれをどうプラグインで定義するのか。それがinitializer関数の中身になります

アトリビュートを定義する場合は、MFnAttributeというクラスを継承しているクラスを用います
用途に応じて選び、今回scaleは数値なのでMFnNumericAttributeを用いて作成します

n_attr = OpenMaya.MFnNumericAttribute()
NoiseDeformerNode.scale = n_attr.create("scale", "sc", OpenMaya.MFnNumericData.kDouble, 1.0)
n_attr.setKeyable(True)

このようにして、createに、ロングネーム、ショートネーム、型、デフォルト値を入れてあげます
その後、アトリビュートの挙動などを設定します。今回scaleはキーフレームを打てるようにしたいので
setKeyableをTrueにします。これでアトリビュートは定義されました。

ここで重要なことは、まだこの時点ではノードに追加されていないということです
ノードに追加する部分は下記の部分です

NoiseDeformerNode.addAttribute(NoiseDeformerNode.scale)
output_geometry = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
NoiseDeformerNode.attributeAffects(NoiseDeformerNode.scale, output_geometry)

まず1行目のaddAttributeで先ほど定義したアトリビュートを追加することで、
ノードにscaleというアトリビュートが追加されます
2行目に注目して下さい、ここは、MayaAPIをPythonで書く場合の特殊な所です
C++にはstaticという概念が存在します。これにアクセスする為に、MayaはOpenMaya.cvar.クラス名_スタティックメソッド(変数)とうルールがあります
今回、MPxDeformerNodeクラスが持つoutputGeomを用いたいので、こんな書き方をしています。
outputGeomとは何か?3行目のattributeAffectsが関係します

これは、アトリビュートの値が変わったとき、どのアトリビュートに影響します
outputGeomは出力先が入ったアトリビュートです。なので、scaleの値が変わったときに出力先が変わりますという定義が3行目です
この定義を行わないと、scaleの値を変更しても結果が変わらない現象が発生します

このようにして、アトリビュートを定義して、ノードに追加して、影響するアトリビュートを設定する
という流れをアトリビュート個数分だけ行います

変形の処理deform関数

次にデフォーマのメインとなるどう変形させるかという、deform関数です

def deform(self, data_block, geometry_iter, matrix, multi_index):

deform関数には4つの値が渡されます

  • data_block: 現在のDeformerNodeにアクセスするためのMDataBlockクラス。ノードのアトリビュートなどにアクセスするのに使用します
  • geometry_iter: ターゲットの点にアクセスする為のMItGeometryクラス。点はNurbsCurveなら各cvが、Meshなら各vertexが入っていて、各点の位置を取得するのに使用します
  • matrix: Object空間からWorld空間へ変換するためのMMatrixクラス
  • multi_index: 現在変形対象としているジオメトリが入力されているindex番号。ターゲットの情報を取得したい場合に用います

noiseDeformerでは、data_blockとgeometryを用います
まずはdata_blockですが、これを用いてノードのアトリビュートにアクセスして
必要な情報を取得します。今回の場合はscaleアトリビュートの値を取得します

DH_scale = data_block.inputValue(self.scale)
scale = DH_scale_handle.asDouble()

inputValue関数にscaleアトリビュートの場所を与えることで、現在の値を取得するためのハンドルが生成されます
これがscaleアトリビュート定義の時に触れた、scaleは値を持っていなく、アトリビュートの場所を示しているイメージという所です
さて、取得したハンドルを通して適切な型で値を取得します。今回は、浮動小数点で欲しいのでasDoubleを指定します
APIを用いるとこういう段階を踏んで欲しい情報を取得することはよく発生します

scaleの次に、envelopeという定義した覚えの無いアトリビュートを取得しているのに気になった人もいるかもしれません
これはDeformerが必ず持っているアトリビュートで、このデフォーマの影響値と考えて貰えれば良いです
影響値なので、envelopeが0の時変形させないように処理を書く必要があります。ちょっとしたことですが重要なことです
この話は後でもまた出てきます

アトリビュートを取得し、事前に用意したいデータが用意出来た所で実際のデフォーム計算に入ります

def deform(self, data_block, geometry_iter, matrix, multi_index):
    """ Deform
    """
    # scaleアトリビュートの値を取得
    DH_scale = data_block.inputValue(self.scale)
    scale = DH_scale_handle.asDouble()
    # envelopeアトリビュートの値を取得
    DH_envelope = data_block.inputValue(self.envelope)
    envelope = DH_envelope_handle.asFloat();
    # deform計算
    while not geometry_iter.isDone():
        # 新しい位置を求める
        P_point = geometry_iter.position()
        P_newpoint = OpenMaya.MPoint()
        P_newpoint.x = P_point.x
        P_newpoint.y = random.random() * scale
        P_newpoint.z = P_point.z
        # 影響度を入れて次の位置にする
        P_point += (P_point - P_newpoint) * envelope
        geometry_iter.setPosition(P_point)
        # 次の点
        geometry_iter.next()

計算の流れは、各点1つ1つにアクセスして、その頂点がどこに移動するのか計算して、結果を設定します

今回各点の情報取得するには、deform関数の引数geometry_iterを用います
これはイテレータで、isDone関数で最後まで到達したか判定し、next関数で次の点を参照するようにします
そして、poisition関数で現在の点を取得し、setPosition関数で現在の点の位置を設定します
これが基本的な流れになります

while not geometry_iter.isDone():
    # 新しい位置を求める
    P_point = geometry_iter.position()
    P_newpoint = OpenMaya.MPoint()
    P_newpoint.x = P_point.x
    P_newpoint.y = random.random() * scale
    P_newpoint.z = P_point.z
    # 影響度を入れて次の位置にする
    P_point += (P_point - P_newpoint) * envelope
    geometry_iter.setPosition(P_point)
    # 次の点
    geometry_iter.next()

position()で返ってくる値は、デフォルトだとオブジェクト空間座標のx,y,zの値が入ったMPointクラスが返ってきます
その点に対して、pythonのrandomモジュールを用いて0.0-1.0のランダムな値を発生させ、scaleで拡大してY座標に入れます
直接P_pointに入れない理由は、envelopeを考慮に入れる必要があるからです
envelopeが0の時は影響しないようにするため、現在の座標(P_point)から新しい座標(P_newpoint)のベクトルを生成し
envelopeを掛けて、新しい座標を確定します。こうすることで、envelopeが0の時は変形しないようになります
最後にsetPosition関数で座標を設定します。この時に座標空間を指定していないので、デフォルトのオブジェクト空間として設定します

envelopeをどこかのフレームで0にした場合、計算するときにpointは変形後の座標が来ているので変形してしまうのでは?
という方もいるかもしれません。ここでnoiseDeformerのノードグラフを見てましょう

デフォーマがいくら変形処理をさせても、大元は変化しない流れがわかると思います
なので、envelopeを0にして影響しないように処理を書くことで、変形していない元の形になります

これまでがnoiseDeformerの解説です
駆け足気味ですが、だいたいのDeformerの流れが理解出来ればと思います

このままだとまだ,点の座標以外の情報を用いた変形をする方法がわかりません
これがわかると作れるDeformerの範囲が広がります。
なので、Meshを例に2,3歩踏み込んだspinDeformerを次に制作してみます

一休憩一休憩….

spinDeformer

spinDeformerでは、考えをどういう風に検証して実装していくのかという点と
点の位置以外の情報を欲しい場合の処理について話します

どんなものか

spinDeformerは平面を対象に、円を描いて回転させます

まずは動かしてみる

まずはどんなものか見てみるために、spinDeformerを実装しましょう
コードを下記の場所からダウンロードして下さい

spinDeformer.py

ダウンロードした.pyを、MAYA_PLUG_IN_PATHが通った場所に配置して、Mayaを起動して下さい
Mayaのスクリプトエディタで下記のコードを実行します

from maya import cmds
deformer_name = "spinDeformer"
plguin_settings = cmds.pluginInfo(deformer_name, q=True, settings=True)
if not plguin_settings[0]:
    cmds.loadPlugin(deformer_file + ".py")
cmds.polyPlane(w=5, h=1, sw=20, sh=5)
cmds.deformer(type=deformer_name)
cmds.scale(15,15,5.0)
cmds.playbackOptions(min=0, max=360)

シーン上に曲がったプレーンが出来ます。
再生してみると、プレーン円を描いて回転します

どう実現するか考えてみる

まず準備として、どういうふうに変形させるのかを考えます
今回はpolyPlaneを対象として、指定したサイズの円を回転させることがゴールです

まず円を描くためには、円上の座標を求める必要があります。
何度か登場している三角関数sin, cosを用いて角度から円上の座標を求めます。検証してみましょう
0-360度まで1度ずつまわして、求めた座標をparticleでプロットします
Maya上で、スクリプトエディタを開き下記のPythonコードを実行しましょう

from maya import cmds
import math

paticles = [(math.sin(math.radians(i)),
math.cos(math.radians(i)), 0.0)
for i in range(0, 360)]
cmds.particle( p=paticles)

半径1の円が出来たと思います。今回は、ユーザが大きさを定義させるようにしたいので
半径を与えて円の大きさを変更するコードに変更してみましょう

from maya import cmds
import math
 
paticles = [(math.sin(math.radians(i)),
             math.cos(math.radians(i)), 0.0)
             for i in range(0, 360)]
cmds.particle( p=paticles)

これで、角度から目的の座標を取得と変数で大きさを変更する方法が出来ました
後は、始点と終点の角度を決めて、間に平面をはることでやりたいことが出来そうです
イメージはこんな感じ

では、平面の始点終点はどう決めましょうということになります
そこで注目するのがuvです。polyPlaneを作成した際のuv展開に注目します
polyPlaneで作成したデフォルトのUVでは、Z方向ではU値に変更がなく、X方向にU値が0~1に変化します

これを用いて、0の場合は始点の角度、1の場合は終点の角度として、計算してあげれば良さそうです

座標の求め方が出来たので、アトリビュートを考えます

  • 円の半径の数値を入力するためのアトリビュート: radius
  • 始点と終点の開き具合(角度)の数値を入力するアトリビュート: radius
  • 時間ごとに変化して欲しいので、時間を入力するアトリビュート: time

この3つを今回のノードのアトリビュートとして定義します
さて材料は揃いましたので、実装していきます

アトリビュートを定義する

さてここからはコードを見ながら解説していきます
まずはアトリビュートを追加する箇所

@staticmethod
def initializer():
    """ Nodeの初期化処理を書く
    アトリビュートの追加など記述
    """
    # radiusアトリビュートを作成
    n_attr = OpenMaya.MFnNumericAttribute()
    SpinDefoemerNode.scale = n_attr.create("radius", "ra",
                                           OpenMaya.MFnNumericData.kDouble,
                                           1.0)
    n_attr.setKeyable(True)
    # degreeアトリビュートを作成
    SpinDefoemerNode.degree = n_attr.create("degree", "de",
                                            OpenMaya.MFnNumericData.kDouble,
                                            90.0)
    n_attr.setKeyable(True)
    # timeアトリビュートを作成
    unit_attr = OpenMaya.MFnUnitAttribute()
    SpinDefoemerNode.time = unit_attr.create("time", "tm",
                                             OpenMaya.MFnUnitAttribute.kTime,
                                             0.0)
    try:
        # 各アトリビュートノードに追加し
        # アトリビュートの値が変更した際に影響するアトリビュートを設定する
        output_geometry = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
        SpinDefoemerNode.addAttribute(SpinDefoemerNode.scale)
        SpinDefoemerNode.attributeAffects(SpinDefoemerNode.scale,
                                          output_geometry)
        SpinDefoemerNode.addAttribute(SpinDefoemerNode.degree)
        SpinDefoemerNode.attributeAffects(SpinDefoemerNode.degree,
                                          output_geometry)
        SpinDefoemerNode.addAttribute(SpinDefoemerNode.time)
        SpinDefoemerNode.attributeAffects(SpinDefoemerNode.time,
                                          output_geometry)
    except:
        msg = "Failed to create attributes of {0} node\n{1}"
        sys.stderr.write(msg.format(SpinDefoemerNode.type_name,
                                    traceback.format_exc()))

radiusuとdegreeは、noiseDeformerのscaleと同じく数値なので新しいことは無いです
ただし、timeについて数値ではなくtimeノードからの入力にしたいので、
MFnUnitAttributeというものを用います。timeノードから受け取ると数値ではなくMTimeというクラスで扱うものになります
ここはdeformの時にまた出てきます。timeもoutputGeomに影響するようにすることで、フレームが変わった時にデフォームが走る仕組みにします

次に時間をどうやって取得するのかという話になります。timeノードを用いるというのは先ほど話しましたが
ではいつこのノードと繋がって欲しいかというと、ノード作成時です。
では、deform作成からコネクトまでのスクリプトを書くのか?というとそうではなく、そこもプラグインの中に書くことが可能です。
それがaccesoryNodeSetupという関数です
この関数はノード作成時に呼ばれ、必要なコネクト、必要なノード作成などしたい時に定義します
今回は、timeノードとコネクトしたいので下記のように書いています

def accessoryNodeSetup(self, cmd):
    """ deformノード作成時に呼ばれる関数
    """
    MIt_nodes = OpenMaya.MItDependencyNodes(OpenMaya.MFn.kTime)
    MObj_time = MIt_nodes.thisNode()
    FnD_timenode = OpenMaya.MFnDependencyNode(MObj_time)
    Plug_outtime = FnD_timenode.findPlug("outTime", False)
    result = cmd.connect(MObj_time,
                         Plug_outtime.attribute(),
                         self.thisMObject(),
                         SpinDefoemerNode.time)
    return result

timeノードを取得して、outTimeアトリビュートとデフォーマのtimeアトリビュートを接続しています
こうすることで、deformerノードを作成したときに自動的にtimeノードと必要なアトリビュートがコネクトされ
deformer作成からコネクトまでのスクリプトを書く必要がなくなります
ノード作成の流れを図にするとこんな感じになります

各点の情報を取得する準備をする

次にdeform関数です
今回は点の座標以外にもuv情報が必要なことから、各点の情報取得にnoiseDeformerで使用したdeformの引数である
geometroy_iterも用いた方法とは違うアプローチをしています
それがMItMeshVertexクラスを用いた方法です

# 現在のMeshを取得する
ADH_output = data_block.outputArrayValue(self.input)
ADH_output.jumpToElement(multiindex)
DH_outputgeom = ADH_output.outputValue()
MObj_inputgeom = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom
DH_inputmesh = DH_outputgeom.child(MObj_inputgeom)
MIt_inputVtx = OpenMaya.MItMeshVertex(DH_inputmesh.asMesh())

MItMeshVertexには、各点の座標以外の情報にアクセスするための関数が用意されています
そのMItMeshVertexを作成するには、変形対象のMesh情報が必要になります。
しかし、その変形対象のMeshを取得するまでにこんなにステップ踏むのかと思われるかもしれません。

1行目の入力だからoutputArrayValueでは?と思うかもしれません。重要なところで
今回取得したMItMeshVertexクラスに、計算後の点を入力します。inputArrayValueで取得すると
入力が変更されたことによって返ってきて循環することになります。これを回避するためにoutputArrayValueを用います
ArrayValueでは全て入っているので、今回の入っているターゲットを配列から取得します
では配列のどのインデックスに入っているのか?それが、deform関数の引数にあるmultiindexです
MArrayDataHandleクラスは[]でアクセスするのではなく、jumpToElementで目的のインデックスを指定し
outputValueでそのインデックスの値を取得します
最終的に取得するMItMeshVertexは何かというと、Meshのバーテックス情報が入ったイテレータになります
これを用いることで、各バーテックスの位置、UV座標を簡単に取得することが可能です

アトリビュート取得

アトリビュートの取得について、timeだけ説明します

# timerアトリビュートの値を取得
DH_time = data_block.inputValue(self.time)
MTime_time = DH_time.asTime()
current_time = MTime_time.value()

timeは前述したとおり、取得したときに数値ではありません。
MTimeというクラスになっています。このクラスは時間をどのような値にするのか決めて取得出来ます
今回はただ単にフレームを取得したいので、value関数を用いて値を取得しています

変形処理

さて、いよいよ変形になります
MItMeshVertexから1点1点取得して、計算して、新しい座標を入れるながらは変わらず
noiseDeformerの時にMItGeometoryと同じイテレータなので、操作方法も変わらず
isDoneで終端まで行ったか確認して、positionで現在の点を取得、nextで次の点へ移動します

今回uvの値を取得する必要があります。取得部分が下記のところです

# UV取得
uv_array = [0, 0]
script_util = OpenMaya.MScriptUtil()
script_util.createFromList(uv_array, 2)
Ptr_uvarray = script_util.asFloat2Ptr()
MIt_inputVtx.getUV(Ptr_uvarray)
uv = [OpenMaya.MScriptUtil.getFloat2ArrayItem(Ptr_uvarray, 0, 0),
OpenMaya.MScriptUtil.getFloat2ArrayItem(Ptr_uvarray, 0, 1)]

UVを取得するための関数getUVは、MayaはFloat2ポインタを要求します。
今Pythonで書いてます。そんなものありません。リストを渡そうものならFloat2ポインタじゃないとエラーを出します。
どうしたものかというですが、ここで登場するのがMScriptUtilです。
これを用いることでポインタを作成、ポインタから値を取得出来ます。
リストからポインタを作成、MItMeshVertexから現在の点のuv値を取得、ポインタから値を取得しuvリストを作成しています
ややこしい。

無事にuv値を取れたら、次に新しい座標の計算です

# 新しい座標を求める
P_newpoint = OpenMaya.MPoint()
start_radian = math.radians(current_time)
end_x_radian = math.radians(degree * uv[0])
end_y_radian = math.radians(degree * uv[0])
P_newpoint.x = radius * math.sin(start_radian + end_x_radian)
P_newpoint.y = radius * math.cos(start_radian + end_y_radian)
P_newpoint.z = P_point.z

開始の角度は、フレームをそのまま角度として扱うようにします
後は現在の点は開始位置から終わりまでのどの位置にいるかということですが
先ほど取得したu値を、最終角度に掛けてあげることで、0なら始点、1なら終点とするようにしています
開始角度と求めた終点の角度を足すことで、回転するようにしています。
あとはsin,cosで座標を求めて、z座標はそのままにします

最後に、求めた新しい座標を影響値を考慮して、設定します

# 影響値を考慮して値をObject空間として入力する
P_point += (P_newpoint - P_point) * envelope
MIt_inputVtx.setPosition(P_point)

以上で説明は終わりです

最後

駆け足でいきましたが、いかがでしたでしょうか。Deformerそのものの流れはイメージ出来ましたでしょうか。
最近ではAPIの説明が全てではないですが日本語になっていたりします

http://docs.autodesk.com/MAYAUL/2014/JPN/Maya-API-Documentation

Pythonでのプラグインのサンプルは

  • Mayaインストールディレクトリ/devkit/plug-ins/scripted

にあります。全部は確かめてないですが、Python版とC++版があるプラグインもあります
2つ見てみるのも良いかも知れません。

この記事が、デフォーマを書く1歩のきっかけになればと思います
ではまた次回

※免責事項※
本記事内で公開している全ての手法の有用性、安全性について、当方は一切の保証を与えるものではありません。
これらの手法を使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご活用ください

コメント

コメントはありません

コメントフォーム

コメントは承認制ですので、即時に反映されません。ご了承ください。

CAPTCHA


 

*