tools

So, if you have been rigging for a while you have probably felt annoyed by having to create and adjust control shapes every time you build a new rig. You have probably found also that mirroring just the shape of a control or copying it to another one is a bit too tedious. There are some scripts and tools online to help you with this, such as the classic comet menu and the mz_ctrlcreator, but they do not offer all the functions we need and also extending them is not very practical. So, let us write our own control shape manager.

tl;dr I am going to walk you through the process of creating your own control shape manager, but if you would rather just use the final code you can find it here. If you would prefer it combined into one large file, you can grab it from here.

Here is a quick demo of some of the features we are going to look at.

Control shape manager in Maya - Demo

Table of contents

  1. Introduction
  2. Part 1: Control Shape Manager
  3. Getting and setting shapes
  4. Saving and loading
  5. Additional
  6. Part 2: Control Shape Functions
  7. Getting lists for the UI
  8. Assigning shapes and colours
  9. Saving to library
  10. Copying and pasting shapes
  11. Flipping shapes
  12. Rebuilding
  13. Part 3: Simpe UI
  14. Conclusion

Introduction

What we want is a python package that allows us to load and save control shapes to a library, copy and paste them to multiple other controls, change colours, flip them, mirror them, etc.

The full code can be found here. I have built it as a package with a few different modules, to be a bit clearer and nicer to maintain. I have also combined everything into one file as well, if anyone wants to just grab it and use it immediately. What we are going to do here though, is go through the code and learn how to build our own control shape manager, because it is much nicer when you actually understand how it works, as then you can extend it and adjust it to suit your needs. For example I have built upon this a bit more in my pipeline, so I can save and load shape versions for each control on a rig, so I can easily rebuild them when I am making changes.

Part 1: Control Shape Manager

We are going to be using a couple of commands from the Maya API, but if you are not very familiar with it, worry not I will explain what each function does. You can always read up on it on the Autodesk docs page or if you prefer more of a tutorial approach have a look at Chad Vernon’s Maya API web page.

Getting and setting shapes

Let us start with the two most important functions – getShape() and setShape().

def getShape(crv=None):
crvShapes = validateCurve(crv)

crvShapeList = []

for crvShape in crvShapes:
crvShapeDict = {
"points": [],
"knots": [],
"form": mc.getAttr(crvShape + ".form"),
"degree": mc.getAttr(crvShape + ".degree"),
"colour": mc.getAttr(crvShape + ".overrideColor")
}
points = []

for i in range(mc.getAttr(crvShape + ".controlPoints", s=1)):
points.append(mc.getAttr(crvShape + ".controlPoints[%i]" % i)[0])

crvShapeDict["points"] = points
crvShapeDict["knots"] = utils.getKnots(crvShape)

crvShapeList.append(crvShapeDict)

return crvShapeList

What this function does is, it gets all the data from a nurbsCurve node that we need to rebuild that curve down the line. We are going to look at the validateCurve() function a bit later, but it essentially checks if the curve we have passed is actually a valid curve and if so returns the shape node.

A list is initialized here which will later be populated with dictionaries for each shape node on the curve in order to work with compound curves.

The crvShapeDict is where the actual data is stored. All of the keys in the dictionary are just the needed data for building a curve. If you do not know what the knots and degree are when it comes to curve, you can read up on it here, but it is not necessary. We will be thinking of them as the essential building blocks of a curve.

You can see that very easily we can get the form, degree and colour ones as they are just attributes on the nurbsCurve node.

To get the points what we need to do is loop through all of the controlPoints of the curve. Initially, I was just using the cv attribute, but it does not work with closed curves, as the cvs are just representation of these points, so we can interact with them, but under the hood maya changes them a bit and they are stored in the controlPoints attribute. So, we just get the number of control points using the s flag on the getAttr command and we store each point in a list.

Now, for the knots initially I used this snippet from Serge Scherbakov, but it does not work with closed curves. I could have gone in and tried to create my own function to do that, but then maya has made it easy for us to get the knots from the API, so I thought I would just use that.

