import pymel.core as pm import maya.OpenMaya as OpenMaya import maya.OpenMayaAnim as OpenMayaAnim import traceback import time import copy import skin_api.Utils as apiUtils def getMfNSkinCluster(clusterName): ''' Helper function to generate an MFnSkinCluster object :param clusterName: :return: ''' try: # get the MFnSkinCluster for clusterName selList = OpenMaya.MSelectionList() selList.add(clusterName) clusterNode = OpenMaya.MObject() selList.getDependNode(0, clusterNode) skinFn = OpenMayaAnim.MFnSkinCluster(clusterNode) return skinFn except: raise "Could not find an MFnSkinCluster named '%s'" % clusterName def getInfluenceNames(skincluster): ''' Returns a list of names of influences (joints) in a skincluster :param skincluster: or :return: ''' return [str(inf) for inf in pm.skinCluster(skincluster, influence=True, q=True)] def getMaxInfluences(skincluster): ''' Returns the maxInfluences for a skincluster as an int :param skincluster: or :return: ''' return pm.getAttr(skincluster + ".maxInfluences") def getSkinClusterNode(node): ''' Gets the connected skincluster node for a given node :param objectName: :return: ''' objHistory = pm.listHistory(pm.PyNode(node)) skinClusterList = pm.ls(objHistory, type="skinCluster") if len(skinClusterList): return str(skinClusterList[0]) return None def getSkinClusterInfo(objectName, saveJointInfo=False): ''' Builds a skincluster info dict. Structured as: {"ObjectName": {skinCluster:{clusterWeights: {}, clusterInflNames: [], clusterMaxInf: int} :param objectName: or :param saveJointInfo: saves joint orient, world transform and parent joint info, default is False :return: ''' skinClustName = getSkinClusterNode(objectName) if skinClustName: skinClustInfoDict = {} skinClustInfoDict["clusterInfluenceNames"] = getInfluenceNames(skinClustName) skinClustInfoDict["clusterMaxInf"] = getMaxInfluences(skinClustName) skinClustInfoDict["clusterWeights"] = getSkinClusterWeights(skinClustName) if saveJointInfo: skinClustInfoDict["skinJointInformation"] = getSkinJointInformation(skinClustInfoDict["clusterInfluenceNames"]) return skinClustInfoDict else: print(objectName + " is not connected to a skinCluster") return False def getSkinJointInformation(influences): jointInformation = {} for inf in influences: jointInfo = {} inf = pm.PyNode(inf) jointInfo["parent"] = str(inf.getParent().name()) jointInfo["matrix"] = inf.getMatrix(worldSpace=True) jointInfo["rotation"] = inf.getRotation() jointInfo["jointOrient"] = inf.getAttr("jointOrient") jointInformation[str(inf)] = copy.deepcopy(jointInfo) return jointInformation def getMPlugObjects(MFnSkinCluster): ''' Gets the plug objects for a given MFnSkinCluster object Weights are stored in objects like this: skinCluster1.weightList[vtxID].weights[influenceId] = floatvalue :param MFnSkinCluster: :return: ''' weightListPlug = MFnSkinCluster.findPlug('weightList') weightsPlug = MFnSkinCluster.findPlug('weights') weightListAttribute = weightListPlug.attribute() weightsAttribute = weightsPlug.attribute() return weightListPlug, weightsPlug, weightListAttribute, weightsAttribute def getSkinClusterWeights(skinCluster): ''' Gets the clusterweights for a given skincluster node Reads them by using the MPlug attribute from OpenMaya :param skinCluster: :return: ''' try: # get the MFnSkinCluster for clusterName skinFn = getMfNSkinCluster(str(skinCluster)) # get the MDagPath for all influence infDagArray = OpenMaya.MDagPathArray() skinFn.influenceObjects(infDagArray) # create a dictionary whose key is the MPlug indice id and # whose value is the influence list id infIds = {} for i in range(infDagArray.length()): infId = int(skinFn.indexForInfluenceObject(infDagArray[i])) infIds[infId] = i # get the MPlug for the weightList and weights attributes wlPlug, wPlug, wlAttr, wAttr = getMPlugObjects(skinFn) wInfIds = OpenMaya.MIntArray() # the weights are stored in dictionary, the key is the vertId, # the value is another dictionary whose key is the influence id and # value is the weight for that influence weights = {} for vId in range(wlPlug.numElements()): vWeights = {} # tell the weights attribute which vertex id it represents wPlug.selectAncestorLogicalIndex(vId, wlAttr) # get the indice of all non-zero weights for this vert wPlug.getExistingArrayAttributeIndices(wInfIds) # create a copy of the current wPlug infPlug = OpenMaya.MPlug(wPlug) for infId in wInfIds: # tell the infPlug it represents the current influence id infPlug.selectAncestorLogicalIndex(infId, wAttr) # add this influence and its weight to this verts weights vWeights[infIds[infId]] = infPlug.asDouble() weights[vId] = vWeights return weights except: print(traceback.format_exc()) print("Unable to query skincluster, influence order have changed on the skincluster.\n Please re-initialize the skincluster with a clean influence index order") return False def setMaxInfluencesDialog(): ''' Uses the setMaxInfluencesEngine and allows user to give input max value in a promptbox :return: success ''' nodeList = pm.ls(sl=True) if len(nodeList): maxValueWin = pm.promptDialog(title="API Max Influence", message='Set Max influence Value', button=['OK']) try: maxInfValue = int(pm.promptDialog(q=True, tx=True)) except: return "Unable to fetch an integer input from the dialog" if maxValueWin == "OK": return setMaxInfluences(maxInfValue, nodeList) else: return "Nothing selected" def setMaxInfluences(maxInfValue, nodeList=[]): ''' Reads the skin weights for nodes given as list. Then renormalizes and prunes the influence weights down to value specified by maxInfValues :param maxInfValue: :param nodeList: :return: ''' if maxInfValue > 0: if not len(nodeList): nodeList = apiUtils.getTransforms(pm.ls(os=True)) if len(nodeList): if None in apiUtils.getShapes(nodeList): print("Non-mesh objects found in selection") return False else: print("Nothing selected") return False for node in nodeList: skinClusterNode = getSkinClusterNode(node) if skinClusterNode: clusterWeights = getSkinClusterWeights(skinClusterNode) # build a new cluster weight list newClusterWeights = {} for vtxId, weights in clusterWeights.items(): sizeSortedDict = {} for x in range(maxInfValue): lastResult = 0.0 lastKey = -1 for key, value in weights.items(): if value >= lastResult and key not in sizeSortedDict.keys(): lastResult = value lastKey = key if lastKey >= 0: sizeSortedDict[lastKey] = lastResult # normalize values to a total of 1 and remove invalid key values from dictionary maxVal = sum(sizeSortedDict.values()) for key in sizeSortedDict.keys(): sizeSortedDict[key] = sizeSortedDict[key] / maxVal # replace old vtxId info with the new normalized and re-sized influence dict newClusterWeights[vtxId] = sizeSortedDict # turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights pm.setAttr('%s.normalizeWeights' % skinClusterNode, 0) pm.skinPercent(skinClusterNode, node, nrm=False, prw=100) pm.setAttr('%s.normalizeWeights' % skinClusterNode, 1) # set new skinweights setSkinWeights(skinClusterNode, newClusterWeights) pm.setAttr(skinClusterNode + ".maxInfluences", maxInfValue) pm.skinCluster(skinClusterNode, e=True, fnw=True) print("Max Influences set for '%s'" % node) else: print("'%s' is not connected to a skinCluster" % node) else: return "Max Influences has to be a positive integer" def setSkinWeights(skinClusterNode, clusterWeightDict, vtxIdFilter=[]): ''' Sets the weight for a given skinCluster using a given weightDict in the format given by .getSkinClusterWeights() :param skinClusterNode: :param weightDict: :param vtxIdFilter: List of objects to gather weight info from :param showLoadingBar: default is True :param saveJointInfo: saves joint orient, world transform and parent joint info, default is False :return: dictionary for skincluster ''' loadBarMaxVal = len(objectList) loadBarObj = apiUtils.LoadingBar() sourceWeightDict = {} for object in objectList: objectAsString = pm.PyNode(object).name() if showLoadingBar: loadBarObj.loadingBar("Building Skinweights Info...", loadBarMaxVal, "Building...") skinClusterInfo = getSkinClusterInfo(object, saveJointInfo=saveJointInfo) if skinClusterInfo: sourceWeightDict[objectAsString] = {} sourceWeightDict[objectAsString]["vtxCount"] = apiUtils.getVtxCount(object) sourceWeightDict[objectAsString]["skinCluster"] = skinClusterInfo if bool(sourceWeightDict): return sourceWeightDict else: return False def transferSkinWeights(transferNodes=None, showLoadingBar=True): ''' transfers skin weights between a given set of meshes by: Getting the source skincluster info from the first mesh in the transfer objects list Calculating the barycentric relationship with all consecutive meshes Zipping the source and target mesh clusterweight dictionaries Importing the resulting skin cluster weight dict to the skinclusterbuilder If no transferObjects are provided the function will attempt to fetch a selection from the scene to operate on :param transferNodes: A list of transfer objects :return: ''' if transferNodes is None: transferNodes = apiUtils.handleTransferNodesList(transferNodes) # buffer and re-assign success = False if len(transferNodes): sourceObj = transferNodes[0] sourceName = sourceObj.name() targetNameList = transferNodes[1:] loadBarMaxVal = len(targetNameList) loadBarObj = apiUtils.LoadingBar() # initialize self.sourceWeightDict which is populated by both functions sourceWeightDict = buildSkinWeightsDict([sourceName], showLoadingBar) if sourceWeightDict: for tgtObject in targetNameList: # deep copy because: Mutable datatypes sourceWeightDictCopy = copy.deepcopy(sourceWeightDict) targetName = str(tgtObject.name()) barycentrWeightDict = apiUtils.getBarycentricWeights(sourceName, targetName) # initialize transferWeightDict transferWeightDict = {} # clone the sourceweight skincluster information transferWeightDict[targetName] = sourceWeightDictCopy[sourceName] # swap the clusterweights for the zipped result from zipClusterWeights transferWeightDict[targetName]["skinCluster"]["clusterWeights"] = apiUtils.zipClusterWeights( barycentrWeightDict, sourceWeightDictCopy[sourceName]["skinCluster"]["clusterWeights"]) skinClusterBuilder(targetName, transferWeightDict) maxInfVal = transferWeightDict[targetName]["skinCluster"]["clusterMaxInf"] setMaxInfluences(maxInfVal, [targetName]) #progress loadingBar if showLoadingBar: loadBarObj.loadingBar("Transferring Skinweights...", loadBarMaxVal, "Transferring...") success = True if success: pm.select(transferNodes, r=True) def skinClusterBuilder(objName, weightDict, deleteHist=True, stripJointNamespaces=False, addNewToHierarchy=False): ''' Builds a skin cluster for a given object and a given weight dictionary By default the function deletes the history on the target object to create a "clean start" :param objName: :param weightDict: :param deleteHist: :param stripJointNamespaces: strips joint namespaces on skinWeights file, default is False :param addNewToHierarchy: adds missing joints, world transform and parent only correct when exported with joint info, default is False :return: ''' startBuildTimer = time.time() # clear any messy history on the validObjects if deleteHist: pm.select(objName, r=True) pm.mel.eval("DeleteHistory;") pm.select(clear=True) clusterInfo = weightDict[objName]["skinCluster"] # lclusterInfo has 4 attribute keys : # ['clusterInfluenceNames', 'clusterMaxInf', 'clusterVtxCount', 'clusterWeights'] clusterJoints = clusterInfo["clusterInfluenceNames"] clusterMaxInf = clusterInfo["clusterMaxInf"] clusterJointInfo = clusterInfo.get("skinJointInformation") for jointNameOrig in clusterJoints: jointName = jointNameOrig if stripJointNamespaces: jointName = jointNameOrig.split(":")[-1] if not pm.objExists(jointName): pm.select(cl=True) joint = pm.joint(p=(0, 0, 0), name=jointName) # putting joint in the hierarchy and setting matrix if addNewToHierarchy and clusterJointInfo: jointInfo = clusterJointInfo.get(jointNameOrig) if not jointInfo: continue parentJoint = jointInfo.get("parent") if stripJointNamespaces: parentJoint = parentJoint.split(":")[-1] if pm.objExists(parentJoint): parentJoint = pm.PyNode(parentJoint) joint.setParent(parentJoint) joint.setMatrix(jointInfo.get("matrix"), worldSpace=True) joint.setAttr("jointOrient", jointInfo.get("jointOrient")) joint.setRotation(jointInfo.get("rotation")) else: joint.setMatrix(jointInfo.get("matrix"), worldSpace=True) pm.select(cl=True) try: if stripJointNamespaces: clusterJoints = [a.split(":")[-1] for a in clusterJoints] clusterNode = pm.skinCluster(clusterJoints, objName, tsb=True, mi=clusterMaxInf, omi=True) # turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights pm.setAttr('%s.normalizeWeights' % clusterNode, 0) pm.skinPercent(clusterNode, objName, nrm=False, prw=100) pm.setAttr('%s.normalizeWeights' % clusterNode, 1) # set the skinweights setSkinWeights(clusterNode, clusterInfo["clusterWeights"]) except RuntimeError: print("Failed to build new skinCluster on %s" % objName) except: print(traceback.format_exc()) endBuildTimer = time.time() print("Skin Cluster Built %s : " % objName + (str(endBuildTimer - startBuildTimer)))