Wednesday, July 10, 2013

Tips n Tricks: Managing skin cluster weights

In this post I'll go over how you can code something to get and set skin cluster weights in Maya. We'll also discuss different things you can do to improve performance, especially if were working with a high resolution model. What will be covered here can be done using the API for more efficiency and better performance. But first we will go over simply using maya.cmds

Our first order of business is to look at how we can query the weights in a skin cluster on a vertex base and parse this information to store in an organized manner that we can use later.

If you search around you'll find than many people suggest using MFnSkinCluster.getWeights method. In its defense it is the fastest to query a high resolution mesh with many influences. However, it return weights for all influences, and as you may already know, not all influences are use for every vertex in the skinCluster, having weight of 0, so you end up with a lot of unnecessary data. Since this stage of querying the weights is to give us data to use later, wether we will be normalizing, modifying, or what have you, when we actually have to iterate through all this 'empty' data there will be a drastic decrease in performance time.

In this example, we will take a look at the skinCluster node itself. The next thing is very important to understand. The node has an attribute called weightList. This attribute has indices based on the vertex id for the mesh. For each index, there is another attribute called weights.


This attribute has index based on the influence object id, and each of these store a weight value for the vertex | influence combination.

Here is some code to query the vertex weights for the skin cluster:
def getSkinWeights(skinClusterName):
    ''' Get the weights per vertex on the meshName with the skinClusterName
    '''
    # to store the weight information
    # the key represents the vertex id and the value another dictionary
    # with keys representing the influence object id and the the value the weight as a double
    # Format: {vertexId: {influenceId: weight, ...}, ...}
    weights = {}
 
    # get the Influence Objects 
    influenceObjects = cmds.skinCluster(skinClusterName, q=True, weightedInfluence=True)
 
    # get the size of the vertex weight list
    weightListSize = cmds.getAttr(skinClusterName + ".weightList", size=True)

    # build weight dictionary
    for vertexId in range(0,weightListSize):
        # to store vertex weights
        vertexWeights = {}

        for influenceId in range(len(influenceObjects)):
            weight = cmds.getAttr(skinClusterName + ".weightList[%d].weights[%d]" % (vertexId,influenceId))
            # only store non-zero weights
            if weight != 0:
                vertexWeights[influenceId] = weight

        # store the weights for the vertex id
        weights[vertexId] = vertexWeights
 
    return weights

Notice I opted for using a dictionary. The way you want to store the data is up to you really. However, dictionaries are really powerful for this sort of thing. We can quickly access data based on the id, and it works great since we can query only the verts we choose to.

With the weights stored we can now turn to how we can set the weights. Once again, if you look, you'll find that using MFnSkinCluster.setWeights is a popular method and fast. But since we opted to query the weights in our own method, we can now quickly set our weight values using cmds.setAttr. In addition, what is really nice is that we also get undo which we don't with setWeights. Trust me this is a really nice feature.

So we've covered how to query the skin weights. After which you can modify the weight values however you wish. Then we can set the weights back to the skinCluster.

Here is some code to set the vertex weights to the skin cluster:
def setSkinWeights(skinClusterName, weights):
    ''' Set the modified weights to the skinClusterName.
    '''
    for vertexId in weights:
        # get influence dictionary
        influenceDict = weights[vertexId]

        # check if vertex is weighted - if there are any influence objects
        if influenceDict.items():
            for influenceId in influenceDict:
                plug = skinClusterName + ".weightList[%d].weights[%d]" % (vertexId,influenceId)

                # set the weight
                weight = influenceDict[influenceId]
                cmds.setAttr(plug, weight)

Its good to point out that this method will set the new weights fine, however it wont check for your if the weights add up to 1, if the weight value is positive, if the influence object doesn't exist, and other possible errors. So its a good idea to have checking method to take care of these things. As far as checking if the weights add up to 1, its an option to normalize them after you've modified them. The cmds.skinPercent has a normalize flag for that. I've noticed that when normalizing the weights while the script is running causes undesired results on vertices that haven't been assigned their new weight values.

This method of using setAttr is already faster than skinPercent, and can be faster depending on what you plan on doing with it, can be faster than MFnSkinCluster as well. Lets just expand our method to be a little more robust.

You could also use prune small weights, also in skinPercent, before setting the new weights to remove all zero weights.

Hero is the modified code to allow us to do just that:
def setSkinWeights(skinClusterName, weights, meshName, nonZero=True):
 ''' Set the modified weights to the skinClusterName. The nonZero flags assures that
  only nonZero weights are set.
 ''' 
 for vertexId in weights:
  # get influence dictionary
  influenceDict = weights[vertexId]

  # check if vertex is weighted - if there are any influence objects
  if influenceDict.items():
   for influenceId in influenceDict:
    if nonZero:
     influenceObj = cmds.listConnections("skinCluster1.matrix[%d]" % influenceId)
  
     if influenceObj:
      influenceObj = influenceObj[0]
    
      cmds.setAttr(influenceObj + ".liw", 0)
  
     # prune small weight values
     # need to turn off normalize to use prune
     plug = skinClusterName + ".normalizeWeights"
     normalize = cmds.getAttr(plug)
     if normalize:
      cmds.setAttr(plug, 0)
  
     cmds.skinPercent(skinClusterName, meshName, nrm=False, prw=.01)
     
     # return normalizing to what it was
     if normalize:
      cmds.setAttr(plug, normalize)
   
    plug = skinClusterName + ".weightList[%d].weights[%d]" % (vertexId,influenceId)

    # set the weight
    weight = influenceDict[influenceId]
    cmds.setAttr(plug, weight)

0 comments:

Post a Comment