def getKnots(crvShape=None):
mObj = om.MObject()
sel = om.MSelectionList()
sel.add(crvShape)
sel.getDependNode(0, mObj)

fnCurve = om.MFnNurbsCurve(mObj)
tmpKnots = om.MDoubleArray()
fnCurve.getKnots(tmpKnots)

return [tmpKnots[i] for i in range(tmpKnots.length())]

The first part of this function deals with getting an API reference to our curve. It basically, adds the passed in crvShape to a virtual selection (without actually selecting anything in the viewport) and gets an MObject from it. That’s the base class in the API and from there we can cast it to the type we actually need – MFnNurbsCurve. Then we create an empty MDoubleArray, which we populate from the curve with the getKnots function. And that’s it. Lastly, we return it as a python list, just so we can interact with it easier.

And with that we have a list of dictionaries containing all the necessary information for rebuilding that curve.

Let’s look at setting the shape now. What is nice about this code is that if you understand how the getShape() works, the setShape() is going to be trivial. The one thing I do not like about this code is that we are not assigning the data to the existing curve, but we delete it and create a new one in place. This could cause issues if there are any connections to or from the shape node, but you can always store and rebuild those. I have not yet found a way around it though.

def setShape(crv, crvShapeList):
crvShapes = validateCurve(crv)

oldColour = mc.getAttr(crvShapes[0] + ".overrideColor")
mc.delete(crvShapes)

for i, crvShapeDict in enumerate(crvShapeList):
tmpCrv = mc.curve(p=crvShapeDict["points"], k=crvShapeDict["knots"], d=crvShapeDict["degree"], per=bool(crvShapeDict["form"]))
newShape = mc.listRelatives(tmpCrv, s=1)[0]
mc.parent(newShape, crv, r=1, s=1)

mc.delete(tmpCrv)
newShape = mc.rename(newShape, crv + "Shape" + str(i + 1).zfill(2))

mc.setAttr(newShape + ".overrideEnabled", 1)

if "colour" in crvShapeDict.keys():
setColour(newShape, crvShapeDict["colour"])
else:
setColour(newShape, oldColour)

We go through the same call to validateCurve() as before and then we store the "overrideColor" of the curve, so we can apply it back after we rebuild the shape. It is important to note that the colour is the one assigned to the first shape child of the curve. And since we have everything we need from the old shapes – the colour – we delete them.

Then for each shape in the list we just use our points, knots, degree and form data from the dictionary to build a new curve with the mc.curve() command. The per flag refers to periodic and basically defines whether our curve is one whole or does it have a start and an end. A bit more info about periodic curves in here.

Once we have created the new shape we parent it to the crv object with the r=1 and s=1 flags for mc.parent() to define that we are working with shapes and to maintain their relative positions. We then can rename the new shape according to our convention. Lastly, we just reapply the colour or we get it from the dictionary.

As I said these two are the most important functions as they are dealing with the actual data. Now that we have them in place we can give them a quick test. Create a nurbsCurve with whatever shape you want. Then let’s create a simple circle and copy the first shape to the circle. Assuming that the first curve is called curve1 and the circle is nurbsCircle1 run the following snippet.

shapeDict = getShape("curve1")
setShape("nurbsCircle1", shapeDict)

I realize this is not very exciting as there are easier ways to do this, but the cool thing is when we start saving and loading them.

Saving and loading

We have been looking only at the manager.py file for know. In the utils.py we have a few more functions mainly dealing with the saving and loading of json data. Loading and saving JSON files is a very popular and fairly trivial python task, but let’s deconstruct it.

def loadData(path=None):
if os.path.isfile(path):
f = open(path, "r")
data = json.loads(f.read())
f.close()
return data
else:
mc.error("The file " + path + " doesn't exist")

def saveData(path=None,
data=None):
if validatePath(path):
f = open(path, "w")
f.write(json.dumps(data, sort_keys=1, indent=4, separators=(",", ":")))
f.close()
return 1
return 0

For loading we start by checking if the file exists and if it does, we use python’s open function to get the raw data and we pass it to a json.loads() function to convert the raw data to a Python dict object.

When saving, we are doing the same thing but instead of reading and converting from raw data to a dict we are passing a dict to the json.dumps() function which serializes our dictionary to JSON and then we write it to the file. You will notice that there is a call to another validation function – validatePath().

def validatePath(path=None):
if os.path.isfile(path):
confirm = mc.confirmDialog(title='Overwrite file?',
message='The file ' + path + ' already exists.Do you want to overwrite it?',
button=['Yes', 'No'],
defaultButton='Yes',
cancelButton='No',
dismissString='No')
if confirm == "No":
mc.warning("The file " + path + " was not saved")
return 0
return 1

All we do here is check if the file we are trying to save already exists and if so gives the option to overwrite it or cancel the save process.

Now that we know how our dictionary data is being load and saved, we just need to have a wrapper function in our manager module to load and save to the defined shape library directory.

Before looking at those though, you need to have the SHAPE_LIBRARY_PATH set at the top of the file. Keep in mind that if the path does not exist, Python will not create it for us but error out.

def loadFromLib(shape=None):
path = os.path.join(SHAPE_LIBRARY_PATH, shape + ".json")
data = utils.loadData(path)
return data

What we do here is define the path to the file we want to load and use the loadData function we talked about to load the actual dictionary.

def saveToLib(crv=None,
shapeName=None):
crvShape = getShape(crv=crv)
path = os.path.join(SHAPE_LIBRARY_PATH, re.sub("\s", "", shapeName) + ".json")
for shapeDict in crvShape:
shapeDict.pop("colour", None)
utils.saveData(path, crvShape)

Then when saving we use re.sub("s", "", shape) in order to strip spaces from the name as they can cause issues and pass the path to the saveData() function. Also, we get rid of the colour keys as we want to save only the shape of the curve.

Additional commands

The rest of the functions in the module are fairly self-explanatory.

def validateCurve(crv=None):
if mc.nodeType(crv) == "transform" and mc.nodeType(mc.listRelatives(crv, c=1, s=1)[0]) == "nurbsCurve":
crvShapes = mc.listRelatives(crv, c=1, s=1)
elif mc.nodeType(crv) == "nurbsCurve":
crvShapes = mc.listRelatives(mc.listRelatives(crv, p=1)[0], c=1, s=1)
else:
mc.error("The object " + crv + " passed to validateCurve() is not a curve")
return crvShapes

The validateCurve() function just checks if we have passed a valid curve and if so it returns the nurbsCurve shape nodes to work with. Otherwise it errors.

Then we have the colour functions which are just simple wrappers around mc.getAttr() and mc.setAttr() commands to interact with the "overrideColor" attribute of shapes.

def setColour(crv, colour):
if mc.nodeType(crv) == "transform":
crvShapes = mc.listRelatives(crv)
else:
crvShapes = [crv]
for crv in crvShapes:
mc.setAttr(crv + ".overrideColor", colour)

def getColour(crv):
if mc.nodeType(crv) == "transform":
crv = mc.listRelatives(crv)[0]
return mc.getAttr(crv + ".overrideColor")

Part 2: Control Shape Functions

Now that we have our core functionality in place we can stop here and just use the code we have so far through our script editor, which is absolutely fine, but is not very scalable and not really user friendly. Additionally, we are still lacking the mirroring and flipping functionality, so let us create a functions.py file which will act as a wrapper to our manager module. The reason we would want this is to prevent messing about with our manager too much and provide a higher level control so we can literally only care about using the tool instead of how it works. Altogether, it is much nicer working with simple short functions. Okay, let us go through the functions.py commands that help us interact with the manager.

Getting lists for the UI

def getAvailableControlShapes():
lib = manager.SHAPE_LIBRARY_PATH
return [(x.split(".")[0], functools.partial(assignControlShape, x.split(".")[0])) for x in os.listdir(lib)]

def getAvailableColours():
return [("index" + str(i).zfill(2), functools.partial(assignColour, i), "shapeColour" + str(i).zfill(2) + ".png") for i in range(32)]

These two functions are mainly here to help us later when we are going to build some sort of UI for our manager. Essentially they return lists of tuples containing the names, commands and in the case of getAvailableColours() images of the available shapes and colours. These are going to be used when building menus that look similar to the following.

Control shape manager - Shapes dropdown
Shapes
Control shape manager - Colours dropdown
Colours

Notice that the second item in the tuple is a functools.partial() call. For more info refer to the docs, but briefly it allows us to get a reference to a function with added arguments as well. So the first argument is a function and then we have a number of arguments which are going to be provided to the function as *args. Let’s have a look at the functions themselves to see how this works.

Assigning shapes and colours

def assignColour(*args):
for each in mc.ls(sl=1, fl=1):
manager.setColour(each, args[0])

def assignControlShape(*args):
sel = mc.ls(sl=1, fl=1)
for each in sel:
manager.setShape(each, manager.loadFromLib(args[0]))
mc.select(sel)

So, both these functions receive *args as an argument, which means that we can provide lots of arguments and they are going to be passed to the function as a list which we can acces by args[n]. In the previous paragraph, we saw that we pass these functions and a single argument to the functools.partial, which means that the first element of args is going to be the second argument of the functools.partial() code. So in the case of functools.partial(assignColour, i), we are going to receive a call equivalent to assignColour(i).

Additionally, keep in mind if these functions that we are defining here are meant to be used from a maya UI, and a lot of the buttons in maya are passing arguments to their commands, so we need to have the *args, because otherwise the functions will error.

Notice that we reselect our initial selection at the end of the function. We will do this in all functions that call the setShape() one, because the creation of the curve inside of it deselects our selection and instead selects the newly created curve, which is not very intuitive.

Saving to library

def saveCtlShapeToLib(*args):
result = mc.promptDialog(title="Save Control Shape to Library",
m="Control Shape Name",
button=["Save", "Cancel"],
cancelButton="Cancel",
dismissString="Cancel")
if result == "Save":
name = mc.promptDialog(q=1, t=1)
manager.saveToLib(mc.ls(sl=1, fl=1)[0], name)
rebuildUI()

As we said the goal here is to make interacting with our control shape manager as smooth as possible. Therefore, we create a wrapper to our saveToLib() command to let us add a name in a nice and familiar dialog. In the end we are calling the rebuildUI() function which we will look at the end of this part, but the reason it is here is that every time we save a new control shape we would like the UI to be rebuild, in order for the menu containing all of our shapes to be up to date.

Copying and pasting shapes

def copyCtlShape(*args):
global ctlShapeClipboard
ctlShapeClipboard = manager.getShape(mc.ls(sl=1, fl=1)[0])
for ctlShape in ctlShapeClipboard:
ctlShape.pop("colour")

def pasteCtlShape(*args):
sel = mc.ls(sl=1, fl=1)
for each in sel:
manager.setShape(each, ctlShapeClipboard)
mc.select(sel)

As we saw previously, it is really easy to copy and paste shapes with the manager alone, but to provide a quick and easy interface these two functions seem to do a good job. Essentially, we are creating a global variable and store the selected shape dictionary inside of it. Again we pop the “colour” key, as we just want to copy the shape. Then we just use the setShape() function on all selected controls with that global variable.

Flipping shapes

Then there are a few functions for flipping the shapes. It’s a bit of a pain to have to do that manually, but it is really easy to scale the points by -1 through script so let’s have a look at the _flipCtlShape() function. You will notice that there are a few more functions for flipping – flipCtlShape(), flipCtlShapeX(), flipCtlShapeY() and flipCtlShapeZ(). They all just make a call to the _flipCtlShape() one, but with different arguments, so we will just look at that one.

def _flipCtlShape(crv=None, axis=[-1, -1, -1]):
shapes = manager.getShape(crv)
newShapes = []
for shape in shapes:
for i, each in enumerate(shape["points"]):
shape["points"][i] = [each[0] * axis[0], each[1] * axis[1], each[2] * axis[2]]
newShapes.append(shape)
manager.setShape(crv, newShapes)
mc.select(crv)

All we do in this one, is just go through each CV and scale it’s x, y and z coordinates by -1 in order to flip the shape. The above mentioned other functions just call this one with the axis set to [-1, 1, 1] for x, [1,-1,1] for y, etc.

I skipped the mirrorCtlShapes() function earlier, because I wanted to already have the flip one in place as we are going to be using it again.

def mirrorCtlShapes(*args):
sel = mc.ls(sl=1, fl=1)
for ctl in sel:
if ctl[0] not in ["L", "R"]:
continue
search = "R_"
replace = "L_"
if ctl[0] == "L":
search = "L_"
replace = "R_"
shapes = manager.getShape(ctl)
for shape in shapes:
shape.pop("colour")
manager.setShape(ctl.replace(search, replace), shapes)
_flipCtlShape(ctl.replace(search, replace))
mc.select(sel)

The bulk of the code here is really for defining the search and replace strings. Since the naming convention that I use is SIDE_NAME_NODETYPE my search and replace strings vary between “L_” and “R_”. Have a look at your convention and modify these strings to make it work. Once they are defined, all we do is copy the shape from the current side to the other one and once done, flip it in all axis. In my pipeline, I have made it so this function does not work with a selection, but instead goes through all my left controls and mirrors them to the right. It is just because I always work from left to right, so I do not need this functionality.

Rebuilding

def rebuildUI(*args):
mc.evalDeferred("""
import controlShapeManager
reload(controlShapeManager)
""")

Lastly, there is a simple function to rebuild the UI. All it does is import the package, as the way I have set it up is that importing just builds the UI which in turn makes the references to all the needed functions. The UI example that I give is very primitive, but obviously you can replace this code with one that will work with your own UI. Keep in mind that it is best to use the mc.evalDeferred() command as otherwise, the rebuild might error as it is being called from the UI that needs to be rebuilt.

Part 3: Simple UI

Now that we have all functions that we need we can build an UI to interact with them. Since everybody has a different pipeline for rigging at place, I am hesitant to suggest any specific way of handling that UI. One might prefer it in a window, other a tool menu or others yet a shelf button like I do. So I have added a very simple shelf button build in the managerUI.py to demonstrate how would we go about it. Additionally, remember how when generating the lists for the available colours we had a third item in the tuple for an image? You can get these here. They are just images of solid colour, corresponding to the index of the overrideColor attribute.

For a more comprehensive intro to building shelves with buttons and popups have a look at my Building custom maya shelves post.

Let’s have a look at it then.

import maya.cmds as mc

# Local import
import functions
reload(functions)

SHELF_NAME = "Custom"
ICON_PATH = "C:/PATH_TO_ICONS"

if SHELF_NAME and mc.shelfLayout(SHELF_NAME, ex=1):
children = mc.shelfLayout(SHELF_NAME, q=1, ca=1) or []
for each in children:
try:
label = mc.shelfButton(each, q=1, l=1)
except:
continue
if label == "ctlShapeManager":
mc.deleteUI(each)

mc.setParent(SHELF_NAME)
mc.shelfButton(l="ctlShapeManager", i="commandButton.png", width=37, height=37, iol="CTL")
popup = mc.popupMenu(b=1)
mc.menuItem(p=popup, l="Save to library", c=functions.saveCtlShapeToLib)

sub = mc.menuItem(p=popup, l="Assign from library", subMenu=1)

for each in functions.getAvailableControlShapes():
mc.menuItem(p=sub, l=each[0], c=each[1])

mc.menuItem(p=popup, l="Copy", c=functions.copyCtlShape)
mc.menuItem(p=popup, l="Paste", c=functions.pasteCtlShape)

sub = mc.menuItem(p=popup, l="Set colour", subMenu=1)

for each in functions.getAvailableColours():
mc.menuItem(p=sub, l=each[0], c=each[1], i=ICON_PATH + each[2])

mc.menuItem(p=popup, l="Flip", c=functions.flipCtlShape)
mc.menuItem(p=popup, l="Mirror", c=functions.mirrorCtlShapes)

So what happens here is we import the functions.py file which in turn imports the manager.py which then imports the utils.py file. Then there are the two variables – SHELF_NAME and ICON_PATH for declaring the shelf to add the button to and the path to the icons. Then we check if a button with the same name already exists in the shelf and if it does we delete it so we can replace it with our new one.

