Files
Nexus/2023/scripts/rigging_tools/skin_api/Skinning.py
2025-11-24 22:26:56 +08:00

582 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
try:
import pymel.core as pm
except ImportError:
pm = None
import maya.cmds as cmds
import maya.OpenMaya as OpenMaya
import maya.OpenMayaAnim as OpenMayaAnim
import traceback
import time
import copy
try:
import skin_api.Utils as apiUtils
except ImportError:
from . import 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>
'''
if pm:
return [str(inf) for inf in pm.skinCluster(skincluster, influence=True, q=True)]
else:
return [str(inf) for inf in cmds.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>
'''
if pm:
return pm.getAttr(skincluster + ".maxInfluences")
else:
return cmds.getAttr(skincluster + ".maxInfluences")
def getSkinClusterNode(node):
'''
Gets the connected skincluster node for a given node
:param objectName:
:return:
'''
if pm:
objHistory = pm.listHistory(pm.PyNode(node))
skinClusterList = pm.ls(objHistory, type="skinCluster")
else:
# cmds 版本
objHistory = cmds.listHistory(node)
if objHistory:
skinClusterList = [h for h in objHistory if cmds.nodeType(h) == "skinCluster"]
else:
skinClusterList = []
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):
"""
获取骨骼信息(父节点、矩阵、旋转、关节方向)
兼容 PyMEL 和 cmds处理无父节点的情况
"""
jointInformation = {}
for inf in influences:
jointInfo = {}
try:
if pm:
infNode = pm.PyNode(inf)
# 安全获取父节点,避免 None.name() 错误
parent = infNode.getParent()
jointInfo["parent"] = str(parent.name()) if parent else ""
jointInfo["matrix"] = infNode.getMatrix(worldSpace=True)
jointInfo["rotation"] = infNode.getRotation()
jointInfo["jointOrient"] = infNode.getAttr("jointOrient")
jointInformation[str(infNode)] = copy.deepcopy(jointInfo)
else:
# cmds 版本
infName = str(inf)
parents = cmds.listRelatives(infName, parent=True)
jointInfo["parent"] = parents[0] if parents else ""
jointInfo["matrix"] = cmds.xform(infName, q=True, matrix=True, worldSpace=True)
jointInfo["rotation"] = cmds.xform(infName, q=True, rotation=True)
jointInfo["jointOrient"] = cmds.getAttr(infName + ".jointOrient")[0]
jointInformation[infName] = copy.deepcopy(jointInfo)
except Exception as e:
print(f"Warning: Failed to get joint information for {inf}: {e}")
# 使用默认值
jointInfo["parent"] = ""
jointInfo["matrix"] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
jointInfo["rotation"] = [0, 0, 0]
jointInfo["jointOrient"] = [0, 0, 0]
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:
try:
if pm:
# 安全转换为字符串,处理可能的 None 或无效对象
obj_node = pm.PyNode(object) if not isinstance(object, pm.PyNode) else object
objectAsString = str(obj_node.name()) if obj_node else str(object)
else:
# cmds 版本 - object 已经是字符串
objectAsString = str(object)
except Exception as e:
print(f"Warning: Failed to process object {object}: {e}")
objectAsString = str(object)
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]
# 安全获取名称
try:
sourceName = str(sourceObj.name()) if hasattr(sourceObj, 'name') else str(sourceObj)
except:
sourceName = str(sourceObj)
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)
# 安全获取名称
try:
targetName = str(tgtObject.name()) if hasattr(tgtObject, 'name') else str(tgtObject)
except:
targetName = str(tgtObject)
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:
if pm:
pm.select(objName, r=True)
pm.mel.eval("DeleteHistory;")
pm.select(clear=True)
else:
cmds.select(objName, r=True)
cmds.delete(objName, constructionHistory=True)
cmds.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]
objExists = pm.objExists(jointName) if pm else cmds.objExists(jointName)
if not objExists:
if pm:
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)
else:
# cmds 版本
cmds.select(clear=True)
joint = cmds.joint(position=(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 cmds.objExists(parentJoint):
cmds.parent(joint, parentJoint)
cmds.xform(joint, matrix=jointInfo.get("matrix"), worldSpace=True)
cmds.setAttr(joint + ".jointOrient", *jointInfo.get("jointOrient"))
cmds.xform(joint, rotation=jointInfo.get("rotation"))
else:
cmds.xform(joint, matrix=jointInfo.get("matrix"), worldSpace=True)
cmds.select(clear=True)
try:
if stripJointNamespaces:
clusterJoints = [a.split(":")[-1] for a in clusterJoints]
if pm:
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)
clusterNodeName = str(clusterNode)
else:
# cmds 版本
clusterNode = cmds.skinCluster(clusterJoints, objName, tsb=True, mi=clusterMaxInf, omi=True)[0]
# turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights
cmds.setAttr('%s.normalizeWeights' % clusterNode, 0)
cmds.skinPercent(clusterNode, objName, normalize=False, pruneWeights=100)
cmds.setAttr('%s.normalizeWeights' % clusterNode, 1)
clusterNodeName = clusterNode
# set the skinweights
setSkinWeights(clusterNodeName, 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)))