Sunday, December 9, 2012

Skinweights Serialization in Pymel



A little while ago I wanted to write a skinweights exporter/importer so I can easily transfer skinweights between models with the same topology. Now, maya has a couple of tools for this, but they are quite poor.

Since I am fairly new to maya, I haven`t had the chance yet to dig a lot into the Maya API, so my weapon of choice was PyMel. Reading through different blogs and talking to other people raised a lot of concern in terms of speed, since Pymel itself is only a wrapper and things like iterating through a lot of vertices becomes extremely slow.

However I found a solution that is fast and very light, due to the nature of Pymel.
I chose to store the data in XML, since its really easy with pythons minidom module.

The desired format I came up with is something like this:
So lets start of by importing pymel.core and minidom for the xml saving/loading and a small function so we can identify skinclusters easier.
import pymel.core as pm
import xml.dom.minidom as minidom

def fnFindSkinCluster(mesh):
skincluster = None
 for each in pm.listHistory(mesh):  
  if type(each)==pm.nodetypes.SkinCluster:   
   skincluster = each
 return skincluster




Next up lets start with our Save Skinweight function. The skinCluster class has a function thats called influenceObjects(), which will return us all joints that are bound to the skincluster. Then we basically iterate through each joint and run the function getPointsAffectedByInfluence() for each influence. This returns a list that contains each vertex with its weights.

We make sure that we store all these values in a nice list and by utilizing the minidom module we can save it into an xml file.
def fnSaveSkinning(mesh,path): 
 ## Collect data
 skincluster = fnFindSkinCluster(mesh)
 
 ## Check if skincluster even exists
 if skincluster!=None:   
  ## Prepare XML xml_document
  xml_doc = minidom.Document()
  xml_header = xml_doc.createElement("influences") 
  xml_doc.appendChild(xml_header)   

  ## Write out the joint id/name table  
  for each in skincluster.getInfluence():
   getData = skincluster.getPointsAffectedByInfluence(each)  
   tmpJoint= xml_doc.createElement(each)
   vertData = []
   if len(getData[0])>0:
    ## Gather all vertex ids and store it in the vertData list
    for each in getData[0][0]:
     vertData.append(each.currentItemIndex())
    ## Then store the vertData list as "vertices" attribute value 
    tmpJoint.setAttribute("vertices",str(vertData))
    tmpJoint.setAttribute("weights",str(getData[1]))
    xml_header.appendChild(tmpJoint)
     
   ## Save XML
  file = open(path, 'w')
  file.write(xml_doc.toprettyxml())
  pm.warning("Saved '%s.skinning' to '%s'"%(mesh,path))
 else:
  pm.warning('No skincluster connected to %s'%mesh)
Now our data can be saved, but we also want to load it. This turned out to be a bit more painful, setting the skinweights can be really slow by using skinPercent. So in this case I used setAttr to set the values directly. This is a LOT faster. Lets go through it bit by bit. First we setup a class to store our data efficiently.
class cSkinning(): 
 def __init__(self,id,infl,weights):
  self.id = id  
  self.influence = infl
  self.weights = weights
Now lets setup the function with a few variables and make sure, in case the mesh is already skinned, to delete that skincluster. We will later create a new one and add all the joints automatically, based on the skinweight file.

def fnLoadSkinning(mesh,path):  
 ## Variables
 jointData = []
 skinData = []
 
 ### Clean up the mesh, in case its already skinned
 skincluster = fnFindSkinCluster(mesh)
 if skincluster!=None:
  pm.runtime.DetachSkin(skincluster)
Now lets parse the xml file and organize all our data. We will create objects based on our custom class cSkinning and store these in a list called skinData.

 ## Parse the document, convert it to XML format and load into memory
 xml_doc = minidom.parse(path)
 xml_doc.toxml()
 
 ## Get root node
 joints = xml_doc.childNodes[0].childNodes
 
 ## Gather all data and store cSkinning objects in the skinData list
 for joint in joints:
  if joint.localName!=None:
   vertices = []
   weights = []
   jointData.append(joint.localName)   
   vertices = eval(joint.attributes.item(1).nodeValue)
   weights = eval(joint.attributes.item(0).nodeValue)
   skinData.append(cSkinning(vertices,joint.localName,weights))
If you print items from skinData, you can see that we have
If you print item.id, item.influence or item.weights you can see the data stored in that class. Influence and weights is a coherent table so we know that index 15 in .weights represents the weights for influence 15.

Now lets create skincluster and add all the joints. Before we apply the weight attributes, we must set the normalizeWeights attribute to 0, otherwise Maya would normalize the skinweights with each iteration we add weights. Also we need to get rid of mayas standard skinweights when we bound the skin by using skinPercent.

 skincluster = pm.animation.skinCluster(mesh,jointData)
 skincluster.setNormalizeWeights(0)
 pm.skinPercent(skincluster, mesh, nrm=False, prw=100)
Now before applying the weights there is one last thing we need to do. Maya stores influences based on IDs. Since we added a new skincluster, these IDs do not match the old ones anymore. This code here simply updates them in our skinData list.

 tmpJoint = []
 for storedJoint in jointData:
  for skinnedJoint in skincluster.influenceObjects():
   if skinnedJoint==storedJoint:
    tmpJoint.append(skincluster.indexForInfluenceObject(storedJoint))
    
 for each in range(0,len(skinData)):  
  skinData[each].influence = tmpJoint[each]
Awesome, now here comes the fun part. Lets add the skinweights by using setAttr. Once we have done that, dont forget to turn on skinweight normalization again.
 for each in range(0,len(skinData)):  
  for vertex in range(0,len(skinData[each].id)):
   vert = skinData[each].id[vertex]
   weights = skinData[each].weights[vertex]   
   pm.setAttr('%s.weightList[%s].weights[%s]'%(skincluster,vert,skinData[each].influence),weights)
For a mesh with about 30k verts saving took me about 3 seconds. Loading about 6 seconds which is quite acceptable. The script was tested on Maya 2012, I heard that some people had trouble getting this to work in Maya 2013, but unfortunately I havent had time to look into that yet. Thanks for listening.

3 comments:

  1. Awesome, thanks for sharing! I like the walkthrough for each section of the code :)

    ReplyDelete
  2. Hey Marcus,

    that's a very quick saving and loading time you've managed to process. I usually worked with the comet cartoon script tools exporting and importing skin weights and that took ages compared to your process and the ones I found on other tech artist blogs.
    I'm just curious, why do you need to create the cSkinning class and the instanced objects to save out id, influence and weights instead of using a normal list or even a dictionary?
    Does the use of objects speed up the process?
    I'm just asking as I'm constantly considering the advantage of OOP compared to the procedural way. ;)
    Is it more a constructive way of organizing data or a real speed up during runtime.

    Chris

    ReplyDelete
  3. How's it going? I like the walk-through. Very clear and has a lot of good info that could be applied to a lot of different projects.

    One thing though, Maya 2012, already has a tool to export/import skin weights as XML. Edit Deformers --> Export Deformer Weights. Just select your skin cluster in the nodes pulldown. Been working great for me. Also totally scriptable using pm.deformerWeights.

    ReplyDelete