【Maya/Python】選択したオブジェクト名をディスプレイ上に表示するプラグインを作ってみた

ちょっと育児でバタバタしていたり、結構前に99%完成はしていたのに詳細は後述しますが

Mayaが落ちる致命的な挙動が解決できなかったりして、なかなか記事にできませんでしたが、

別のスクリプトを制作中にスクリプトのリファレンスを見ていて偶然その打開策を発見したので

ようやく記事にすることができました。

 

今回作ったのはSoftimageでは標準であった「選択しているオブジェクトの名前を

ディスプレイ上に表示する」という機能ですが、残念ながらMayaにはなかったので作ってみました。

 

ただ、Mirageさん(@MirageYM)が書いて下さったサンプルを元に拡張していっただけなんですけどもね・・・(;´∀`)

改めてMirageさん、サンプル大変参考になりました。ありがとうございました!

 

使い方は、まずこちらからファイルをDLしてもらい、Mayaのプラグインパスの通っている所(よく分からない方は C:\Users\ユーザー名\Documents\maya\2016\ja_JP\plug-ins など)にファイルを入れて下さい。

 

Mayaを起動して、メインメニュー>ウィンドウ>プラグインマネージャー を開いてその中のkkDisplaySelectedNameのロードのチェックをONにして下さい。

そうすると、メインメニュー>ディスプレイ の一番下にDisplay Selected Nameが追加されていると思います。

(※もしプラグインマネージャーに表示されていない場合はプラグインのファイルを置く場所がパス通ってないか、

もしくはプラグインのTypeIdが他のモノと被っている可能性があるのでコードの18行目を 0xF0F0F1 とは違うものに変えてみて下さい。)

 

さらにそこのチェックボックスをONにするとONになっている場合のみ、

選択したオブジェクトの名前がディスプレイ上に表示されるようになります。

 

モノによってはその表示される名前の色が見づらい場合があると思います。

その場合は先程のDisplay Selected Nameメニューの右側の四角いボックスをクリックしてもらうと

色変更のメニューが出てきますのでお好きな色に変更して下さい。

このDisplay Selected NameメニューのONOFFをショートカットで変更したい方は

ホットキーエディタに下記のように登録して下さい。

 

これだけでONOFFをトグルできます。


import maya.cmds as mc

dsn = "MayaWindow|mainDisplayMenu|DisplaySelectedName"

if(mc.menuItem(dsn, exists=True)):
        mc.menuItem(dsn, e=True, checkBox = not mc.menuItem(dsn, q=True, checkBox=True))

コードはDLして見てもらってもいいのですが、一応こちらにも載せておきます。

 

簡単に処理内容を言いますと、SelectionChangedイベントのscriptJobに、

「専用ノード生成しそれに選択したオブジェクトの名前をセットしてペアレントコンストレイントする」

というのを登録しています。

 

冒頭で書いたMayaが落ちてしまうのを回避できた解決策は

265行目のscriptJobの登録時にcompressUndoフラグをTrueにしたことです。(+UndoInfo)

これがないときは選択したオブジェクトがdeleteされていた場合にUndoすると削除されてもう存在しないモノに

名前を表示するノードを作ろうとしたためか落ちたり、逆にこのスクリプトの挙動がUndoに残ってしまい戻れなくなるなど起きていました。

 

一応今の状態でプリミティブのキューブ1000個全選択して、

名前を表示し終わるまでの時間を計測した所、平均0.8秒でした。

(※Maya2016 : CPU Corei7-4790K  : GPU GeForce GTX 980 で計測)

 

undoInfoのopenChunk/closeChunkで多少早くはなっていますが、

もっと処理が早くなりそうな書き方をご存知の方いらっしゃいましたらご教授下さい。

 

あと、色々と自分でも試してはいますが、

不具合が出ても責任は取れませんので自己責任でお試し下さい。

(ちなみに2016でしか試せていませんがたぶん他のバージョンでも大丈夫かと…)


# -*- coding: utf-8 -*-

import sys

import maya.api.OpenMaya as om2
import maya.api.OpenMayaUI as om2u
import maya.api.OpenMayaRender as om2r

import maya.cmds as mc

maya_useNewAPI = True

#===============================================================
class kkDisplaySelectedNameNode( om2u.MPxLocatorNode ):

        kPluginNodeTypeName = "kkDisplaySelectedNameNode"
        # TypeIdは適当な番号 (0x00000000以上、0x0007ffff以下で指定。他のプラグインとかぶってはいけない)
        NodeId = om2.MTypeId( 0xF0F0F1 )

        # ノードがDrawOverrideを使うためにはOverrideをMayaに登録しておく必要がある。以下2つはそのためのID
        classfication = 'drawdb/geometry/kkDisplaySelectedName'
        registrantId  = 'kkDisplaySelectedNamePlugin'

        # ノードのアトリビュート。aStringにセットした文字列が描画される
        aString = om2.MObject()
        ColorR  = om2.MObject()
        ColorG  = om2.MObject()
        ColorB  = om2.MObject()

        #===============================================================
        def __init__( self ):
                om2u.MPxLocatorNode.__init__( self )

        #===============================================================
        def draw( self, view, path, style, status ):
                pass

        #===============================================================
        def isBounded( self ):
                return True

        #===============================================================
        def boundingBox( self ):
                return om2.MBoundingBox( om2.MPoint( -0.5, -0.5, -0.5 ), om2.MPoint( 0.5, 0.5, 0.5 ) )

        #===============================================================
        # creator
        @staticmethod
        def nodeCreator():
                return kkDisplaySelectedNameNode()

        #===============================================================
        # initializer
        @staticmethod
        def nodeInitializer():
                fnTypedAttr    = om2.MFnTypedAttribute()
                fnNumericAttr1 = om2.MFnNumericAttribute()
                fnNumericAttr2 = om2.MFnNumericAttribute()
                fnNumericAttr3 = om2.MFnNumericAttribute()

                kkDisplaySelectedNameNode.aString = fnTypedAttr.create( "String", "str", om2.MFnData.kString )
                kkDisplaySelectedNameNode.addAttribute( kkDisplaySelectedNameNode.aString )

                kkDisplaySelectedNameNode.ColorR = fnNumericAttr1.create( "ColorR", "colR", om2.MFnNumericData.kFloat, 1.0 )
                kkDisplaySelectedNameNode.addAttribute( kkDisplaySelectedNameNode.ColorR )

                kkDisplaySelectedNameNode.ColorG = fnNumericAttr2.create( "ColorG", "colG", om2.MFnNumericData.kFloat, 1.0 )
                kkDisplaySelectedNameNode.addAttribute( kkDisplaySelectedNameNode.ColorG )

                kkDisplaySelectedNameNode.ColorB = fnNumericAttr3.create( "ColorB", "colB", om2.MFnNumericData.kFloat, 1.0 )
                kkDisplaySelectedNameNode.addAttribute( kkDisplaySelectedNameNode.ColorB )

                return True

#===============================================================
class UserData( om2.MUserData ):
        """
        DrawOverrideにユーザー定義の情報を供給する為のデータ構造
        """

        #===============================================================
        def __init__( self ):
                om2.MUserData.__init__( self, False )
                self.datas = []

#===============================================================
class api_annotationLoc2Override( om2r.MPxDrawOverride ):
        """
        注釈のDrawOverride
        """

        #===============================================================
        def __init__( self, obj ):
                om2r.MPxDrawOverride.__init__( self, obj, api_annotationLoc2Override.draw )

        #===============================================================
        @staticmethod
        def draw( context, data ):
                pass

        #===============================================================
        def supportedDrawAPIs( self ):
                """
                OpenGLとDirextX11をサポート
                """

                return om2r.MRenderer.kOpenGL | om2r.MRenderer.kDirectX11

        #===============================================================
        def hasUIDrawables( self ):
                """
                addUIDrawablesを使うために必要
                """
                return True

        #===============================================================
        def isBounded( self, objPath, cameraPath ):
                return True

        #===============================================================
        def boundingBox( self, objPath, cameraPath ):
                bbox = om2.MBoundingBox( om2.MPoint( -0.5, -0.5, -0.5 ),
                                                                om2.MPoint( 0.5, 0.5, 0.5 ) )
                return bbox

        #===============================================================
        def disableInternalBoundingBoxDraw( self ):
                return True

        #===============================================================
        def prepareForDraw( self, objPath, cameraPath, frameContext, oldData ):

                # ロケーターからアトリビュートを読んでUserDataとして返す
                if( objPath ):
                        newData = None

                        # oldDataがあった場合はoldDataをnewDataに入れて、UserDataのdatasを初期化する
                        if( oldData ):
                                newData = oldData
                                newData.datas = []
                        else:
                                newData = UserData()

                        thisNode = objPath.node()
                        fnNode   = om2.MFnDependencyNode( thisNode )

                        strPlug  = fnNode.findPlug( 'String', False )
                        colRPlug = fnNode.findPlug( 'ColorR', False )
                        colGPlug = fnNode.findPlug( 'ColorG', False )
                        colBPlug = fnNode.findPlug( 'ColorB', False )

                        # asStringやasFloatがget関数
                        newData.datas.append( strPlug.asString() )
                        newData.datas.append( colRPlug.asFloat() )
                        newData.datas.append( colGPlug.asFloat() )
                        newData.datas.append( colBPlug.asFloat() )

                        return newData

                return None

        #===============================================================
        def addUIDrawables( self, objPath, drawManager, frameContext, data ):

                # UI描画。MUIDrawManagerを使うことで簡単なライン描画とかもできる
                if( data ):
                        drawManager.beginDrawable()

                        dColor = om2.MColor()
                        dColor[0] = mc.optionVar(q = "kkDisplaySelectedName_ColorR") #data.datas[1] #Red
                        dColor[1] = mc.optionVar(q = "kkDisplaySelectedName_ColorG") #data.datas[2] #Green
                        dColor[2] = mc.optionVar(q = "kkDisplaySelectedName_ColorB") #data.datas[3] #Blue

                        drawManager.setColor( dColor )
                        drawManager.setFontSize( om2r.MUIDrawManager.kDefaultFontSize )
                        drawManager.text( om2.MPoint( 0.5, 0.5 ), data.datas[0] )
                        drawManager.endDrawable()

                return True

        #===============================================================
        # creator
        @staticmethod
        def creator( obj ):
                return api_annotationLoc2Override( obj )

#===============================================================

def changeColorUI(*args):
        if mc.window("dispSelName_ColorWin", exists=True):
                mc.deleteUI("dispSelName_ColorWin")

        mc.window("dispSelName_ColorWin", title='Colors' )
        mc.columnLayout()
        mc.colorInputWidgetGrp("dispSelName_ColorInpWG",
                                                label='Color',
                                                rgb=(1, 1, 1),
                                                changeCommand = changeColorAttr
                                                )
        mc.showWindow()

def changeColorAttr(*args):
        changCol = mc.colorInputWidgetGrp("dispSelName_ColorInpWG", q=True, rgbValue=True)

        if mc.optionVar(exists = "kkDisplaySelectedName_ColorR") == True:
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorR", changCol[0]])

        if mc.optionVar(exists = "kkDisplaySelectedName_ColorG") == True:
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorG", changCol[1]])

        if mc.optionVar(exists = "kkDisplaySelectedName_ColorB") == True:
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorB", changCol[2]])

        try:
                annotateList = om2.MGlobal.getSelectionListByName("kkDisplaySelectedNameNode*")

                for x in range(annotateList.length()):
                        annotateDepNode = om2.MFnDependencyNode().setObject( annotateList.getDependNode(x) )

                        colR = annotateDepNode.findPlug('ColorR', False )
                        colG = annotateDepNode.findPlug('ColorG', False )
                        colB = annotateDepNode.findPlug('ColorB', False )

                        colR.setFloat( changCol[0] )
                        colG.setFloat( changCol[1] )
                        colB.setFloat( changCol[2] )

        except:
                pass

#===============================================================
# initialize the script plug-in
def initializePlugin( obj ):

        mplugin = om2.MFnPlugin( obj )

        try:
                # Overrideを使うためにはオーバーロードされた専用のredisterNode関数を使う必要がある
                mplugin.registerNode( kkDisplaySelectedNameNode.kPluginNodeTypeName,
                                                        kkDisplaySelectedNameNode.NodeId,
                                                        kkDisplaySelectedNameNode.nodeCreator,
                                                        kkDisplaySelectedNameNode.nodeInitializer,
                                                        om2.MPxNode.kLocatorNode,
                                                        kkDisplaySelectedNameNode.classfication
                                                        )

                # DrawOverride登録
                om2r.MDrawRegistry.registerDrawOverrideCreator( kkDisplaySelectedNameNode.classfication,
                                                                                                                kkDisplaySelectedNameNode.registrantId,
                                                                                                                api_annotationLoc2Override.creator
                                                                                                                )

                # ディスプレイメニューに追加 チェックボックスはデフォルトOFF
                mc.menuItem("DisplaySelectedName",
                                        label="Display Selected Name",
                                        checkBox=False,
                                        parent="MayaWindow|mainDisplayMenu"
                                        )

                mc.menuItem(optionBox = True,
                                        command = changeColorUI,
                                        parent="MayaWindow|mainDisplayMenu"
                                        )

                mc.scriptJob( event = ["SelectionChanged", createAnnotation] , compressUndo=True)

                # optionVarに文字カラー用の変数を作成
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorR", 1])
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorG", 1])
                mc.optionVar(intValue = ["kkDisplaySelectedName_ColorB", 1])

        except:
                sys.stderr.write( "Failed to register node: %s" % kkDisplaySelectedNameNode.kPluginNodeTypeName )
                raise

#===============================================================
# uninitialize the script plug-in
def uninitializePlugin( obj ):

        mplugin = om2.MFnPlugin( obj )
        try:
                mplugin.deregisterNode( kkDisplaySelectedNameNode.NodeId )
                om2r.MDrawRegistry.deregisterDrawOverrideCreator( kkDisplaySelectedNameNode.classfication,
                                                                                                                kkDisplaySelectedNameNode.registrantId
                                                                                                                )

                if mc.menuItem("MayaWindow|mainDisplayMenu|DisplaySelectedName", exists=True):
                        mc.deleteUI("MayaWindow|mainDisplayMenu|DisplaySelectedName", menuItem=True)

                mc.undoInfo(stateWithoutFlush=False)

                mc.delete( mc.ls( 'annotationDSN_*' ) )

                mc.undoInfo(stateWithoutFlush=True)

                # optionVarから文字カラー用の変数を削除
                mc.optionVar(remove="kkDisplaySelectedName_ColorR")
                mc.optionVar(remove="kkDisplaySelectedName_ColorR")
                mc.optionVar(remove="kkDisplaySelectedName_ColorR")

                jobList = mc.scriptJob(listJobs=True)

                jobNum = 0

                for job in jobList :
                        # ジョブリストから登録したジョブの内容を検索する
                        # 単純にそのインデックスを使えばいいと思ったら
                        # インデックスとジョブ番号が違うことがあるのでスライスで取得する
                        if 'conditionTrue  = ["SomethingSelected", createAnnotation]' in job :
                                jobNum = job [0 : job .find(":")]

                        # 上の処理でjobNumに数値が入っていたらそのスクリプトジョブを停止させる
                        if jobNum > 0:
                                print("killing_jobNum__%s " %jobNum)
                                mc.scriptJob( kill=jobNum, force=True)

                print(" Unload >> old_kDisplaySelName Plugin ")


        except:
                sys.stderr.write( "Failed to deregister node: %s" % kkDisplaySelectedNameNode.kPluginNodeTypeName )
                raise


#=======================================================================================================================

def createAnnotation( *args ):

        try:
                # Undoのチャンクを開く. これをすることで速度が早くなる
                mc.undoInfo(openChunk=True)

                # すでに annotationDSN_ が付いているものがある場合、削除しておく
                annotationList = mc.ls( "annotationDSN_*" )

                if len( annotationList ) > 0:
                        mc.delete( annotationList )

                if mc.menuItem("MayaWindow|mainDisplayMenu|DisplaySelectedName", q=True, checkBox=True):

                        # 選択しているリスト
                        selList = om2.MGlobal.getActiveSelectionList()

                        if selList.length() > 0:
                                # Transformのみのフィルターをする
                                selListIter = om2.MItSelectionList(selList, om2.MFn.kTransform)

                                while not selListIter.isDone():

                                        mObj2    = selListIter.getDependNode()
                                        selDagFn = om2.MFnDagNode( mObj2 )

                                        if not "annotationDSN" in selDagFn.name():
                                                ant = mc.createNode("kkDisplaySelectedNameNode")

                                                antDagPath = om2.MGlobal.getSelectionListByName( ant ).getDagPath(0)
                                                antDagFn   = om2.MFnDagNode( antDagPath )
                                                antParent  = om2.MFnDependencyNode().setObject( antDagFn.parent(0) )

                                                antStrPlug = antDagFn.findPlug( 'String', False )
                                                antStrPlug.setString( selDagFn.name() )

                                                dgmf = om2.MDGModifier()
                                                dgmf.renameNode(antDagFn.parent(0), "annotationDSN_1")
                                                dgmf.doIt()

                                                mc.parentConstraint(selDagFn.fullPathName(), antParent.name(), skipRotate="none")

                                        selListIter.next()

                                # 選択をオブジェクトに戻す
                                om2.MGlobal.setActiveSelectionList(selList)

        except Exception as e:
                print(e.args)
                pass

        finally:
                # Undoのチャンクを閉じる
                mc.undoInfo(closeChunk=True)

コメントをお書きください

コメント: 2
  • #1

    aaa (火曜日, 22 1月 2019 10:28)

    2018で動かず。

  • #2

    ブログ管理者 (月曜日, 29 4月 2019 01:23)

    aaaさん、ご返信遅くなり大変申し訳無いです。

    2018では試せてないのですが、2019では問題なく動くのを確認できました。
    もし宜しければ、もう少し動かなかったaaaさんの実行環境の状況(Mayaの2018のUpdateのバージョンいくつだとか、プリファレンス>ディスプレイのレンダリングエンジンの設定など)を教えてもらえますでしょうか?