diff --git a/2023/icons/ngskintools.png b/2023/icons/ngskintools.png new file mode 100644 index 0000000..1665e94 Binary files /dev/null and b/2023/icons/ngskintools.png differ diff --git a/2023/icons/skinapi.png b/2023/icons/skinapi.png new file mode 100644 index 0000000..b2bab70 Binary files /dev/null and b/2023/icons/skinapi.png differ diff --git a/2023/scripts/rigging_tools/skin_api/README.md b/2023/scripts/rigging_tools/skin_api/README.md new file mode 100644 index 0000000..3caf2bc --- /dev/null +++ b/2023/scripts/rigging_tools/skin_api/README.md @@ -0,0 +1,155 @@ +# Skin API + +高性能的 Maya 蒙皮权重管理工具,使用 Maya API 进行快速的权重导出、导入和操作。 + +## 功能特性 + +### 1. 权重导出 (WeightExport) +- 导出选中物体的蒙皮权重到文件 +- 保存骨骼层级信息 +- 支持多物体批量导出 +- 使用 pickle 格式高效存储 + +### 2. 权重导入 (WeightImport) +- 从文件导入蒙皮权重 +- 自动匹配场景中的物体 +- 支持选择导入或全场景导入 +- 自动重建骨骼层级 +- 支持命名空间处理 + +### 3. 解绑蒙皮 (UnbindSkin) +- 移除选中物体的蒙皮集群 +- 保留模型几何体 +- 支持批量操作 +- 安全确认对话框 + +## 使用方法 + +### 从工具架使用 + +在 Rigging 工具架上点击对应按钮: +- **Export** - 导出权重 +- **Import** - 导入权重 +- **Unbind** - 解绑蒙皮 + +### 从 Python 使用 + +```python +from rigging_tools.skin_api import ui + +# 导出权重 +ui.WeightExport() + +# 导入权重 +ui.WeightImport() + +# 解绑蒙皮 +ui.UnbindSkin() +``` + +### 高级用法 + +```python +from rigging_tools.skin_api import apiVtxAttribs + +# 创建 API 实例 +api = apiVtxAttribs.ApiVtxAttribs() + +# 导出选中物体的权重 +msg = api.exportSkinWeights(selected=True, saveJointInfo=True) +print(msg) + +# 导入权重到选中物体 +msg = api.importSkinWeights(selected=True, stripJointNamespaces=False, addNewToHierarchy=True) +print(msg) + +# 导出所有场景物体的权重 +msg = api.exportSkinWeights(filePath="D:/weights.skinWeights", selected=False) + +# 导入权重到所有匹配的场景物体 +msg = api.importSkinWeights(filePath="D:/weights.skinWeights", selected=False) +``` + +### 底层 API 使用 + +```python +from rigging_tools.skin_api import Skinning, Utils + +# 获取蒙皮集群信息 +skinInfo = Skinning.getSkinClusterInfo("pSphere1", saveJointInfo=True) + +# 构建权重字典 +weightDict = Skinning.buildSkinWeightsDict(["pSphere1", "pCube1"]) + +# 保存权重到文件 +Utils.pickleDumpWeightsToFile(weightDict, "D:/weights.skinWeights") + +# 从文件加载权重 +loadedWeights = Utils.getPickleObject("D:/weights.skinWeights") + +# 应用权重到物体 +Skinning.skinClusterBuilder("pSphere1", skinInfo, stripJointNamespaces=False) +``` + +## 模块结构 + +- **Skinning.py** - 核心蒙皮权重操作函数 + - `getSkinClusterInfo()` - 获取蒙皮集群信息 + - `getSkinClusterWeights()` - 获取权重数据 + - `setSkinWeights()` - 设置权重数据 + - `buildSkinWeightsDict()` - 构建权重字典 + - `skinClusterBuilder()` - 重建蒙皮集群 + - `transferSkinWeights()` - 传递权重 + +- **Utils.py** - 工具函数 + - `filePathPrompt()` - 文件对话框 + - `pickleDumpWeightsToFile()` - 保存权重文件 + - `getPickleObject()` - 加载权重文件 + - `matchDictionaryToSceneMeshes()` - 匹配场景物体 + - `getBarycentricWeights()` - 计算重心坐标权重 + +- **ui.py** - 用户界面函数 + - `WeightExport()` - 导出权重 UI + - `WeightImport()` - 导入权重 UI + - `UnbindSkin()` - 解绑蒙皮 UI + +- **apiVtxAttribs.py** - 顶点属性 API 操作 + +## 文件格式 + +权重文件使用 `.skinWeights` 扩展名,内部为 pickle 格式的 Python 字典: + +```python +{ + "objectName": { + "vtxCount": 482, + "skinCluster": { + "clusterInfluenceNames": ["joint1", "joint2", ...], + "clusterMaxInf": 4, + "clusterWeights": {...}, + "skinJointInformation": {...} + } + } +} +``` + +## 性能优化 + +- 使用 Maya OpenMaya API 进行快速权重读写 +- 批量操作减少 Maya 命令调用 +- 进度条显示长时间操作 +- 支持大规模模型(10万+顶点) + +## 注意事项 + +1. 导出前确保物体已绑定蒙皮 +2. 导入时会删除物体历史记录 +3. 骨骼名称需要匹配(支持命名空间处理) +4. 顶点数量需要匹配 +5. 解绑操作不可撤销,建议先保存场景 + +## 依赖 + +- PyMEL +- Maya OpenMaya API +- Maya OpenMayaAnim API diff --git a/2023/scripts/rigging_tools/skin_api/Skinning.py b/2023/scripts/rigging_tools/skin_api/Skinning.py new file mode 100644 index 0000000..43d523b --- /dev/null +++ b/2023/scripts/rigging_tools/skin_api/Skinning.py @@ -0,0 +1,551 @@ +try: + import pymel.core as pm +except ImportError: + pm = None + +import maya.cmds as cmds +import maya.OpenMaya as OpenMaya +import maya.OpenMayaAnim as OpenMayaAnim +import traceback +import time +import copy + +try: + import skin_api.Utils as apiUtils +except ImportError: + from . import Utils as apiUtils + + +def getMfNSkinCluster(clusterName): + ''' + Helper function to generate an MFnSkinCluster object + :param clusterName: + :return: + ''' + try: + # get the MFnSkinCluster for clusterName + selList = OpenMaya.MSelectionList() + selList.add(clusterName) + clusterNode = OpenMaya.MObject() + selList.getDependNode(0, clusterNode) + skinFn = OpenMayaAnim.MFnSkinCluster(clusterNode) + return skinFn + except: + raise "Could not find an MFnSkinCluster named '%s'" % clusterName + + + +def getInfluenceNames(skincluster): + ''' + Returns a list of names of influences (joints) in a skincluster + :param skincluster: or + :return: + ''' + if pm: + return [str(inf) for inf in pm.skinCluster(skincluster, influence=True, q=True)] + else: + return [str(inf) for inf in cmds.skinCluster(skincluster, influence=True, q=True)] + + + +def getMaxInfluences(skincluster): + ''' + Returns the maxInfluences for a skincluster as an int + :param skincluster: or + :return: + ''' + if pm: + return pm.getAttr(skincluster + ".maxInfluences") + else: + return cmds.getAttr(skincluster + ".maxInfluences") + + + +def getSkinClusterNode(node): + ''' + Gets the connected skincluster node for a given node + :param objectName: + :return: + ''' + + if pm: + objHistory = pm.listHistory(pm.PyNode(node)) + skinClusterList = pm.ls(objHistory, type="skinCluster") + else: + # cmds 版本 + objHistory = cmds.listHistory(node) + if objHistory: + skinClusterList = [h for h in objHistory if cmds.nodeType(h) == "skinCluster"] + else: + skinClusterList = [] + + if len(skinClusterList): + return str(skinClusterList[0]) + + return None + + + +def getSkinClusterInfo(objectName, saveJointInfo=False): + ''' + Builds a skincluster info dict. Structured as: + {"ObjectName": {skinCluster:{clusterWeights: {}, clusterInflNames: [], clusterMaxInf: int} + :param objectName: or + :param saveJointInfo: 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 = {} + if pm: + infNode = pm.PyNode(inf) + jointInfo["parent"] = str(infNode.getParent().name()) + jointInfo["matrix"] = infNode.getMatrix(worldSpace=True) + jointInfo["rotation"] = infNode.getRotation() + jointInfo["jointOrient"] = infNode.getAttr("jointOrient") + jointInformation[str(infNode)] = copy.deepcopy(jointInfo) + else: + # cmds 版本 + infName = str(inf) + parents = cmds.listRelatives(infName, parent=True) + jointInfo["parent"] = parents[0] if parents else "" + jointInfo["matrix"] = cmds.xform(infName, q=True, matrix=True, worldSpace=True) + jointInfo["rotation"] = cmds.xform(infName, q=True, rotation=True) + jointInfo["jointOrient"] = cmds.getAttr(infName + ".jointOrient")[0] + jointInformation[infName] = copy.deepcopy(jointInfo) + 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: + :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: 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: + :param nodeList: + :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: + :param weightDict: + :param vtxIdFilter: List of objects to gather weight info from + :param showLoadingBar: default is True + :param saveJointInfo: saves joint orient, world transform and parent joint info, default is False + :return: dictionary for skincluster + ''' + loadBarMaxVal = len(objectList) + loadBarObj = apiUtils.LoadingBar() + + sourceWeightDict = {} + for object in objectList: + if pm: + objectAsString = pm.PyNode(object).name() + else: + # cmds 版本 - object 已经是字符串 + objectAsString = str(object) + + if showLoadingBar: + loadBarObj.loadingBar("Building Skinweights Info...", loadBarMaxVal, "Building...") + + skinClusterInfo = getSkinClusterInfo(object, saveJointInfo=saveJointInfo) + + if skinClusterInfo: + sourceWeightDict[objectAsString] = {} + sourceWeightDict[objectAsString]["vtxCount"] = apiUtils.getVtxCount(object) + sourceWeightDict[objectAsString]["skinCluster"] = skinClusterInfo + + if bool(sourceWeightDict): + return sourceWeightDict + else: + return False + + + +def transferSkinWeights(transferNodes=None, showLoadingBar=True): + ''' + transfers skin weights between a given set of meshes by: + Getting the source skincluster info from the first mesh in the transfer objects list + Calculating the barycentric relationship with all consecutive meshes + Zipping the source and target mesh clusterweight dictionaries + Importing the resulting skin cluster weight dict to the skinclusterbuilder + + If no transferObjects are provided the function will attempt to fetch a selection from the scene to operate on + :param transferNodes: 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: + :param deleteHist: + :param stripJointNamespaces: strips joint namespaces on skinWeights file, default is False + :param addNewToHierarchy: adds missing joints, world transform and parent only correct when exported with joint info, default is False + :return: + ''' + startBuildTimer = time.time() + + # clear any messy history on the validObjects + if deleteHist: + if pm: + pm.select(objName, r=True) + pm.mel.eval("DeleteHistory;") + pm.select(clear=True) + else: + cmds.select(objName, r=True) + cmds.delete(objName, constructionHistory=True) + cmds.select(clear=True) + + clusterInfo = weightDict[objName]["skinCluster"] + + # lclusterInfo has 4 attribute keys : + # ['clusterInfluenceNames', 'clusterMaxInf', 'clusterVtxCount', 'clusterWeights'] + clusterJoints = clusterInfo["clusterInfluenceNames"] + clusterMaxInf = clusterInfo["clusterMaxInf"] + clusterJointInfo = clusterInfo.get("skinJointInformation") + + for jointNameOrig in clusterJoints: + jointName = jointNameOrig + if stripJointNamespaces: + jointName = jointNameOrig.split(":")[-1] + + objExists = pm.objExists(jointName) if pm else cmds.objExists(jointName) + if not objExists: + if pm: + pm.select(cl=True) + joint = pm.joint(p=(0, 0, 0), name=jointName) + # putting joint in the hierarchy and setting matrix + if addNewToHierarchy and clusterJointInfo: + jointInfo = clusterJointInfo.get(jointNameOrig) + if not jointInfo: + continue + parentJoint = jointInfo.get("parent") + if stripJointNamespaces: + parentJoint = parentJoint.split(":")[-1] + if pm.objExists(parentJoint): + parentJoint = pm.PyNode(parentJoint) + joint.setParent(parentJoint) + joint.setMatrix(jointInfo.get("matrix"), worldSpace=True) + joint.setAttr("jointOrient", jointInfo.get("jointOrient")) + joint.setRotation(jointInfo.get("rotation")) + else: + joint.setMatrix(jointInfo.get("matrix"), worldSpace=True) + pm.select(cl=True) + else: + # cmds 版本 + cmds.select(clear=True) + joint = cmds.joint(position=(0, 0, 0), name=jointName) + # putting joint in the hierarchy and setting matrix + if addNewToHierarchy and clusterJointInfo: + jointInfo = clusterJointInfo.get(jointNameOrig) + if not jointInfo: + continue + parentJoint = jointInfo.get("parent") + if stripJointNamespaces: + parentJoint = parentJoint.split(":")[-1] + if cmds.objExists(parentJoint): + cmds.parent(joint, parentJoint) + cmds.xform(joint, matrix=jointInfo.get("matrix"), worldSpace=True) + cmds.setAttr(joint + ".jointOrient", *jointInfo.get("jointOrient")) + cmds.xform(joint, rotation=jointInfo.get("rotation")) + else: + cmds.xform(joint, matrix=jointInfo.get("matrix"), worldSpace=True) + cmds.select(clear=True) + try: + if stripJointNamespaces: + clusterJoints = [a.split(":")[-1] for a in clusterJoints] + + if pm: + clusterNode = pm.skinCluster(clusterJoints, objName, tsb=True, mi=clusterMaxInf, omi=True) + # turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights + pm.setAttr('%s.normalizeWeights' % clusterNode, 0) + pm.skinPercent(clusterNode, objName, nrm=False, prw=100) + pm.setAttr('%s.normalizeWeights' % clusterNode, 1) + clusterNodeName = str(clusterNode) + else: + # cmds 版本 + clusterNode = cmds.skinCluster(clusterJoints, objName, tsb=True, mi=clusterMaxInf, omi=True)[0] + # turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights + cmds.setAttr('%s.normalizeWeights' % clusterNode, 0) + cmds.skinPercent(clusterNode, objName, normalize=False, pruneWeights=100) + cmds.setAttr('%s.normalizeWeights' % clusterNode, 1) + clusterNodeName = clusterNode + + # set the skinweights + setSkinWeights(clusterNodeName, clusterInfo["clusterWeights"]) + + + except RuntimeError: + print("Failed to build new skinCluster on %s" % objName) + except: + print(traceback.format_exc()) + + endBuildTimer = time.time() + print("Skin Cluster Built %s : " % objName + (str(endBuildTimer - startBuildTimer))) + + + diff --git a/2023/scripts/rigging_tools/skin_api/Utils.py b/2023/scripts/rigging_tools/skin_api/Utils.py new file mode 100644 index 0000000..2494ce0 --- /dev/null +++ b/2023/scripts/rigging_tools/skin_api/Utils.py @@ -0,0 +1,535 @@ +try: + import pymel.core as pm +except ImportError: + pm = None + +import maya.cmds as cmds +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: + 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) + else: + # cmds 版本 + nodeType = cmds.nodeType(node) + if nodeType == "transform": + shapeNodes = cmds.listRelatives(node, shapes=True, path=True) + + if not shapeNodes: + shapeNodes = [] + + for shapeNode in shapeNodes: + isIntermediate = cmds.getAttr("%s.intermediateObject" % shapeNode) + + if intermediate and isIntermediate and cmds.listConnections(shapeNode, source=False): + return shapeNode + + elif not intermediate and not isIntermediate: + return shapeNode + + if shapeNodes: + return shapeNodes[0] + + elif nodeType in ["mesh", "nurbsCurve", "nurbsSurface"]: + return node + + return None + + +def getShapes(nodeList): + ''' + Runs getShape on a list of nodes + :param nodeList: + :return: + ''' + + 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: + ''' + + if pm: + 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 + else: + # cmds 版本 + nodeType = cmds.nodeType(node) + if nodeType == 'transform': + # 检查是否有 mesh shape + shapes = cmds.listRelatives(node, shapes=True, type='mesh') + if shapes: + return node + elif nodeType == "mesh": + parents = cmds.listRelatives(node, parent=True) + if parents: + return parents[0] + return None + + +def getTransforms(nodeList): + ''' + Gets the Transform nodes for all nodes in a given list + returns list of nodes + :param nodeList: + :return: + ''' + + 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: name of node + :return: + ''' + + 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 + :param srcMesh: or + :param tgtMesh: or + :param flipX: flip the source mesh in X before calculating weights + :return: + ''' + baryWeightDict = {} + + if pm: + if pm.nodeType(srcMesh) != "mesh" and pm.nodeType(tgtMesh) != "mesh": + srcMesh = getShape(srcMesh) + tgtMesh = getShape(tgtMesh) + else: + if cmds.nodeType(srcMesh) != "mesh" and cmds.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: + :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: + :param positiveSide: + :return: 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: + :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: weight dict structured by the Utils modules .getBarycentricWeights() + :param srcWeightDict: clusterWeight dict structured by eg. the Skinning module's .getSkinWeights() + :return: 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: vertex count + ''' + # lazy buffer if object is passed + shapeNode = getShape(shapeNode) + if pm: + return len(shapeNode.vtx) + else: + # cmds 版本 + vtxCount = cmds.polyEvaluate(shapeNode, vertex=True) + return vtxCount if vtxCount else 0 + + +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: 0 is export path, 1 is save path + :param caption: title of dialog + :param dirPath: starting directory + :param filter: file type filter in format "(*.fileType)" + :return: file path + ''' + + if pm: + filePath = pm.system.fileDialog2(fileMode=dialogMode, + caption=caption, + dir=dirPath, + fileFilter=filter) + else: + filePath = cmds.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: weightDictionary generated by the apiVtxAttribs module + :param selected: operate on selection + :return: , reformatted weight dictionary, found matching objects in scene + ''' + validNodeList = [] + sceneWeightDict = {} + # find all matching or valid objects in the scene + for dictNodeName in weightDictionary.keys(): + if pm: + sceneNodeList = pm.ls(dictNodeName, recursive=True, type="transform") + else: + sceneNodeList = cmds.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] + if pm: + sceneNodeList = pm.ls(shortDictNodeName, recursive=True, type="transform") + else: + sceneNodeList = cmds.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 + if pm: + sceneWeightDict[sceneNode.name()] = weightDictionary[dictNodeName] + validNodeList.append(sceneNode.name()) + else: + sceneWeightDict[sceneNode] = weightDictionary[dictNodeName] + validNodeList.append(sceneNode) + + # filter on selection + if selected: + selectionMatchedList = [] + if pm: + selectedNodes = getTransforms(pm.ls(sl=True)) + else: + selectedNodes = getTransforms(cmds.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: or + :param vtxCount: + :return: + ''' + node = getShape(node) + try: + if pm: + nodeVtxCount = len(node.vtx) + else: + nodeVtxCount = cmds.polyEvaluate(node, vertex=True) + if nodeVtxCount == vtxCount: + return True + return False + except: + print("Unable to retrieve .vtx list from %s" % node) + return False + + +def getPickleObject(filePath): + ''' + Unpacks a the pickled weights files into a human readable weight dictionary + :param filePath: + :return: + ''' + 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: Dictionary of clusterweights + :param filePath: + :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: + :return: + ''' + + # operate on selection + if transferNodes is None: + if pm: + transferNodes = getTransforms(pm.ls(os=True)) + else: + transferNodes = getTransforms(cmds.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: + if pm: + pm.progressWindow(title=ltitle, progress=self.lprogressAmount, status=lstatus, isInterruptable=False, max=lmaxValue) + else: + cmds.progressWindow(title=ltitle, progress=self.lprogressAmount, status=lstatus, isInterruptable=False, maxValue=lmaxValue) + self.lprogressAmount += 1 + #increases progress in progressWindow + if pm: + pm.progressWindow(edit=True, progress=self.lprogressAmount) + else: + cmds.progressWindow(edit=True, progress=self.lprogressAmount) + #if progressAmount is lmaxValue, finish progressWindow + if self.lprogressAmount == lmaxValue: + if pm: + pm.progressWindow(endProgress=True) + else: + cmds.progressWindow(endProgress=True) + self.progressAmount = 0 diff --git a/2023/scripts/rigging_tools/skin_api/__init__.py b/2023/scripts/rigging_tools/skin_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/2023/scripts/rigging_tools/skin_api/apiVtxAttribs.py b/2023/scripts/rigging_tools/skin_api/apiVtxAttribs.py new file mode 100644 index 0000000..5dc22b0 --- /dev/null +++ b/2023/scripts/rigging_tools/skin_api/apiVtxAttribs.py @@ -0,0 +1,130 @@ +import time +try: + import pymel.core as pm +except ImportError: + pm = None + +import maya.cmds as cmds +# import atcore.atvc.atvc as atvc +import os + +try: + import skin_api.Utils as apiUtils + import skin_api.Skinning as apiSkinning +except ImportError: + from . import Utils as apiUtils + from . import 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): + if pm: + self.activeScenePath = os.path.dirname(pm.sceneName()) + else: + scene_name = cmds.file(q=True, sn=True) + self.activeScenePath = os.path.dirname(scene_name) if scene_name else "" + # 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: default is None + :param selected: default is False + :param saveJointInfo: 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: + if pm: + transNodes = apiUtils.getTransforms(pm.ls(sl=True)) + else: + transNodes = apiUtils.getTransforms(cmds.ls(sl=True)) + else: + if pm: + transNodes = apiUtils.getTransforms(pm.ls(type="mesh")) + else: + # cmds.ls 也使用 type 参数 + transNodes = apiUtils.getTransforms(cmds.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: default is None, accepts .skinWeights files + :param selected: default is False + :param stripJointNamespaces: strips joint namespaces on skinWeights file, default is False + :param addNewToHierarchy: 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 diff --git a/2023/scripts/rigging_tools/skin_api/ui.py b/2023/scripts/rigging_tools/skin_api/ui.py new file mode 100644 index 0000000..8c7b5a7 --- /dev/null +++ b/2023/scripts/rigging_tools/skin_api/ui.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Skin API UI Functions +提供权重导出、导入和解绑的用户界面函数 +""" + +# 确保 Maya 环境已初始化 +try: + import maya.standalone + maya.standalone.initialize() +except: + pass + +try: + import pymel.core as pm +except ImportError: + pm = None + +import maya.cmds as cmds +import maya.mel as mel + +try: + from . import apiVtxAttribs +except ImportError: + import rigging_tools.skin_api.apiVtxAttribs as apiVtxAttribs + + +def WeightExport(): + """ + 导出选中物体的蒙皮权重 + Export skin weights for selected objects + """ + try: + # 检查是否有选中物体 + if pm: + selectedNodes = pm.ls(sl=True) + if not selectedNodes: + pm.warning("请选择至少一个蒙皮物体 / Please select at least one skinned object") + return + else: + selectedNodes = cmds.ls(sl=True) + if not selectedNodes: + cmds.warning("请选择至少一个蒙皮物体 / Please select at least one skinned object") + return + + # 使用 ApiVtxAttribs 类导出权重 + api = apiVtxAttribs.ApiVtxAttribs() + msg = api.exportSkinWeights(selected=True, saveJointInfo=True) + + if msg: + print(msg) + if pm: + pm.confirmDialog( + title="Export Success", + message="Skin weights exported successfully!", + button=["OK"] + ) + else: + cmds.confirmDialog( + title="Export Success", + message="Skin weights exported successfully!", + button=["OK"] + ) + else: + print("Export cancelled") + + except Exception as e: + error_msg = f"Failed to export skin weights: {str(e)}" + if pm: + pm.error(error_msg) + else: + cmds.error(error_msg) + + +def WeightImport(): + """ + 导入蒙皮权重到选中物体或场景中匹配的物体 + Import skin weights to selected or matching objects in scene + """ + try: + # 检查是否有选中物体 + if pm: + selectedNodes = pm.ls(sl=True) + else: + selectedNodes = cmds.ls(sl=True) + useSelection = len(selectedNodes) > 0 + + print(f"Import mode: {'selected objects' if useSelection else 'all matching objects'}") + + # 使用 ApiVtxAttribs 类导入权重 + api = apiVtxAttribs.ApiVtxAttribs() + msg = api.importSkinWeights(selected=useSelection, stripJointNamespaces=False, addNewToHierarchy=True) + + print(f"Import result: {repr(msg)}") + + if msg and msg != False: + print(msg) + if "No valid objects found" in str(msg): + warning_msg = "No valid objects found in scene!\nMake sure:\n1. Object names match\n2. Vertex counts match\n3. Objects exist in scene" + if pm: + pm.warning(warning_msg) + pm.confirmDialog( + title="Import Failed", + message=warning_msg, + button=["OK"] + ) + else: + cmds.warning(warning_msg) + cmds.confirmDialog( + title="Import Failed", + message=warning_msg, + button=["OK"] + ) + elif "Could not find" in str(msg): + if pm: + pm.warning(msg) + else: + cmds.warning(msg) + else: + # 成功导入 + if pm: + pm.confirmDialog( + title="Import Complete", + message="Skin weights imported successfully!", + button=["OK"] + ) + else: + cmds.confirmDialog( + title="Import Complete", + message="Skin weights imported successfully!", + button=["OK"] + ) + else: + print("Import cancelled or no file selected") + + except Exception as e: + import traceback + error_msg = f"Failed to import skin weights: {str(e)}\n{traceback.format_exc()}" + print(error_msg) + if pm: + pm.error(error_msg) + else: + cmds.error(error_msg) + + +def UnbindSkin(): + """ + 解绑选中物体的蒙皮 + Unbind skin from selected objects + """ + try: + # 获取选中的物体 + if pm: + selectedNodes = pm.ls(sl=True) + if not selectedNodes: + pm.warning("请选择至少一个物体 / Please select at least one object") + return + else: + selectedNodes = cmds.ls(sl=True) + if not selectedNodes: + cmds.warning("请选择至少一个物体 / Please select at least one object") + return + + # 确认对话框 + if pm: + result = pm.confirmDialog( + title="Unbind Skin", + message=f"确定要解绑 {len(selectedNodes)} 个物体的蒙皮吗?\nAre you sure you want to unbind skin from {len(selectedNodes)} object(s)?", + button=["Yes", "No"], + defaultButton="Yes", + cancelButton="No", + dismissString="No" + ) + else: + result = cmds.confirmDialog( + title="Unbind Skin", + message=f"确定要解绑 {len(selectedNodes)} 个物体的蒙皮吗?\nAre you sure you want to unbind skin from {len(selectedNodes)} object(s)?", + button=["Yes", "No"], + defaultButton="Yes", + cancelButton="No", + dismissString="No" + ) + + if result != "Yes": + print("Unbind cancelled") + return + + # 使用 MEL 命令解绑蒙皮 + mel.eval('doDetachSkin "2" { "1","1" };') + print("已取消蒙皮!/ Skin unbound!") + + if pm: + pm.confirmDialog( + title="Unbind Complete", + message="Skin unbound successfully!", + button=["OK"] + ) + else: + cmds.confirmDialog( + title="Unbind Complete", + message="Skin unbound successfully!", + button=["OK"] + ) + + except Exception as e: + error_msg = f"Failed to unbind skin: {str(e)}" + if pm: + pm.error(error_msg) + else: + cmds.error(error_msg) diff --git a/2023/shelves/shelf_Nexus_Rigging.mel b/2023/shelves/shelf_Nexus_Rigging.mel index 3015b3c..ecb72b8 100644 --- a/2023/shelves/shelf_Nexus_Rigging.mel +++ b/2023/shelves/shelf_Nexus_Rigging.mel @@ -14,27 +14,97 @@ global proc shelf_Nexus_Rigging () { -manage 1 -visible 1 -preventOverride 0 - -annotation "Nexus Test - Verify plugin system" + -annotation "Export Skin Weights - Export skin weights to file" -enableBackground 0 -backgroundColor 0 0 0 -highlightColor 0.321569 0.521569 0.65098 -align "center" - -label "Test" + -label "Export" -labelOffset 0 -rotation 0 -flipX 0 -flipY 0 -useAlpha 1 -font "plainLabelFont" - -imageOverlayLabel "Test" + -imageOverlayLabel "Export" -overlayLabelColor 0.8 0.8 0.8 -overlayLabelBackColor 0 0 0 0.5 - -image "commandButton.png" - -image1 "commandButton.png" + -image "skinapi.png" + -image1 "skinapi.png" -style "iconOnly" -marginWidth 0 -marginHeight 1 - -command "import nexus_test\nnexus_test.run_test()" + -command "from rigging_tools.skin_api import ui\nui.WeightExport()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Import Skin Weights - Import skin weights from file" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "Import" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -imageOverlayLabel "Import" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "skinapi.png" + -image1 "skinapi.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "from rigging_tools.skin_api import ui\nui.WeightImport()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Unbind Skin - Remove skin cluster from selected objects" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "Unbind" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -imageOverlayLabel "Unbind" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "skinapi.png" + -image1 "skinapi.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "from rigging_tools.skin_api import ui\nui.UnbindSkin()" -sourceType "python" -commandRepeatable 1 -flat 1