From then on we have simple maya UI commands to build our buttons and menus. If you are not familiar with UIs in maya it is worth having a look at the docs. Essentially, all we do is create a single mc.shelfButton() and we attach a mc.popupMenu() to it. Which then we populate with mc.menuItem()s where the l flag stands for label and the c for command. So there we pass our functions commands. Notice that we are not adding the () after the function name as that would call it and return the output. Instead we want to pass a reference to that function.

Then for the shapes and colours menuItems we add the subMenu flag so they become deeper level menus and we populate them with the results of our functions.getAvailableControlShapes() and functions.getAvailableColours() commands, which results in lists containing the shapes in our library and all 32 available colours.

Conclusion

And that is it. We have built our own control shape manager. With some easy extensions you can improve it to have almost like a version control system for your rigs, so you do not ever have to worry about your control shapes anymore.

Again, here is the link to the repo and the gist containing everything in one file if you would rather just use something quickly.

EDIT: I would advise against using this exact same script as the parentConstraints have been known to cause issues and are not a graceful solution at all. Until I update the article, either try replacing them with xform calls or have a look at Alessandra’s comment.

I remember the first time I tried to set up a seamless IK FK switch with Python vividly. There was this mechanical EVA suit that I was rigging for a masterclass assignment at uni given by Frontier. The IK to FK switching was trivial and there were not many issues with that, but I had a very hard time figuring out the FK to IK one, as I had no idea what the pole vector really is and also, my IK control was not oriented the same way as my FK one.

Im sure that throughout the web there are many solutions to the problem, but most of the ones I found were in MEL and some of them were a bit unstable, because they were relying too much on the xform command or the rotate one with the ws flag, which I am assuming causes issues sometimes when mapping from world space to relative, where a joint will have the exact same world rotation, so it looks perfectly, but if you blend between IK and FK you can see it shifting and then coming back in place. That’s why I decided to use constraints to achieve my rotations, which seems to be a simple enough and stable solution.

EDIT: It seems like even with constraints it is possible to get that issue in the case where the IK control is oriented differently. What fixes is though is switching back and forth once more.

Here is what we are trying to achieve

Seamless IK FK swich demo

Basically, there is just one command for the seamless IK FK Switch, which detects the current kinematics and switches to the other one maintaining the pose. I have added the button to a custom marking menu for easier access.

So, in order to give you a bit of a better context I have uploaded the example scene that I am using, so you can have a look at the exact structure, but feel free to use your own scene with IK/FK blending setup. The full code (which is very short anyway) is in this gist and there are three scene files in here for each version of our setup. The files contain just a simple IK/FK blending system, on which we can test our matching setup, but with different control orientations.

It is important to understand the limitations of a seamless IK FK switch before we dive in. Mainly, I am talking about the limited rotation of the second joint in the chain, as IK setups allow for rotations only in one axis. What this means is that if we have rotations in multiple axis on our FK control for that middle joint (elbow, knee, etc.) the IK/FK matching will not work properly. All this is due to the nature of inverse kinematics.

Also, for easier explaining I assume we are working on an arm and hand setup, but obviously the same approach would work for any IK/FK chain.

We will consider three cases:
All controls and joints are oriented the same
IK Control oriented in world space
IK Control and IK hand joint both oriented in world

Again, you do not have to use the same file as I do as it is just an example, but it is important to be clear on the existing setup. We assume that we have an arm joint chain – L_arm01_JNT > L_arm02_JNT > L_arm03_JNT and a hand joint chain – L_hand01_JNT > L_hand02_JNT with their correspondent IK and FK chains – L_armIk01_JNT > …, L_armFk01_JNT > …, etc. These two chains are blended via a few blendColors nodes for translate, rotate and scale into the final chain. The blending is controlled by L_armIkFk_CTL.fkIk. Then we have a simple non-stretchy IK setup, but obviously a stretchy one would work in the same way. Lastly, the L_hand01_JNT is point constrained to L_arm03_JNT and we only blend the rotate and scale attributes on it, as otherwise the wrist becomes dislocated during blending, because we are interpolating linearly translation values.

