Website powered by

Maya Python API: Getting/Setting Skin Weights

Tutorial / 22 April 2019

The Maya API is an incredibly useful tool to have in your toolbelt as a td. With the data it gives you access to, you can do operations that would be slow, if not impossible, to do with MEL or maya.cmds


In this post, I'll document how I get and set Skin Cluster weights so as to do operations like influence pruning, smoothing, copy/paste average, and more. 

The basic structure of these scripts is all the same. You get the selected components, find the skinCluster they belong to, get its influence and weight list, do something to change that list, and then set the weights again. I'm using the Python API for this, but if you know C++ and how to compile it with Maya you are at a huge speed advantage, especially when iterating over a ton of components. 

API Reference

Getting the Selection

The first thing I usually do is store the original selection in a MSelectionList object so that we can reset the selection to that at the very end. This helps keep the user's workflow (generally mine) less interrupted by constant object/component level switching. 

    original_sel = om.MSelectionList()
    om.MGlobal.getActiveSelectionList(original_sel)

"om" is my chosen abbreviation for the maya.OpenMaya module. 


Getting the Selected Mesh and Components

The next step is to a quick way to make sure you're iterating over vertices. We do this since all the data we'll be looking for is stored at a vertex level. In this case, we're only going to do operations over one mesh. If you wanted to do this over multiple meshes at once, you'd use a MItSelectionList object. 

    cmds.select(cmds.polyListComponentConversion(toVertex=True))


    # get the selected mesh and components
    sel = om.MSelectionList()
    om.MGlobal.getActiveSelectionList(sel)


    if not sel.length():
      return


    selected_components = om.MObject()
    dag = om.MDagPath()
    sel.getDagPath(0, dag, selected_components)


    dag.extendToShape()


    if dag.apiType() != 296:
      om.MGlobal.displayError("Selection must be a polygon mesh.")
      return

Above, I'm using a handy maya.cmds function to quickly convert our selections to vertices. I do this after I store the original selection and before I store it in our selection list since I want that selection list to hold vertices. If I didn't, it wouldn't be able to derive the correct vertices from the edges/faces/objects I had selected. 

If there isn't anything selected, I get out of the function. Otherwise I get the MDagPath object of the selection as well as any components on it that are currently selected. Then I do a check to see if the apiType of its shape node is 296, which is the type for polygonal meshes. This iterator isn't designed for anything except skinClusters on polygons. That's what I'm almost always using anyways. 


Getting the SkinCluster's Name and MObject

Next I use a handy function that again utilizes some quick maya.cmds functions to get us data quicker. If you're using the Maya Python API, you have the advantage of also using maya.cmds as you work. Don't kill yourself over iterating up a DG if you can do the same thing in one line of maya.cmds. If speed is your concern, don't use maya.cmds in loops that iterate over thousands of components. 

def getSkinCluster(self, dag):
    """A convenience function for finding the skinCluster deforming a mesh.

    params:
      dag (MDagPath): A MDagPath for the mesh we want to investigate. 
    """

    # useful one-liner for finding a skinCluster on a mesh
    skin_cluster = cmds.ls(cmds.listHistory(dag.fullPathName()), type="skinCluster")

    if len(skin_cluster) > 0:
      # get the MObject for that skinCluster node if there is one
      sel = om.MSelectionList()
      sel.add(skin_cluster[0])
      skin_cluster_obj = om.MObject()
      sel.getDependNode(0, skin_cluster_obj)

      return skin_cluster[0], skin_cluster_obj

    else:
      raise RuntimeError("Selected mesh has no skinCluster")



The line cmds.ls(cmds.listHistory(dag.fullPathName()), type="skinCluster")  is an especially useful one-liner that I use ALL THE TIME. I'm using the ls command to filter any skinCluster type objects out of an object's history list. If there is a length to that list, then one exists. I'm then using MSelectionList to get the MObject from that skin cluster node. 


Getting the Skin Weights

Now that we have the MObject for our skinCluster we can make our MFnSkinCluster object and get access to all the handy functions it provides. Much of what I do next is gathering the information required to get and set the weights for our skinCluster. One of those is an MDagPathArray of the influence objects (joints) that influence our mesh. The other is a pointer to an unsigned integer object that holds the amount of influence objects on the skinCluster. The last is an MIntArray that basically just goes from 0 to 1 minus the amount of influence objects. Again, all just information we need to pass to MFnSkinCluster.getWeights, but once you do it right, you don't really need to write it again. 

    # doing this can speed up iteration and also allows you to undo all of this
    cmds.skinPercent(skin_cluster, pruneWeights=0.005)

    mFnSkinCluster = omAnim.MFnSkinCluster(skin_cluster_obj)

    inf_objects = om.MDagPathArray()
    # returns a list of the DagPaths of the joints affecting the mesh
    mFnSkinCluster.influenceObjects(inf_objects)
    inf_count_util = om.MScriptUtil(inf_objects.length())

    # c++ utility needed for the get/set weights functions
    inf_count_ptr = inf_count_util.asUintPtr()
    inf_count = inf_count_util.asInt()
    influence_indices = om.MIntArray()

    # create an MIntArray that just counts from 0 to inf_count
    for i in range(0, inf_count):
      influence_indices.append(i)

    old_weights = om.MDoubleArray()
    # don't use the selected_components MObject we made since we want to get the weights for each vertex 
    # on this mesh, not just the selected one
    empty_object = om.MObject()
    mFnSkinCluster.getWeights(dag, empty_object, old_weights, inf_count_ptr)

    # new_weights just starts as a copy of old_weights
    new_weights = om.MDoubleArray(old_weights)


Iterating and DOING STUFF

Now we're ready to actually DO STUFF to the weight list we got. That weight list is kind of strange at first and can be tricky to work with. It's a one-dimensional list of length (influence_count * component_count) so that the "weights" for one vertex is at the subset weight_list[this_vert_weight_index: this_vert_weight_index + inf_count]. 

So now we iterate over the selected components using a MItMeshVertex object, which again gives us a TON of information at a vertex level like its index (which will correspond to the correct index in the weight list), its neighbors, and its normal vector. If you needed more information or needed to do stuff like raycasting, you can make a MFnMesh object outside of the iterator and get even more information from your mesh. Pretty nice, huh. 

    # iterate over the selected verts
    itVerts = om.MItMeshVertex(dag, selected_components)
    
    while not itVerts.isDone():
      this_vert_weight_index = itVerts.index() * inf_count
      vert_weights = list(new_weights[this_vert_weight_index: this_vert_weight_index + inf_count])

      # makes the weights for the closest vertex equal to the outer vertex
      new_weights[this_vert_weight_index: this_vert_weight_index + inf_count] = SOME AWESOME FUNCTION

      itVerts.next()

    # set weights all at once
    mFnSkinCluster.setWeights(dag, empty_object, influence_indices, new_weights, True, old_weights)

    om.MGlobal.setActiveSelectionList(original_sel)

Technical Direction for Rigging and Animation

Tutorial / 23 February 2019

This post is an excerpt from a larger essay I've written on Digital Character pipelines.

Rigging

As with Surfacing, Rigging, home of Character Technical Directors like me, is another highly technical department that is difficult to give quick advice on. There are, however, certain workflow tips that I think could be universally helpful to an aspiring Character TD. If you do not know, Character TDs are responsible for creating the skeleton of the character, defining how that skeleton will move the mesh, and making control systems to make animating the skeleton as friendly as possible. They also write scripts and pipeline tools to make the technical side of computer animation as streamlined as possible. Skeletons are hand-placed and are often shared across similar character build types for efficiency. When a character is being developed, the entire motion system for the character will almost consistently be evolving based on animation and shot-specific needs. For that reason, rigs, the motion systems that drive the skeleton, are usually scripted to make the complex process of setting them up faster, consistent, and flexible. 

As stated previously, rigging can begin in some of the earliest stages of a character’s look development. If, for example, you know from the beginning your character is going to be a standard biped, you could pretty quickly narrow in on the basic proportions and volumes of the character and apply your rig to it. The animator can then reference in that rig and begin to animate on it. As long as attributes like the names, orientation, and rotation orders of the controls stay consistent through a rig’s development, animation generally transfers pretty well when the rig (and reference) is updated. More on this later. 

As the model gets updated, the rigger can replace the outdated portions of the character with the more final versions of the mesh. They can do this transfer efficiently using the CopySkinWeights tool or a custom extension of it. Art updates like adding UVs or adjusting the model happen on an almost daily basis in game and film production and are not a huge deal to implement. As long as the location of the character’s joints stays the same and volumes don’t change too much, a rig can be developed for a character from a very early stage. Faces, as stated before, are generally much more difficult to apply art updates to and should be as dialed-in as possible before rigging is begun (linear development). 

Sending a rig to a game engine can be a pretty confusing process at first. The important thing to understand is that all the engine accepts from rigs are its mesh, joints, skinning, and keyframes. As a best practice, the mesh and joints should be children of a single node. 


Something like: 

Character
> char_geo (can be a grouped if more than one mesh)
> root (the skeleton of the rig)
  > cog
      …


Notice that the hierarchy there contains none of the rig components or constraints. All it has are the joints plus the meshes they are bound to and nothing else. Therefore if you have a regular rig and you want to export a clean fbx to an engine like Unreal, these are the steps you’d take:

  • Select all of the bind joints and do Edit>Keys>BakeSimulation 

    • Doing this ensures that when we delete the rig, the transform values on the joints will stay the same.

  • Delete everything in the rig other than the final bind geometry and joints

  • Select the whole hierarchy and ExportSelection as an FBX

    • If you’re only exporting the fbx as a skelemesh (no animation) then uncheck Animation

    • FBX version 2014/15 works pretty consistently in my experience.

FBX is basically a filetype kind of like an obj except that it can hold a lot of extra data like animation, joints, cameras, etc...


Animation

Animating a complex character in a parallel pipeline is as necessary as it is practical. With render-time polycounts for characters constantly increasing, the necessity for “lite” versions of characters–with a model density that can give real-time playback speed–has become commonplace. Many animators may already be familiar with this type of development as many student rigs give options for “proxy”, “low” and “high” resolution versions of the model to increase manipulation speeds. These different model resolutions are useful at different parts in the animation process: “proxy” for blocking, “low” for breakdowns, and “high” for polish. As long as the volumes and proportions of those representations are the same, animation can be transferred up in model resolution without much artistic issue. 


With this in mind, you can really start to see how little an animator really needs to start progress on a shot. A “proxy” version of a character model can be created very early in the design process as long as the character’s proportions and volumes have been somewhat established. As the animator works, she uses Maya Referencing to load the rig into her scene. This enables the Character TD to work alongside the animator to pass them updated geometry as it comes down the pipeline. Furthermore, it enables them to find the best possible rig for that character as the animator can critique details like joint placement, skinning, and rig features. If one uses referencing while animating, updates like these can be pushed to the rig without much change to the animator’s work. 

There are, however, a few changes that will pose a real technical challenge to keeping the animator’s animation how it was established. These problematic changes can include:


Proportions. Animations done using IK controls, which are based on Translation, will likely now be broken. Imagine an animation where a character reaches to drink from a cup in IK but the arm is now 20% shorter. What used to be an acceptable reach might now fully extend the arm and the hand may not even be able to reach the cup. This is a simple example of a common problem in retargeting motion capture animation across a variety of differently-sized rigs. Insomniac Games’ Lead Character TD Adalbert Kinsey gave a great GDC presentation on how they achieved this technical hurdle in their game Sunset Overdrive.


Orientation and Rotate Order. This is a big one and should be figured out as early as possible in a rig’s parallel development as there is not much opportunity for clean fixing here. The Orientation of a control refers to it's 3D axis of orientation in local space. Changing this could mean, for example, that an animation that used to twist a joint could now be bending it. Clearly, as that multiplies up the many controls in a rig, this would really break an animation. “Maya” does not understand your intention when you animate something. 

Changing the Rotate Order of a control is a more technical issue as it delves into the 3D math behind rotations, but for any riggers out there, just don’t do this unless absolutely necessary. Cleanly establish it early on.


Volumes. This is closely related to the kinds of issues that occur when changing the proportions of a character. Besides from the obvious artistic issues with transferring an animation from a heavier to a lighter character or vice versa, there will be issues with intersecting volumes. This doesn’t just refer to the body type of a character: if an accessory is added that drastically affects a character’s range of motion, animation will, again, have to be checked. 

As stated before, certain elements of a character’s rig are best developed in certain ways. While body systems are much more flexible in their development, facial rigs are much more touchy in the kinds of technical changes that can be made during their development. 


FACS Rigs: When animating on a work-in-progress FACS rig, it is a good idea to hold off on using tweaker controls for as long as possible. For context, a FACS rig is one based off of Paul Ekman’s Facial Action Coding System. Rigs built in this way break up expressions into their many muscular parts that the animator can drive using attributes. These shapes are best made in collaboration with the modeler, rigger, and animator, and the important thing to recognize is that they can be changed.

As a layer of finesse over the facial setup, what I call tweaker controls are put in to allow the animator to shape the facial shapes to their needs. However, if these controls are used too early on in development to fix unappealing facial shapes created by the FACS rig, it becomes very difficult for the animation done on the face to stay correct. 

For example, if the rigger made a Jaw_Down shape that needs work but the animator animated to the bad shape, the shape will always be wrong. But if the animator collaborated with the rigger and showed them the shape they generally want the Jaw_Down slider to make, the better shape can be baked into the rig itself and the animator will no longer need to animate around it. 

These kinds of situations come up a lot, and communication here is key. If some behavior of a rig is unappealing, it is always preferable to communicate that to a rigger and see if the problem can be fixed.