Save/Load SkinWeights 125x Faster
DISCLAIMER/WARNING: Trying to implement this in production I have found other items on top of the massive list that do not work. The ignore names flag ‘ig’ causes a hard crash on file load, the weight precision flag ‘wp’ isn’t implemented though it’s documented, the weight tolerance flag ‘wt’ causes files to hang indefinitely on load. When it does load weights properly, it often does so in a way that crashes the Paint Skin Weights tool. I have reported this in the Maya Beta forums.
Previously I discussed the promise in Maya command ‘deformerWeights‘. The tool that ships with Maya was not very useful, but the code it called was 125 times faster than python if you used it correctly..
Let’s make a python class that can save and load skin weights. You hand it a few hundred skinned meshes (avg Paragon character) and it saves the weights and then you delete history on the meshes, and it loads the weights back on. What I just described is the process riggers go through when updating a rig or a mesh _every day_.
Below we begin the class, we import a python module to parse XML, and we say “if the user passed in a path, let’s parse it.”
#we import an xml parser that ships with python import xml.etree.ElementTree #this will be our class, which can take the path to a file on disk class SkinDeformerWeights(object): def __init__(self, path=None): self.path = path if self.path: self.parseFile(self.path) |
Next, let’s make this parseFile function. Why is parsing the file important? In the last post we found out that there’s a bug that doesn’t appropriately apply saved weights unless you have a skinCluster with the *exact* same joints as were exported. We’re going to read the file and make a skinCluster that works.
#the function takes a path to the file we want to parse def parseFile(self, path): root = xml.etree.ElementTree.parse(path).getroot() #set the header info for atype in root.findall('headerInfo'): self.fileName = atype.get('fileName') for atype in root.findall('weights'): jnt = atype.get('source') shape = atype.get('shape') clusterName = atype.get('deformer') |
Now we’re getting some data here, we know that the format can save deformers for multiple shapes, let’s make a shape class and store these.
class SkinnedShape(object): def __init__(self, joints=None, shape=None, skin=None, verts=None): self.joints = joints self.shape = shape self.skin = skin self.verts = verts |
Let’s use that when we parse the file, let’s then store the data we paresed in our new shape class:
#the function takes a path to the file we want to parse def parseFile(self, path): root = xml.etree.ElementTree.parse(path).getroot() #set the header info for atype in root.findall('headerInfo'): self.fileName = atype.get('fileName') for atype in root.findall('weights'): jnt = atype.get('source') shape = atype.get('shape') clusterName = atype.get('deformer') if shape not in self.shapes.keys(): self.shapes[shape] = self.skinnedShape(shape=shape, skin=clusterName, joints=[jnt]) else: s = self.shapes[shape] s.joints.append(jnt) |
So now we have a dictionary of our shape classes, and each knows the shape, cluster name, and all influences. This is important because, if you read the previous post, the weights will only load onto a skinCluster with the exact same number and names of joints.
Now we write a method to apply the weight info we parsed:
def applyWeightInfo(self): for shape in self.shapes: #make a skincluster using the joints if cmds.objExists(shape): ss = self.shapes[shape] skinList = ss.joints skinList.append(shape) cmds.select(cl=1) cmds.select(skinList) cluster = cmds.skinCluster(name=ss.skin, tsb=1) fname = self.path.split('\\')[-1] dir = self.path.replace(fname,'') cmds.deformerWeights(fname , path = dir, deformer=ss.skin, im=1) |
And there you go. Let’s also write a method to export/save the skinWeights from a list of meshes so we never have to use the Export DeformerWeights tool:
def saveWeightInfo(self, fpath, meshes, all=True): t1 = time.time() #get skin clusters meshDict = {} for mesh in meshes: sc = mel.eval('findRelatedSkinCluster '+mesh) #not using shape atm, mesh instead msh = cmds.listRelatives(mesh, shapes=1) if sc != '': meshDict[sc] = mesh else: cmds.warning('>>>saveWeightInfo: ' + mesh + ' is not connected to a skinCluster!') fname = fpath.split('\\')[-1] dir = fpath.replace(fname,'') for skin in meshDict: cmds.deformerWeights(meshDict[skin] + '.skinWeights', path=dir, ex=1, deformer=skin) elapsed = time.time()-t1 print 'Exported skinWeights for', len(meshes), 'meshes in', elapsed, 'seconds.' |
You give this a folder and it’ll dump one file per skinCluster into that folder.
Here is the final class we’ve created [deformerWeights.py], and let’s give it a test run.
sdw = skinDeformerWeights() sdw.saveWeightInfo('e:\\gadget\\', cmds.ls(sl=1)) >>>Exported skinWeights for 214 meshes in 2.433 seconds. |
Let’s now load them back, we will iterate through the files in the directory and parse each, applying the weights:
import os t1=time.time() path = "e:\\gadget\\" files = 0 for file in os.listdir(path): if file.endswith(".skinWeights"): fpath = path + file sdw = skinDeformerWeights(path=fpath) sdw.applyWeightInfo() files += 1 elapsed = time.time() - t1 print 'Loaded skinWeights for', files, 'meshes in', elapsed, 'seconds.' >>> Loaded skinWeights for 214 meshes in 8.432 seconds. |
“>>> Loaded skinWeights for 214 meshes in 8.432 seconds.”
So that’s a simple 50 line wrapper to save and load skinWeights using the deformerWeights command. No longer do we need to write C++ API plugins to save/load weights quickly.