Zombie Playground’s Character Pipeline: Designing a Modular Rig Solution

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.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s