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