在 maya 中创建操纵器的示例,根据官方文档描述,所有要创建的操纵器,都应该满足以下要求:

  • 所有基本操纵器都应该在实例化的 OpenMayaMPx.MPxManipContainer() 容器中生成
  • 在容器中实现 createChildren(self) 函数功能,使用与容器对应的方法生成指定的操纵器
  • connectToDependNode(self, node) 函数用于链接操纵器到选中的节点的属性接口
  • 完成 addPlugToManipConversion(theIndex) 和 manipToPlugConversion(index) 并通过其对应的回调函数,分别可以进行节点属性接口控制容器值,以及容器值控制节点属性接口的目的

以下示例插件完成类似Maya默认快捷键“T”的方向操纵器的功能:

# -*- coding:utf-8 -*-
# !/usr/bin/env python2
# Author: Mirror
# File: MayaPlugins_AimMinapulator.py
# Time: 2022-12-03 21:43
# Update: 2022-12-11 22:05
# Environment:PyCharm
# Blog: www.mirrorcg.com
# Description: 旨在给 arnold 面光源添加一个方向操控器,在大纲中不产生节点,可用于任何节点
# 插件工具架使用示例:
#         import maya.mel as mm
#         import maya.cmds as cmds
#
#         if not cmds.shelfLayout("Shelf1",q=1,ex=1) :
#             mainTopShelfTab = mm.eval('global string $gShelfTopLevel;string $a=$gShelfTopLevel;')  # 获取工具架顶级布局
#             newShelfLayout = cmds.shelfLayout("Shelf1",p=mainTopShelfTab)  # 创建工具架分页
#         cmds.loadPlugin("MayaPlugins_AimMinapulator.py")
#         if not cmds.aimManipCtxCmd("aimManipCtxCmd1", ex=1, q=1):
#             cmds.aimManipCtxCmd( 'spAimManipContext1' )
#         cmds.setParent( 'AimMinap' )
#         cmds.toolButton( 'aimManip', cl='toolCluster', t='spAimManipContext1', i1="aimManip.xpm" )
# 这将在工具架的"Shelf1"选项卡中创建一个名为"AimMinap"的新按钮。创建一个arnold 面光源,然后单击工具架上的按钮。选择对象时,将出现Z轴方向操纵器。
#
# 插件快捷键使用示例:
#         在快捷键中设置以下命令,并设置快捷键为 Ctrl+Shift+T(可自行设置):
#             import maya.mel as mm
#             import maya.cmds as cmds
#             pluginNmae = 'Maya_pythonPlugin.py'
#             try:
#                 if not cmds.pluginInfo(pluginNmae,l=True,q=1):
#                     cmds.loadPlugin(pluginNmae)
#             except:
#                 cmds.warning(u"插件路径中没有名为'%s'的插件" % pluginNmae)
#                 raise
#             if not cmds.aimManipCtxCmd("aimManipCtxCmd1", ex=1, q=1):
#                 cmds.aimManipCtxCmd( 'aimManipCtxCmd1' )
#             mm.eval("setToolTo aimManipCtxCmd1")
# =========================================
import sys
import maya.OpenMaya as OpenMaya
import maya.OpenMayaUI as OpenMayaUI
import maya.OpenMayaMPx as OpenMayaMPx

aimManipId = OpenMaya.MTypeId(0x8, 133)  # 自定义ID 防止冲突
contextCmdName = "aimManipCtxCmd"
nodeName = "aimManip"


class AimManip(OpenMayaMPx.MPxManipContainer):

    def __init__(self):
        OpenMayaMPx.MPxManipContainer.__init__(self)
        self.fFreePointManip = OpenMaya.MDagPath()
        self.fDirectionManip = OpenMaya.MDagPath()
        self.fScaleManip = OpenMaya.MDagPath()

        self.fNodePath = OpenMaya.MDagPath()
        self.initiDirection = OpenMaya.MVector()
        self.startDirection = OpenMaya.MVector()  # 记录物体的初始位置
        self.endDirection = OpenMaya.MVector()  # 记录物体移动后的位置

    def createChildren(self):
        u"""添加基础操纵器"""
        # FreePointTriadManip
        self.fFreePointManip = self.addFreePointTriadManip("pointManip", "freePoint")
        freePointTriadManipFn = OpenMayaUI.MFnFreePointTriadManip(self.fFreePointManip)

        # # ScaleManip
        # self.fScaleManip = self.addScaleManip("scaleManip", "scale")
        # scaleManipFn = OpenMayaUI.MFnScaleManip(self.fScaleManip)

        # DirectionManip
        self.fDirectionManip = self.addDirectionManip("directionManip", "direction")
        directionManipFn = OpenMayaUI.MFnDirectionManip(self.fDirectionManip)
        directionManipFn.setNormalizeDirection(False)

    def addPlug(self, node):
        u"""添加记录属性保存操作历史"""
        dagNodeFn = OpenMaya.MFnDagNode(node)
        dagNodeFn.getPath(self.fNodePath)
        nodeFn = OpenMaya.MFnDependencyNode()
        nodeFn.setObject(node)
        # 获取选中节点的朝向
        _Matrix = OpenMaya.MTransformationMatrix(self.fNodePath.inclusiveMatrix()).asMatrix()  # .asRotateMatrix()
        self.initiDirection = OpenMaya.MVector(_Matrix(2, 0), _Matrix(2, 1), _Matrix(2, 2))
        if not nodeFn.hasAttribute("arrowDirection"):
            numericFn = OpenMaya.MFnNumericAttribute()
            aArrow2DirectionX = numericFn.create("arrowDirectionX", "ax", OpenMaya.MFnNumericData.kDouble,
                                                 self.initiDirection(0) * -5)
            aArrow2DirectionY = numericFn.create("arrowDirectionY", "ay", OpenMaya.MFnNumericData.kDouble,
                                                 self.initiDirection(1) * -5)
            aArrow2DirectionZ = numericFn.create("arrowDirectionZ", "az", OpenMaya.MFnNumericData.kDouble,
                                                 self.initiDirection(2) * -5)
            aArrow2Direction = numericFn.create("arrowDirection", "dir", aArrow2DirectionX, aArrow2DirectionY,
                                                aArrow2DirectionZ)
            nodeFn.addAttribute(aArrow2Direction)
        else:
            directionX = nodeFn.findPlug("arrowDirectionX").asFloat()
            directionY = nodeFn.findPlug("arrowDirectionY").asFloat()
            directionZ = nodeFn.findPlug("arrowDirectionZ").asFloat()
            length = (directionX ** 2 + directionY ** 2 + directionZ ** 2) ** 0.5
            self.initiDirection = self.initiDirection * (-length)

            nodeFn.findPlug("arrowDirectionX").setFloat(self.initiDirection(0))
            nodeFn.findPlug("arrowDirectionY").setFloat(self.initiDirection(1))
            nodeFn.findPlug("arrowDirectionZ").setFloat(self.initiDirection(2))

    def connectToDependNode(self, node):
        u"""链接操纵器到选中的节点的属性接口"""
        # 获取 DAG path
        dagNodeFn = OpenMaya.MFnDagNode(node)
        dagNodeFn.getPath(self.fNodePath)
        parentNode = dagNodeFn.parent(0)
        parentNodeFn = OpenMaya.MFnDagNode(parentNode)

        nodeFn = OpenMaya.MFnDependencyNode()
        nodeFn.setObject(node)
        # 链接操纵器到对应的接口
        # FreePointTriadManip
        freePointManipFn = OpenMayaUI.MFnFreePointTriadManip(self.fFreePointManip)
        try:
            tPlug = nodeFn.findPlug("translate")
            freePointManipFn.connectToPointPlug(tPlug)
        except:
            sys.stdout.write(u"移动操纵器链接 translate 属性失败\n")

        # # ScaleManip
        # scaleManipFn = OpenMayaUI.MFnScaleManip(self.fScaleManip)
        # try:
        #     scalePlug = nodeFn.findPlug("scale")
        #     scaleManipFn.connectToScalePlug(scalePlug)
        #     scaleManipFn.displayWithNode(node)
        # except:
        #     sys.stdout.write(u"缩放操纵器链接 scale 属性失败\n")

        # 以下有自定义接口
        # DirectionManip
        directionManipFn = OpenMayaUI.MFnDirectionManip()
        directionManipFn.setObject(self.fDirectionManip)
        self.addPlug(node)
        self.startLocate = self.nodeTranslation()  # 获取位置初值
        try:
            sys.stdout.write(u"生成方向操纵器\n")
            directionPlug = nodeFn.findPlug("arrowDirection")
            directionManipFn.connectToDirectionPlug(directionPlug)
            startPointIndex = directionManipFn.startPointIndex()
            self.addPlugToManipConversion(startPointIndex)
        except:
            sys.stdout.write(u"方向操作器链接 arrowDirection 属性失败\n")

        try:
            rPlugin = nodeFn.findPlug("rotate")
            self.initiDirection = OpenMaya.MVector(0, 0, -6)
            directionManipFn.setDirection(self.initiDirection)  # 每次生成操纵器都需要设置初始方向
            self.addManipToPlugConversion(rPlugin)
        except:
            sys.stdout.write(u"物体旋转方向链接方向操作器 direction 属性失败\n")

        OpenMayaMPx.MPxManipContainer.finishAddingManips(self)
        OpenMayaMPx.MPxManipContainer.connectToDependNode(self, node)

    def draw(self, view, path, style, status):
        u"""绘制提示语"""
        # todo VP2.0
        OpenMayaMPx.MPxManipContainer.draw(self, view, path, style, status)
        view.beginGL()
        textPos = OpenMaya.MPoint(self.nodeTranslation())
        sys.stdout.write("draw,1111111111111111111111111111111")
        view.drawText("Swiss Army Manipulator", textPos, OpenMayaUI.M3dView.kLeft)
        view.endGL()

    def manipToPlugConversion(self, index):
        u"""链接选中容器指定属性到物体接口"""
        numData = OpenMaya.MFnNumericData()
        numDataObj = numData.create(OpenMaya.MFnNumericData.k3Float)
        directionManipFn = OpenMayaUI.MFnDirectionManip(self.fDirectionManip)

        direction = OpenMaya.MVector()
        self.getConverterManipValue(directionManipFn.directionIndex(), direction)  # 获取容器方向
        quaternion = self.initiDirection.rotateTo(direction)
        euler = OpenMaya.MTransformationMatrix(quaternion.asMatrix()).eulerRotation()  # 通过四元数转旋转角度
        numData.setData3Float(euler.x, euler.y, euler.z)
        return OpenMayaUI.MManipData(numDataObj)

    def plugToManipConversion(self, theIndex):
        u"""链接选定物体的中心点到操纵器起始点接口"""
        if theIndex == 8:
            numData = OpenMaya.MFnNumericData()
            numDataObj = numData.create(OpenMaya.MFnNumericData.k3Float)
            vec = self.nodeTranslation()
            numData.setData3Float(vec.x, vec.y, vec.z)
            manipData = OpenMayaUI.MManipData(numDataObj)

            self.updateArrowDirectionValue(vec)
            return manipData
        else:
            sys.stdout.write(theIndex)
            sys.stdout.write("\n^index error\n")

    def nodeTranslation(self):
        u"""获取选中对象的中心点"""
        dagFn = OpenMaya.MFnDagNode(self.fNodePath)
        path = OpenMaya.MDagPath()
        dagFn.getPath(path)
        # path.pop()  # pop from the shape to the transform 禁用,报错对象和方法不兼容
        transformFn = OpenMaya.MFnTransform(path)
        return transformFn.getTranslation(OpenMaya.MSpace.kWorld)

    def updateArrowDirectionValue(self, endLocate):
        u"""更新接口值,移动物体位置时更新物体朝向的向量"""
        self.endLocate = endLocate
        relativeLocate = self.endLocate - self.startLocate
        self.startLocate = self.endLocate

        dagFn = OpenMaya.MFnDagNode(self.fNodePath)
        directionPlugX = dagFn.findPlug("arrowDirectionX").asFloat()
        directionPlugY = dagFn.findPlug("arrowDirectionY").asFloat()
        directionPlugZ = dagFn.findPlug("arrowDirectionZ").asFloat()
        directLocate = OpenMaya.MVector(directionPlugX, directionPlugY, directionPlugZ)
        newLacate = directLocate - relativeLocate

        dagFn.findPlug("arrowDirectionX").setFloat(newLacate(0))
        dagFn.findPlug("arrowDirectionY").setFloat(newLacate(1))
        dagFn.findPlug("arrowDirectionZ").setFloat(newLacate(2))
        # sys.stdout.write("change arrorDirectionZ \n")
        directionManipFn = OpenMayaUI.MFnDirectionManip()
        directionManipFn.setObject(self.fDirectionManip)
        directionManipFn.setDirection(newLacate)  # 更新方向操纵器的方向,以同步数据


