Building an animation exporter with the Maya Python API 2.0
The Maya Python API 2.0 has been extended in the latest release with new classes like
MFnAnimCurve and MItDependencyNodes, and I could not be more excited to try some code with them.
There are multiple ways to handle animation data in Maya, from the available keyframe, setKeyframe, copyKey, pasteKey, etc. commands, to the integrated ATOM file type, clips and several built-in and external methods. However, I like my data to be as software independent as possible, and I find this release to be a nice opportunity to write a custom animation data handler that will allow us to, in the future, take this data into other applications, so let's start!
I'll keep the source code on GitHub and update this blog with a summary and important points.
There are two main levels of data that we care about when working with animation nodes in maya: data stored at the curve level (is it weighted? what infinity values does it have?) and the one stored at the key level (frame & value, tangents information).
To get the maya api object of a curve, we can do it through an MSelectionList:
import maya.api.OpenMaya as om
import maya.api.OpenMayaAnim as omanim
dg_name = 'pCube1_translateX' #animCurve node name in the dependency graph
omslist = om.MSelectionList()
omslist.add(dg_name)
mcurve = omanim.MFnAnimCurve(omslist.getDependNode(0))
At the curve level, the data that we care about is stored as MFnAnimCurve properties:
mcurve.isStatic
mcurve.isWeighted
mcurve.preInfinityType
mcurve.postInfinityType
At the key level, we need to pass a key index to a method on our curve object. We can know the amount of the keys on a curve with its numKeys property:
mcurve.value(0) #returns the value of the first key
mcurve.input(0) #returns the MTime (for time based curves) of the first key
mcurve.input(0).value #frame of the first key
mcurve.isBreakdown(0)
mcurve.inTangentType(0)
for i in xrange(mcurve.numKeys):
print mcurve.value(i)
For some tangent methods, the boolean isInTangent parameter will tell maya if we're talking about the in or out tangent.
mcurve.getTangentXY(0, True) #in
mcurve.getTangentXY(0, False) #out
Ok, we're prepared to extract our raw animation data from curve nodes, and now that we know the data levels that we are going to work with and their types (properties, methods), how about we try the following?:
_API_ATTRS = { 'curve' : {'isStatic', 'isWeighted', 'preInfinityType', 'postInfinityType'},
'key' : {'value', 'isBreakdown', 'tangentsLocked', 'inTangentType', 'outTangentType'}}
def _animCurveData(mcurve):
data = {attr:getattr(mcurve, attr) for attr in _API_ATTRS['curve']}
data['keys'] = {k_i : _animCurveKeyData(mcurve, k_i) for k_i in xrange(mcurve.numKeys)}
return data
def _animCurveKeyData(mcurve, index):
data = {attr:getattr(mcurve, attr)(index) for attr in _API_ATTRS['key']}
data.update(time=mcurve.input(index).value,
in_tangent={'xy' : mcurve.getTangentXY(index, True)},
out_tangent={'xy' : mcurve.getTangentXY(index, False)})
return data
data = _animCurveData(mcurve)
data should look something like this:
{'isStatic': False,
'isWeighted': False,
'keys': {0: {'inTangentType': 2,
'in_tangent': {'xy': (1.0, 0.0)},
'isBreakdown': False,
'outTangentType': 2,
'out_tangent': {'xy': (0.2140544354915619,
0.9768217206001282)},
'tangentsLocked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'inTangentType': 2,
'in_tangent': {'xy': (0.2140544354915619, 0.9768217206001282)},
'isBreakdown': False,
'outTangentType': 2,
'out_tangent': {'xy': (1.0, 0.0)},
'tangentsLocked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'postInfinityType': 4,
'preInfinityType': 4}
Looks good, except for the inconsistencies between mixedCase and underscore_name keys. I'd like to be consistent among our naming convention, so let's add a quick fix and evaluate if it's really worth it to have the code like this in the next chapter.
import re
def _toUnderscores(string):
parts = re.findall('[A-Z]?[a-z]+', string)
for i, p in enumerate(parts):
parts[i] = p.lower()
return '_'.join(parts)
_API_SOURCE = { 'curve' : {'isStatic', 'isWeighted', 'preInfinityType', 'postInfinityType'},
'key' : {'value', 'isBreakdown', 'tangentsLocked', 'inTangentType', 'outTangentType'}}
_API_ATTRS = {level:{_toUnderscores(attr):attr for attr in _API_ATTRS[level]} for level in _API_SOURCE}
Now we only need to change the first line on our _animCurveData and _animCurveKeyData to be compatible with our new _API_ATTRS dictionary:
def _animCurveData(mcurve):
data = {at_k:getattr(mcurve, at_api) for at_k, at_api in _API_ATTRS['curve'].iteritems()}
def _animCurveKeyData(mcurve, index):
data = {at_k:getattr(mcurve, at_api)(index) for at_k, at_api in _API_ATTRS['key'].iteritems()}
data should have consistency across its keys now:
{'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0, 0.0)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (0.2140544354915619,
0.9768217206001282)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'in_tangent': {'xy': (0.2140544354915619, 0.9768217206001282)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (1.0, 0.0)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'post_infinity_type': 4,
'pre_infinity_type': 4}
Let's write the functions to set our data.
def setAnimCurveData(mcurve, data):
mcurve.setIsWeighted(data['is_weighted'])
mcurve.setPreInfinityType(data['pre_infinity_type'])
mcurve.setPostInfinityType(data['post_infinity_type'])
for k_i, k_values in data['keys'].iteritems():
k_time = om.MTime(k_values['time'])
if not mcurve.find(k_time): #if there is no key at the specified source frame, we will create a new one
mcurve.addKey(k_time, k_values['value'])
_setAnimCurveKeyData(mcurve, mcurve.find(k_time), k_values) #notice that we get the actual index with mcurve.find(k_time), this allows us to merge curve data into already populated curves.
def _setAnimCurveKeyData(mcurve, index, data):
mcurve.setTangentsLocked(index, False)
mcurve.setInTangentType(index, data['in_tangent_type'])
x, y = data['in_tangent']['xy']
mcurve.setTangent(index, x, y, True, convertUnits=False) #IN TANGENT
mcurve.setOutTangentType(index, data['out_tangent_type'])
x, y = data['out_tangent']['xy']
mcurve.setTangent(index, x, y, False, convertUnits=False) #OUT TANGENT
mcurve.setIsBreakdown(index, data['is_breakdown'])
if data['tangents_locked']:
mcurve.setTangentsLocked(index, True)
new_curve = omanim.MFnAnimCurve()
new_curve.create(omanim.MFnAnimCurve.kAnimCurveTL) #we will test our data on a time to linear curve
setAnimCurveData(new_curve, data)
That's it for the part of our raw data.
To set the data to a node attribute, we could search if an animation node is already connected to it and if not, create a new one and work with it:
omslist = om.MSelectionList()
omslist.add('pSphere1.translateX')
mplug = omslist.getPlug(0)
mcurve = omanim.MFnAnimCurve(mplug)
try:
mcurve.name() #errors if does not exist
except:
mcurve.create(mplug) #if the curve does not exist, create it and attach it to the plug
setAnimCurveData(mcurve, data)
Now, if we want to extract information of a lot of curves, we need to keep it organized. Even though we don't care to what plug the source animation node was connected to, it can be useful to separate them into a graph like: sourcenode/attribute/animdata.
Right now we're not going to cover constraints, but it is necesary for us to know that we will get to a point where some nodes will not be the actual destination of the curves at the core (like pairBlend nodes that are created when a constraint is added, or mute nodes). We are going to consider those nodes as bridge nodes. In this example we're only taking into account pairBlend nodes, but we will add support for others in the future.
from maya import cmds
def getAnimCurveData(dg_name, mcurve):
outputs = cmds.listConnections(dg_name, s=False, d=True, p=True, scn=True)
dag_outputs = cmds.listConnections(dg_name, s=False, d=True, p=True, scn=True, type='dagNode')
is_blended = False
for output in outputs:
if not cmds.nodeType(output)=='pairBlend':
continue
is_blended = True
pb_node, plug = output.split('.')
pb_plug = plug.replace('in', 'out')[:-1]
pb_outputs = cmds.listConnections('{}.{}'.format(pb_node, pb_plug), s=False, d=True, p=True, scn=True) or dag_outputs
output = pb_outputs[0]
if not is_blended:
output = outputs[0]
output_node, output_attr = output.split('.')
destination = {'node':cmds.ls(output_node, l=True)[0], 'attr':cmds.attributeQuery(output_attr, node=output_node, ln=True)}
data = _animCurveData(mcurve)
return {'outputs':outputs, 'is_blended':is_blended, 'destination':destination, 'data':data}
data = getAnimCurveData(mcurve.name(), mcurve)
data should look something like this:
{'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0, 0.0)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'in_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (1.0, 0.0)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateX', 'node': u'|pSphere1'},
'is_blended': False,
'outputs': [u'pSphere1.translateX']}
Notice how the is_blended and outputs value change if we add a point constraint to our pSphere1 node, while the destination value stays the same:
{'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0, 0.0)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'in_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (1.0, 0.0)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateX', 'node': u'|pSphere1'},
'is_blended': True,
'outputs': [u'pairBlend1.inTranslateX1']}
Now we can add support for multiple animation curve nodes!
def getCurvesAnimData(*dg_names):
omslist = om.MSelectionList()
data = {}
for i, c in enumerate(dg_names):
omslist.add(c)
mcurve = omanim.MFnAnimCurve((omslist.getDependNode(i)))
curve_data = getAnimCurveData(c, mcurve)
data.setdefault(curve_data['destination']['node'], {}).setdefault(curve_data['destination']['attr'], curve_data)
return data
data = getCurvesAnimData(*[c for c in cmds.ls(type='animCurveTL') if cmds.listConnections(c, s=False, d=True, p=True, scn=True)])
data should look something like this:
{u'|pCube1': {u'translateX': {'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0,
0.0)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (0.2140544354915619,
0.9768217206001282)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'in_tangent': {'xy': (0.2140544354915619,
0.9768217206001282)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (1.0,
0.0)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateX',
'node': u'|pCube1'},
'is_blended': False,
'outputs': [u'pCube1.translateX']},
u'translateY': {'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0,
0.0)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (0.2983231842517853,
-0.9544649124145508)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 1.0,
'value': 4.676647772022545},
1: {'in_tangent': {'xy': (0.2983231842517853,
-0.9544649124145508)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (1.0,
0.0)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 60.0,
'value': -3.1886234809247824}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateY',
'node': u'|pCube1'},
'is_blended': False,
'outputs': [u'pCube1.translateY']},
u'translateZ': {'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0,
0.0)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (0.41950732469558716,
-0.9077519178390503)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 1.0,
'value': 1.8873159113891838},
1: {'in_tangent': {'xy': (0.41950732469558716,
-0.9077519178390503)},
'in_tangent_type': 2,
'is_breakdown': False,
'out_tangent': {'xy': (1.0,
0.0)},
'out_tangent_type': 2,
'tangents_locked': True,
'time': 60.0,
'value': -3.432154048131956}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateZ',
'node': u'|pCube1'},
'is_blended': False,
'outputs': [u'pCube1.translateZ']}},
u'|pSphere1': {u'translateX': {'data': {'is_static': False,
'is_weighted': False,
'keys': {0: {'in_tangent': {'xy': (1.0,
0.0)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 1.0,
'value': -5.394801740406098},
1: {'in_tangent': {'xy': (0.2140544354915619,
0.976821780204773)},
'in_tangent_type': 1,
'is_breakdown': False,
'out_tangent': {'xy': (1.0,
0.0)},
'out_tangent_type': 1,
'tangents_locked': True,
'time': 60.0,
'value': 5.8236216588255445}},
'post_infinity_type': 4,
'pre_infinity_type': 4},
'destination': {'attr': u'translateX',
'node': u'|pSphere1'},
'is_blended': True,
'outputs': [u'pairBlend1.inTranslateX1']}}}
That's all for now! I hope this was interesting in some way; if you have doubts, comments or feedback please do not hesitate to get in touch with me!
If you're more interested, take a look at the code on GitHub, where I've added documentation and implemented some other functions, including getNodeAnimData and setNodeAnimData, so that animation transfer can be achieved by just the following lines of code:
source, target = cmds.ls(sl=True, l=True)
anim_data = getNodeAnimData(source) #collect the data
setNodeAnimData(target, anim_data[source]) #set the data
I'll continue with this project when time permits,
Cheers!