With a greater number of potential characters than Mothhead, our previous Unity project; I went into Zombie Playground’s pre-production with my mind already set on creating an automated rigging solution. I took some inspiration from a Modular Rigging course I followed on 3dBuzz and begun to break down my rigging process into object-oriented code in suite I named Automato.
class BaseSubstruct(object): ''' Abstract class to derive new rig modules from ''' NAME = 'abstractobject' NICE_NAME = '' CREATION_PARAM_JSON_ATTRNAME = 'creationParams' DESCRIPTION = 'generic abstract description' MIN_JOINTS = 1 # number of joints needed to create substruct REQUIRES_SKELETON_CHAIN = True # if true, subsruct requires a skeletal chain for creation def __init__(self, basename, joints, parent, mainControl): ''' Builds rig in maya and creates an asset node to contain relevant nodes ''' self.container = None self.layer = None self.containedNodes = list() self._rigControls = dict() self.lockAttrs = list() self.basename = basename # unique name to identify nodes created by this substruct self.parent = parent # parent to attach top-most rig node self.joints = joints # base skeleton joints to attach self.mainControl = mainControl self.mainColorAttr = None if self.parent is None: self.parent = self.joints[0].firstParent2() if self.parent: self.parent = pm.PyNode(self.parent) else: if mainControl: self.parent = mainControl self.verifyParams() self.container = pm.container(name='_'.join([Prefix.CONTAINER, basename, self.NAME])) ## Parameter dictionary for storing settings for assets ## Children classes can further expand this dictionary with parameters of their own self.paramDict = {'classname': self.NAME, 'basename': self.basename, 'joints': [str(i) for i in self.joints], 'parent': str(self.parent), 'mainControl': str(self.mainControl), 'container': str(self.container)} pm.addAttr(self.container, dataType='string', ln=self.CREATION_PARAM_JSON_ATTRNAME) pm.addAttr(self.container, at=int, ln='color', min=0, max=31, defaultValue=0, keyable=False, hidden=False) self.mainColorAttr = self.container.attr('color') self.mainColorAttr.showInChannelBox(True) if self.layer is None: self.layer = Layers.getLayer(Layers.ANIMATION_CONTROLS) self.transform = self.install() utils.lockAndHide(self.lockAttrs, True) connectControlColors(self.rigControls, self.mainColorAttr) self.updateSelectionSets() self.layer.addMembers(self._rigControls.values()) self.containedNodes.append(self.transform) self.containedNodes.extend(self._rigControls.values()) self.container.addNode(self.containedNodes) self.containerPublish(self.container) self.saveCreationParams() pm.parent(self.transform, self.mainControl) @property def rigControls(self): return self._rigControls.values() def verifyParams(self): if self.REQUIRES_SKELETON_CHAIN and not checkJointsAreHierarchy(self.joints): raise SkeletonError('The specified joints must be from the same skeletal chain') if len(self.joints) < self.MIN_JOINTS: raise SkeletonError('Only works with {0} joints!!'.format(self.MIN_JOINTS)) def install(self): ''' Main juice function Basic rig framework is created Return the top-most group node for __init__ method to utilize ''' raise NotImplementedError("Derive me please")
This is a small snippet of the base class I designed all the rig components (known as “Substructs” in my code) to derive from. The BaseSubstruct class contains a set of parameters stored as members that most, or all, of my rigging structures (leg, arm, etc.) share. Using a OOP approach to creating the rig makes for a very scaleable solution for our pipeline. As new creatures are designed and implemented; I can plan out the automation right away, simply making a new class with most of the ground work derived from the original base class.
Control Customization
One of the immediate benefits of Automato is the reduction of repetitive, error-prone steps in the rigging process. Once the first pass of Automato was implemented and mostly bug free, I felt a large amount of mental-bandwith suddenly available. I eventually begun to focus on new features that weren’t normally possible in our usual timeframe. One of the features I had the most fun implementing was control customization. All controls generated by my Automato script generate a cluster for manipulating the curve objects’ CVs. The cluster allowed me to expose specific transforms to the animator so they can resize and re-position controls as they see fit even while they’re animating. This ended up giving a lot more power to the animator so they can really tweak the rig to fit their needs and prevented minimize common rig nitpicks that from adding up on my to-do list.
Maya Containers for “encapsulation”
In an effort to better structure the nodes generated for each Substruct, I used this project to experiment with Maya Containers for the first time in an effort to encapsulate the rig. I ended up learning a lot about what containers can and can’t do, as well as some limitations.
The biggest issue I’m currently finding with the use of containers for rigging is the encapsulation makes it difficult to navigate connections within the container. If I choose to graph any node linked to the container, the Hypershade/Hypergraph will give me a graph of the entire container and it’s contents. Without an easy way to arrange the graph of container nodes, I have to manually sort through the container to find the nodes I want to work with. Fortunately, I’m able to perform most rig debugging through code, but I plan to find a replacement for Containers in future implementations of modular rigs.
JSON import/export
In order to make adjustments to joints with minimal downtime in animation, I designed a solution that allowed the finished rig to be saved as a “template”. Because all the rig components exist as Python classes, I found it possible to store all the important creation data into dictionaries and these dictionaries are later dumped into a text file using JSON formatting.
In its current form, it’s not true JSON formatting, as I use multiple root blocks, but it still works for my needs. Saving the rig into text files allows the rig to exist independent of the character skeleton. Not only did this solve my initial desire of being able to adjust joints fairly late into the pipeline, but the JSON functionality evolved into a auto-rig solution. In the case of similar characters, instead of building new rig modules from scratch using Automato, I could load in a JSON file from a completed character and have a new character fully rigged in minutes.
Benefits All Around
Automating the rigs using a modular approach was a big help. With the rigs being paramaterized to the point of living inside json files, updating rigs to fix animation problems or adding features is a matter of changing/adding to the code. With repetitive tasks at a minimum I’m able to focus more time on improving the rigs and tackling new features.