top of page
  • Mason Smigel

This post will outline the process I used to build a post-rig deformation system for better shaping of characters.

The goal of this system is to provide higher control over the shape and silhouette of characters, after the main pose. I saw a similar system from a paper written at Dreamworks called the "Contour Cage." I wanted to try to create something similar within Maya.



GOALS

Deformations like this could pretty easily be achieved by creating a local rig and piping the deformation into the render mesh via a blendshape, however, a solution like this would be rather expensive, and unable to take advantage of Maya's parallel evaluation.

With that in mind, I had a couple of main goals when developing this system:

1. The setup itself should be fast and fully optimized for parallel evaluation

2. To speed up the creation and maintain editability the only input would be a poly mesh. We can build the controls and skinning from this!

3. The system should be flexible enough to add onto most rigs



Overview

To give a rough overview of the system works:

  1. First model an input mesh, we can skin this mesh and use it to drive a series of controls and bind joints.

  2. The bind joints will be skinned to a duplicate of the mesh, and using the bindPreMatrix attribute of the joints deform the skin based on the offset between the bpm(bindPreMatrix) joint and the bind joint.

  3. This skin cluster can finally be stacked on top of the existing skinCluster to produce the final deformation.


Data flow overview


The Input mesh

The control points for the rig are built from an input mesh called the cage mesh. This is a manually modeled mesh that fits the shape of the character, with resolution around the areas you want to control. Of course, there is a trade-off with the number of vertices and the speed of the rig later on so it's helpful to keep this mesh really low. I found that using N-gons can be a great trick to adding a bit more resolution where you need it (similar to the profile curves used at Pixar).


Deform Cage geometry for Diagoro


Later on, we will use the cage to create and drive the control points for the rig, but to make the system more intuitive it would be helpful to have them follow along with the rig. For this, we can skin the deformCage to the bind joints of the main rig. An important note here is that each vertex should maintain a maximum influence of 2. This will important later when we drive the controls from the cage.




The control Cage

The control cage will be used to drive the final deformation. Each control point has a small hierarchy consisting of an offset, control as well as two joints, a bind and a bpm (bindPreMatrix), these will come into play later!

Control Hierarchy for each control point


We can then pretty easily create a series of control points based on the vertex position and orient them to the vertex normals.


Now comes the fun part! Since we only skinned each vertex to a maximum of two joints we can now get the influences of the vertex and create a blended matrix constraint between the two influences with weights based on the weights from our skin cluster.


Each control point is weighted based on the skin cluster weights


Notice the skinCluster influence joints are connected as inputs to the blend matrix node, and the skin weight is used as a blend between the influences. Now when the rig is deformed the controls "stick" to the vertices, but they have a completely independent DG graph from the geometry!


While it may look like these controls are connected to the verts, they're only driven by their offset parent matrix.



One final thing I added to the controls was a way to display the connectivity of vertices, without this the controls tend to get lost and can be confusing as to what part of the body they control.

Even Daigoro doesn't know what these controls do!


For this, we can create a new NURBS curve with CVs connected to each control point of the rig. The result is a bunch of NURBS control points curves with connections to the controlPoint.position of the curve. Each Control point of the curve follows the appropriate rig Control, resulting in something that looks a bit like a lattice.


Complete connectivity display for Diagoro



Skinning and connecting

Finally, we need to connect our control rig points to the actual mesh. There are really two major parts to this step, skinning the deformation cage and connecting that deformation to the existing skinning.


Luckily we modeled a super nice low poly proxy earlier that we can also use for skinning the deformation cage. By smoothing it a couple of times and skinning it to the control cage joints we can get some really nice and smooth skinning.

Output high res-skinning proxy

We can now copy the skinning from the high res proxy geometry we created to the final mesh. Here the bpm joints we created earlier become super important. If we look back at the hierarchy they are parented under the offset but not the control. That means they follow along with the matrix connection to the bind joints, but not when we move the control.


If we connect the worldInverseMatrix of the bpm joints to the BindPreMatrix attribute of the skin cluster (following the order in which the influences were connected) we can essentially offset the 'bind pose' of the joints.


Example bpm matrix connections. For a real skin cluster you'd have tons of inputs so its easier to script this.


Connecting the bpm joints makes the deformation relative to the distance between the start(bpm) and end (bind). I like to think about this as converting the deformation from being based on a point, to a vector.


By repeating this setup on the skinned mesh we get a result that looks like this:

Deform cage skinCluster is calculated only from relative offsets. so it looks 'disconnected' from the main rig.


While this may look a bit awkward on its own, once this skin cluster is stacked on top of the existing skinning it works really nicely with the existing deformations. (for more information about stacking skin clusters check out Charles Waldraws article and course on rigging Dojo)



If you want to use this in your own projects or just dig into the code this is all written into my larger rigging system Rigamajig2 which you can check out here!

  • Mason Smigel

Updated: Feb 21, 2023

Building a rig requires more than just the components, it also requires inputs like the model, skeleton, and post-build operations like constructing deformations, pose readers, and final publishing steps.


For this rigamajig2 uses the builder, a system of short scripts to wrap up all the required methods to build a rig.



Managing data

Rigs contain a lot of data. A key design choice of rigamajig2 was that almost all data is saved outside of Maya so the entire rig can be rebuilt. or this, the builder relies on an external folder structure to manage the data.



The builder uses a .rig file to store relative paths to data needed in the build. The build has the functionality to both load and save the data for the rig.

{
    "type": "AbstractData", 
    "user": "masonsmigel", 
    "time": "2021-11-22 23:12:13", 
    "data": {
        "model_file": null, 
        "skeleton_file": "skeleton.ma", 
        "skeleton_pos": "skeleton_pos.json", 
        "pre_scripts": [], 
        "post_scripts": [], 
        "pub_scripts": [], 
        "control_shapes" : "controlShapes.json", 
        "guides": "guides.json",
        "components": "components.json",
        "psd": "psd.json",
        "output_file": "../out/biped_rig.ma"
    }

An example .rig file



Using the builder


The builder class is a system of methods to sequentially run code. Under the hood the code looks like this:


import rigamajig2.maya.rig.builder as builder

# initialize the builder 
path = "/Users/masonsmigel/Documents/dev/maya/rigamajig2/archetypes/biped/biped.rig"
b = builder.Builder(rigFile=path)

# artist can manually run each step... 
b.load_required_plugins()
b.pre_script()
b.import_model()
b.import_skeleton()
b.load_joint_positions()
b.load_components()
b.initalize()
b.build()
b.connect()
b.finalize()
b.load_controlShapes()
b.load_deform_data()
b.post_script()
b.optimize()
b.pub_script()
b.publish()

# ... or use the run() method to run all the above code at once. 
b.run()

Because rigamajig2 relies entirely on external data the final rig can be built without any manual input. The primary method for publishing rigs to the end-user (aka animator) is to use the command line.


Using the command line allows the rigger to rebuild multiple rigs at once. As an example use case, if a bug is found in the rig and is present across all characters in a production a TD could adjust the code in the component and rebuild all rigs to push the change to the animators.


However, the command line is only useful during the publishing phase of building a rig, while data is still being created the builder UI allows users to more intuitively edit and walk through rig construction step by step.

Between steps, the rigger has time to author new data within Maya. This can range from positioning joints and guides to editing control shapes to skinning and blendshapes. All this data is pushed back to external files in the rig environment and referenced the next time the rig is built.



  • Mason Smigel

Updated: Mar 23, 2022

The foundation of the rig is the components. While the system utilizes other scripts and procedures to create the character, components create all behaviors of the rig.


What is a component

Within rigamajig2 components have two contexts: On the TD end (Python) they define code structure and execution and on the rigging end (Maya) they control rig behaviors.


On the TD side components are python classes that follow specific rules about the code to set up and run procedures in a logical order. Because they use classes, components can be sub-classed for more control and proceduralism when creating new components.


For the rigger, components can define any setup within a rig, ranging from an ikfk limb with soft ik, to a single control. However, they must be contained within a single system (space switches are an exception). The Maya rig component is the output of the code.



Component Code structure

Components are designed to be simple to understand and edit, allowing anyone with technical knowledge to jump in and write their own components.


input

While components can be expanded on within the subclass, there are several parameters all components expect.


  • name: Name of the component is required to build a component. This is also where the user can define a side by adding a side token (_l, _left, _lf, _r, etc...).

  • input: All components require some input node, most often this is a list of joints.

    • The specifics depend on the developer, for example my arm component requires a clavicle, shoulder, elbow, and wrist joint, while the chain component only requires a start and end joint.

  • size: (Optional) Default size of the controls. All built in control shapes are use a size to make sense with cm. However control shapes are almost always customized.

  • rigParent: (Optional) Node the component should attach to in the rig. For example the legs should connect to the bottom of the spine.


execution

During execution, components are managed through the builder. The builder uses a list of components, as python objects, to step through component construction.


The builder creates components in 5 steps.

  1. initialize_cmpt: create necessary containers (discussed below), parameters to control the component, and intractable guides

  2. build_cmpt: the main workhorse of the component, creates behaviors of the component

  3. connect_cmpt: connect the component to other input sources

  4. finalize_cmpt: publish controls and cleanup hierarchy

  5. optimize_cmpt: run performance optimizations that make the rig less friendly to a rigger. Includes locking/ moving attributes.

These 5 steps are called from softly protected methods that call the functionality of other methods reimplemented in subclasses to control the behavior of new components. This creates clear rules for developers so they can primarily focus on the behavior of the component not repetitive tasks like publishing controllers, wrangling containers, or proper order of executing steps.


def _build_cmpt(self):
    """ build the rig """
    self._load_meta_to_component()

    if not self.getStep() >= 2:

        # anything that manages or creates nodes should set the active container
        with rigamajig2.maya.container.ActiveContainer(self.container):
            self.initalHierachy()
            self.preRigSetup()
            self.rigSetup()
            self.postRigSetup()
            self.setupAnimAttrs()
        self.setStep(2)
    else:
        logger.debug('component {} already built.'.format(self.name))

As an example, the _build_cmpt() method calls the initalHeirarchy, preRigSetup, rigSetup, postRigSetup, and setupAnimAttrs (each of these methods have specific guidelines for construction). However, it also checks the current build step to ensure we aren't trying to build a component that is already built and uses a with-statement to add any created nodes to the active container.




Containers

I've mentioned containers in several places throughout the post because they are an essential part of rigamajig2 components. Containers are Maya nodes that create relationships between the container and node agnostic of the DAG or DG. I derived my philosophy on containers from Raffaele Fragapane and his ideas on the cult of rig streams.


Within rigamajig2 they serve several purposes:

  • interface for user input

  • store component metadata

  • keep a list of all nodes in a component


Interface

The container serves as the main interface for adjusting the component settings. It stores settings in attributes allowing the user to edit them directly within Maya (in the future this might be expanded into a UI). The settings here are re-collected from the Maya node and updated within the python object before each step.


rig in initialize step.


MEtaData

The container settings double as metadata for the component. From the container node, rigamajig2 gathers all the settings to save and load the component with JSON.


Aside from component settings rigamajig2 also uses a tagging system to manage other metadata. After doing performance tests between controller tags, message connections, and querying for nodes with an attribute I was surprised to find that querying attributes were the fastest solution to return lists of nodes.


The tagging system adds a private attribute to the given nodes and can be returned by searching for nodes with that attribute:


def tag(nodes, tag, type=None):
    """
    Tag the specified nodes with the proper type
    :param nodes: nodes to add the tag to
    :type nodes: str | list
    :param tag: tag to add
    :type tag: str
    :param type: type of tag
    :type type: str
    """
    nodes = common.toList(nodes)
    for node in nodes:
        if cmds.objExists(node):
            if not cmds.objExists("{}.__{}__".format(node, tag)):
                cmds.addAttr(node, ln='__{}__'.format(tag), at='message')
            if type:
                if not cmds.objExists("{}.__{}_{}__".format(node, type, tag)):
                    cmds.addAttr(node, ln='__{}_{}__'.format(type, tag), at='message')

def getTagged(tag, namespace=None):
    """
    Get a list of all the objects with a tag in a scene.
    :param tag: tag to get
    :type tag: str
    :param namespace: Get controls found within a specific namespace
    :type namespace: str
    :return:
    """
    if not namespace:
        return [s.split(".")[0] for s in cmds.ls("*.__{}__".format(tag))]
    else:
        return [s.split(".")[0] for s in cmds.ls("{}:*.__{}__".format(namespace, tag))]


rebuilding components

Keeping a list of all related notes becomes important for rebuilding only specified components. This keeps RnD intuitive and iterative so the rigger can test out different setups without waiting for the entire rig to rebuild. The code below is used to delete a rig setup:


def deleteSetup(self):
    """ delete the rig setup"""
    logger.info("deleting component {}".format(self.name))
    cmds.select(self.container, r=True)
    mel.eval("doDelete;")

    for input in self.input:
        rigamajig2.maya.attr.unlock(input, rigamajig2.maya.attr.TRANSFORMS)
        


Maya Structure

Components also have specific rules to how they should be structured within Maya.


  • Must be completely self contained. Aside from rigParent and space switches

  • Component must be scalable

  • Parameters exist on a separate node (for optimization purposes)

  • No joints bound to the skin should live in the component

  • Anything added to the component should live under the component root (ikfk, space switches, bend, etc... )



Base Class

All components are subclasses of the base component. Below is a slightly simplified version of the base class showing only the initialize and build steps. But the idea extends to later steps as well.


class Base(object):

    def __init__(self, name, input=[], size=1, rigParent=str()):
        """
        :param name: name of the components
        :type name: str
        :param input: list of input joints.
        :type input: list
        :param size: default size of the controls:
        :param rigParent: node to parent to connect the component to in the heirarchy
        :type size: float
        """
        self.name = name
        self.cmpt_type = ".".join([self.__module__.split('cmpts.')[-1], self.__class__.__name__])
        self.input = input
        self.container = self.name + '_container'
        self.metaNode = None

        # element lists
        self.joints = list()
        self.controlers = list()

        # node metaData
        self.cmptData = OrderedDict()
        self.cmptData['name'] = self.name
        self.cmptData['type'] = self.cmpt_type
        self.cmptData['input'] = self.input
        # node cmpt settings
        self.cmptSettings = OrderedDict(size=size, rigParent=rigParent)

    def _intialize_cmpt(self):
        """
        setup all intialize functions for the component
        """
        if not self.getStep() >= 1:
            # fullDict = dict(self.metaData, **self.cmptSettings)
            self.setInitalData()
            self.createContainer()

            # Store to component node
            self.metaNode = rigamajig2.maya.meta.MetaNode(self.container)
            self.metaNode.setDataDict(data=self.cmptData, hide=True, lock=True)
            self.metaNode.setDataDict(data=self.cmptSettings, hide=True)

            # anything that manages or creates nodes should set the active container
            with rigamajig2.maya.container.ActiveContainer(self.container):
                self.preScript()  # run any pre-build scripts
                self.createBuildGuides()
            self.setStep(1)

    def _build_cmpt(self):
        """
        build the rig
        """
        self._load_meta_to_component()

        if not self.getStep() >= 2:

            # anything that manages or creates nodes should set the active container
            with rigamajig2.maya.container.ActiveContainer(self.container):
                self.initalHierachy()
                self.preRigSetup()
                self.rigSetup()
                self.postRigSetup()
                self.setupAnimAttrs()
            self.setStep(2)
       
    # -----------------------------------------------------------------
    # functions
    # -----------------------------------------------------------------
    def createBuildGuides(self):
        """Add additional guides"""
        pass

    def setInitalData(self):
        """ Set inital component data. """
        pass

    def createContainer(self, data={}):
        """Create a Container for the component"""
        if not cmds.objExists(self.container):
            self.container = rigamajig2.maya.container.create(self.container)
            rigamajig2.maya.meta.tag(self.container, 'component')

    def preScript(self):
        pass

    def setupAnimAttrs(self):
        """Setup animation attributes. implement in subclass"""
        pass

    def initalHierachy(self):
        """Setup the inital Hirarchy. implement in subclass"""
        pass

    def preRigSetup(self):
        """Pre rig setup. implement in subclass"""
        pass

    def rigSetup(self):
        """Add the rig setup. implement in subclass"""
        pass

    def postRigSetup(self):
        """Add the post setup. implement in subclass"""
        pass


bottom of page