Scripting Your Rig: Creating a Three-Joint Chain, Part 2

In the first part of this series, we scripted out locators that can be placed around the scene to create an arm (or leg). Today we’ll continue by turning those locators into a joint chain. If you missed the first post or need a recap, check the link below:

Scripting Your Rig: Creating a Three-Joint Chain, Part 1

Now then, since we setup our file so nicely with a class and a function, let’s make a new function for creating the joints. It’s important to remember that when making rig joints we can’t just spawn arbitrary joints around (like we did in the first post) because the orientations are going to be wrong. In this “createJoints” function, we need to pass in a few arguments to tell it how we want the joints oriented. The specifics won’t matter in this case, but they do need to be consistent. When you orient joints in Maya you choose a primary orientation and an up vector, so let’s make those our arguments:

def createJoints(self, ojVal, saoVal):

Here we’re passing in an orientJoint value (ojVal) and a secondaryAxisOrientation value (saoVal). Not bad, but let’s think one step further shall we? Right now, our createJoints function is missing a key feature in rigging… a name! We did define self.JNTNAMES = [‘Arm’, ‘Elbow’, ‘Wrist’, ‘Twist’] earlier, but that describes the piece, not where it exists. In order to make these uniquely named, we can add a third argument to serve as a prefix. For this case, let’s call it “side”.

def createJoints(self, side, ojVal, saoVal):

Okay, our setup is ready to go! Here are the first few lines:

def createJoints(self, side, ojVal, saoVal):
        """
        Main procedure to create the rig.
        """
        #### Create Some Joints

        self.PREFIX = side

        locs= self.locatorJointsList/code>

For starters, our “side” might not be unique to just the joints. Controllers, groups, or whatever else we write later on might want to reference it. Instead of leaving it a local variable, we bind it to the class scope as self.PREFIX. Similarly, we want that locator list from our other function, but don’t want to reference the massive return name every time, so instead it gets stored as a local variable “locs”.

        j1T = cmds.xform(locs[0], q=1, ws=1, t=1, a=1)
        j2T = cmds.xform(locs[1], q=1, ws=1, t=1, a=1)
        j3T = cmds.xform(locs[2], q=1, ws=1, t=1, a=1)

        j2TL = cmds.xform(locs[1], q=1, os=1, t=1, r=1)
        j3TL = cmds.xform(locs[2], q=1, os=1, t=1, r=1)

Grab the positions of these locators. We need to grab the world space for all three, and the object space additionally for the latter two. Since they are parented, we can use this to find out their relative distances. Now we need to jump briefly into the API!

        j1TV = om.MVector(j1T)
        j2TV = om.MVector(j2T)
        j3TV = om.MVector(j3T)

        lenJ1J2 = om.MVector(j2TV - j1TV).length()
        lenJ2J3 = om.MVector(j3TV - j2TV).length()

Only five lines! Not so scary right? What we’re doing here is creating a 3D Vector out of the world-space translate value for each locator. With vectors, we can quickly perform math operations in 3D space that would be a bit of a pain otherwise. Once all three vectors are established, we can create two new Vectors by subtracting and grabbing the length of that distance. This is similar to saying “5-2” and length being the “=3” end of the equation. The difference of course, is that we’re subtracting 3D coordinate spaces as opposed to simple integers. Anyway, now the length (distance) between joints 1 and 2 as well as between joints 2 and 3 have been saved into variables.

        cmds.select(d=1)
        a1 = cmds.joint(p=j1T, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[0]))
        a2 = cmds.joint(p=(0, j2TL[1], j2TL[2]), r=1, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[1]))
        a3 = cmds.joint(p=(0, j3TL[1], j3TL[2]), r=1, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[2]))

        self.bindJoints = [a1,a2,a3]

Before creating any joints, it’s a good habit to deselect everything. If a joint happens to be selected, you’ll spawn the new joint as a child of that, which we do not want. And using some easy string substitution we’re finally putting those variable names to use! We also need to store those joints as an array to reference later on as self.bindJoints. Time to start orientation!

        cmds.joint(self.bindJoints[0], e=1, oj=ojVal, sao=saoVal, ch=1, zso=1)
        cmds.setAttr("%s.translate%s" % (self.bindJoints[1],ojVal[0].upper()), lenJ1J2)
        cmds.setAttr("%s.translate%s" % (self.bindJoints[2],ojVal[0].upper()), lenJ2J3)

This probably looks a bit janky, so here’s what’s actually happening. We’re grabbing that first joint and orienting it to the values we pass in as arguments. For this example, let’s say “xyz” and “yup” are the arguments, since those tend to be the Maya defaults anyway. Next we’re going to take those lengths we had previously, and position our joints. This is not truly necessary, since we placed those joints on creation, but I’m using it as a sanity check in case the values from Maya were somehow truncated.

        cmds.select(d=1)

        self.ikH = cmds.ikHandle(sj=self.bindJoints[0], ee=self.bindJoints[2], sol="ikRPsolver")[0]
        cmds.xform(self.ikH, t=j3T, a=1)

        pvLoc = cmds.spaceLocator()[0]
        cmds.xform(pvLoc, t=j2T, ws=1, a=1)

        cmds.poleVectorConstraint(pvLoc, self.ikH)   

        #### Create Some More Joints

        cmds.delete(self.ikH, pvLoc)
        cmds.makeIdentity(self.bindJoints[0], t=1, r=1, a=1, jo=0)
        cmds.joint(self.bindJoints[2],e=1,oj='none')

