Understanding how to manipulate transformation matrices can greatly expand your Maya rigging repertoire. This article will eschew theoretical discussions. Instead, it will demonstrate fundamental concepts by guiding you through a simple node-based constraint setup.
Maya Node Math: Hands-On Matrices
Matrices, Transform Nodes and Hierarchy
Matrices are used across CG software for quick calculation of complex object transformations. Matrices are not intended for artist use. This is partly because they define rotation and scaling using an unintuitive compound of vectors. In Maya, posing and animation are instead typically achieved using transform nodes.
A transform node is any scene object with translate, rotate and scale channels (attributes). When these channels are edited––for example via the Channel Box––their values are composed into a local transformation matrix, which is stored in the transform node’s .matrix attribute. (As we’ll see later, matrices can also be decomposed.)
When transforms are nested in hierarchies, their matrices are multiplied in a chain, from innermost to outermost, to arrive at the final pose for the object in the scene. This is also known as its world matrix:
Your First Matrix-Based Constraint
A good way to grasp the above is to replicate a parenting hierarchy manually, using matrix nodes. To get started:
- Create a locator, and give it some random translate / rotate / scale values. Name this locator ‘master’.
- Group the locator, and give the group some random translate / rotate / scale values. Name this group ‘master_parent1’.
- Group the previous group once more, and give the new group some random translate / rotate / scale values. Name this group ‘master_parent2’.
At this point, you should have a locator with ‘mangled’ transformations:
- Multiply the .matrix attributes of ‘master’, ‘master_parent1’ and ‘master_parent2’ in a chain using multMatrix nodes.
- Create a decomposeMatrix node, and connect the matrixSum output of the last multMatrix node in the chain into the decomposeMatrix’s inputMatrix attribute.
- Create a polygon cube, and name this ‘slave’.
- Connect the outputTranslate, outputRotate, outputScale and outputShear attributes of the decomposeMatrix node into the translate, rotate, scale and shear attributes of ‘slave’.
At this point you’ve implemented the basics of a parent-and-scale constraint from the ‘master’ locator to the ‘slave’ cube, albeit one without the ‘maintain offset’ option (I may cover this in a future post). Try it by moving the locator, or any of its parents, around:
Your network should look something like this (click / tap to enlarge):
You can reverse a matrix multiplication by multiplying the result with the inverse of each matrix involved except the first one. The matrix order must be reversed as well.
For example, to get back to the local matrix of ‘master’, you could perform:
worldMatrix * inverse of master_parent2.matrix * inverse of master_parent1.matrix = driver.matrix (local)
You can use the inverseMatrix node to invert matrix outputs. However, you may not need to; read on for quicker, and more indirect, ways of accessing transform node matrix calculations.
The above exercise was illustrative; you don’t typically have to work with every .matrix output in a hierarchy. Maya exposes typical matrix calculations for you in the following attributes, found on every transform node:
- .inverseMatrix –– gives you the inverse of the transform node’s local matrix
- .worldMatrix –– gives you the world pose matrix for the transform node; this is the same as object.matrix * parent1.matrix * parent2.matrix etc.
- .worldInverseMatrix –– gives you the inverse of the world pose matrix for the transform node; this is the same as object.worldMatrix * parent2.inverseMatrix * parent1.inverseMatrix * object.inverseMatrix
- .parentMatrix –– this is equivalent to the worldMatrix of the immediate parent
- .parentInverseMatrix –– this is equivalent to the worldInverseMatrix of the immediate parent
Hence, our ‘parent constraint’ network can be condensed to:
An added advantage of accessing the .worldMatrix attribute directly is that you don’t have to be aware of the parent hierarchy above ‘master’.
HOW MAYA DOES IT
The matrix constraint we’ve created above will work correctly as long as the ‘slave’ object is not parented to any other objects which are then moved. To understand how this can be addressed, let’s take a look at how Maya handles parenting operations:
- Save your previous matrix constraint setup somewhere, and start a new scene.
- Create a cube and pose it randomly.
- Create two locators and pose them randomly as well. Don’t parent anything. Leave all objects at the root level of the Outliner.
- Observe what happens if you now parent the cube to each locator in turn: Even though the cube doesn’t move, its Channel Box values change. This makes sense; as mentioned before, these values are always relative to an object’s immediate parent.
- Bring up the Edit > Parent [Options] window, and switch Preserve Position OFF.
- Try reparenting the cube again. Notice that, this time, the cube moves, but its Channel Box values remain the same.
- You don’t want to leave Preserve Position off, so switch it back on.
So what’s going on?
Recall that an object’s final position in 3D space, i.e. its world matrix, is a factor of its local matrix as well as the local matrices of all its parents. When you reparent an object with “Preserve Position” ON (the default), Maya has to ‘cook’ the same world matrix––the same end pose––using different parent matrices. Here’s what happens specifically:
- A snapshot is saved of the object’s world matrix.
- Working backwards, Maya multiplies this world matrix with the inverse of the destination hierarchy’s matrices (see ‘Reversing Multiplications’ above) to arrive at a new local matrix.
- The local matrix is decomposed to yield new translate / rotate / scale values for the reparented object.
Click / tap to enlarge:
MAKING OUR CONSTRAINT HIERARCHY-PROOF
We can use everything we’ve covered so far to apply a similar type of compensation to our matrix constraint as well. This will prevent it from jumping if the ‘slave’ object is reparented. Proceed as follows:
- Go back to your matrix constraint scene.
- Break the connection between ‘master’ object’s .worldMatrix output and the inputMatrix input of the decomposeMatrix node.
- Create another multMatrix node, and multiply the ‘master’ object’s .worldMatrix with the ‘slave’ object’s .parentInverseMatrix attribute.
- Connect the .matrixSum output of this new multMatrix node into the .inputMatrix of the decomposeMatrix node.
Your network should now look like this (click / tap to enlarge):
Group the ‘slave’ object twice. Name the bottom-most (first) parent ‘slave_parent1’ and the topmost ‘slave_parent2’. Try moving either parent around. The ‘slave’ object should stay pinned in place.
Notice that we are using the slave’s parentInverseMatrix attribute, and not its worldInverseMatrix, because the worldInverseMatrix also includes the slave’s local matrix, which is exactly what we are overriding with our matrix constraint. Hence, this would result in an evaluation cycle.
You could also say that using the slave’s worldInverseMatrix would take our constraint matrix into the child space under the slave object, and not into the slave’s local space under parent1. More about spaces in a second.
Wrapping Up: Space Switching
Hopefully this tutorial has given you a grasp of the basic stages involved in controlling objects using custom transformation matrices:
- Determine the driver world-space matrix for the slave object. In this tutorial we merely took another object’s world matrix, but it could be anything.
- Multiply this world matrix with the slave’s .parentInverseMatrix to create a new local matrix for it. This will allow the attachment to work against any hierarchy.
- Decompose this new local matrix and connect the result into the slave’s translate, rotate, scale and / or shear channels.
As a parting shot, you can generalise everything you’ve learned about going from ‘local’ to ‘world’ with matrices to perform coordinate conversions for points and vectors as well.
For example, we already know there are three ways of getting the world matrix for an object:
- You can multiply its .matrix with that of each of each parents.
- You can multiply its .matrix with its .parentMatrix (which is merely shorthand for the parent stack).
- You can just grab it from the object’s .worldMatrix attribute.
The tutorial World Position Of An Object elsewhere on this site uses a modified version of approach #2, multiplying an object’s translate values with its .parentMatrix instead to convert them into world space. You could turn this into a point constraint by, in turn, multiplying the world space values with the parentInverseMatrix of a slave object, and then piping the result into its translate channel.