Updated
This commit is contained in:
		
							
								
								
									
										477
									
								
								Scripts/Animation/skin_api/Skinning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										477
									
								
								Scripts/Animation/skin_api/Skinning.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,477 @@
 | 
			
		||||
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: <skincluster object> or <string>
 | 
			
		||||
    :return: <list>
 | 
			
		||||
    '''
 | 
			
		||||
    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: <skincluster object> or <string>
 | 
			
		||||
    :return: <int>
 | 
			
		||||
    '''
 | 
			
		||||
    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: <shape> or <transform>
 | 
			
		||||
    :param saveJointInfo: <bool> 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: <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: <bool> 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: <int>
 | 
			
		||||
    :param nodeList: <list>
 | 
			
		||||
    :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: <skinCluster>
 | 
			
		||||
    :param weightDict: <dict>
 | 
			
		||||
    :param vtxIdFilter: <dict BETA: Very untested way of filtering vertices in setting weights
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    # get the mfnskinCluster node
 | 
			
		||||
    skinFn = getMfNSkinCluster(str(skinClusterNode))
 | 
			
		||||
 | 
			
		||||
    # get the MPlug for the weightList and weights attributes
 | 
			
		||||
    wlPlug, wPlug, wlAttr, wAttr = getMPlugObjects(skinFn)
 | 
			
		||||
 | 
			
		||||
    # weights are stored in objects like this:
 | 
			
		||||
    # skinCluster1.weightList[vtxID].weights[influenceId] = floatvalue
 | 
			
		||||
    # {0: {0: 0.5, 1: 0.5}
 | 
			
		||||
    # or {vtxId: {weights: floatvalue}}
 | 
			
		||||
 | 
			
		||||
    if not len(vtxIdFilter):
 | 
			
		||||
        for vId in clusterWeightDict.keys():
 | 
			
		||||
            wPlug.selectAncestorLogicalIndex(vId, wlAttr)
 | 
			
		||||
            infPlug = OpenMaya.MPlug(wPlug)
 | 
			
		||||
            for infId in clusterWeightDict[vId]:
 | 
			
		||||
                infPlug.selectAncestorLogicalIndex(infId, wAttr)
 | 
			
		||||
                infPlug.setDouble(clusterWeightDict[vId][infId])
 | 
			
		||||
    else:
 | 
			
		||||
        for vId in clusterWeightDict.keys():
 | 
			
		||||
            if vId in vtxIdFilter:
 | 
			
		||||
                wPlug.selectAncestorLogicalIndex(vId, wlAttr)
 | 
			
		||||
                infPlug = OpenMaya.MPlug(wPlug)
 | 
			
		||||
                for infId in clusterWeightDict[vId]:
 | 
			
		||||
                    infPlug.selectAncestorLogicalIndex(infId, wAttr)
 | 
			
		||||
                    infPlug.setDouble(clusterWeightDict[vId][infId])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def buildSkinWeightsDict(objectList, showLoadingBar=True, saveJointInfo=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Builds a weight dictionary for skin weights in the apiVtxAttribs format
 | 
			
		||||
    :param objectList: <list> List of objects to gather weight info from
 | 
			
		||||
    :param showLoadingBar: <bool> default is True
 | 
			
		||||
    :param saveJointInfo: <bool> saves joint orient, world transform and parent joint info, default is False
 | 
			
		||||
    :return: <dict> 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: <list> 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: <dict>
 | 
			
		||||
    :param deleteHist: <bool>
 | 
			
		||||
    :param stripJointNamespaces: <bool> strips joint namespaces on skinWeights file, default is False
 | 
			
		||||
    :param addNewToHierarchy: <bool> 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)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										451
									
								
								Scripts/Animation/skin_api/Utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										451
									
								
								Scripts/Animation/skin_api/Utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,451 @@
 | 
			
		||||
import pymel.core as pm
 | 
			
		||||
import maya.OpenMaya as OpenMaya
 | 
			
		||||
import traceback
 | 
			
		||||
import pickle
 | 
			
		||||
# import cPickle as pickle
 | 
			
		||||
 | 
			
		||||
# import atcore.atvc.atvc as atvc
 | 
			
		||||
# import atcore.atmaya.utils as utils
 | 
			
		||||
 | 
			
		||||