When joints get created in 3D space and not along a single plane, no amount of orienting is going to solve the errors you’ll get because Maya’s Joint Orient only operates in 2D space. Why that’s even an issue after 16+ versions of Maya is anyone’s guess, but here’s my cheap workaround. Starting with the first joint, we temporarily create an ikHandle to the last joint and create a poleVector at the middle joint. This establishes a proper plane of existence for our joint chain. Then those get deleted, the joints get frozen out, and just to make it pretty the last joint gets oriented to “world” which in this case means “parent”. Here’s the final code:

from maya import cmds
import maya.api.OpenMaya as om

class Arm():
    """
    This will create an arm setup.
    """

    def __init__(self):

        self.PART = "Arm"
        self.JNTNAMES = ['Arm', 'Elbow', 'Wrist', 'Twist']
        self.xyz = [[3, 18, 0],[3, 14, -0.25],[3, 10, 0]]

    def placers(self):
        """
        This will create locators for positioning.
        """

        # Create Locators
        self.FirstLoc = cmds.spaceLocator(n=self.JNTNAMES[0])
        cmds.xform(self.FirstLoc, ws=1, t=self.xyz[0])
        self.SecondLoc = cmds.spaceLocator(n=self.JNTNAMES[1])
        cmds.xform(self.SecondLoc, ws=1, t=self.xyz[1])
        self.ThirdLoc = cmds.spaceLocator(n=self.JNTNAMES[2])
        cmds.xform(self.ThirdLoc, ws=1, t=self.xyz[2])

        # Scale for ease of visibility
        for segment in [self.FirstLoc, self.SecondLoc, self.ThirdLoc]:
            cmds.setAttr(segment[0] + ".localScaleX", .2)
            cmds.setAttr(segment[0] + ".localScaleY", .2)
            cmds.setAttr(segment[0] + ".localScaleZ", .2)

        # Parent to maintain angle and move easier
        cmds.parent(self.ThirdLoc, self.SecondLoc)
        cmds.parent(self.SecondLoc, self.FirstLoc)

        # Create pseudo-bones to make it easier to visualize
        cmds.select(d=1)
        tempA = cmds.joint(n="%s_Temp"%self.JNTNAMES[0])

        tempC = cmds.joint(n="%s_Temp"%self.JNTNAMES[1])
        tempD = cmds.joint(n="%s_Temp"%self.JNTNAMES[2])
        cmds.setAttr(tempA + ".overrideEnabled", 1)
        cmds.setAttr(tempA + ".overrideDisplayType", 1)

        cmds.setAttr(tempC + ".overrideEnabled", 1)
        cmds.setAttr(tempC + ".overrideDisplayType", 1)

        cmds.parentConstraint(self.FirstLoc, tempA)
        cmds.parentConstraint(self.SecondLoc, tempC)
        cmds.parentConstraint(self.ThirdLoc, tempD)

        cmds.parent(tempA, self.FirstLoc)

        self.locatorList = [self.FirstLoc, self.SecondLoc, self.ThirdLoc]
        self.locatorJointsList = [tempA, tempC, tempD]
        cmds.select(d=1)
        return self.locatorList, self.locatorJointsList
        
        
    def createJoints(self, side, ojVal, saoVal):
        """
        Main procedure to create the bind joints.
        Requires a side input string (ex. "L")
        Requires a joint orient string (ex. "xyz")
        Requires a secondary axis string (ex. "yup")
        """
        #### Create Some Joints

        self.PREFIX = side

        locs = self.locatorJointsList

        j1T = cmds.xform(locs[0], q=1, ws=1, t=1, a=1)
        j2T = cmds.xform(locs[1], q=1, ws=1, t=1, a=1)
        j3T = cmds.xform(locs[2], q=1, ws=1, t=1, a=1)

        j2TL = cmds.xform(locs[1], q=1, os=1, t=1, r=1)
        j3TL = cmds.xform(locs[2], q=1, os=1, t=1, r=1)

        j1TV = om.MVector(j1T)
        j2TV = om.MVector(j2T)
        j3TV = om.MVector(j3T)

        lenJ1J2 = om.MVector(j2TV - j1TV).length()
        lenJ2J3 = om.MVector(j3TV - j2TV).length()

        cmds.select(d=1)
        a1 = cmds.joint(p=j1T, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[0]))
        a2 = cmds.joint(p=(0, j2TL[1], j2TL[2]), r=1, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[1]))
        a3 = cmds.joint(p=(0, j3TL[1], j3TL[2]), r=1, n="%s_%s_BJ" % (self.PREFIX, self.JNTNAMES[2]))

        self.bindJoints = [a1,a2,a3]

        cmds.joint(self.bindJoints[0], e=1, oj=ojVal, sao=saoVal, ch=1, zso=1)
        cmds.setAttr("%s.translate%s" % (self.bindJoints[1],ojVal[0].upper()), lenJ1J2)
        cmds.setAttr("%s.translate%s" % (self.bindJoints[2],ojVal[0].upper()), lenJ2J3)

        cmds.select(d=1)

        self.ikH = cmds.ikHandle(sj=self.bindJoints[0], ee=self.bindJoints[2], sol="ikRPsolver")[0]
        cmds.xform(self.ikH, t=j3T, a=1)

        pvLoc = cmds.spaceLocator()[0]
        cmds.xform(pvLoc, t=j2T, ws=1, a=1)

        cmds.poleVectorConstraint(pvLoc, self.ikH)   

        #### Create Some More Joints

        cmds.delete(self.ikH, pvLoc)
        cmds.makeIdentity(self.bindJoints[0], t=1, r=1, a=1, jo=0)
        cmds.joint(self.bindJoints[2],e=1,oj='none')

We can test the code by running the following lines:

myNewArm = Arm()
myNewArm.placers()
myNewArm.createJoints("L","xyz","yup")

Leave a Reply

Your email address will not be published. Required fields are marked *