Files
MetaBox/Scripts/Animation/skin_api/Skinning.py
2025-04-17 04:52:48 +08:00

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)))