This article, written by Adrian Herbez, author of Maya Programming with Python Cookbook, will cover various recipes related to animating objects with scripting:
Querying animation data
Working with animation layers
Copying animation from one object to another
Setting keyframes
Creating expressions via script
(For more resources related to this topic, see here.)
In this article, we'll be looking at how to use scripting to create animation and set keyframes. We'll also see how to work with animation layers and create expressions from code.
Querying animation data
In this example, we'll be looking at how to retrieve information about animated objects, including which attributes are animated and both the location and value of keyframes. Although this script is unlikely to be useful by itself, knowing the number, time, and values of keyframes is sometimes a prerequisite for more complex animation tasks.
Getting ready
To make get the most out of this script, you'll need to have an object with some animation curves defined. Either load up a scene with animation or skip ahead to the recipe on setting keyframes.
How to do it...
Create a new file and add the following code:
import maya.cmds as cmds
def getAnimationData():
objs = cmds.ls(selection=True)
obj = objs[0]
animAttributes = cmds.listAnimatable(obj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True,
keyframeCount=True)
if (numKeyframes > 0):
print("---------------------------")
print("Found ", numKeyframes, " keyframes on ",
attribute)
times = cmds.keyframe(attribute, query=True,
index=(0,numKeyframes), timeChange=True)
values = cmds.keyframe(attribute, query=True,
index=(0,numKeyframes), valueChange=True)
print('frame#, time, value')
for i in range(0, numKeyframes):
print(i, times[i], values[i])
print("---------------------------")
getAnimationData()
If you select an object with animation curves and run the script, you should see a readout of the time and value for each keyframe on each animated attribute. For example, if we had a simple bouncing ball animation with the following curves:
We would see something like the following output in the script editor:
---------------------------
('Found ', 2, ' keyframes on ', u'|bouncingBall.translateX')
frame#, time, value
(0, 0.0, 0.0)
(1, 190.0, 38.0)
---------------------------
---------------------------
('Found ', 20, ' keyframes on ', u'|bouncingBall.translateY')
frame#, time, value
(0, 0.0, 10.0)
(1, 10.0, 0.0)
(2, 20.0, 8.0)
(3, 30.0, 0.0)
(4, 40.0, 6.4000000000000004)
(5, 50.0, 0.0)
(6, 60.0, 5.120000000000001)
(7, 70.0, 0.0)
(8, 80.0, 4.096000000000001)
(9, 90.0, 0.0)
(10, 100.0, 3.276800000000001)
(11, 110.0, 0.0)
(12, 120.0, 2.6214400000000011)
(13, 130.0, 0.0)
(14, 140.0, 2.0971520000000008)
(15, 150.0, 0.0)
(16, 160.0, 1.6777216000000008)
(17, 170.0, 0.0)
(18, 180.0, 1.3421772800000007)
(19, 190.0, 0.0)
---------------------------
How it works...
We start out by grabbing the selected object, as usual. Once we've done that, we'll iterate over all the keyframeable attributes, determine if they have any keyframes and, if they do, run through the times and values. To get the list of keyframeable attributes, we use the listAnimateable command:
objs = cmds.ls(selection=True)
obj = objs[0]
animAttributes = cmds.listAnimatable(obj)
This will give us a list of all the attributes on the selected object that can be animated, including any custom attributes that have been added to it.
If you were to print out the contents of the animAttributes array, you would likely see something like the following:
|bouncingBall.rotateX
|bouncingBall.rotateY
|bouncingBall.rotateZ
Although the bouncingBall.rotateX part likely makes sense, you may be wondering about the | symbol. This symbol is used by Maya to indicate hierarchical relationships between nodes in order to provide fully qualified node and attribute names. If the bouncingBall object was a child of a group named ballGroup, we would see this instead:
|ballGroup|bouncingBall.rotateX
Every such fully qualified name will contain at least one pipe (|) symbol, as we see in the first, nongrouped example, but there can be many more—one for each additional layer of hierarchy. While this can lead to long strings for attribute names, it allows Maya to make use of objects that may have the same name, but under different parts of a larger hierarchy (to have control objects named handControl for each hand of a character, for example).
Now that we have a list of all of the possibly animated attributes for the object, we'll next want to determine if there are any keyframes set on it. To do this, we can use the keyframe command in the query mode.
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True,
keyframeCount=True)
At this point, we have a variable (numKeyframes) that will be greater than zero for any attribute with at least one keyframe. Getting the total number of keyframes on an attribute is only one of the things that the keyframe command can do; we'll also use it to grab the time and value for each of the keyframes.
To do this, we'll call it two more times, both in the query mode—once to get the times and once to get the values:
times = cmds.keyframe(attribute, query=True,
index=(0,numKeyframes), timeChange=True)
values = cmds.keyframe(attribute, query=True,
index=(0,numKeyframes), valueChange=True)
These two lines are identical in everything except what type of information we're asking for. The important thing to note here is the index flag, which is used to tell Maya which keyframes we're interested in. The command requires a two-element argument representing the first (inclusive) and last (exclusive) index of keyframes to examine. So, if we had total 20 keyframes, we would pass in (0,20), which would examine the keys with indices from 0 to 19.
The flags we're using to get the values likely look a bit odd—both valueChange and timeChange might lead you to believe that we would be getting relative values, rather than absolute. However, when used in the previously mentioned manner, the command will give us what we want—the actual time and value for each keyframe, as they appear in the graph editor.
If you want to query information on a single keyframe, you still have to pass in a pair of values- just use the index that you're interested in twice- to get the fourth frame, for example, use (3,3).
At this point, we have two arrays—the times array, which contains the time value for each keyframe, and the values array that contains the actual attribute value. All that's left is to print out the information that we've found:
print('frame#, time, value')
for i in range(0, numKeyframes):
print(i, times[i], values[i])
There's more...
Using the indices to get data on keyframes is an easy way to run through all of the data for a curve, but it's not the only way to specify a range. The keyframe command can also accept time values. If we wanted to know how many keyframes existed on a given attribute between frame 1 and frame 100, for example, we could do the following:
numKeyframes = cmds.keyframe(attributeName, query=True,
time=(1,100) keyframeCount=True)
Also, if you find yourself with highly nested objects and need to extract just the object and attribute names, you may find Python's built-in split function helpful. You can call split on a string to have Python break it up into a list of parts. By default, Python will break up the input string by spaces, but you can specify a particular string or character to split on. Assume that you have a string like the following:
|group4|group3|group2|group1|ball.rotateZ
Then, you could use split to break it apart based on the | symbol. It would give you a list, and using −1 as an index would give you just ball.rotateZ. Putting that into a function that can be used to extract the object/attribute names from a full string would be easy, and it would look something like the following:
def getObjectAttributeFromFull(fullString):
parts = fullString.split("|")
return parts[-1]
Using it would look something like this:
inputString = "|group4|group3|group2|group1|ball.rotateZ"
result = getObjectAttributeFromFull(inputString)
print(result) # outputs "ball.rotateZ"
Working with animation layers
Maya offers the ability to create multiple layers of animation in a scene, which can be a good way to build up complex animation. The layers can then be independently enabled or disabled, or blended together, granting the user a great deal of control over the end result.
In this example, we'll be looking at how to examine the layers that exist in a scene, and building a script will ensure that we have a layer of a given name. For example, we might want to create a script that would add additional randomized motion to the rotations of selected objects without overriding their existing motion. To do this, we would want to make sure that we had an animation layer named randomMotion, which we could then add keyframes to.
How to do it...
Create a new script and add the following code:
import maya.cmds as cmds
def makeAnimLayer(layerName):
baseAnimationLayer = cmds.animLayer(query=True, root=True)
foundLayer = False
if (baseAnimationLayer != None):
childLayers = cmds.animLayer(baseAnimationLayer,
query=True, children=True)
if (childLayers != None) and (len(childLayers) > 0):
if layerName in childLayers:
foundLayer = True
if not foundLayer:
cmds.animLayer(layerName)
else:
print('Layer ' + layerName + ' already exists')
makeAnimLayer("myLayer")
Run the script, and you should see an animation layer named myLayer appear in the Anim tab of the channel box.
How it works...
The first thing that we want to do is to find out if there is already an animation layer with the given name present in the scene. To do this, we start by grabbing the name of the root animation layer:
baseAnimationLayer = cmds.animLayer(query=True, root=True)
In almost all cases, this should return one of two possible values—either BaseAnimation or (if there aren't any animation layers yet) Python's built-in None value.
We'll want to create a new layer in either of the following two possible cases:
There are no animation layers yet
There are animation layers, but none with the target name
In order to make the testing for the above a bit easier, we first create a variable to hold whether or not we've found an animation layer and set it to False:
foundLayer = False
Now we need to check to see whether it's true that both animation layers exist and one of them has the given name. First off, we check that there was, in fact, a base animation layer:
if (baseAnimationLayer != None):
If this is the case, we want to grab all the children of the base animation layer and check to see whether any of them have the name we're looking for. To grab the children animation layers, we'll use the animLayer command again, again in the query mode:
childLayers = cmds.animLayer(baseAnimationLayer, query=True,
children=True)
Once we've done that, we'll want to see if any of the child layers match the one we're looking for. We'll also need to account for the possibility that there were no child layers (which could happen if animation layers were created then later deleted, leaving only the base layer):
if (childLayers != None) and (len(childLayers) > 0):
if layerName in childLayers:
foundLayer = True
If there were child layers and the name we're looking for was found, we set our foundLayer variable to True.
If the layer wasn't found, we create it. This's easily done by using the animLayer command one more time, with the name of the layer we're trying to create:
if not foundLayer:
cmds.animLayer(layerName)
Finally, we finish off by printing a message if the layer was found to let the user know.
There's more...
Having animation layers is great, in that we can make use of them when creating or modifying keyframes. However, we can't actually add animation to layers without first adding the objects in question to the animation layer.
Let's say that we had an object named bouncingBall, and we wanted to set some keyframes on its translateY attribute, in the bounceLayer animation layer. The actual command to set the keyframe(s) would look something like this:
cmds.setKeyframe("bouncingBall.translateY", value=yVal,
time=frame, animLayer="bounceLayer")
However, this would only work as expected if we had first added the bouncingBall object to the bounceLayer animation layer. To do it, we could use the animLayer command in the edit mode, with the addSelectedObjects flag. Note that because the flag operates on the currently selected objects, we would need to first select the object we want to add:
cmds.select("bouncingBall", replace=True)
cmds.animLayer("bounceLayer", edit=True, addSelectedObjects=True)
Adding the object will, by default, add all of its animatable attributes. You can also add specific attributes, rather than entire objects. For example, if we only wanted to add the translateY attribute to our animation layer, we could do the following:
cmds.animLayer("bounceLayer", edit=True,
attribute="bouncingBall.translateY")
Copying animation from one object to another
In this example, we'll create a script that will copy all of the animation data on one object to one or more additional objects, which could be useful to duplicate motion across a range of objects.
Getting ready
For the script to work, you'll need an object with some keyframes set. Either create some simple animation or skip ahead to the example on creating keyframes with script, later in this article.
How to do it...
Create a new script and add the following code:
import maya.cmds as cmds
def getAttName(fullname):
parts = fullname.split('.')
return parts[-1]
def copyKeyframes():
objs = cmds.ls(selection=True)
if (len(objs) < 2):
cmds.error("Please select at least two objects")
sourceObj = objs[0]
animAttributes = cmds.listAnimatable(sourceObj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True,
keyframeCount=True)
if (numKeyframes > 0):
cmds.copyKey(attribute)
for obj in objs[1:]:
cmds.pasteKey(obj,
attribute=getAttName(attribute), option="replace")
copyKeyframes()
Select the animated object, shift-select at least one other object, and run the script. You'll see that all of the objects have the same motion.
How it works...
The very first part of our script is a helper function that we'll be using to strip the attribute name off a full object name/attribute name string. More on it will be given later.
Now on to the bulk of the script. First off, we run a check to make sure that the user has selected at least two objects. If not, we'll display a friendly error message to let the user know what they need to do:
objs = cmds.ls(selection=True)
if (len(objs) < 2):
cmds.error("Please select at least two objects")
The error command will also stop the script from running, so if we're still going, we know that we had at least two objects selected. We'll set the first one to be selected to be our source object. We could just as easily use the second-selected object, but that would mean using the first selected object as the destination, limiting us to a single target:
sourceObj = objs[0]
Now we're ready to start copying animation, but first, we'll need to determine which attributes are currently animated, through a combination of finding all the attributes that can be animated, and checking each one to see whether there are any keyframes on it:
animAttributes = cmds.listAnimatable(sourceObj);
for attribute in animAttributes:
numKeyframes = cmds.keyframe(attribute, query=True,
keyframeCount=True)
If we have at least one keyframe for the given attribute, we move forward with the copying:
if (numKeyframes > 0):
cmds.copyKey(attribute)
The copyKey command will cause the keyframes for a given object to be temporarily held in memory. If used without any additional flags, it will grab all of the keyframes for the specified attribute, exactly what we want in this case. If we wanted only a subset of the keyframes, we could use the time flag to specify a range.
We're passing in each of the values that were returned by the listAnimatable function. These will be full names (both object name and attribute). That's fine for the copyKey command, but will require a bit of additional work for the paste operation.
Since we're copying the keys onto a different object than the one that we copied them from, we'll need to separate out the object and attribute names. For example, our attribute value might be something like this:
|group1|bouncingBall.rotateX
From this, we'll want to trim off just the attribute name (rotateX) since we're getting the object name from the selection list. To do this, we created a simple helper function that takes a full-length object/attribute name and returns just the attribute name. That's easy enough to do by just breaking the name/attribute string apart on the . and returning the last element, which in this case is the attribute:
def getAttName(fullname):
parts = fullname.split('.')
return parts[-1]
Python's split function breaks apart the string into an array of strings, and using a negative index will count back from the end, with −1 giving us the last element.
Now we can actually paste our keys. We'll run through all the remaining selected objects, starting with the second, and paste our copied keyframes:
for obj in objs[1:]:
cmds.pasteKey(obj, attribute=getAttName(attribute),
option="replace")
Note that we're using the nature of Python's for loops to make the code a bit more readable. Rather than using an index, as would be the case in most other languages, we can just use the for x in y construction. In this case, obj will be a temporary variable, scoped to the for loop, that takes on the value of each item in the list. Also note that instead of passing in the entire list, we use objs[1:] to indicate the entire list, starting at index 1 (the second element). The colon allows us to specify a subrange of the objs list, and leaving the right-hand side blank will cause Python to include all the items to the end of the list.
We pass in the name of the object (from our original selection), the attribute (stripped from full name/attribute string via our helper function), and we use option="replace" to ensure that the keyframes we're pasting in replace anything that's already there.
Original animation (top). Here, we see the result of pasting keys with the default settings (left) and with the replace option (right). Note that the default results still contain the original curves, just pushed to later frames
If we didn't include the option flag, Maya would default to inserting the pasted keyframes while moving any keyframes already present forward in the timeline.
There's more...
There are a lot of other options for the option flag, each of which handles possible conflicts with the keys you're pasting and the ones that may already exist in a slightly different way. Be sure to have a look at the built-in documentation for the pasteKeys command for more information.
Another, and perhaps better option to control how pasted keys interact with existing one is to paste the new keys into a separate animation layer. For example, if we wanted to make sure that our pasted keys end up in an animation layer named extraAnimation, we could modify the call to pasteKeys as follows:
cmds.pasteKey(objs[i], attribute=getAttName(attribute),
option="replace", animLayer="extraAnimation")
Note that if there was no animation layer named extraAnimation present, Maya would fail to copy the keys. See the section on working with animation layers for more information on how to query existing layers and create new ones.
Setting keyframes
While there are certainly a variety of ways to get things to move in Maya, the vast majority of motion is driven by keyframes. In this example, we'll be looking at how to create keyframes with code by making that old animation standby—a bouncing ball.
Getting ready
The script we'll be creating will animate the currently selected object, so make sure that you have an object—either the traditional sphere or something else you'd like to make bounce.
How to do it...
Create a new file and add the following code:
import maya.cmds as cmds
def setKeyframes():
objs = cmds.ls(selection=True)
obj = objs[0]
yVal = 0
xVal = 0
frame = 0
maxVal = 10
for i in range(0, 20):
frame = i * 10
xVal = i * 2
if i % 2 == 1:
yVal = 0
else:
yVal = maxVal
maxVal *= 0.8
cmds.setKeyframe(obj + '.translateY', value=yVal,
time=frame)
cmds.setKeyframe(obj + '.translateX', value=xVal,
time=frame)
setKeyframes()
Run the preceding script with an object selected and trigger playback. You should see the object move up and down.
How it works...
In order to get our object to bounce, we'll need to set keyframes such that the object alternates between a Y-value of zero and an ever-decreasing maximum so that the animation mimics the way a falling object loses velocity with each bounce. We'll also make it move forward along the x-axis as it bounces.
We start by grabbing the currently selected object and setting a few variables to make things easier to read as we run through our loop. Our yVal and xVal variables will hold the current value that we want to set the position of the object to. We also have a frame variable to hold the current frame and a maxVal variable, which will be used to hold the Y-value of the object's current height.
This example is sufficiently simple that we don't really need separate variables for frame and the attribute values, but setting things up this way makes it easier to swap in more complex math or logic to control where keyframes get set and to what value.
This gives us the following:
yVal = 0
xVal = 0
frame = 0
maxVal = 10
The bulk of the script is a single loop, in which we set keyframes on both the X and Y positions.
For the xVal variable, we'll just be multiplying a constant value (in this case, 2 units). We'll do the same thing for our frame. For the yVal variable, we'll want to alternate between an ever-decreasing value (for the successive peaks) and zero (for when the ball hits the ground).
To alternate between zero and non-zero, we'll check to see whether our loop variable is divisible by two. One easy way to do this is to take the value modulo (%) 2. This will give us the remainder when the value is divided by two, which will be zero in the case of even numbers and one in the case of odd numbers.
For odd values, we'll set yVal to zero, and for even ones, we'll set it to maxVal. To make sure that the ball bounces a little less each time, we set maxVal to 80% of its current value each time we make use of it.
Putting all of that together gives us the following loop:
for i in range(0, 20):
frame = i * 10
xVal = i * 2
if (i % 2) == 1:
yVal = 0
else:
yVal = maxVal
maxVal *= 0.8
Now we're finally ready to actually set keyframes on our object. This's easily done with the setKeyframe command. We'll need to specify the following three things:
The attribute to keyframe (object name and attribute)
The time at which to set the keyframe
The actual value to set the attribute to
In this case, this ends up looking like the following:
cmds.setKeyframe(obj + '.translateY', value=yVal, time=frame)
cmds.setKeyframe(obj + '.translateX', value=xVal, time=frame)
And that's it! A proper bouncing ball (or other object) animated with pure code.
There's more...
By default, the setKeyframe command will create keyframes with both in tangent and out tangent being set to spline. That's fine for a lot of things, but will result in overly smooth animation for something that's supposed to be striking a hard surface.
We can improve our bounce animation by keeping smooth tangents for the keyframes when the object reaches its maximum height, but setting the tangents at its minimum to be linear. This will give us a nice sharp change every time the ball strikes the ground.
To do this, all we need to do is to set both the inTangentType and outTangentType flags to linear, as follows:
cmds.setKeyframe(obj + ".translateY", value=animVal, time=frame,
inTangentType="linear", outTangentType="linear")
To make sure that we only have linear tangents when the ball hits the ground, we could set up a variable to hold the tangent type, and set it to one of two values in much the same way that we set the yVal variable.
This would end up looking like this:
tangentType = "auto"
for i in range(0, 20):
frame = i * 10
if i % 2 == 1:
yVal = 0
tangentType = "linear"
else:
yVal = maxVal
tangentType = "spline"
maxVal *= 0.8
cmds.setKeyframe(obj + '.translateY', value=yVal, time=frame,
inTangentType=tangentType, outTangentType=tangentType)
Creating expressions via script
While most animation in Maya is created manually, it can often be useful to drive attributes directly via script, especially for mechanical objects or background items. One way to approach this is through Maya's expression editor.
In addition to creating expressions via the expression editor, it is also possible to create expressions with scripting, in a beautiful example of code-driven code. In this example, we'll be creating a script that can be used to create a sine wave-based expression to smoothly alter a given attribute between two values. Note that expressions cannot actually use Python code directly; they require the code to be written in the MEL syntax. But this doesn't mean that we can't use Python to create expressions, which is what we'll do in this example.
Getting ready
Before we dive into the script, we'll first need to have a good handle on the kind of expression we'll be creating. There are a lot of different ways to approach expressions, but in this instance, we'll keep things relatively simple and tie the attribute to a sine wave based on the current time.
Why a sine wave? Sine waves are great because they alter smoothly between two values, with a nice easing into and out of both the minimum and maximums. While the minimum and maximum values range from −1 to 1, it's easy enough to alter the output to move between any two numbers we want. We'll also make things a bit more flexible by setting up the expression to rely on a custom speed attribute that can be used to control the rate at which the attribute animates.
The end result will be a value that varies smoothly between any two numbers at a user-specified (and keyframeable) rate.
How to do it...
Create a new script and add the following code:
import maya.cmds as cmds
def createExpression(att, minVal, maxVal, speed):
objs = cmds.ls(selection=True)
obj = objs[0]
cmds.addAttr(obj, longName="speed", shortName="speed", min=0,
keyable=True)
amplitude = (maxVal – minVal)/2.0
offset = minVal + amplitude
baseString = "{0}.{1} = ".format(obj, att)
sineClause = '(sin(time * ' + obj + '.speed)'
valueClause = ' * ' + str(amplitude) + ' + ' + str(offset) +
')'
expressionString = baseString + sineClause + valueClause
cmds.expression(string=expressionString)
createExpression('translateY', 5, 10, 1)
How it works...
The first that we do is to add a speed attribute to our object. We'll be sure to make it keyable for later animation:
cmds.addAttr(obj, longName="speed", shortName="speed", min=0,
keyable=True)
It's generally a good idea to include at least one keyframeable attribute when creating expressions. While math-driven animation is certainly a powerful technique, you'll likely still want to be able to alter the specifics. Giving yourself one or more keyframeable attributes is an easy way to do just that.
Now we're ready to build up our expression. But first, we'll need to understand exactly what we want; in this case, a value that smoothly varies between two extremes, with the ability to control its speed. We can easily build an expression to do that using the sine function, with the current time as the input. Here's what it looks like in a general form:
animatedValue = (sin(time * S) * M) + O;
Where:
S is a value that will either speed up (if greater than 1) or slow down (if less) the rate at which the input to the sine function changes
M is a multiplier to alter the overall range through which the value changes
O is an offset to ensure that the minimum and maximum values are correct
You can also think about it visually—S will cause our wave to stretch or shrink along the horizontal (time) axis, M will expand or contract it vertically, and O will move the entire shape of the curve either up or down.
S is already taken care of; it's our newly created "speed" attribute. M and O will need to be calculated, based on the fact that sine functions always produce values ranging from −1 to 1.
The overall range of values should be from our minVal to our maxVal, so you might think that M should be equal to (maxVal – minVal). However, since it gets applied to both −1 and 1, this would leave us with double the desired change. So, the final value we want is instead (maxVal – minVal)/2. We store that into our amplitude variable as follows:
amplitude = (maxVal – minVal)/2.0
Next up is the offset value O. We want to move our graph such that the minimum and maximum values are where they should be. It might seem like that would mean just adding our minVal, but if we left it at that, our output would dip below the minimum for 50% of the time (anytime the sine function is producing negative output). To fix it, we set O to (minVal + M) or in the case of our script:
offset = minVal + amplitude
This way, we move the 0 position of the wave to be midway between our minVal and maxVal, which is exactly what we want.
To make things clearer, let's look at the different parts we're tacking onto sin(), and the way they effect the minimum and maximum values the expression will output. We'll assume that the end result we're looking for is a range from 0 to 4.
Expression
Additional component
Minimum
Maximum
sin(time)
None- raw sin function
−1
1
sin(time * speed)
Multiply input by "speed"
−1 (faster)
1 (faster)
sin(time * speed) * 2
Multiply output by 2
−2
2
(sin(time * speed) * 2) + 2
Add 2 to output
0
4
Note that 2 = (4-0)/2 and 2 = 0 + 2.
Here's what the preceding progression looks like when graphed:
Four steps in building up an expression to var an attribute from 0 to 4 with a sine function.
Okay, now that we have the math locked down, we're ready to translate that into Maya's expression syntax. If we wanted an object named myBall to animate along Y with the previous values, we would want to end up with:
myBall.translateY = (sin(time * myBall.speed) * 5) + 12;
This would work as expected if entered into Maya's expression editor, but we want to make sure that we have a more general-purpose solution that can be used with any object and any values. That's straightforward enough and just requires building up the preceding string from various literals and variables, which is what we do in the next few lines:
baseString = "{0}.{1} = ".format(obj, att)
sineClause = '(sin(time * ' + obj + '.speed)'
valueClause = ' * ' + str(amplitude) + ' + ' + str(offset) + ')'
expressionString = baseString + sineClause + valueClause
I've broken up the string creation into a few different lines to make things clearer, but it's by no means necessary. The key idea here is that we're switching back and forth between literals (sin(time *, .speed, and so on) and variables (obj, att, amplitude, and offset) to build the overall string. Note that we have to wrap numbers in the str() function to keep Python from complaining when we combine them with strings.
At this point, we have our expression string ready to go. All that's left is to actually add it to the scene as an expression, which is easily done with the expression command:
cmds.expression(string=expressionString)
And that's it! We will now have an attribute that varies smoothly between any two values.
There's more...
There are tons of other ways to use expressions to drive animation, and all sorts of simple mathematical tricks that can be employed.
For example, you can easily get a value to move smoothly to a target value with a nice easing-in to the target by running this every frame:
animatedAttribute = animatedAttribute + (targetValue –
animatedAttribute) * 0.2;
This will add 20% of the current difference between the target and the current value to the attribute, which will move it towards the target. Since the amount that is added is always a percentage of the current difference, the per-frame effect reduces as the value approaches the target, providing an ease-in effect.
If we were to combine this with some code to randomly choose a new target value, we would end up with an easy way to, say, animate the heads of background characters to randomly look in different positions (maybe to provide a stadium crowd).
Assume that we had added custom attributes for targetX, targetY, and targetZ to our object that would end up looking something like the following:
if (frame % 20 == 0)
{
myCone.targetX = rand(time) * 360;
myCone.targetY = rand(time) * 360;
myCone.targetZ = rand(time) * 360;
}
myObject.rotateX += (myObject.targetX - myCone.rotateX) * 0.2;
myObject.rotateY += (myObject.targetY - myCone.rotateY) * 0.2;
myObject.rotateZ += (myObject.targetZ - myCone.rotateZ) * 0.2;
Note that we're using the modulo (%) operator to do something (setting the target) only when the frame is an even multiple of 20. We're also using the current time as the seed value for the rand() function to ensure that we get different results as the animation progresses.
The previously mentioned example is how the code would look if we entered it directly into Maya's expression editor; note the MEL-style (rather than Python) syntax. Generating this code via Python would be a bit more involved than our sine wave example, but would use all the same principles—building up a string from literals and variables, then passing that string to the expression command.
Summary
In this article, we primarily discussed scripting and animation using Maya.
Resources for Article:
Further resources on this subject:
Introspecting Maya, Python, and PyMEL [article]
Discovering Python's parallel programming tools [article]
Mining Twitter with Python – Influence and Engagement [article]
Read more