# # init versionControl object
 | 
			
		||||
# vc = atvc.VersionControl()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getShape(node, intermediate=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Gets the shape of a given node
 | 
			
		||||
    Returns None if unable to find shape
 | 
			
		||||
    Returns shape if found
 | 
			
		||||
    :param node:
 | 
			
		||||
    :param intermediate:
 | 
			
		||||
    :return:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    if pm.nodeType(node) == "transform":
 | 
			
		||||
        shapeNodes = pm.listRelatives(node, shapes=True, path=True)
 | 
			
		||||
 | 
			
		||||
        if not shapeNodes:
 | 
			
		||||
            shapeNodes = []
 | 
			
		||||
 | 
			
		||||
        for shapeNode in shapeNodes:
 | 
			
		||||
            isIntermediate = pm.getAttr("%s.intermediateObject" % shapeNode)
 | 
			
		||||
 | 
			
		||||
            if intermediate and isIntermediate and pm.listConnections(shapeNode, source=False):
 | 
			
		||||
                return shapeNode
 | 
			
		||||
 | 
			
		||||
            elif not intermediate and not isIntermediate:
 | 
			
		||||
                return shapeNode
 | 
			
		||||
 | 
			
		||||
        if shapeNodes:
 | 
			
		||||
            return shapeNodes[0]
 | 
			
		||||
 | 
			
		||||
    elif pm.nodeType(node) in ["mesh", "nurbsCurve", "nurbsSurface"]:
 | 
			
		||||
        return pm.PyNode(node)
 | 
			
		||||
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getShapes(nodeList):
 | 
			
		||||
    '''
 | 
			
		||||
    Runs getShape on a list of nodes
 | 
			
		||||
    :param nodeList:
 | 
			
		||||
    :return: <list>
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    shapeList = []
 | 
			
		||||
 | 
			
		||||
    for node in nodeList:
 | 
			
		||||
        shapeNode = getShape(node)
 | 
			
		||||
        if shapeNode:
 | 
			
		||||
            shapeList.append(shapeNode)
 | 
			
		||||
 | 
			
		||||
    return shapeList
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getTransform(node):
 | 
			
		||||
    '''
 | 
			
		||||
    Gets the Transform node if a given node
 | 
			
		||||
    Returns None if no Transform was found
 | 
			
		||||
    Returns Transform object if found
 | 
			
		||||
    :param node:
 | 
			
		||||
    :return:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    node = pm.PyNode(node)
 | 
			
		||||
    if node.type() == 'transform':
 | 
			
		||||
        lhistory = node.listHistory(type='mesh')
 | 
			
		||||
        if len(lhistory) > 0:
 | 
			
		||||
            return node
 | 
			
		||||
    elif node.type() == "mesh":
 | 
			
		||||
        transNode = node.getParent()
 | 
			
		||||
        if transNode:
 | 
			
		||||
            return transNode
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getTransforms(nodeList):
 | 
			
		||||
    '''
 | 
			
		||||
    Gets the Transform nodes for all nodes in a given list
 | 
			
		||||
    returns list of nodes
 | 
			
		||||
    :param nodeList:
 | 
			
		||||
    :return: <list>
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    transList = []
 | 
			
		||||
 | 
			
		||||
    for node in nodeList:
 | 
			
		||||
        transNode = getTransform(node)
 | 
			
		||||
        if transNode:
 | 
			
		||||
            transList.append(transNode)
 | 
			
		||||
 | 
			
		||||
    return transList
 | 
			
		||||
 | 
			
		||||
def getMDagPath(nodeName):
 | 
			
		||||
   '''
 | 
			
		||||
   Helper function to create an MDagPath for a given object name
 | 
			
		||||
   :param nodeName: <string> name of node
 | 
			
		||||
   :return: <MDagPath>
 | 
			
		||||
   '''
 | 
			
		||||
 | 
			
		||||
   selectionList = OpenMaya.MSelectionList()
 | 
			
		||||
   selectionList.add(str(nodeName))
 | 
			
		||||
   objDagPath = OpenMaya.MDagPath()
 | 
			
		||||
   selectionList.getDagPath(0, objDagPath)
 | 
			
		||||
   return objDagPath
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getBarycentricWeights(srcMesh, tgtMesh, flipX=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Builds a barycentric weight dictionary for a given source and target mesh
 | 
			
		||||
    Iterates over each vtx in the target mesh finding the relation to the closest point and triangle on the source mesh
 | 
			
		||||
    Dict structure:
 | 
			
		||||
    {targetVtxId: {srcTriVtxId1: weight, srcTriVtxId2: weight, srcTriVtxId3: weight}}
 | 
			
		||||
    FlipX allows you to flip the target mesh point array over the world X-axis. Useful for mirror functions.
 | 
			
		||||
    :param srcMesh: shapeNode
 | 
			
		||||
    :param tgtMesh: shapeNode
 | 
			
		||||
    :param flipX: <bool>
 | 
			
		||||
    :return: <dictionary>
 | 
			
		||||
    '''
 | 
			
		||||
    baryWeightDict = {}
 | 
			
		||||
 | 
			
		||||
    if pm.nodeType(srcMesh) != "mesh" and pm.nodeType(tgtMesh) != "mesh":
 | 
			
		||||
        srcMesh = getShape(srcMesh)
 | 
			
		||||
        tgtMesh = getShape(tgtMesh)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        srcMeshDagPath = getMDagPath(srcMesh)
 | 
			
		||||
        tgtMeshDagPath = getMDagPath(tgtMesh)
 | 
			
		||||
    except:
 | 
			
		||||
        print(traceback.format_exc())
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # create mesh iterator
 | 
			
		||||
    comp = OpenMaya.MObject()
 | 
			
		||||
    currentFace = OpenMaya.MItMeshPolygon(srcMeshDagPath, comp)
 | 
			
		||||
 | 
			
		||||
    # get all points from target mesh
 | 
			
		||||
    meshTgtMPointArray = OpenMaya.MPointArray()
 | 
			
		||||
    meshTgtMFnMesh = OpenMaya.MFnMesh(tgtMeshDagPath)
 | 
			
		||||
    meshTgtMFnMesh.getPoints(meshTgtMPointArray, OpenMaya.MSpace.kWorld)
 | 
			
		||||
 | 
			
		||||
    # create mesh intersector
 | 
			
		||||
    matrix = srcMeshDagPath.inclusiveMatrix()
 | 
			
		||||
    node = srcMeshDagPath.node()
 | 
			
		||||
    intersector = OpenMaya.MMeshIntersector()
 | 
			
		||||
    intersector.create(node, matrix)
 | 
			
		||||
 | 
			
		||||
    # create variables to store the returned data
 | 
			
		||||
    pointInfo = OpenMaya.MPointOnMesh()
 | 
			
		||||
    uUtil, vUtil = OpenMaya.MScriptUtil(0.0), OpenMaya.MScriptUtil(0.0)
 | 
			
		||||
    uPtr = uUtil.asFloatPtr()
 | 
			
		||||
    vPtr = vUtil.asFloatPtr()
 | 
			
		||||
    pointArray = OpenMaya.MPointArray()
 | 
			
		||||
    vertIdList = OpenMaya.MIntArray()
 | 
			
		||||
 | 
			
		||||
    # dummy variable needed in .setIndex()
 | 
			
		||||
    dummy = OpenMaya.MScriptUtil()
 | 
			
		||||
    dummyIntPtr = dummy.asIntPtr()
 | 
			
		||||
 | 
			
		||||
    # For each point on the target mesh
 | 
			
		||||
    # Find the closest triangle on the source mesh.
 | 
			
		||||
    # Get the vertIds and the barycentric coords.
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    if flipX:
 | 
			
		||||
        meshTgtMPointArray = flipMPointArrayInX(meshTgtMPointArray)
 | 
			
		||||
 | 
			
		||||
    for i in range(meshTgtMPointArray.length()):
 | 
			
		||||
 | 
			
		||||
        intersector.getClosestPoint(meshTgtMPointArray[i], pointInfo)
 | 
			
		||||
        pointInfo.getBarycentricCoords(uPtr, vPtr)
 | 
			
		||||
        u = uUtil.getFloat(uPtr)
 | 
			
		||||
        v = vUtil.getFloat(vPtr)
 | 
			
		||||
 | 
			
		||||
        faceId = pointInfo.faceIndex()
 | 
			
		||||
        triangleId = pointInfo.triangleIndex()
 | 
			
		||||
 | 
			
		||||
        currentFace.setIndex(faceId, dummyIntPtr)
 | 
			
		||||
        currentFace.getTriangle(triangleId, pointArray, vertIdList, OpenMaya.MSpace.kWorld)
 | 
			
		||||
 | 
			
		||||
        weightDict = {}
 | 
			
		||||
        weightDict[vertIdList[0]] = u
 | 
			
		||||
        weightDict[vertIdList[1]] = v
 | 
			
		||||
        weightDict[vertIdList[2]] = 1 - u - v
 | 
			
		||||
 | 
			
		||||
        baryWeightDict[i] = weightDict
 | 
			
		||||
 | 
			
		||||
    return baryWeightDict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def flipMPointArrayInX(MPointArray):
 | 
			
		||||
    '''
 | 
			
		||||
    Flips an MPoint array in X
 | 
			
		||||
    :param MPointArray: <MPointArray>
 | 
			
		||||
    :return:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    flippeMPointArray = OpenMaya.MPointArray()
 | 
			
		||||
 | 
			
		||||
    for i in range(MPointArray.length()):
 | 
			
		||||
        pX = MPointArray[i][0] * -1
 | 
			
		||||
        pY = MPointArray[i][1]
 | 
			
		||||
        pZ = MPointArray[i][2]
 | 
			
		||||
        flippeMPointArray.append(OpenMaya.MPoint(pX, pY, pZ))
 | 
			
		||||
 | 
			
		||||
    return flippeMPointArray
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def findVertsOnSidesOfX(MPointArray):
 | 
			
		||||
    '''
 | 
			
		||||
    Bins an MPointArray into two lists of verticeIndexes.
 | 
			
		||||
    Points living within -0.0001 to 0.0001 on the X axis are discarded.
 | 
			
		||||
    :param MPointArray: <MPointArray>
 | 
			
		||||
    :param positiveSide: <bool>
 | 
			
		||||
    :return: <list> <list> positiveVtxs, negativeVtxs
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    positiveVtxs = []
 | 
			
		||||
    negativeVtxs = []
 | 
			
		||||
 | 
			
		||||
    for i in range(MPointArray.length()):
 | 
			
		||||
        pX = MPointArray[i][0]
 | 
			
		||||
 | 
			
		||||
        # not equal to 0 as we are not interested in vertices in the "perfect middle"
 | 
			
		||||
        if pX > 0.0001:
 | 
			
		||||
            positiveVtxs.append(i)
 | 
			
		||||
        elif pX < -0.0001:
 | 
			
		||||
            negativeVtxs.append(i)
 | 
			
		||||
 | 
			
		||||
    return positiveVtxs, negativeVtxs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getMPointArray(MDagPath):
 | 
			
		||||
    '''
 | 
			
		||||
    Gets an MPointArray for all points in a mesh.
 | 
			
		||||
    MPoint is usable like a python list in the structure
 | 
			
		||||
    MPoint[index][x-pos, y-pos, z-pos]
 | 
			
		||||
    :param MDagPath: <MDagPath>
 | 
			
		||||
    :return:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    MPointArray = OpenMaya.MPointArray()
 | 
			
		||||
    meshTgtMFnMesh = OpenMaya.MFnMesh(MDagPath)
 | 
			
		||||
    meshTgtMFnMesh.getPoints(MPointArray, OpenMaya.MSpace.kWorld)
 | 
			
		||||
 | 
			
		||||
    return MPointArray
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def zipClusterWeights(baryWeightDict, clusterWeightDict):
 | 
			
		||||
    '''
 | 
			
		||||
    Zip a barycentric weight dictionary with a source cluster weight dictionary.
 | 
			
		||||
    By iterating over each target vertice in the baryWeightDict, build resulting weights, store in zippedCluster
 | 
			
		||||
    :param baryWeightDict: <dict> weight dict structured by the Utils modules .getBarycentricWeights()
 | 
			
		||||
    :param srcWeightDict: <dict> clusterWeight dict structured by eg. the Skinning module's .getSkinWeights()
 | 
			
		||||
    :return: <dict> of final cluster weights for the transfer
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    zippedCluster = {}
 | 
			
		||||
 | 
			
		||||
    for tgtVtxId in baryWeightDict.keys():
 | 
			
		||||
        zippedCluster[tgtVtxId] = {}
 | 
			
		||||
 | 
			
		||||
        for srcVtxId, baryWeight in baryWeightDict[tgtVtxId].items():
 | 
			
		||||
 | 
			
		||||
            for infVtxId, infWeight in clusterWeightDict[srcVtxId].items():
 | 
			
		||||
                zippedWeight = infWeight * baryWeight
 | 
			
		||||
 | 
			
		||||
                # if influence already exists, add them together
 | 
			
		||||
                if infVtxId in zippedCluster[tgtVtxId].keys():
 | 
			
		||||
                    zippedWeight += zippedCluster[tgtVtxId][infVtxId]
 | 
			
		||||
 | 
			
		||||
                zippedCluster[tgtVtxId][infVtxId] = zippedWeight
 | 
			
		||||
 | 
			
		||||
    return zippedCluster
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getVtxCount(shapeNode):
 | 
			
		||||
    '''
 | 
			
		||||
    Gets the vertex count for a given shape node
 | 
			
		||||
    :param shapeNode:
 | 
			
		||||
    :return: <int> vertex count
 | 
			
		||||
    '''
 | 
			
		||||
    # lazy buffer if object is passed
 | 
			
		||||
    shapeNode = getShape(shapeNode)
 | 
			
		||||
    return len(shapeNode.vtx)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filePathPrompt(dialogMode, caption="SkinWeights", dirPath="D:/", filter="API Skin weights file (*.skinWeights)"):
 | 
			
		||||
    '''
 | 
			
		||||
    Generates a maya file dialogue window and returns a filepath
 | 
			
		||||
    :param fileMode: <int> 0 is export path, 1 is save path
 | 
			
		||||
    :param caption: <string> title of dialog
 | 
			
		||||
    :param dirPath: <string> starting directory
 | 
			
		||||
    :param filter: <string> file type filter in format "(*.fileType)"
 | 
			
		||||
    :return: <string> file path
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    filePath = pm.system.fileDialog2(fileMode=dialogMode,
 | 
			
		||||
                                     caption=caption,
 | 
			
		||||
                                     dir=dirPath,
 | 
			
		||||
                                     fileFilter=filter)
 | 
			
		||||
    if filePath:
 | 
			
		||||
        # fileDialog2 returns a list if successful so we fetch the first element
 | 
			
		||||
        filePath = filePath[0]
 | 
			
		||||
 | 
			
		||||
    return filePath
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def matchDictionaryToSceneMeshes(weightDictionary, selected=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Matches names of the dictionary.keys() with meshes in the scene.
 | 
			
		||||
    Matches ALL objects, then filters based on user selection
 | 
			
		||||
    :param weightDictionary: <dict> weightDictionary generated by the apiVtxAttribs module
 | 
			
		||||
    :param selected: <bool> operate on selection
 | 
			
		||||
    :return: <dict> <list>, reformatted weight dictionary, found matching objects in scene
 | 
			
		||||
    '''
 | 
			
		||||
    validNodeList = []
 | 
			
		||||
    sceneWeightDict = {}
 | 
			
		||||
    # find all matching or valid objects in the scene
 | 
			
		||||
    for dictNodeName in weightDictionary.keys():
 | 
			
		||||
        sceneNodeList = pm.ls(dictNodeName, recursive=True, type="transform")
 | 
			
		||||
 | 
			
		||||
        if not len(sceneNodeList):
 | 
			
		||||
            # we have not found matches by the stored name, try short names
 | 
			
		||||
            shortDictNodeName = dictNodeName.split("|")[-1]
 | 
			
		||||
            sceneNodeList = pm.ls(shortDictNodeName, recursive=True, type="transform")
 | 
			
		||||
        print(sceneNodeList)
 | 
			
		||||
        for sceneNode in sceneNodeList:
 | 
			
		||||
            if vtxCountMatch(sceneNode, weightDictionary[dictNodeName]["vtxCount"]):
 | 
			
		||||
                # found match on both name and vtxcount, copy info to the local sceneWeightDict
 | 
			
		||||
                sceneWeightDict[sceneNode.name()] = weightDictionary[dictNodeName]
 | 
			
		||||
                validNodeList.append(sceneNode.name())
 | 
			
		||||
 | 
			
		||||
    # filter on selection
 | 
			
		||||
    if selected:
 | 
			
		||||
        selectionMatchedList = []
 | 
			
		||||
        selectedNodes = getTransforms(pm.ls(sl=True))
 | 
			
		||||
        for selectedNode in selectedNodes:
 | 
			
		||||
            if selectedNode in validNodeList:
 | 
			
		||||
                selectionMatchedList.append(str(selectedNode))
 | 
			
		||||
 | 
			
		||||
        # replace validNodeList with selection match
 | 
			
		||||
        validNodeList = selectionMatchedList
 | 
			
		||||
 | 
			
		||||
    if sceneWeightDict and validNodeList:
 | 
			
		||||
        return sceneWeightDict, validNodeList
 | 
			
		||||
    else:
 | 
			
		||||
        return {}, []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def vtxCountMatch(node, vtxCount):
 | 
			
		||||
    '''
 | 
			
		||||
    Queries if the vtx count of the given node and the given vtxCount matches
 | 
			
		||||
    :param node: <shape> or <transform>
 | 
			
		||||
    :param vtxCount: <int>
 | 
			
		||||
    :return: <bool>
 | 
			
		||||
    '''
 | 
			
		||||
    node = getShape(node)
 | 
			
		||||
    try:
 | 
			
		||||
        nodeVtxCount = len(node.vtx)
 | 
			
		||||
        if nodeVtxCount == vtxCount:
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
    except:
 | 
			
		||||
        print("Unable to retrieve .vtx list from %s" % node)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getPickleObject(filePath):
 | 
			
		||||
    '''
 | 
			
		||||
    Unpacks a the pickled weights files into a human readable weight dictionary
 | 
			
		||||
    :param filePath: <string>
 | 
			
		||||
    :return: <dict>
 | 
			
		||||
    '''
 | 
			
		||||
    with open(filePath, 'rb') as fp:
 | 
			
		||||
        try:
 | 
			
		||||
            pickleObject = pickle.Unpickler(fp)
 | 
			
		||||
            return pickleObject.load()
 | 
			
		||||
        except:
 | 
			
		||||
            raise Exception("Unable to unpack %s" % filePath)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pickleDumpWeightsToFile(weightDict, filePath):
 | 
			
		||||
    '''
 | 
			
		||||
    Export a weights file and handle versioncontrol
 | 
			
		||||
    :param weightDict: <dict> Dictionary of clusterweights
 | 
			
		||||
    :param filePath: <string>
 | 
			
		||||
    :return:
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    # if vc.checkout([filePath]):
 | 
			
		||||
    #     with open(filePath, 'wb') as fp:
 | 
			
		||||
    #         pickle.dump(weightDict, fp, pickle.HIGHEST_PROTOCOL)
 | 
			
		||||
    #     vc.add([filePath])
 | 
			
		||||
    # else:
 | 
			
		||||
    #     with open(filePath, 'wb') as fp:
 | 
			
		||||
    #         pickle.dump(weightDict, fp, pickle.HIGHEST_PROTOCOL)
 | 
			
		||||
    #     print "WARNING: Successfully exported weights but was unable to connect to perforce"
 | 
			
		||||
 | 
			
		||||
    with open(filePath, 'wb') as fp:
 | 
			
		||||
        pickle.dump(weightDict, fp, pickle.HIGHEST_PROTOCOL)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def handleTransferNodesList(transferNodes=None):
 | 
			
		||||
    '''
 | 
			
		||||
    Handle empty transferNodeslist in transfer functions
 | 
			
		||||
    Basically fetches the maya scene selection to the transfernodeslist if nothing is provided
 | 
			
		||||
    Checks if all transfer objects are valid mesh objects
 | 
			
		||||
    :param transferNodes: <list>
 | 
			
		||||
    :return: <list>
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    # operate on selection
 | 
			
		||||
    if transferNodes is None:
 | 
			
		||||
        transferNodes = getTransforms(pm.ls(os=True))
 | 
			
		||||
 | 
			
		||||
    if len(transferNodes) < 2:
 | 
			
		||||
        print("# Error: Select atleast 2 objects, a source and a target")
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
    if None in getShapes(transferNodes):
 | 
			
		||||
        print("# Error: Non-mesh objects found in selection")
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
    return transferNodes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoadingBar():
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.lprogressAmount = 0
 | 
			
		||||
 | 
			
		||||
    def loadingBar(self, ltitle, lmaxValue, lstatus="Working..."):
 | 
			
		||||
        #if loadingBar is accessed the first time, create progressWindow
 | 
			
		||||
        if self.lprogressAmount == 0:
 | 
			
		||||
            pm.progressWindow(title=ltitle, progress=self.lprogressAmount, status=lstatus, isInterruptable=False, max=lmaxValue)
 | 
			
		||||
        self.lprogressAmount += 1
 | 
			
		||||
        #increases progress in progressWindow
 | 
			
		||||
        pm.progressWindow(edit=True, progress=self.lprogressAmount)
 | 
			
		||||
        #if progressAmount is lmaxValue, finish progressWindow
 | 
			
		||||
        if self.lprogressAmount == lmaxValue:
 | 
			
		||||
            pm.progressWindow(endProgress=True)
 | 
			
		||||
            self.progressAmount = 0
 | 
			
		||||
							
								
								
									
										0
									
								
								Scripts/Animation/skin_api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Scripts/Animation/skin_api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										110
									
								
								Scripts/Animation/skin_api/apiVtxAttribs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								Scripts/Animation/skin_api/apiVtxAttribs.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
