478 lines
17 KiB
Python
478 lines
17 KiB
Python
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)))
|
|
|
|
|
|
|