It is possible to write basic scripts that use the wrapper, iterator and function set classes of the Maya API. These scripts can query and manipulate the Maya model but are not fully integrated into Maya. A scripted plug-in provides a more complex solution that is tightly integrated into Maya. In this section, we discuss how to write both basic and scripted plug-in scripts along with standalone scripts.
As this is a Python based API, knowledge of Python is required.
The Maya Python API is contained in a number of Python modules. You must import the functionality that you wish to use in your script. Additionally, the Maya Python API lives in the Maya namespace; therefore, an extra prefix is required. To import the OpenMaya module, run the following:
import maya.OpenMaya
Information can be displayed about any of the modules or classes using the help command. For example, if you wish to display the class information for MVector, use:
It is also possible to display the information of an entire module:
This operation will take a while to return since the OpenMaya module is very large.
The Maya Python API modules contain the classes that are available for Python programming. These classes are separated into different categories and have appropriate naming conventions to signify their association. Classes include:
These classes are iterators and work on MObjects similar to the way a function set does. For example, MItCurveCV is used to operate on an individual NURBS curve CV (there is no MFnNurbsCurveCV), or, iteratively, on all the CVs of a curve.
Classes with this prefix are all "Proxies", that is, API classes designed for you to derive from and create your own object types.
Most, although not all, of these classes are "Wrappers". Examples of this class are: MVector, MIntArray, and so forth.
We can use wrapper and function set classes to write scripts such as the following:
import maya.OpenMaya vector1 = maya.OpenMaya.MVector(0,1,0) vector2 = maya.OpenMaya.MVector(1,0,0) vector3 = maya.OpenMaya.MVector(0,0,2) newVector = vector1 + vector2 + vector3 print "newVector %f, %f, %f " % (newVector.x, newVector.y, newVector.z)
It is possible to shorten the symbol names used by modifying the import command:
import maya.OpenMaya as OpenMaya vector1 = OpenMaya.MVector(0,1,0)
Scripts can access dependency graph information using the Maya Python API classes. The following is a script that finds the persp node and prints out its translateX attribute value:
# import the OpenMaya module import maya.OpenMaya as OpenMaya # function that returns a node object given a name def nameToNode( name ): selectionList = OpenMaya.MSelectionList() selectionList.add( name ) node = OpenMaya.MObject() selectionList.getDependNode( 0, node ) return node # function that finds a plug given a node object and plug name def nameToNodePlug( attrName, nodeObject ): depNodeFn = OpenMaya.MFnDependencyNode( nodeObject ) attrObject = depNodeFn.attribute( attrName ) plug = OpenMaya.MPlug( nodeObject, attrObject ) return plug # Find the persp camera node print "Find the persp camera" perspNode = nameToNode( "persp" ) print "APItype %d" % perspNode.apiType() print "APItype string %s" % perspNode.apiTypeStr() # Print the translateX value translatePlug = nameToNodePlug( "translateX", perspNode ) print "Plug name: %s" % translatePlug.name() print "Plug value %g" % translatePlug.asDouble())
The example above demonstrates the following:
Scripted plug-ins allow a developer to create a solution that is tightly coupled with Maya. Scripted plug-ins allow a developer to support functionality such as the undoing of commands and the building of appropriate requires lines into the Maya scene file. Another advantage of using a scripted plug-in is that its functionality is available in both MEL and Python.
We have extended the Maya Plug-in Manager to support the loading and unloading of scripted plug-ins.
Any file ending with a .py extension that is on the MAYA_PLUG_IN_PATH is displayed in the Plug-in Manager. Select the Loaded check-box or the Auto load check box to either load or auto-load the scripted plug-in.
The plug-in can either be loaded from the Plug-in Manager or from the MEL or Python command tabs. In MEL, use the loadPlugin() command. In Python, use the maya.cmds.loadPlugin() command
To run an example such as helixCmd.py, load the plug-in and enter the following in the Python Editor tab:
import maya maya.cmds.spHelix().
Invoking this Python script does the following:
This plug-in can also be unloaded using the Python command: maya.cmds.unloadPlugin("helixCmd.py")
The load and execute steps can also be invoked in the MEL editor using:
loadPlugin helixCmd.py; spHelix();
Writing a scripted plug-in requires the definition of some specialized functions within the plug-in. The scripted plug-in must:
The following sections describe these pieces in more detail with examples.
Python uses the import keyword to include functionality from a module into a script. For example:
import maya.OpenMaya as OpenMaya import maya.OpenMayaMPx as OpenMayaMPx import sys
It is possible for a scripted plug-in to be split among several files. The import command is used to load the functionality of the secondary file into the scripted plug-in.
import polyModifier
Any secondary scripts must be located in the same directory as the scripted plug-in.
When a scripted plug-in is loaded, Maya searches for an initializePlugin() function in its definition. Within this function, all proxy nodes are registered:
# Initialize the script plug-in def initializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.registerCommand( kPluginCmdName, cmdCreator ) except: sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName ) raise
If the initializePlugin() function is not found, the scripted plug-in fails to load. In addition, during the load, Maya searches for an uninitializePlugin() function. If this is not found, then the scripted plug-in fails to load.
When Maya is attempting to unload the plug-in, the previously found uninitializePlugin() function is called to unload the resources of the plug-in.
def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.deregisterCommand( kPluginCmdName ) except: sys.stderr.write( "Failed to unregister command: %s\n" % kPluginCmdName ) raise
Creator functions are used to return a derived version of a proxy class to Maya. Virtual methods are implemented on the derived class which are called from Maya. An example of a class definition and a creator function is:
class scriptedCommand(OpenMayaMPx.MPxCommand): # ... def cmdCreator(): return OpenMayaMPx.asMPxPtr( scriptedCommand() )
It is very important to call the OpenMayaMPx.asMPxPtr() on the newly created proxy object. This call transfers ownership of the object from Python to Maya. Program errors will occur if you do not make this call since Python can unreference this object and destroy it. This will leave a dangling pointer in Maya.
Implementing a proxy class requires deriving from the Maya Python API object.
class scriptedCommand(OpenMayaMPx.MPxCommand): def __init__(self): OpenMayaMPx.MPxCommand.__init__(self) def doIt(self,argList): print "Hello World!"
The scriptedCommand class is derived from OpenMayaMPx.MPxCommand. The constructor or __init__ method must call the parent class __init__ method. All class methods require self as the first parameter, followed by the normal argument list. This command’s doIt() method simply prints out "Hello World!".
Initialization functions are used within scripted plug-ins that define new proxy nodes using the MPxNode class. The following is an example that demonstrates how to create a simple scripted plug-in node, the output of which is the sine function.
import math, sys import maya.OpenMaya as OpenMaya import maya.OpenMayaMPx as OpenMayaMPx kPluginNodeTypeName = "spSineNode" sineNodeId = OpenMaya.MTypeId(0x8700) # Node definition class sineNode(OpenMayaMPx.MPxNode): # class variables input = OpenMaya.MObject() output = OpenMaya.MObject() def __init__(self): OpenMayaMPx.MPxNode.__init__(self) def compute(self,plug,dataBlock): if ( plug == sineNode.output ): dataHandle = dataBlock.inputValue( sineNode.input ) inputFloat = dataHandle.asFloat() result = math.sin( inputFloat ) * 10.0 outputHandle = dataBlock.outputValue( sineNode.output ) outputHandle.setFloat( result ) dataBlock.setClean( plug ) # creator def nodeCreator(): return OpenMayaMPx.asMPxPtr( sineNode() ) # initializer def nodeInitializer(): # input nAttr = OpenMaya.MFnNumericAttribute() sineNode.input = nAttr.create( "input", "in", OpenMaya.MFnNumericData.kFloat, 0.0 ) nAttr.setStorable(1) # output nAttr = OpenMaya.MFnNumericAttribute() sineNode.output = nAttr.create( "output", "out", OpenMaya.MFnNumericData.kFloat, 0.0 ) nAttr.setStorable(1) nAttr.setWritable(1) # add attributes sineNode.addAttribute( sineNode.input ) sineNode.addAttribute( sineNode.output ) sineNode.attributeAffects( sineNode.input, sineNode.output ) # initialize the script plug-in def initializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.registerNode( kPluginNodeTypeName, sineNodeId, nodeCreator, nodeInitializer ) except: sys.stderr.write( "Failed to register node: %s" % kPluginNodeTypeName ) raise # uninitialize the script plug-in def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.deregisterNode( sineNodeId ) except: sys.stderr.write( "Failed to register node: %s" % kPluginNodeTypeName ) raise
The nodeInitializer() function is passed to registerNode() in the initializePlugin() function. As the plug-in loads, Maya calls the nodeInitializer() function to create the attributes of the node.
The Maya Python API uses Python exceptions for querying and setting error states in script. In most cases, exceptions are used even though the class documentation indicates that a method has a return value. There are many situations where an exception can occur:
def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.deregisterNode( sineNodeId ) except: sys.stderr.write( "Failed to deregister node: %s" % kPluginNodeTypeName ) raise
In this example, if the deregisterNode() call failed, the uninitializePlugin() call passes the exception back to Maya and the plug-in fails to unload.
This code can be modified to catch the error and still allow the plug-in to unload if the deregisterNode() call fails:
def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.deregisterNode( sineNodeId ) except: sys.stderr.write( "Failed to deregister node: %s" % kPluginNodeTypeName ) pass
The only change being that the raise keyword has been changed to pass. This technique is useful for writing iterator code that may fail if incorrect objects are being examined.
3. Unknown parameter return value
In the Maya Python API, an unknown parameter return value is used to indicate that a method cannot handle a specific case and it is up to the caller to take care of the operation. One such method is MPxNode::compute(). In this situation, the Python code would return OpenMaya.kUnknownParameter.
As noted in the previous section, the Maya Python API uses exceptions to communicate status information rather than MStatus values. When trying to divine the Pythonic behavior of an API method from its C++ documentation, the rules below hold.
There are two ways in which C++ API methods can return MStatus values, either as the method's return value:
MStatus someMethod(Type arg1, Type arg2, ...)
or through an optional pointer to an MStatus variable in its parameter list, usually as the final parameter:
Type someMethod(Type arg1, Type arg2, ..., MStatus* ReturnStatus = NULL)
When the method returns an MStatus as its function value, that return value is handled in Python as follows:
The reason for the special handling of MS::kUnknownParameter is to accomodate MPxNode::compute().
There are no special API-specific exceptions. Maya simply uses Python's standard RuntimeError and passes the error string as its argument.
When an API method returns an MStatus through a pointer variable in its parameter list, then that MStatus is handled as follows:
This means that users writing plug-ins in C++ can continue to return MStatus codes as they normally would, regardless of whether their code is called from C++ or Python. Maya will internally convert those codes to Python exceptions if necessary.
Users writing plug-ins in Python should raise exceptions rather than return MStatus values, unless they want their compute() method to indicate that it is not going to handle a plug. In this case, it should return maya.OpenMaya.kUnknownParameter.
All of the number arrays (MIntArray, MUintArray, MUint64Array, MFloatArray, and MDoubleArray) support Python-style slicing. For example:
import maya.OpenMaya as OpenMaya array = OpenMaya.MUintArray() for i in range(0,9): array.append( i ) array[2:8:2]
The proxy classes provide some standard information to a developer about the node that is being used. This includes attribute objects that are used to define the node. To access a static class MObject in the Maya Python API, similar code can be used:
envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope
After making this call, the envelope will be an MObject for MPxDeformerNode::envelope.
Message classes are supported in the Maya Python API. A Python function is passed for the callback. This function must have the exact number of parameters required by the callback message. If it does not, an exception will occur when the message is invoked and information will be written to the console. Client data in the form of a python object can also be passed with most messages. The following is an example of a message:
# Message callback def dagParentAddedCallback( child, parent, clientData ): print "dagParentAddedCallback..." print "\tchild %s" % child.fullPathName() print "\tparent %s" % parent.fullPathName() print "\tclient data %s" % clientData # Create the mesage def createParentAddedCallback(stringData): try: id = OpenMaya.MDagMessage.addParentAddedCallback( dagParentAddedCallback, stringData ) except: sys.stderr.write( "Failed to install dag parent added callback\n" ) messageIdSet = False else: messageIdSet = True return id # Call the message creator messageId = createParentAddedCallback( "_noData_" )
In Python, it is best to modify a parameter rather than use an assignment. The code below contains an assignment and demonstrates how an error can occur:
import maya.OpenMaya as OpenMaya def vectorTest(v): lv = OpenMaya.MVector(1,5,9) v = lv print "%g %g %g" % (v.x,v.y,v.z) v = OpenMaya.MVector() vectorTest(v) print "%g %g %g" % (v.x,v.y,v.z)
The second print command will emit all zeroes. In Python, either modify the parameter value or write the code so that a new value is returned. Rewrite the vectorTest() function as follows:
def vectorTest(v): lv = OpenMaya.MVector(1,5,9) v.x = lv.x v.y = lv.y v.z = lv.z print "%g %g %g" % (v.x,v.y,v.z)
The Maya Python API contains many calls in which return values or parameters are references to basic types such as: int&, char&, float& etc. In the Maya Python API, all references are treated as pointers. As a result, special calls are required to create, set and access the values of these items.
A utility class called MScriptUtil that exists in the OpenMaya.py module provides methods for working with these type of parameters and return values. This class allows for creating data and then acquiring a pointer to the data so that information can be passed to class methods requiring references. See MScriptUtil in the API Reference documentation.
The following example should help to clarify the usage of MScriptUtil. It shows how to get the x, y and z values from a call to MFnLattice.getDivisions():
import maya.OpenMaya as OpenMaya import maya.OpenMayaAnim as Anim xutil = OpenMaya.MScriptUtil() xutil.createFromInt(0) xptr = xutil.asUintPtr() yutil = OpenMaya.MScriptUtil() yutil.createFromInt(0) yptr = yutil.asUintPtr() zutil = OpenMaya.MScriptUtil() zutil.createFromInt(0) zptr = zutil.asUintPtr() it = OpenMaya.MItDependencyNodes(OpenMaya.MFn.kFFD) while not it.isDone(): latDefFn = Anim.MFnLatticeDeformer( it.thisNode() ) latFn = Anim.MFnLattice( latDefFn.deformLattice() ) latFn.getDivisions(xptr, yptr, zptr) x = xutil.getUint(xptr) y = yutil.getUint(yptr) z = zutil.getUint(zptr) doSomethingUseful(x, y, z) it.next()
Since getUint() is a static method of MScriptUtil, an alternative way of retrieving the final values is to call it directly from the class. For example:
x = OpenMaya.MScriptUtil.getUint(xptr)
References to enumerated types should be treated as references to short ints.
A command which has flags must specify its syntax using an MSyntax object and parse the flags using the MArgParser or MArgDatabase classes, otherwise the flags will not be accessible when the command is used from within Python. This is true regardless of whether the command is written in Python or C++. Below is an example of a scripted plug-in which implements a command with flags:
import maya.OpenMaya as OpenMaya import maya.OpenMayaMPx as OpenMayaMPx import sys, math kPluginCmdName="spHelix" kPitchFlag = "-p" kPitchLongFlag = "-pitch" kRadiusFlag = "-r" kRadiusLongFlag = "-radius" # command class scriptedCommand(OpenMayaMPx.MPxCommand): def __init__(self): OpenMayaMPx.MPxCommand.__init__(self) def doIt(self, args): deg = 3 ncvs = 20 spans = ncvs - deg nknots = spans+2*deg-1 radius = 4.0 pitch = 0.5 # Parse the arguments. argData = OpenMaya.MArgDatabase(self.syntax(), args) if argData.isFlagSet(kPitchFlag): pitch = argData.flagArgumentDouble(kPitchFlag, 0) if argData.isFlagSet(kRadiusFlag): radius = argData.flagArgumentDouble(kRadiusFlag, 0) controlVertices = OpenMaya.MPointArray() knotSequences = OpenMaya.MDoubleArray() # Set up cvs and knots for the helix for i in range(0, ncvs): controlVertices.append( OpenMaya.MPoint( radius * math.cos(i), pitch * i, radius * math.sin(i) ) ) for i in range(0, nknots): knotSequences.append( i ) # Now create the curve curveFn = OpenMaya.MFnNurbsCurve() nullObj = OpenMaya.MObject() try: # This plugin normally creates the curve by passing in the # cv's. A function to create curves by passing in the ep's # has been added. Set this to False to get that behaviour. if True: curveFn.create( controlVertices, knotSequences, deg, OpenMaya.MFnNurbsCurve.kOpen, 0, 0, nullObj ) else: curveFn.createWithEditPoints(controlVertices, 3, OpenMaya.MFnNurbsCurve.kOpen, False, False, False) except: sys.stderr.write( "Error creating curve.\n" ) raise # Creator def cmdCreator(): # Create the command return OpenMayaMPx.asMPxPtr( scriptedCommand() ) # Syntax creator def syntaxCreator(): syntax = OpenMaya.MSyntax() syntax.addFlag(kPitchFlag, kPitchLongFlag, OpenMaya.MSyntax.kDouble) syntax.addFlag(kRadiusFlag, kRadiusLongFlag, OpenMaya.MSyntax.kDouble) return syntax # Initialize the script plug-in def initializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject, "Autodesk", "1.0", "Any") try: mplugin.registerCommand( kPluginCmdName, cmdCreator, syntaxCreator ) except: sys.stderr.write( "Failed to register command: %s\n" % kPluginCmdName ) raise # Uninitialize the script plug-in def uninitializePlugin(mobject): mplugin = OpenMayaMPx.MFnPlugin(mobject) try: mplugin.deregisterCommand( kPluginCmdName ) except: sys.stderr.write( "Failed to unregister command: %s\n" % kPluginCmdName ) raise
This example includes the syntax creator function along with parsing operations in the doIt() method of the class.
The Maya Python API contains several methods that should only be called from the class the method belongs to. We follow the Python designation of using an _ as the first letter of the method name to indicate that protection applies to this method. Several examples of these methods exist in the MPxNode class:
_forceCache() _setMPSafe()
Please respect the method usage requirements of protected methods in the Maya Python API.
Some API methods return your custom objects as instances of their base MPx class rather than instances of your derived class. For example, MPxContext._newToolCommand() returns your custom tool command as an MPxToolCommand object and MPxSurfaceShapeUI.surfaceShape() returns your custom surface shape as an MPxSurfaceShape object. While these base instances refer to the same internal Maya objects as the original derived instances, they are separate Python objects which do not retain any of the member variable values set in the derived instances, so calling methods or accessing member variables on the returned objects may give incorrect results.
You can work around this problem by taking advantage of the properties of the OpenMayaMPx.asHashable() function. OpenMayaMPx.asHashable() takes an MPx object and returns a hash value which uniquely identifies the underlying Maya object. It doesn't matter whether the object is an instance of the base MPx class or your derived class: so long as they both refer to the same underlying Maya object they will return the same hash value. This allows you to set up a dictionary which can translate a base instance into its corresponding derived instance.
The first step is to add an empty dictionary to the class:
class MoveToolCmd(OpenMayaMPx.MPxToolCommand): kTrackingDictionary = {}
In the class's __init__ method, add an entry to the dictionary, mapping the new object's hash value to the object itself:
def __init__(self): OpenMayaMPx.MPxToolCommand.__init__(self) MoveToolCmd.kTrackingDictionary[OpenMayaMPx.asHashable(self)] = self
In the class's __del__ method, remove the deleted object from the dictionary:
def __del__(self): del MoveToolCmd.kTrackingDictionary[OpenMayaMPx.asHashable(self)]
Now that you have the tracking dictionary, you can use it to retrieve the derived object from its corresponding base object:
class MoveContext(OpenMayaMPx.MPxSelectionContext): ... def doPress(self, event): # Create an instance of the move tool command. baseCmd = self._newToolCommand() derivedCmd = MoveToolCmd.kTrackingDictionary.get( OpenMayaMPx.asHashable(baseCmd), None ) # Call its methods. derivedCmd.setVector(0.0, 0.0, 0.0)
When defining custom node types (MPxNode, MPxSurfaceShape, etc) some additional effort is required. When Maya does a file open or new file operation it destroys the scene without executing the __del__ method for each node. That means that the entries for those nodes are never removed from the tracking dictionary. To get around this, callbacks must be set up in your initializePlugin method to clear their tracking dictionaries on file open and file new:
fileOpenCB = 0 fileNewCB = 0 def clearTrackingDictionaries(unused): MyNode.kTrackingDictionary = {} MySurfaceShape.kTrackingDictionary = {} def initializePlugin(plugin): ... fileOpenCB = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kBeforeOpen, clearTrackingDictionaries ) fileNewCB = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kBeforeNew, clearTrackingDictionaries ) ... } def uninitializePlugin(plugin): ... if fileOpenCB != 0: OpenMaya.MSceneMessage.removeCallback(fileOpenCB) fileOpenCB = 0 if fileNewCB != 0: OpenMaya.MSceneMessage.removeCallback(fileNewCB) fileNewCB = 0
There are some methods in the Maya Python API that require <iosteam> operating system types. As these are not included in Python, a MStreamUtils class is available for creating and using these type of objects. Please check the Developer Kit for examples on how to use this class.
Often when writing an MPx proxy class, the scripts will require calling into the parent class. This is done using notation such as the following:
We have provided a wrapper class MGLFunctionTable for using OpenGL functionality in script on all of our support platforms. To acquire a reference to this class use the following code:
glRenderer = OpenMayaRender.MHardwareRenderer.theRenderer() glFT = glRenderer.glFunctionTable()
When writing nested iterators, it is normal to have the current item of the outer loop passed into an inner loop. For example, the current item of an MItSelectionList is passed into a MItSurfaceCV iterator. You may encounter garbage collection issues in this case where the inner iterator is holding on to information that may no longer be valid. You can work around this by resetting the inner iterator to None once its loop has been completed. This releases the iterator’s information before the outer loop continues, which is important if current items are being rebuilt or updated.
It is possible to write standalone scripts that make use of the wrapper classes and function sets to modify the Maya model. These scripts are run from the command line. A simple "hello world" standalone script follows:
import maya.standalone import maya.OpenMaya as OpenMaya import sys def main( argv = None ): try: maya.standalone.initialize( name='python' ) except: sys.stderr.write( "Failed in initialize standalone application" ) raise sys.stderr.write( "Hello world! (script output)\n" ) OpenMaya.MGlobal().executeCommand( "print \"Hello world! (command script output)\\n\"" ) maya.standalone.uninitialize() if __name__ == "__main__": main()
After the standalone is initialized, Maya commands and API classes can be used to create and modify a Maya model. When finished, the standalone is uninitialized to allow Maya to clean up after itself. Do not attempt to use Maya commands or API classes before the call to initialize() or after the call to uninitialize(); otherwise, unstable behaviour may result.
A standalone script must be run using the Python executable that is supplied with Maya. For example: