こんにちはヨセミテです。
スープカレーは最近食べれていません。食べたい。
先日行われたCEDEC2017ではラウンドテーブルの司会という非常に貴重な経験をさせていただきました。改めてこの場で、来て下さった方々にお礼を言いたいと思います。ありがとうございました。
今回はMaya Python API2.0 でコマンドプラグインを作成していきたいと思います。
コマンドプラグインを作成すると新しいMELコマンド(Pythonだとcmdsライブラリ)の新しい命令を定義できます。
■ [ctrl + z]が効く高速なスクリプトが組めること
■ MELで自分からスクリプトを組むようなデザイナーさんが居る環境で使えるコマンドを増やせること
■ skinPercentの様な、そのまま使うと少々重過ぎるコマンドを場合に合わせたカスタマイズして高速なものを用意できること
などが主な利点だと思います。
「元に戻す機能」、「引数」を利用できるものを作ってみたかったので以下の二つのサンプルをベースに作成しました。
作成してみたものが以下になります。頂点、もしくはメッシュ全体を選択して法線方向に引数の分だけ移動する、という処理です。
コメントが探り探り感あふれるのはご容赦下さい。探り探りです。
# -*- coding: utf-8 -*- import sys import maya.api.OpenMaya as om import maya.api.OpenMayaUI as omui kPluginCmdName = "blogCommand" # MELコマンド名 kShortFlagName = "-mv" # 引数のショートネーム kLongFlagName = "-moveValue" # 引数のロングネーム def maya_useNewAPI(): # この関数が記述されているスクリプトはプラグインが生成されます、とMayaに伝えるための関数らしい pass ########################################################## # Plug-in :メインの記述部分 ########################################################## class BlogCommandClass( om.MPxCommand ): def __init__(self): ''' Constructor. ''' om.MPxCommand.__init__(self) def doIt(self, args): # doIt内部は一度だけ呼ばれるコマンドらしい # 公式のサンプルでredoItに実行処理書いてこっちでもredoitよびだしてたので習う # 引数を解析しているところ moveValue = self.parseArguments( args ) # 対象のメッシュの諸々を取得 moveTargetMesh_selList = om.MGlobal.getActiveSelectionList() moveTargetMesh_dag = moveTargetMesh_selList.getDagPath(0) self.moveTargetMesh_MFnMeshIns = om.MFnMesh(moveTargetMesh_dag) # 選択対象のMFnMeshインスタンス moveTargetMesh_componentObject = moveTargetMesh_selList.getComponent(0)[1] # 選択頂点のコンポーネント if moveTargetMesh_componentObject.apiType() == 548: # 選択対象が頂点なら頂点のindexList取得 moveTargetMesh_ComponentIns = om.MFnSingleIndexedComponent(moveTargetMesh_componentObject) moveTargetMesh_selectedIndecies = moveTargetMesh_ComponentIns.getElements() # 選択頂点のindex else: # 選択対象が頂点なら頂点じゃないならindexListは頂点数分 moveTargetMesh_selectedIndecies = xrange(self.moveTargetMesh_MFnMeshIns.numVertices) # undo処理のために元々の頂点座標を保存 moveTargetPointArray_Origin = self.moveTargetMesh_MFnMeshIns.getPoints(space = om.MSpace.kWorld) self.movePointArray_OriginClone = moveTargetPointArray_Origin[:] # 実行処理を繰り返し処理部分に書いているため、実行処理の呼びだし self.redoIt(moveTargetMesh_selList, moveTargetMesh_dag, moveValue,moveTargetMesh_selectedIndecies) def parseArguments(self, args): # 引数を読み込むための部分 # 必須ではないが、このように分けると判り易いですよ、との事 # 定義した引数が用いられているかどうかの確認 # 定義した引数が異なるとココでエラーが発生する模様 argData = om.MArgParser(self.syntax(), args) if argData.isFlagSet( kShortFlagName ): # flagValueにintにして受け渡ししてるそうで flagValue = argData.flagArgumentFloat( kShortFlagName, 0) return flagValue def redoIt(self, moveTargetMesh_selList, moveTargetMesh_dag, moveValue, moveTargetMesh_selectedIndecies): # 繰り返しをした際に呼び出される処理? # 公式のサンプルではココに実効的な処理を記述していたのでそれに習い、 # ココに実効的な処理を書き、doit内部からredoitを呼び出す形式にする vertxNum = self.moveTargetMesh_MFnMeshIns.numVertices points = self.moveTargetMesh_MFnMeshIns.getPoints(space = om.MSpace.kWorld) normals = self.moveTargetMesh_MFnMeshIns.getVertexNormals(False, om.MSpace.kWorld) movedPosArray = om.MPointArray() # 移動後の座標を格納する配列 movedPosArray.setLength(vertxNum) # 初期化 for currentIndex in range(vertxNum): if currentIndex in moveTargetMesh_selectedIndecies: currentPos = points[currentIndex] # 対象頂点の位置 currentNormal = om.MVector(normals[currentIndex]) # 対象頂点の法線方向 movedPos = currentPos + currentNormal * moveValue # 移動後の座標 movedPosArray[currentIndex] = movedPos else: movedPosArray[currentIndex] = points[currentIndex] # 選択対象以外のindexはそのままの座標を格納 self.moveTargetMesh_MFnMeshIns.setPoints(movedPosArray) # 移動後座標を適用 def undoIt(self): # undo時に呼び出すための逆処理を書く self.moveTargetMesh_MFnMeshIns.setPoints(self.movePointArray_OriginClone, space = om.MSpace.kWorld) # 確保しておいた元々の座標を適用 def isUndoable(self): # 命令が実行可能かどうかを判定する部分 # これが無いとundoできない模様 return True ########################################################## # Plug-in initialization. ########################################################## def cmdCreator(): return BlogCommandClass() def syntaxCreator(): # syntaxCreaterは作成するコマンドに引数を持たせる場合は記述する必要がある。 syntax = om.MSyntax() # ここで引数として持たせたいデータ型を指定する必要がある。 syntax.addFlag( kShortFlagName, kLongFlagName, om.MSyntax.kDouble ) return syntax def initializePlugin( mobject ): mplugin = om.MFnPlugin( mobject ) try: # 引数を持たせる場合と持たせない場合でココもちょっと変わる mplugin.registerCommand( kPluginCmdName, cmdCreator, syntaxCreator ) except: sys.stderr.write( 'Failed to register command: ' + kPluginCmdName ) def uninitializePlugin( mobject ): mplugin = om.MFnPlugin( mobject ) try: mplugin.deregisterCommand( kPluginCmdName ) except: sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName )
これをプラグインパスが通っている場所に置くと保存したファイル名でプラグインの読み込みに追加されています。
プラグインを読み込んだら、スクリプト内部で定義したMELコマンド、もしくはpythonのcmdsライブラリのコマンドで実行できます。
cmds.blogCommand(moveValue = 1) # Python
で実行できます。
試してみたところ速度がちょっと微妙…。10万頂点くらいだと2~3秒かかってしまいました。選択頂点で判定する処理をいれた辺りでとても遅くなったのでその辺にボトルネックがありそう…。入れる前だと100万頂点でも1秒掛からなかったのでプラグイン自体はとても早いです。
実行すると以下のように動きました。[ctrl + z]も効いて引数で移動量もちゃんと変わっているようです。
コマンドプラグインはcmdsでスクリプトを組むことに比べればやはり時間的なコストはかかってしまいます。が、頂点数が増えてくるとその強みが実感できます。Maya Python API2.0 ならC++で作成するよりはるかにテストやイテレーションをまわすのが楽なので是非試してみてください。
では。