import time
 | 
			
		||||
import pymel.core as pm
 | 
			
		||||
 | 
			
		||||
# import atcore.atvc.atvc as atvc
 | 
			
		||||
import os
 | 
			
		||||
import skin_api.Utils as apiUtils
 | 
			
		||||
import skin_api.Skinning as apiSkinning
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
apiVtxAttribs is a "caller"-class in order to organize the simplified returns of the other modules such as EACloth or Skinning
 | 
			
		||||
I deem it the "garbage" collector for any general combination of the Utils, Skinning, EACloth, Havok modules that we find useful
 | 
			
		||||
eg a list with skinning, blendshape and paintableAttrs looks like this
 | 
			
		||||
{"ObjectName":    {skinCluster:{clusterWeights, clusterInflNames, clusterMaxInf}
 | 
			
		||||
                  {blendShape:{clusterWeights, clusterTargets}
 | 
			
		||||
                  {paintableAttr:{clusterWeights}
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ApiVtxAttribs():
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.activeScenePath       = os.path.dirname(pm.sceneName())
 | 
			
		||||
        # self.vc = atvc.VersionControl()
 | 
			
		||||
 | 
			
		||||
        self.filePath               = ""
 | 
			
		||||
 | 
			
		||||
        self.sourceWeightDict       = {}
 | 
			
		||||
        self.transferWeightDict     = {}
 | 
			
		||||
        self.barycentrWeightDict    = {}
 | 
			
		||||
 | 
			
		||||
        self.skinWeightsFilter      = "API Skin weights file (*.skinWeights)"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def exportSkinWeights(self, filePath=None, selected=False, saveJointInfo=False):
 | 
			
		||||
        '''
 | 
			
		||||
        Export skinweights for meshes in maya
 | 
			
		||||
        If no filePath is provided the function prompts a file dialogue window
 | 
			
		||||
        If selected is False, function will operate on all valid objects (meshes) found in the active maya scene.
 | 
			
		||||
        :param filePath: <string> default is None
 | 
			
		||||
        :param selected: <bool> default is False
 | 
			
		||||
        :param saveJointInfo: <bool> saves joint orient, world transform and parent joint info, default is False
 | 
			
		||||
        :return:
 | 
			
		||||
        '''
 | 
			
		||||
        msg = ""
 | 
			
		||||
        # get filepath for skinweightsfile
 | 
			
		||||
        if not filePath:
 | 
			
		||||
            filePath = apiUtils.filePathPrompt(dialogMode=0, caption= "Export Skinweights", dirPath=self.activeScenePath, filter = self.skinWeightsFilter)
 | 
			
		||||
 | 
			
		||||
        if filePath:
 | 
			
		||||
            if selected:
 | 
			
		||||
                transNodes = apiUtils.getTransforms(pm.ls(sl=True))
 | 
			
		||||
            else:
 | 
			
		||||
                transNodes = apiUtils.getTransforms(pm.ls(type="mesh"))
 | 
			
		||||
 | 
			
		||||
            start = time.time()
 | 
			
		||||
            skinWeightsDict = apiSkinning.buildSkinWeightsDict(transNodes, saveJointInfo=saveJointInfo)
 | 
			
		||||
 | 
			
		||||
            apiUtils.pickleDumpWeightsToFile(skinWeightsDict, filePath)
 | 
			
		||||
            end = time.time()
 | 
			
		||||
            msg = "Skinning Exported to %s in: " % filePath + str(end - start) + " seconds"
 | 
			
		||||
        
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def importSkinWeights(self, filePath=None, selected=False, stripJointNamespaces=False, addNewToHierarchy=False):
 | 
			
		||||
        '''
 | 
			
		||||
        Import skinweights for meshes in maya.
 | 
			
		||||
        If no filePath is provided the function prompts a file dialogue window
 | 
			
		||||
        If selected is False, function will operate on all valid objects (meshes) found in the active maya scene.
 | 
			
		||||
        Runs a filtering process on the given .skinWeights file, matching objects based on name AND vtx count.
 | 
			
		||||
        :param filePath: <string> default is None, accepts .skinWeights files
 | 
			
		||||
        :param selected: <bool> default is False
 | 
			
		||||
        :param stripJointNamespaces: <bool> strips joint namespaces on skinWeights file, default is False
 | 
			
		||||
        :param addNewToHierarchy: <bool> adds missing joints, world transform and parent only correct when exported with joint info, default is False
 | 
			
		||||
        :return:
 | 
			
		||||
        '''
 | 
			
		||||
        msg = ""
 | 
			
		||||
        # get filepath for skinweightsfile
 | 
			
		||||
        if not filePath:
 | 
			
		||||
            filePath = apiUtils.filePathPrompt(dialogMode= 1,
 | 
			
		||||
                                               caption="Import Skinweights",
 | 
			
		||||
                                               dirPath=self.activeScenePath,
 | 
			
		||||
                                               filter=self.skinWeightsFilter)
 | 
			
		||||
        if filePath:
 | 
			
		||||
            if os.path.exists(filePath) and filePath.endswith(".skinWeights"):
 | 
			
		||||
 | 
			
		||||
                fileWeightDict = apiUtils.getPickleObject(filePath)
 | 
			
		||||
 | 
			
		||||
                if fileWeightDict: # then build a filtered local "scene" weight dictionary and a list of valid Objects
 | 
			
		||||
 | 
			
		||||
                    sceneWeightDict, validNodeList = apiUtils.matchDictionaryToSceneMeshes(weightDictionary=fileWeightDict, selected=selected)
 | 
			
		||||
 | 
			
		||||
                    if len(validNodeList) > 0:
 | 
			
		||||
                        loadBarMaxVal = len(validNodeList)
 | 
			
		||||
                        loadBarObj = apiUtils.LoadingBar()
 | 
			
		||||
 | 
			
		||||
                        msg = "Importing skinning for : " + str(validNodeList)
 | 
			
		||||
                        for validNode in validNodeList:
 | 
			
		||||
                            loadBarObj.loadingBar("Importing Skinweights...", loadBarMaxVal, "Importing...")
 | 
			
		||||
                            apiSkinning.skinClusterBuilder(validNode, sceneWeightDict, stripJointNamespaces=stripJointNamespaces, addNewToHierarchy=addNewToHierarchy)
 | 
			
		||||
                    else:
 | 
			
		||||
                        # log.error("No valid objects found in scene!")
 | 
			
		||||
                        msg = "No valid objects found in scene!"
 | 
			
		||||
                        return False
 | 
			
		||||
            else:
 | 
			
		||||
                msg = "Could not find a .skinWeights file with path %s" % filePath
 | 
			
		||||
                # return False
 | 
			
		||||
        
 | 
			
		||||
        return msg
 | 
			
		||||
		Reference in New Issue
	
	Block a user