def aimManipCreator():
    return OpenMayaMPx.asMPxPtr(AimManip())


def aimManipInitialize():
    OpenMayaMPx.MPxManipContainer.initialize()


class aimManipContext(OpenMayaMPx.MPxSelectionContext):
    def __init__(self):
        OpenMayaMPx.MPxSelectionContext.__init__(self)
        self.updateManipulatorsCallbackID = None

    def toolOnSetup(self, event):
        updateManipulators(self)
        self.updateManipulatorsCallbackID = OpenMaya.MModelMessage.addCallback(
            OpenMaya.MModelMessage.kActiveListModified, updateManipulators, self)

    def toolOffCleanup(self):
        self.deleteManipulators()
        try:
            if self.updateManipulatorsCallbackID != None:
                OpenMaya.MModelMessage.removeCallback(self.updateManipulatorsCallbackID)
        except:
            sys.stderr.write(u"安装前调用清理失败.\n")
        super(aimManipContext, self).toolOffCleanup()


def updateManipulators(clientData):
    clientData.deleteManipulators()
    selectionList = OpenMaya.MSelectionList()

    OpenMaya.MGlobal.getActiveSelectionList(selectionList)
    selectionIter = OpenMaya.MItSelectionList(selectionList, OpenMaya.MFn.kInvalid)
    while not selectionIter.isDone():
        dependNode = OpenMaya.MObject()
        selectionIter.getDependNode(dependNode)
        if dependNode.isNull() or not dependNode.hasFn(OpenMaya.MFn.kDependencyNode):
            print(u"depend node is null")
            continue

        dependNodeFn = OpenMaya.MFnDependencyNode(dependNode)
        tPlug = dependNodeFn.findPlug("translate", False)
        rPlug = dependNodeFn.findPlug("rotate", False)
        sPlug = dependNodeFn.findPlug("scale", False)
        if tPlug.isNull() or rPlug.isNull()or sPlug.isNull():
            print(u"translate and/or rotate and/or scale plugs are null")
            selectionIter.next()
            continue

        manipObject = OpenMaya.MObject()
        manipulator = OpenMayaMPx.MPxManipContainer.newManipulator(nodeName, manipObject)
        if manipulator is not None:
            clientData.addManipulator(manipObject)
            manipulator.connectToDependNode(dependNode)
        selectionIter.next()


class aimManipCtxCmd(OpenMayaMPx.MPxContextCommand):
    def __init__(self):
        OpenMayaMPx.MPxContextCommand.__init__(self)

    def makeObj(self):
        return OpenMayaMPx.asMPxPtr(aimManipContext())


def contextCmdCreator():
    return OpenMayaMPx.asMPxPtr(aimManipCtxCmd())


# 注册插件
def initializePlugin(mobject):
    mplugin = OpenMayaMPx.MFnPlugin(mobject, "www.mirrorcg.com", "1.0.0", "Any")
    try:
        mplugin.registerContextCommand(contextCmdName, contextCmdCreator)
    except:
        print(u"该上下文命令注册失败: %s" % contextCmdName)
        raise
    try:
        mplugin.registerNode(nodeName, aimManipId, aimManipCreator, aimManipInitialize,
                             OpenMayaMPx.MPxNode.kManipContainer)
    except:
        print(u"该节点注册失败: %s" % nodeName)
        raise


# 取消注册插件
def uninitializePlugin(mobject):
    mplugin = OpenMayaMPx.MFnPlugin(mobject)
    try:
        mplugin.deregisterContextCommand(contextCmdName)
    except:
        print(u"该上下文命令取消注册失败: %s" % contextCmdName)
        raise
    try:
        mplugin.deregisterNode(aimManipId)
    except:
        print(u"该节点取消注册失败: %s" % nodeName)
        raise