This commit is contained in:
2025-04-17 04:52:48 +08:00
commit 9985b73dc1
3708 changed files with 2387532 additions and 0 deletions

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

View 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

View File

View 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