Python in Maya
Maya’s scripting commands come in the module package maya.cmds.
view sourceprint?import maya.cmds
People usually use the shorthand
view sourceprint?import maya.cmds as cmds
Or
view sourceprint?import maya.cmds as mc
You will notice that if you type in help(cmds), you do not get anything useful besides a list of function names. This is because Autodesk converted all of their MEL commands to Python procedurally. To get help with Maya’s Python commands, you will need to refer to the Maya documentation.
Figure 14 - Maya Python Command Documentation
There are so many commands that there is no reason to memorize them all. You will come to memorize many of the commands simply by using them a lot. How do you get started learning commands? Do what you are trying to accomplish with Maya’s interface and look at the script editor. Most the actions you do with the Maya interface output what commands were called in the script editor. The only caveat is that the output is in MEL so we’ll have to do a little bit of translating. Since all of the commands and arguments are the same between the MEL and Python commands, translating between the two is pretty easy.
The Maya Python Command Documentation
In this section, we will go over how to learn Maya’s Python commands by studying the MEL output in the script editor when interacting with Maya. We will then decipher the MEL output and reconstruct the command using the Maya Python documentation.
Creating a polygon sphere outputs the following MEL command into the script editor:
polySphere -r 1 -sx 20 -sy 20 -ax 0 1 0 -cuv 2 -ch 1;MEL commands usually consist of the command name followed by several flags. In the above code, polySphere is the command, and each group of letters following a “-“ is a flag. The numbers after each flag are arguments of their corresponding flag. For example, “-r” is a flag with an argument of 1, “-sx” is a flag with an argument of 20, “-ax” is a flag with 3 arguments: 0, 1, 0. Using this information, we can look up the command in the Maya Python command documentation and write its Python equivalent. Like I said earlier, commands in MEL and Python have the exact same name, look up “polySphere” in the Python documentation. It looks like this:
Figure 15 - Sample Documentation
The documentation page contains all the information you need to work with the command. The Synopsis shows the function and all the possible arguments that can be passed in along with a description of what the command does. In this case, it creates a new polygonal sphere. The return value tells us what the function returns. The polySphere command returns a list containing two strings. The first element of the list will be the name of the transform of the new polygon sphere. The second string in the list will be the name of the polySphere node, which controls how the sphere is constructed.
view sourceprint?>>> x = cmds.polySphere()
>>> print x
[u'pSphere1', u'polySphere1']
Notice that each string has a ‘u’ before it. This stands for Unicode string which is a type of string that you can ignore for now. Unicode strings help with international languages so just assume they are the same as normal strings.
Following the Return value section is a list of related Maya Commands. Following these links is a good way to learn about other commands. After the related commands is the Flags section. This section should really be called Arguments or Parameters; Flags are more of a MEL construct. The list of Flags (arguments) contains all the arguments that can be passed into the documented function. Each argument description contains the argument name, an abbreviated argument name, what kind of data you can pass into the argument, in what context the argument is valid, and a description of the argument. Take the radius argument for example. By passing this argument to the polySphere function, we can control the radius of the created sphere.
view sourceprint?>>> x = cmds.polySphere(radius=2.5)
or the abbreviated form
view sourceprint?>>> x = cmds.polySphere(r=2.5)
>>> print x
[u'pSphere2', u'polySphere2']
Personally, I tend to avoid the abbreviated form as I can never remember what all the abbreviations mean when I read my code. Using the full name is more typing and makes your code longer, but I find it easier to read.
The documentation is not always clear about what type of data is expected with an argument. For example, the documentation says that the radius argument expects some data of type linear. Usually by looking at the equivalent MEL command, you can figure out what to pass into the Python command. However, there are some cases where the documented format of the expected data is just really vague or cryptic. In these cases, if you can’t figure out how to format the command, ask on a forum or mailing list.
After the list of arguments, there is usually an examples section that gives various usage examples of the given command.
Going back to our example MEL command:
polySphere -r 1 -sx 20 -sy 20 -ax 0 1 0 -cuv 2 -ch 1;we can see that each of the MEL flags corresponds to an argument in the Python function. By looking up the flags in the Python documentation, we can write the equivalent Python command:
view sourceprint?cmds.polySphere(r=1, sx=20, sy=20, ax=(0, 1, 0), cuv=2, ch=1)
As a personal preference, I would write this command as
view sourceprint?cmds.polySphere(radius=1, subdivisionsX=20, subdivisionsY=20, axis=(0, 1, 0), createUVs=2, constructionHistory=True)
It’s up to you on whether to use the abbreviated flags or not. Note that I also swapped the ch=1 for ch=True. Refering to the documentation, the constructionHistory argument expects a Boolean value. Remember from the Booleans and Comparing Values section that all non-zero numbers are evaluated as True. I like to actually pass in the value True (or False if you want False) to these types of arguments just for my own preference.
You will also notice in the documentation the letters in the colored squares. The C, Q, and E stand for Create, Query, and Edit (You can ignore the M, I never pay attention to it). These letters tell you in what context an argument is valid. Many commands have different functionality depending on what context you are running the command in. In the previous example, we were creating a sphere, so all the arguments marked C were valid.
When you run a command in query mode, you can find information about an object created with the same command. You run a command in query mode by passing query=True as an argument.
view sourceprint?>>> x = cmds.polySphere(radius=2.5)
>>> print cmds.polySphere(x[1], query=True, radius=True)
2.5
When you query a command, you pass in the object you want to query first, followed by your arguments. When in query mode, you pass a True or False to the argument you want to query regardless of what the expected type for that argument is documented as. You should only query one argument at a time. You’ll notice that when you query a value, the value returned from the function may not be the same as what the documentation says is returned. In create mode, the polySphere command returns a list of 2 strings. In query mode, the return type depends on the value you are querying.
view sourceprint?>>> x = cmds.polySphere()
>>> print cmds.polySphere(x[0], query=True, radius=True)
1.0
>>> print cmds.polySphere(x[0], query=True, axis=True)
[0.0, 1.0, 0.0]
Besides create and query modes, you can also run a command in edit mode. Edit mode lets you edit values of an existing node created with the command. You run a command in edit mode by passing in edit=True as an argument to the function.
view sourceprint?>>> x = cmds.polySphere()
>>> cmds.polySphere(x[1], edit=True, radius=5) # Change the radius to 5
You now know how to look up command syntax and decipher the documentation. You have just about all the knowledge you need now to write your own Maya scripts in Python. All you need now is to learn the various commands. A really good way to do that is to look at other people’s scripts.
Sample Scripts
In most of the sample scripts, you will notice that I always put the code in functions. When you import a module, all of the code in the module gets executed. However code inside functions does not get run until the function is called. When writing scripts for Maya, it is good practice to structure your code as functions to be called by users. Otherwise, you may surprise your users by executing unwanted code when they import your modules.
lightIntensity.py
view sourceprint?import maya.cmds as cmds
def changeLightIntensity(percentage=1.0):
"""
Changes the intensity of each light in the scene by a percentange.
Parameters:
percentage - Percentange to change each light's intensity. Default value is 1.
Returns:
Nothing.
"""
# The ls command is the list command. It is used to list various nodes
# in the current scene. You can also use it to list selected nodes.
lightsInScene = cmds.ls(type='light')
# If there are no lights in the scene, there is no point running this script
if not lightsInScene:
raise RuntimeError, 'There are no lights in the scene!'
# Loop through each light
for light in lightsInScene:
# The getAttr command is used to get attribute values of a node
currentIntensity = cmds.getAttr('%s.intensity' % light)
# Calculate a new intensity
newIntensity = currentIntensity * percentage
# Change the lights intensity to the new intensity
cmds.setAttr('%s.intensity' % light, newIntensity)
# Report to the user what we just did
print 'Changed the intensity of light %s from %.3f to %.3f' % (light, currentIntensity, newIntensity)
Concepts used: functions, lists, for loops, conditional statements, string formatting, exceptions.
To run this script, in the script editor type:
view sourceprint?import samples.lightIntensity as lightIntensity
lightIntensity.changeLightIntensity(1.2)
renamer.py
view sourceprint?import maya.cmds as cmds
def rename(name, nodes=None):
"""
Renames a hierarchy of transforms using a naming string with '#' characters.
If you select the root joint of a 3 joint chain and pass in 'C_spine##_JNT',
the joints will be named C_spine01_JNT, C_spine02_JNT, and C_spine03_JNT.
Parameters:
name - A renaming format string. The string must contain a consecutive
sequence of '#' characters
nodes - List of root nodes you want to rename. If this argument is omitted,
the script will use the currently selected nodes.
Returns:
Nothing.
"""
# The variable "nodes" has a default value of None. If we do not specify a value
# for nodes, it will be None. If this is the case, we will store a list of the
# currently selected nodes in the variable nodes.
if nodes == None:
# The ls command is the list command. Get all selected nodes
# of type transform.
nodes = cmds.ls(sl=True, type='transform')
# If nothing is selected, nodes will be None so we don't need
# to continue with the script
if nodes == None:
raise RuntimeError, 'Select a root node to rename.'
# Find out how many '#' characters are in the passed in name.
numDigits = name.count('#')
if numDigits == 0:
raise RuntimeError, 'Name has no # sequence.'
# We need to verify that all the '#' characters are in one sequence.
substring = '#' * numDigits # '#' * 3 is the same as '###'
newsubstring = '0' * numDigits # '0' * 3 is the same as '000'
# The replace command of a string will replace all occurances of the first
# argument with the second argument. If the first argument is not found in
# the string, the original string is returned.
newname = name.replace(substring, newsubstring)
# If the string returned after the replace command is the same as
# the original string, it means that the sequence of '#' was not found in
# our specified name. This would happen if the '#' characters were not all
# consecutive (e.g. 'my##New##Name').
if newname == name:
raise RuntimeError, 'Pound signs must be consecutive..'
# Here we are creating a format string to use in our naming. The number of digits
# is determined by the number of consecutive '#' characters.
# Example 'C_spine##_JNT' has 2 '#' characters.
# In a chain of 3 joints, we would want to name the joints
# C_spine01_JNT, C_spine02_JNT, C_spine03_JNT.
# In a format string we would want to say 'C_spine%02d_JNT' % number.
# We are creating the '%02d' part here.
name = name.replace(substring, '%0' + str(numDigits) + 'd')
# Start at number 1
number = 1
for node in nodes:
# Loop through each selected node and rename its child hierarchy.
number = renameChain(node, name, number)
def renameChain(node, name, number):
"""
Recursive function that renames the passed in node to name % number.
Parameters:
node - The node to rename.
name - A renaming format string. The string must contain a
consecutive sequence of '#' characters
number - The number to use in the renaming.
Returns:
The next number to use in the renaming chain.
"""
# Create the new name. The variable name is a string like 'C_spine%02d_JNT'
# so when we say name % number, it is the same as 'C_spine%02d_JNT' % number
newName = (name % number)
# The rename command renames a node. Sometimes you have to be careful.
# If you try to rename a node and there's already a node with the same name,
# Maya will add a number to the end of your new name. The returned string of
# the rename command is the name that Maya actually assigns the node.
node = cmds.rename(node, newName)
# The listRelatives command is used to get a list of nodes in a dag
# hierarchy. You can get child nodes, parent nodes, shape nodes, or all
# nodes in a hierarchy. Here we are getting the child nodes.
children = cmds.listRelatives(node, children=True, type='transform', fullPath=True)
# Since we renamed the current node, we increment the number for the next
# node to be renamed.
number += 1
if children:
for child in children:
# We will call the renameChain function for each child of this node.
number = renameChain(child, name, number)
return number
Concepts used: functions, recursive functions, lists, for loops, conditionals, string formatting, exceptions.
To run this script, select the root joint of a joint chain and in the script editor type:
view sourceprint?import samples.renamer as renamer
renamer.rename('C_tail##_JNT')
The renamer script uses a concept called recursive functions. A recursive function is a function that calls itself. Recursive function are useful when you are performing operations on data in a hierarchical graph, like Maya’s DAG. In recursive functions, you must specify an ending condition or else the function will call itself in an infinite loop. In the above example, the function calls itself for each child node in the hierarchy. Since there are always a limited number of children in a Maya hierarchy, the recursive function is guaranteed to stop at some point.
blendShapes.py
view sourceprint?import maya.cmds as cmds
import os
# The weights of these shapes are the product of the weights of the two listed shapes
COMBINATION_SHAPES = {
'innerbrowraiser' : ('browsdown', 'browsup'),
'outerbrowraiser' : ('browsup', 'nosewrinkler'),
'nosewrinklesmile' : ('nosewrinkler', 'lipcornerpuller'),
'eyesclosedsquint' : ('eyesclosed', 'squint'),
'browsdowneyesclosedsquint' : ('eyesclosedsquint', 'browsdown'),
'nosewrinklerbrowsdown' : ('nosewrinkler', 'browsdown'),
'innerbrowraisernosewrinkler' : ('innerbrowraiser', 'nosewrinkler'),
'lipcornerdepressorpucker' : ('lipcornerdepressor', 'lippucker'),
'lipcornerpullerpucker' : ('lipcornerpuller', 'lippucker'),
'lipcornerpullerupperlipraiser' : ('lipcornerpuller', 'upperlipraiser'),
'eyeslookupleft' : ('eyeslookup', 'eyeslookleft'),
'eyeslookupright' : ('eyeslookup', 'eyeslookright'),
'eyeslookdownleft' : ('eyeslookdown', 'eyeslookleft'),
'eyeslookdownright' : ('eyeslookdown', 'eyeslookright'),
}
def importShapes(directory):
"""
Imports the shapes from directory.
Parameters:
directory - Directory that holds all the shape obj's.
Returns:
The created blendShape node, the neutral transform, and a list of all the imported shape transforms.
"""
# Get shape file paths
shapes = []
for root, dirs, files in os.walk(directory):
for name in files:
path = os.path.join(root, name)
if name.startswith('neutral') and name.endswith('.obj'):
# Put the neutral at the start of the list
shapes.insert(0, path)
elif name.endswith('.obj'):
# Append the shape to the list
shapes.append(path)
# Create node to control all shape weights in 0-1 range.
faceShapesTransform = cmds.createNode('transform', name='faceShapes')
targets = []
neutral = ''
# Import shapes from blendshape folder and create blendShape node on neutral
for shape in shapes:
# Import the shape into a namespace called TEMP
cmds.file(shape, i=True, namespace='TEMP', type='OBJ', options='mo=0')
# Get the imported shape
transform = cmds.ls('TEMP:*', type='transform')[0]
# Rename the shape based off of the file name
newName = os.path.basename(shape).split('.')[0]
transform = cmds.rename(transform, newName)
print 'Importing %s' % transform
# Delete everything else in the namespace
try:
cmds.delete('TEMP:*')
except:
pass
# Remove the temporary namespace since we don't need it anymore
cmds.namespace(removeNamespace='TEMP')
if transform.startswith('neutral'):
# Create blendShape on neutral.
neutral = transform
# Unfreeze the normals, sometimes they get frozen in obj's
cmds.polyNormalPerVertex(transform, unFreezeNormal=True)
# Soften the normals on the neutral
cmds.polySoftEdge(transform, angle=180)
# Delete history on the neutral
cmds.delete(transform, constructionHistory=True)
# Create a blendshape on the neutral
blendShape = cmds.blendShape(neutral, name='faceShapes_BLS')[0]
else:
# Store the target transform in the list
targets.append(transform)
# Sort the targets alphabetically
targets.sort()
# index will store the index of the target on the blendshape node
index = 0
# inbetweens will store all the inbetween targets and what index they belong to on
# the blendshape node
inbetweens = []
for target in targets:
if target.endswith('_100'):
# Strip off the '_100' from the name
target = cmds.rename(target, target[:-4])
# Add the target to the blendshape
addShapeToBlendShape(neutral, blendShape, target, index, faceShapesTransform)
index += 1
else:
# Add inbetweens later since the target needs to exist first
inbetweens.append((target, index))
# Add the inbetweens
for node in inbetweens:
# Called the weight at which the inbetween should turn on between 0-1
onIndex = float('%.3f' % (int(node[0].split('_')[-1]) / 100.0))
# Add the inbetween target to the blendshape
addShapeToBlendShape(neutral, blendShape, node[0], node[1], faceShapesTransform, onIndex)
# Connect combination shapes
for key in COMBINATION_SHAPES.keys():
# Combination shapes are blendshapes on top of other blendshapes
# When two or more shapes are turned on, they trigger another shape
# to be turned on
# Create a multiplyDivide node to multiply two weights together
mdn = cmds.createNode('multiplyDivide', name='%s_MDN' % key)
# Hook up the inputs to the multiplyDivide node
cmds.connectAttr('%s.%s' % (faceShapesTransform, COMBINATION_SHAPES[key][0]), '%s.input1X' % mdn)
cmds.connectAttr('%s.%s' % (faceShapesTransform, COMBINATION_SHAPES[key][1]), '%s.input2X' % mdn)
print 'Connecting combination shape %s driven by the product of %s and %s' % (key, COMBINATION_SHAPES[key][0], COMBINATION_SHAPES[key][1])
# Connect the output of the multiplyDivide node to the blendShape control
cmds.connectAttr('%s.outputX' % mdn, '%s.%s' % (faceShapesTransform, key))
def addShapeToBlendShape(neutral, blendShape, mesh, index, faceShapesTransform, onIndex=1.0):
"""
Adds a shape to the blendShape node.
Parameters:
neutral - The neutral mesh
blendShape - The blendShape node to add shapes to.
mesh - Target mesh
index - BlendShape target index
faceShapesTransform - Node to add control attributes to.
onIndex - Index at which the target is fully on. Normally this
is 1.0. For inbetweens, this is between 0 and 1.
Returns:
Nothing
"""
if onIndex == 1.0:
# Add a new shape
print 'Adding shape %s to %s at index %d' % (mesh, blendShape, index)
cmds.blendShape(blendShape, edit=True, target=(neutral, index, mesh, onIndex))
else:
# Add an inbetween shape
print 'Adding inbetween shape %s to %s at index %d with an onWeight of %.3f' % (mesh, blendShape, index, onIndex)
cmds.blendShape(blendShape, edit=True, target=(neutral, index, mesh, onIndex), inBetween=True)
if onIndex == 1.0:
# Add attribute to the control node to control this blendshape target
cmds.addAttr(faceShapesTransform, longName=mesh, shortName=mesh, attributeType='float', keyable=True, min=0.0, max=1.0)
# Connect the new control attribute to the blendshape weight
cmds.connectAttr('%s.%s' % (faceShapesTransform, mesh), '%s.%s' % (blendShape, mesh))
cmds.delete(mesh)
# Sometimes Maya crashes when it runs out of memory so free some up by clear the
# undo queue.
cmds.flushUndo()
Concepts used: functions, lists, for loops, conditionals, string formatting, dictionaries.
To run this script, in the script editor type:
view sourceprint?import samples.blendShapes as blendShapes
blendShapes.importShapes(r”C:\pathToBlendShapesFolder”)
Calling MEL from Python
Most Maya commands (MEL commands) have been implemented into the maya.cmds module. There are still some cases where MEL must be used because Maya does not fully incorporate Python is all aspects of its architecture.
MEL can be called from Python with the maya.mel module:
view sourceprint?import maya.cmds as cmds
selection = cmds.ls( sl=True )
import maya.mel as mel
selection = mel.eval( "ls -sl" )
We can also invoke python from MEL
string $list[] = python( "['a', 'b', 'c']" );
size( $list );
// Result: 3 //
To source/execute existing MEL scripts in Python:
view sourceprint?import maya.mel as mel
mel.eval('source "myScript.mel"')
mel.eval('source "myOtherScript.mel"')
mel.eval('mySourcedFunction(1)')
Maya Python Standalone Applications
Maya provides the maya.standalone module for creating command-line applications. These applications allow us to create and run operations without opening Maya’s interface. Maya Python standalone applications are run with the mayapy interpreter.
Run “mayapy” from the Run… dialog in Windows, or run “mayapy” from a command line:
Figure 16 - Running Maya from the Command Line
To open a file in batch mode, you would run:
mayapy X:\file.pyYou can also write scripts to run operations on Maya files in batch mode.
Example: Open a Maya file, assign the default shader to all meshes, and save the scene.
view sourceprint?import maya.standalone
import maya.cmds as cmds
def assignDefaultShader(fileToOpen):
# Start Maya in batch mode
maya.standalone.initialize(name='python')
# Open the file with the file command
cmds.file(fileToOpen, force=True, open=True)
# Get all meshes in the scene
meshes = cmds.ls(type="mesh", long=True)
for mesh in meshes:
# Assign the default shader to the mesh by adding the mesh to the
# default shader set.
cmds.sets(mesh, edit=True, forceElement='initialShadingGroup')
# Save the file
cmds.file(save=True, force=True)
In order to run this script, you need to use the mayapy Python interpreter and not the normal Python interpreter. In addition to stand alone scripts, I recommend reading about the optparse module which would allow you to start the mayapy interpreter and call the script all in one line from the command line.
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment