This commit is contained in:
2025-11-24 01:37:59 +08:00
parent 0eeeb54784
commit 6940f17517
9 changed files with 1659 additions and 6 deletions

BIN
2023/icons/ngskintools.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
2023/icons/skinapi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -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

View File

@@ -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: <skincluster object> or <string>
:return: <list>
'''
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: <skincluster object> or <string>
:return: <int>
'''
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: <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 = {}
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: <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:
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: <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:
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)))

View File

@@ -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: <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:
'''
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: <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
:param srcMesh: <shape> or <transform>
:param tgtMesh: <shape> or <transform>
:param flipX: <bool> flip the source mesh in X before calculating weights
:return: <dict>
'''
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: <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)
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: <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
'''
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: <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():
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: <shape> or <transform>
:param vtxCount: <int>
:return: <bool>
'''
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: <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:
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

View File

@@ -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: <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:
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: <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

View File

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

View File

@@ -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