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

452 lines
14 KiB
Python

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