In this article I will talk about a really common procedure that can be found in today character rig: space switching or dynamic parenting.
All the following story started after reading a post on maulik blog. His attempt at writing a group Constraint made me want to see if I would be able to came up with a stable implementation in order to use less nodes and provide a more streamlined workflow .
1) Once upon a rig…:
At its core , space switching or dynamic parenting is rooted on animation constraint: the basic idea is to link an object to several “parent” and to be able to transfer this child between parent over time.
This system is usually build to keep track over which driver takes precedence in controlling an object by:
- managing the constraint creation ( setting up additional meta data, connecting message attribute to retrieve node and parents easily )
- baking the difference of position and orientation when user want to attach an object to a different “parent” .
- computing weight value for each driver linked to constraint node .
My first encounter with this concept was from maya meister Alex Alvarez’s gnomon legacy DVD, and Chris Landreth’s no Sensei material( maya techniques: Ryan making off). Much more information can also be found in Jason Schleifer’s Animator Friendly Rigging DVD .
In Maya a joint hierarchy can be split by function:
- the deformation rig will structure all the joint/deformer and additional setup needed to convey a creature skin appearance.
- the animation rig bring all the tool and functionality to breath life to a character through movement .
Several duplicate hierarchy ( that can sometimes be different than the bound skeleton rig ) can be used as animation driver for a joint chain: one chain can be use in FK mode another in IK etc…
(image from Javier Solsona tutorial)
(In 3dsmax, things can be different , as we can stack different controller in list to achieve the same effect ).
2) Current trends and methodology:
Several solution were developed and showcased using maya element like scriptNode, setDrivenKey, custom scripts.
An intesting tool by john Patrick ( script can be found here )
You can see in the UI two tabs : the first one deals with the setup of the switch, and the animation tabs compute the required offset to prevent the driven node to jump in space.
( snapshot from the blake rig by Jason Baskin )
On the channelBox , users can select from an enumerate attribute the animation driver( the same principle is valid for space switching)
( Above TD-Matt explains to great length how to setup a space switch with regular maya node )
You can also setup custom marking menu to have a faster access to the parent selection( above rig from Lee Zhen Yang demo reel ).
Each of these tools respect the overall principle of space switching, but what is interesting is how TD tackle the same problem in their day to day work , sometimes in a creative different way.
3) Advanced systems:
a – “Broken hierarchy” and motion isolation:
(Snapshot from Maya techniques: Custom character Toolkit, click on the the image for full size )
In this presentation(AWGUA Siggraph 2003 – Maya Master Class Seminar!! ), Erick Miller and Paul thuriot talked about various techniques to build a character pipeline: one of them was the use of broken hierarchy.
This system relies heavily on a base rig, a regular hierarchy of joints where:
- every child is connected to the appropriate place,
- the transformation are frozen
- the joint orientation and rotation order are setup in a coherent manner.
Down the animation pipeline, this base rig will then be bound to a control rig. This asset is generally used by animators with low resolution cut off version of creature mesh, and featured several element to assist artist to pose a puppet in the desired configuration: IK controls, FK mode to achieve better arc or presets ( like slider and offset group for hand position , pose library to manage face expressions ,etc))
–> control rig driven by cut off portion, patch back a parent socket with orient/position constraint: encourage modularity, cleaner scene organization
–>( premise of procedural modular rigging system )
b – Human IK, MotionBuilder and full body IK solver:
–> the IK / FK control rig that can be aligned automatically
–> posing tool : pinning translation/ rotation
–> pulling whole body
humanIK game middleware
cat and biped interactive IK mode for fk chain
iKinema motion capture solver
c-Bi-Directional Constraints and dependency graph hacking:
2 master classes on BiDirectionalConstraining
turn and enhanced into a commercial application –> exotool
d-Modular character system:
–> jan berger mvc system, bidirectional contraint/ exotool, mobu ik solver, full body IK( to be continued …)
on the fly, relashionship modification
(to be continued)
4) Current implementation and workflow:
The core of this project was to write a python scripted plugin in order to do the heavy matrix math lifting . Once created this node is managed by 2 python class nicely wrapped( i hope so ) in a script file.
The first step for the user is to initialize a new spaceswitchNode for a transform node
To initialize a new spaceswitchNode the user has to use a popup menu to explicitly do that action , much like a constraint node it doesn’t make sense( but is still possible ) in maya to layer this type of node hence once a switchNode is linked to an object the UI will not display this menu anymore .
Most of the time ( for obvious time constraint and budget ), I write plain and “simple” user interface with regular maya controls and layout. This time i wanted to try a resizable and dockable tool . With the supremacy of wide screen nowadays it made sense to have a vertical layout to have a minimal footprint on the user workspace .( in scripting term this means messing with a formLayout in order to attach control between each other or to the edge of a parent container )
The first tool accessible is used for the parent setup. Here you can add and remove object that will act as parent. The UI will filter from a selection the objects that will cause a cycle in the system ( either by undoing or disconnecting the unstable input )
As a convention the node start to be active when at least one “parent” is connected . The first time this condition is met, an offset group is inserted in the driven object hierarchy and received the switch node translate and rotate output value.For the same reason ( and also to limit the amount of code needed to release this tool ),the UI doesn’t allow the average user to change the order of the parent or delete the first one.
In the second part of the UI , the user has access to different tool to change the parent that will influence the driven object. In most case this UI is not mandatory , as this action can be done( with the attribute editor or in the channelBox ) by simply changing the index number of the parent driver.
The node also doesn’t rely on absolute number to work : the user may have connect 5 parent at the following index : [3,7,11,12,20], and in this case index 0 will means the first number ( 3 ) in this list.
The “Key Driver Index” index will ensure that key will be added with a stepped tangent, and “Key parent Transform” will key all the parent connected to the spaceswitch node at each switching frame
( A cleaner node network, matrix attributes are feeding the spaceswitch node which then feeds the offset group rotate and translate channel)
5) Basic algorithm and tricky limitation that were overcome :
As usual , I started this project by doing a prototype with regular nodes and simple python scripted plugin. Using a constraint is equivalent to “translate” the information of an object into the space of another and this operation often involves matrix multiplication.
In this configuration where two object live in worldspace ( they have no parent ) the locator drives the cube mesh in the simplest manner : its world spatial state information is separated into translate / rotate / scale information (using the default rotation order ).
When the driven object is part of a hierarchy this setup needs to be modified slightly: these spatial transformation value must be convert into the parent space.
This what the multMatrix node is used for:
- multiplying the world matrix of the locator by the parent inverse matrix of the driven cube( in the correct order ).
More information on practical matrix usage in CG and 3d application can be found in:
- David Gould’s Complete maya programming
- Borislav Petrov’s The Matrix Explained at cgAcademy.net
The first graph and procedure was matching an object to only one target, it was only logical to extend it to cope with multiple parent.
One important difference with a constraint node is that a switchNode doesn’t need to blend the contribution of each parent based on a weight value: the current parent fully influence the driven object.
The choice node was then an ideal candidate for this task: with a generic attribute as input and output, creative TD can plug mesh, curve, numeric value, vector or matrices to complete this network.
The last part of the work was to maintain the distance and orientation as the driven object was jumping from one space to another.
This information can also be store as a matrix and is extracted with the world matrix of the driven object and the parent inverse matrix of the future parent.
From a practical standpoint , maya provide a node that compute a matrix from 16 numbers if you need to dynamically change it or can create and write a regular matrix as a storage solution.
It is also funny to see the same functionality reflected in the API:
import maya.cmds as cmds import maya.OpenMaya as OpenMaya util = OpenMaya.MScriptUtil() drivenWorldTM = OpenMaya.MMatrix() parenHolderINVMAT = OpenMaya.MMatrix() worldState = cmds.getAttr('%s.worldMatrix'%drivenNode ) inverseParentState = cmds.getAttr('%s.worldInverseMatrix'%holderNode ) util.createMatrixFromList(worldState,drivenWorldTM) util.createMatrixFromList(inverseParentState,parenHolderINVMAT) offsetMatrix = drivenWorldTM*parenHolderINVMAT offsetMatrixValue =  for row in range(4): for clm in range(4): offsetMatrixValue.append(offsetMatrix(row,clm)) cmds.setAttr('%s.bindMatrix'%switchNode,offsetMatrixValue,type='matrix')
This code snippet extract an offset matrix by using regular commands and API function.( In the near future It will be more clear why this offset matrix was named bindMatrix )
(Above: the final node network , click the image to see it in full size )
When you animate the parent driver index , you must use a script to compute an offset matrix and add / overwrite one element in a second choice node.
6) Time context, frame cache and switch consolidation:
a) First draft analysis:
( above : list of all the attributes used by this node )
Building a node which replicates this network functionalities was quite easy. Only 2 additionnal elements were needed to make it works in a compute method :
- a bindMatrix
- and a previous index attribute.
The previous index attribute was the most obvious addition. Each time the user choose to change the parent driver index , this action will refresh an internal offset matrix . and is done by having a point of reference to do a simple comparison
oldDriverIndex = bakeIndex_Handle.asShort() if oldDriverIndex != driverIndex_Val: bakeIndex_Handle.setShort(driverIndex_Val) bakeIndex_Handle.setClean() #refresh the offset matrix attribute here
You can immediately notice that to have a sense of memory the node reads the value of an output data handle (bakeIndex_Handle) which is legal and dont refresh maya dependency graph.
The bindMatrix concept was more fun to implement as the initial concept needs to be compatible with maya architecture:
- Most people request or use constraint with an external object to grab the driven object spatial informations.
- In a node this lead to a cyclic dependency and leads to unstable behavior( node must operates on data from its own input attributes, this doesn’t mean developer can go outside their node but involves a more cautious approach )
My solution for this problem was found from one simple observation : the offset matrix combined with the current parent worldMatrix always returns a value in world coordinates, thus querying outside element were unnecessary.
This bindMatrix sole purpose was then to pass an initial state for the internal offset matrix and can also be used in an override mode.
b) Unexpected limitation:
As soon as this draft was finished I did a test with a simple cube animation. I was disappointing to see that the switch condition already implemented was clearly weak :
- on some playback condition( when maya reads the last frame and rewind the animation to the first )
- or when user jump from one time value to another.
Part of my solution came after studying maya frameCache node (once again) and the animation ghosting tool.
(above: the green cube with a frameCache node reads the animation curve from the attribute translateY of the blue cube: here there is a positive offset of 8 frames )
(Here we can see an interesting implementation: the index in the output array is used as a time offset value)
import maya.OpenMaya as OpenMaya import maya.OpenMayaAnim as OpenMayaAnim def get_mobject(node): selectionList = OpenMaya.MSelectionList() selectionList.add(node) oNode = OpenMaya.MObject() selectionList.getDependNode(0, oNode) return oNode def extract_animation_data_from_Plug(nodeName,attributeName): animfromNode = get_mobject(nodeName) depFn = OpenMaya.MFnDependencyNode( animfromNode ) PlugObj = depFn.findPlug(attributeName) attributeIsAnimated = OpenMayaAnim.MAnimUtil.isAnimated(PlugObj) if attributeIsAnimated == True animCurveNode = OpenMayaAnim.MFnAnimCurve(PlugObj) numKeys = animCurveNode.numKeys() animValues =  timeList =  for k in range(numKeys): keyTime = animCurveNode.time(k) animValues.append(animCurveNode.value(k)) timeList.append(keyTime.value()) return [animValues,animTimes,timeList] else: return None # usage: animate the translateX value of cube1 created in your scene then invoke: animData = extract_animation_data_from_Plug('cube1','translateX') print animData
( Above : code snippet to extract useful information like time/value from an animationCurve , this is what the regular keyframe command do behind the scene )
Although interesting the frameCache node turns to be impractical to use as the driver connected to it will often behaves incorrectly( it will be lock, jumps after mouse release etc… ). This study was important to track the parent index throughout time and guess in the current time range what are the the two parent that was used to compute the current offset matrix.
On the same subject 3d application often have a ghosting option for animated object: this drawing mode enable user to see the state of an object before and after the current frame ( much like legacy hand drawn 2d animation where people were able to stack several layer of paper to judge the spacing between key movement ).
Recently Anzovin studio has release a plugin for this purpose., and a similar node can be found in peter shipkov soup node: the timeOffset node.
What these tools are doing on the most basic level, is taking advantage of maya architecture by querying an attribute value at a different time and can be done this way:
cmds.getAttr( '%s.%s'%( yourObject, attributeToquery), time=newTimeValue )
In the API this action is possible with the MPlug Class used in conjunction with an MDGContext.
import math, sys import maya.OpenMaya as OpenMaya def get_mobject(node): selectionList = OpenMaya.MSelectionList() selectionList.add(node) oNode = OpenMaya.MObject() selectionList.getDependNode(0, oNode) return oNode def grab_value_at_time(AnimatedMeshShape,timeDrivenMeshShape,driverAttribute,drivenAttribute,requestTimeValue): AnimPolyObj = get_mobject(AnimatedMeshShape) timeDrivenPolyObj = get_mobject(timeDrivenMeshShape) depNodeFn = OpenMaya.MFnDependencyNode(AnimPolyObj) outMeshPlug = depNodeFn.findPlug(driverAttribute) timeDrivenDepNodeFn = OpenMaya.MFnDependencyNode(timeDrivenPolyObj ) dirvenOutMeshPlug = timeDrivenDepNodeFn.findPlug(drivenAttribute) currentCTX = OpenMaya.MDGContext(OpenMaya.MTime(requestTimeValue) ) mobjValueHolder = outMeshPlug.asMObject(currentCTX) polyDG = OpenMaya.MDGModifier() polyDG.newPlugValue(dirvenOutMeshPlug ,mobjValueHolder) polyDG.doIt() return polyDG # --> In a scene with to identical mesh ( one being deformed and animated ) invoke timeDrivenMeshShape = 'pCubeShape2' #--> replace by your shapeName AnimatedMeshShape = 'pCubeShape1' #--> replace by your shapeName driverAttribute = 'outMesh' drivenAttribute = 'outMesh' requestTimeValue = 15 dgMod = grab_value_at_time(AnimatedMeshShape,timeDrivenMeshShape,driverAttribute,drivenAttribute,requestTimeValue) #you can undo this modification dgMod.undoIt()
Although legal these action can put a serious strain on maya general performance, as each call would require a separate dependency graph evaluation.
c) Switch history and offsetMatrix consolidation: