• Mason Smigel

Components

Updated: Mar 23

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