Now that we know what we have to work with, let us get on with it.

Seamless IK FK Switch when everything shares orientation

So, in this case, all of our controls and joints have the exact same orientation in both IK and FK. What this means is that essentially all we need to do to match the kinematics is to just plug the rotations from one setup to the other. Let’s have a look. The scene file for this one is called ikFkSwitch_sameOrient.ma

IK to FK

This one is always the easier setup, as FK controls generally just need to get the same rotation values as the IK joints and that’s it. Now, initially I tried copying the rotation via rotate and xform commands, but whenever a control was rotated a bit too extreme these would cause flipping when blending between IK and FK, which I am assuming is because these commands have a hard time converting the world space rotation to a relative one, causing differences of 360 degrees. So, even though in full FK and full IK everything looks perfect, in-between the joint rotates 360 degrees. Luckily, maya has provided us with constraints which have all the math complexity built in. Assuming you have named your joints the same way as me we use the following code.

mc.delete(mc.orientConstraint("L_armIk01_JNT", "L_armFk01_CTL"))
mc.delete(mc.orientConstraint("L_armIk02_JNT", "L_armFk02_CTL"))
mc.delete(mc.orientConstraint("L_handIk01_JNT", "L_handFk01_CTL"))

mc.setAttr("L_armIkFk_CTL.fkIk", 0)

As I said, this one is fairly trivial. We just orient each of our FK controls to match the rotations of the IK joints. Then in the end we change our blending control to FK to finalize the switch.

FK to IK

Now, this one was a pain the first time I was trying to do it, because I had no idea how pole vectors worked at all. As soon as I understood that all we need to know about them is that they just need to lie on the same plane as the three joints in the chain, it became easy. So essentially, we need to place the IK control on the FK joint to solve the end position. And then to get the elbow (or whatever your mid joint is representing) to match the FK, we just place the pole vector control at the exact location of the corresponding joint in the FK chain. So, we get something like this.

mc.delete(mc.parentConstraint("L_handFk01_JNT", "L_armIk_CTL"))
mc.xform("L_armPv_CTL", t=mc.xform("L_armFk02_JNT", t=1, q=1, ws=1), ws=1)

mc.setAttr("L_armIkFk_CTL.fkIk", 1)

Now even though this does the matching job perfectly it is not great for the animators to have the control snap at the mid joint location as it might go inside the geometry, which is just an unnecessary pain. What we can do is, get the two vectors from arm01 to arm02 and from arm03 to arm02 and use them to offset our pole vector a bit. Here’s the way we do that.

arm01Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk01_JNT", t=1, ws=1, q=1)[i] for i in range(3)]
arm02Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk03_JNT", t=1, ws=1, q=1)[i] for i in range(3)]

mc.xform("L_armPv_CTL", t=[mc.xform("L_armFk02_JNT", t=1, q=1, ws=1)[i] + arm01Vec[i] * .75 + arm02Vec[i] * .75 for i in range(3)], ws=1)

So, since xform returns lists in order to subtract them to get the vectors we just loop through them and subtract the individual elements. If you are new to the shorter loop form in Python have a look at this. Then once we have the two vectors we add 75% of them to the position of the arm02 FK joint and we arrive at a position slightly offset from the elbow, but still on the same plane, thus the matching is still precise. Then our whole FK to IK code would look like this

mc.delete(mc.parentConstraint("L_handFk01_JNT", "L_armIk_CTL"))

arm01Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk01_JNT", t=1, ws=1, q=1)[i] for i in range(3)]
arm02Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk03_JNT", t=1, ws=1, q=1)[i] for i in range(3)]

mc.xform("L_armPv_CTL", t=[mc.xform("L_armFk02_JNT", t=1, q=1, ws=1)[i] + arm01Vec[i] * .75 + arm02Vec[i] * .75 for i in range(3)], ws=1)

mc.setAttr("L_armIkFk_CTL.fkIk", 1)

Seamless IK FK switch when the IK control is oriented in world space

