'''
Example export:
.. code-block:: python
layerData = LayerData()
layerData.loadFrom('skinnedMesh')
exporter = JsonExporter()
jsonContents = exporter.process(layerData)
# string "jsonContents" can now be saved to an external file
Example import:
.. code-block:: python
# assume that jsonContents is already loaded from file
importer = JsonImporter()
layerData = importer.process(jsonContents)
layerData.saveTo('skinnedMesh')
'''
from __future__ import with_statement
from ngSkinTools.mllInterface import MllInterface
from ngSkinTools.utils import Utils, MessageException
from maya import cmds
from ngSkinTools.skinClusterFn import SkinClusterFn
from ngSkinTools.meshDataExporter import MeshDataExporter
[docs]class Influence(object):
'''
Single influence in a layer
.. py:attribute:: weights
vertex weights for this influence. Set to float list, containing
as many values as there are vertices in a target mesh.
.. py:attribute:: influenceName
Full path of the influence in the scene. Required value when importing
data back into skin cluster, as influences are associated by name in
current implementation.
.. py:attribute:: logicalIndex
Logical index for this influence in a skin cluster. Not required for
import and only provided in export as a reference.
'''
def __init__(self):
# influence logical index in a skin cluster
self.logicalIndex = -1
# full path of the influence in the scene
self.influenceName = None
# influence weights for each vertex (list of double)
self.weights = []
def __repr__(self):
return "[Infl %r]" % (self.influenceName)
[docs]class Layer(object):
'''
Represents single layer; can contain any amount of influences.
.. py:attribute:: name
layer name. Default value: None; set/use as any python string.
.. py:attribute:: opacity
layer opacity. Defaults to 0.0. Set to float value between 0.0 and 1.0
.. py:attribute:: enabled
layer on/off flag. Default value is False. Set to True or False.
.. py:attribute:: influences
list of :class:`Influence` objects.
.. py:attribute:: mask
layer mask: list of floats. Set to None for uninitialized mask,
or to float list, containing as many values as there are vertices
in a target mesh.
.. py:attribute:: dqWeights
dual quaternion blend weights. None if not defined for this layer,
or float list, one value per vertex in the target mesh.
.. py:attribute:: parent
index of parent layer (in the context of this model's layer list)
'''
def __init__(self):
# layer name
self.name = None
# layer opacity
self.opacity = 0.0
# layer on/off flag
self.enabled = False
# list of influences in this layer with their weights (list of Influence)
self.influences = []
# layer mask (could be None or list of double)
self.mask = None
self.dqWeights = None
self.parent = None
[docs] def addInfluence(self, influence):
'''
Add an influence in this layer.
:param Influence influence: influence to be added
'''
assert isinstance(influence, Influence)
self.influences.append(influence)
def __repr__(self):
return "[Layer n:'{0}' parent:{1} o:{2} en:{3} infl:{4}]".format(self.name,self.parent,self.opacity,self.enabled,self.influences)
class MeshInfo(object):
def __init__(self):
# vertex positions for each vertex, listing x y z for first vertex, then second, etc.
# total 3*(number of vertices) values
self.verts = []
# vertex IDs for each triangle, listing three vertex indexes for first triangle, then second, etc
# total 3*(number of triangles) values
self.triangles = []
[docs]class InfluenceInfo(object):
'''
Metadata about an influence in a skin cluster
.. py:attribute:: pivot
influence pivot in world-space coordinates
.. py:attribute:: path
influence node path
.. py:attribute:: logicalIndex
influence logical index in the skin cluster.
'''
def __init__(self,pivot=None,path=None,logicalIndex=None):
self.pivot = pivot
self.path = path
self.logicalIndex = logicalIndex
def __repr__(self):
return "[InflInfo %r %r %r]" % (self.logicalIndex,self.path,self.pivot)
[docs]class LayerData(object):
'''
Intermediate data object between ngSkinTools core and importers/exporters,
representing all layers info in one skin cluster.
.. py:attribute:: layers
a list of :py:class:`Layer` objects.
.. py:attribute:: influences
a list of :py:class:`InfluenceInfo` objects. Provides information about influences
that were found on exported skin data, and used for influence matching when importing.
'''
def __init__(self):
self.layers = []
self.mll = MllInterface()
self.meshInfo = MeshInfo()
self.influences = []
# a map [sourceInfluenceName] -> [destinationInfluenceName]
self.mirrorInfluenceAssociationOverrides = None
self.skinClusterFn = None
[docs] def addMirrorInfluenceAssociationOverride(self,sourceInfluence,destinationInfluence=None,selfReference=False,bidirectional=True):
'''
Adds mirror influence association override, similar to UI of "Add influences association".
Self reference creates a source<->source association, bidirectional means that destination->source
link is added as well
'''
if self.mirrorInfluenceAssociationOverrides is None:
self.mirrorInfluenceAssociationOverrides = {}
if selfReference:
self.mirrorInfluenceAssociationOverrides[sourceInfluence] = sourceInfluence
return
if destinationInfluence is None:
raise MessageException("destination influence must be specified")
self.mirrorInfluenceAssociationOverrides[sourceInfluence] = destinationInfluence
if bidirectional:
self.mirrorInfluenceAssociationOverrides[destinationInfluence] = sourceInfluence
[docs] def addLayer(self, layer):
'''
register new layer into this data object
:param Layer layer: layer object to add.
'''
assert isinstance(layer, Layer)
self.layers.append(layer)
@staticmethod
def getFullNodePath(nodeName):
result = cmds.ls(nodeName,l=True)
if result is None or len(result)==0:
raise MessageException("node %s was not found" % nodeName)
return result[0]
def loadInfluenceInfo(self):
self.influences = self.mll.listInfluenceInfo()
[docs] def loadFrom(self, mesh):
'''
loads data from actual skin cluster and prepares it for exporting.
supply skin cluster or skinned mesh as an argument
'''
self.mll.setCurrentMesh(mesh)
meshExporter = MeshDataExporter()
self.meshInfo = MeshInfo()
if mesh!=MllInterface.TARGET_REFERENCE_MESH:
mesh,skinCluster = self.mll.getTargetInfo()
meshExporter.setTransformMatrixFromNode(mesh)
meshExporter.useSkinClusterInputMesh(skinCluster)
self.meshInfo.verts,self.meshInfo.triangles = meshExporter.export()
else:
self.meshInfo.verts = self.mll.getReferenceMeshVerts()
self.meshInfo.triangles = self.mll.getReferenceMeshTriangles()
self.loadInfluenceInfo()
# map LayerId->layerIndex
layerIndexById = {}
for index, (layerID, layerName, parentId) in enumerate(self.mll.listLayers()):
layerIndexById[layerID] = index
self.mirrorInfluenceAssociationOverrides = self.mll.getManualMirrorInfluences()
if len(self.mirrorInfluenceAssociationOverrides)==0:
self.mirrorInfluenceAssociationOverrides = None
layer = Layer()
layer.name = layerName
self.addLayer(layer)
layer.opacity = self.mll.getLayerOpacity(layerID)
layer.enabled = self.mll.isLayerEnabled(layerID)
layer.mask = self.mll.getLayerMask(layerID)
layer.dqWeights = self.mll.getDualQuaternionWeights(layerID)
# transform parent ID to local index in the model
if parentId is not None:
layer.parent = layerIndexById[parentId]
for inflName, logicalIndex in self.mll.listLayerInfluences(layerID,activeInfluences=True):
if inflName=='':
inflName = None
influence = Influence()
if inflName is not None:
influence.influenceName = self.getFullNodePath(inflName)
influence.logicalIndex = logicalIndex
layer.addInfluence(influence)
influence.weights = self.mll.getInfluenceWeights(layerID, logicalIndex)
def __validate(self):
numVerts = self.mll.getVertCount()
def validateVertCount(count,message):
if count!=numVerts:
raise Exception(message)
for layer in self.layers:
if layer.mask is not None and len(layer.mask) != 0:
validateVertCount(len(layer.mask), "Invalid vertex count for mask in layer '%s': expected size is %d" % (layer.name, numVerts))
for influence in layer.influences:
validateVertCount(len(influence.weights), "Invalid weights count for influence '%s' in layer '%s': expected size is %d" % (influence.influenceName, layer.name, numVerts))
if self.skinClusterFn:
influence.logicalIndex = self.skinClusterFn.getLogicalInfluenceIndex(influence.influenceName)
[docs] @Utils.undoable
def saveTo(self, mesh):
'''
saveTo(self,mesh)
saves data to actual skin cluster
'''
# set target to whatever was provided
self.mll.setCurrentMesh(mesh)
if mesh==MllInterface.TARGET_REFERENCE_MESH:
self.mll.setWeightsReferenceMesh(self.meshInfo.verts, self.meshInfo.triangles)
if not self.mll.getLayersAvailable():
self.mll.initLayers()
if not self.mll.getLayersAvailable():
raise Exception("could not initialize layers")
# is skin cluster available?
if mesh!=MllInterface.TARGET_REFERENCE_MESH:
mesh, self.skinCluster = self.mll.getTargetInfo()
self.skinClusterFn = SkinClusterFn()
self.skinClusterFn.setSkinCluster(self.skinCluster)
self.__validate()
# set target to actual mesh
self.mll.setCurrentMesh(mesh)
with self.mll.batchUpdateContext():
if self.mirrorInfluenceAssociationOverrides:
self.mll.setManualMirrorInfluences(self.mirrorInfluenceAssociationOverrides)
# IDS of created layers, in reverse order
layerIds = []
for layer in reversed(self.layers):
layerId = self.mll.createLayer(name=layer.name, forceEmpty=True)
layerIds.append(layerId)
self.mll.setCurrentLayer(layerId)
if layerId is None:
raise Exception("import failed: could not create layer '%s'" % (layer.name))
self.mll.setLayerOpacity(layerId, layer.opacity)
self.mll.setLayerEnabled(layerId, layer.enabled)
self.mll.setLayerMask(layerId, layer.mask)
self.mll.setDualQuaternionWeights(layerId, layer.dqWeights)
self.mll.setLayerWeightsBufferSize(layerId, len(layer.influences))
for influence in layer.influences:
# because layer was just created and will belong to same undo chunk, disabling undo
# for setting weights bit
self.mll.setInfluenceWeights(layerId, influence.logicalIndex, influence.weights, undoEnabled=False)
# layer.parent reference index is in normal order, therefore need to reverse it again
layerIds = list(reversed(layerIds))
for index,layer in enumerate(self.layers):
if layer.parent is not None:
self.mll.setLayerParent(layerIds[index], layerIds[layer.parent])
# reparenting will move it to the end of the list; move it to the bottom instead
self.mll.setLayerIndex(layerIds[index], 0)
def __repr__(self):
return "[LayerDataModel(%r)]" % self.layers
[docs] def getAllInfluences(self):
'''
a convenience method to retrieve a list of names of all influences used in this layer data object
'''
result = set()
for layer in self.layers:
for influence in layer.influences:
result.add(influence.influenceName)
return tuple(result)
[docs]class JsonExporter:
def __influenceToDictionary(self, influence):
result = {}
result['name'] = influence.influenceName
result['index'] = influence.logicalIndex
result['weights'] = influence.weights
return result
def __layerToDictionary(self, layer):
'''
:type layer: Layer
'''
result = {}
result['name'] = layer.name
result['opacity'] = layer.opacity
result['enabled'] = layer.enabled
result['mask'] = layer.mask
result['dqWeights'] = layer.dqWeights
result['influences'] = []
if layer.parent is not None:
result['parent'] = layer.parent
for infl in layer.influences:
result['influences'].append(self.__influenceToDictionary(infl))
return result
def __meshInfoToDictionary(self,meshInfo):
result = {}
result['verts'] = meshInfo.verts
result['triangles'] = meshInfo.triangles
return result
def __modelToDictionary(self, model):
result = {}
result['meshInfo'] = self.__meshInfoToDictionary(model.meshInfo)
if model.mirrorInfluenceAssociationOverrides:
result['manualInfluenceOverrides'] = dict(model.mirrorInfluenceAssociationOverrides.items())
result['layers'] = []
for layer in model.layers:
result['layers'].append(self.__layerToDictionary(layer))
if model.influences:
result['influences'] = self.__serializeInfluences(model.influences)
return result
def __serializeInfluences(self, influences):
result = {}
for i in influences:
result[i.logicalIndex] = {'path':i.path,'index':i.logicalIndex,'pivot': i.pivot}
return result
[docs] def process(self, layerDataModel):
'''
transforms LayerDataModel to JSON
:param LayerData layerDataModel: layers information as object;
:return: string containing a json document
'''
modelDictionary = self.__modelToDictionary(layerDataModel);
import json
import re
exportOutput = json.dumps(modelDictionary,indent=2)
# remove line break if next line is "whitespace + closing bracket or positive/negative number"
exportOutput = re.sub(r'\n\s+(\]|\-?\d)',r"\1",exportOutput)
return exportOutput
[docs]class JsonImporter:
[docs] def process(self, jsonDocument):
'''
transform JSON document () into layerDataModel
:param str jsonDocument: layers info, previously serialized as json string
:rtype: LayerData
'''
import json
self.document = json.loads(jsonDocument)
model = LayerData()
meshInfo = self.document.get('meshInfo')
if meshInfo:
model.meshInfo.verts = meshInfo['verts']
model.meshInfo.triangles = meshInfo['triangles']
model.mirrorInfluenceAssociationOverrides = self.document.get("manualInfluenceOverrides")
influences = self.document.get('influences')
if influences:
model.influences = []
for i in influences.values():
model.influences.append(InfluenceInfo(pivot=i['pivot'], path=i['path'], logicalIndex=i['index']))
for layerData in self.document["layers"]:
layer = Layer()
model.addLayer(layer)
layer.enabled = layerData.get('enabled',True)
layer.mask = layerData.get('mask')
layer.dqWeights = layerData.get('dqWeights')
layer.name = layerData['name']
layer.opacity = layerData.get('opacity',1.0)
layer.influences = []
layer.parent = layerData.get('parent',None)
for influenceData in layerData.get('influences',[]):
influence = Influence()
layer.addInfluence(influence)
influence.weights = influenceData['weights']
influence.logicalIndex = influenceData['index']
influence.influenceName = influenceData['name']
return model
class Format:
def __init__(self):
self.title = ""
self.exporterClass = None
self.importerClass = None
# recommended file extensions for UI, e.g. file dialog
self.recommendedExtensions = ()
def export(self,mesh):
'''
returns file contents that was produced with
'''
model = LayerData()
model.loadFrom(mesh)
return self.exporterClass().process(model)
def import_(self,fileContents,mesh):
'''
parses fileContents with importerClass and loads data onto given mesh
'''
model = self.importerClass().process(fileContents)
model.saveTo(mesh)
class Formats:
@staticmethod
def getJsonFormat():
f = Format()
f.title = "JSON"
f.exporterClass = JsonExporter
f.importerClass = JsonImporter
f.recommendedExtensions = ("json", "txt")
return f