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 = weightsNow 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.
Awesome, thanks for sharing! I like the walkthrough for each section of the code :)
ReplyDeleteHey Marcus,
ReplyDeletethat'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
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.
ReplyDeleteOne 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.