Now, in this case, the orientation of the IK control is not the same as the hand01 joint. I think in most cases people go for that kind of setup as it is much nicer for animators to have the world axis to work with in IK. The scene file for this one is called ikFkSwitch_ikWorld.ma.

The IK to FK switch is exactly the same as the previous one, so we will skip it.

FK to IK

So, in order to get this to work, we need to do the same as what we did in the previous case, but introduce an offset for our IK control. How do we get this offset then? Well, since we can apply transformations only on the controls, we need to calculate what rotation we need to apply to that control in order to get the desired rotation. Even though, we can calculate the offsets using maths and then apply them using maths, we might run into the same issue with flipping that I discussed in the previous case. So, instead, a much easier solution, but somewhat dirtier is to create a locator which will act as our dummy object to orient to.

Then, in our case where only the IK control is oriented differently from the joints, what we need to do is create a locator and have it assume the transformation of the IK control. The easiest way would be to just parent it underneath the control and zero out the transformations. Then parent the locator to the L_handFk01_JNT, as that’s the one that we want to match to. Now wherever that handFk01 joint goes, we have the locator parented underneath which shares the same orientation as our IK control. Therefore, just using parentConstraint will give us our matching pose. Assuming the locator is called L_hand01IkOfs_LOC all we do is this.

mc.delete(mc.parentConstraint("L_hand01IkOfs_LOC", "L_armIk_CTL"))

This will get our wrist match the pose perfectly. Then we apply the same code as before to get the pole vector to match as well and set the IK/FK blend attribute to IK.

arm01Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk01_JNT", t=1, ws=1, q=1)[i] for i in range(3)]
arm02Vec = [mc.xform("L_armFk02_JNT", t=1, ws=1, q=1)[i] - mc.xform("L_armFk03_JNT", t=1, ws=1, q=1)[i] for i in range(3)]

mc.xform("L_armPv_CTL", t=[mc.xform("L_armFk02_JNT", t=1, q=1, ws=1)[i] + arm01Vec[i] * .75 + arm02Vec[i] * .75 for i in range(3)], ws=1)

mc.setAttr("L_armIkFk_CTL.fkIk", 1)

Seamless IK FK switch when the IK control and joint are both oriented in world space

Now, in this last scenario, we have the handIk01 joint oriented in world space, as well as the control. The reason you would want to do this again is to give the animators the easiest way to interact with the hand. In the previous case, the axis of the IK control do not properly align with the joint which is a bit awkward. So a solution would be to have the handIk01 joint oriented in the same space as our control, so the rotation is 1 to 1 and it is a bit more intuitive. The scene for this one is ikFkSwitch_ikJointWorld.ma and it looks like this.

It is important to note that the IK joint is just rotated to match the position of the control, but the jointOrient attributes are still the same as the FK one and the blend one.

Seamless IK FK Switch with IK control and joint oriented in world space
IK FK Switch with IK control and joint oriented in world space

So again, going from IK to FK is the same as before, we are skipping it. Let us have a look at the FK to IK.

FK to IK

This one is very similar to the previous one, where we have an offset transform object to snap to. The difference is that now instead of having that offset be calculated just from the difference between the IK control and the FK joint, we also need to adjust for the existing rotation of the IK joint as well. So, we start with our locator the same way as before – parent it to the IK control, zero out transformations and parent to the handFk01 joint. And then, the extra step here is to apply the negative rotation of the IK joint to the locator in order to get the needed offset. So, this calculation would look like this.

ikRot = [-1 * mc.xform("L_handIk01_JNT", ro=1 ,q=1)[i] for i in range(3)]
mc.xform("L_hand01IkOfs_LOC", ro=ikRot, r=1)

We just take the rotation of the IK joint and multiply it by -1, which we then apply as a relative rotation to the locator.

And then again, as previously we just apply the pole vector calculation and we’re done.

Conclusion

So, as you can see, scripting a seamless IK FK switch is not really that complicated at all, but if you are trying to figure it out for the first time, without being very familiar with rigging and 3D maths it might be a bit of a pain. Again, if you want to see the full code it is in this gist.