452 lines
14 KiB
Python
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
|