123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- __author__ = 'Sean Griffin'
- __version__ = '1.0.0'
- __email__ = '[email protected]'
- import sys
- import os.path
- import json
- import shutil
- from pymel.core import *
- from maya.OpenMaya import *
- from maya.OpenMayaMPx import *
- kPluginTranslatorTypeName = 'Three.js'
- kOptionScript = 'ThreeJsExportScript'
- kDefaultOptionsString = '0'
- FLOAT_PRECISION = 8
- class ThreeJsWriter(object):
- def __init__(self):
- self.componentKeys = ['vertices', 'normals', 'colors', 'uvs', 'faces',
- 'materials', 'diffuseMaps', 'specularMaps', 'bumpMaps', 'copyTextures',
- 'bones', 'skeletalAnim', 'bakeAnimations', 'prettyOutput']
- def write(self, path, optionString, accessMode):
- self.path = path
- self._parseOptions(optionString)
- self.verticeOffset = 0
- self.uvOffset = 0
- self.normalOffset = 0
- self.vertices = []
- self.materials = []
- self.faces = []
- self.normals = []
- self.uvs = []
- self.morphTargets = []
- self.bones = []
- self.animations = []
- self.skinIndices = []
- self.skinWeights = []
- if self.options["bakeAnimations"]:
- print("exporting animations")
- self._exportAnimations()
- self._goToFrame(self.options["startFrame"])
- if self.options["materials"]:
- print("exporting materials")
- self._exportMaterials()
- if self.options["bones"]:
- print("exporting bones")
- select(map(lambda m: m.getParent(), ls(type='mesh')))
- runtime.GoToBindPose()
- self._exportBones()
- print("exporting skins")
- self._exportSkins()
- print("exporting meshes")
- self._exportMeshes()
- if self.options["skeletalAnim"]:
- print("exporting keyframe animations")
- self._exportKeyframeAnimations()
- print("writing file")
- output = {
- 'metadata': {
- 'formatVersion': 3.1,
- 'generatedBy': 'Maya Exporter'
- },
- 'vertices': self.vertices,
- 'uvs': [self.uvs],
- 'faces': self.faces,
- 'normals': self.normals,
- 'materials': self.materials,
- }
- if self.options['bakeAnimations']:
- output['morphTargets'] = self.morphTargets
- if self.options['bones']:
- output['bones'] = self.bones
- output['skinIndices'] = self.skinIndices
- output['skinWeights'] = self.skinWeights
- output['influencesPerVertex'] = self.options["influencesPerVertex"]
- if self.options['skeletalAnim']:
- output['animations'] = self.animations
- with file(path, 'w') as f:
- if self.options['prettyOutput']:
- f.write(json.dumps(output, sort_keys=True, indent=4, separators=(',', ': ')))
- else:
- f.write(json.dumps(output, separators=(",",":")))
- def _allMeshes(self):
- if not hasattr(self, '__allMeshes'):
- self.__allMeshes = filter(lambda m: len(m.listConnections()) > 0, ls(type='mesh'))
- return self.__allMeshes
- def _parseOptions(self, optionsString):
- self.options = dict([(x, False) for x in self.componentKeys])
- for key in self.componentKeys:
- self.options[key] = key in optionsString
- if self.options["bones"]:
- boneOptionsString = optionsString[optionsString.find("bones"):]
- boneOptions = boneOptionsString.split(' ')
- self.options["influencesPerVertex"] = int(boneOptions[1])
- if self.options["bakeAnimations"]:
- bakeAnimOptionsString = optionsString[optionsString.find("bakeAnimations"):]
- bakeAnimOptions = bakeAnimOptionsString.split(' ')
- self.options["startFrame"] = int(bakeAnimOptions[1])
- self.options["endFrame"] = int(bakeAnimOptions[2])
- self.options["stepFrame"] = int(bakeAnimOptions[3])
- def _exportMeshes(self):
- if self.options['vertices']:
- self._exportVertices()
- for mesh in self._allMeshes():
- self._exportMesh(mesh)
- def _exportMesh(self, mesh):
- print("Exporting " + mesh.name())
- if self.options['faces']:
- print("Exporting faces")
- self._exportFaces(mesh)
- self.verticeOffset += len(mesh.getPoints())
- self.uvOffset += mesh.numUVs()
- self.normalOffset += mesh.numNormals()
- if self.options['normals']:
- print("Exporting normals")
- self._exportNormals(mesh)
- if self.options['uvs']:
- print("Exporting UVs")
- self._exportUVs(mesh)
- def _getMaterialIndex(self, face, mesh):
- if not hasattr(self, '_materialIndices'):
- self._materialIndices = dict([(mat['DbgName'], i) for i, mat in enumerate(self.materials)])
- if self.options['materials']:
- for engine in mesh.listConnections(type='shadingEngine'):
- if sets(engine, isMember=face) or sets(engine, isMember=mesh):
- for material in engine.listConnections(type='lambert'):
- if self._materialIndices.has_key(material.name()):
- return self._materialIndices[material.name()]
- return -1
- def _exportVertices(self):
- self.vertices += self._getVertices()
- def _exportAnimations(self):
- for frame in self._framesToExport():
- self._exportAnimationForFrame(frame)
- def _framesToExport(self):
- return range(self.options["startFrame"], self.options["endFrame"], self.options["stepFrame"])
- def _exportAnimationForFrame(self, frame):
- print("exporting frame " + str(frame))
- self._goToFrame(frame)
- self.morphTargets.append({
- 'name': "frame_" + str(frame),
- 'vertices': self._getVertices()
- })
- def _getVertices(self):
- return [coord for mesh in self._allMeshes() for point in mesh.getPoints(space='world') for coord in [round(point.x, FLOAT_PRECISION), round(point.y, FLOAT_PRECISION), round(point.z, FLOAT_PRECISION)]]
- def _goToFrame(self, frame):
- currentTime(frame)
- def _exportFaces(self, mesh):
- typeBitmask = self._getTypeBitmask()
- for face in mesh.faces:
- materialIndex = self._getMaterialIndex(face, mesh)
- hasMaterial = materialIndex != -1
- self._exportFaceBitmask(face, typeBitmask, hasMaterial=hasMaterial)
- self.faces += map(lambda x: x + self.verticeOffset, face.getVertices())
- if self.options['materials']:
- if hasMaterial:
- self.faces.append(materialIndex)
- if self.options['uvs'] and face.hasUVs():
- self.faces += map(lambda v: face.getUVIndex(v) + self.uvOffset, range(face.polygonVertexCount()))
- if self.options['normals']:
- self._exportFaceVertexNormals(face)
- def _exportFaceBitmask(self, face, typeBitmask, hasMaterial=True):
- if face.polygonVertexCount() == 4:
- faceBitmask = 1
- else:
- faceBitmask = 0
- if hasMaterial:
- faceBitmask |= (1 << 1)
- if self.options['uvs'] and face.hasUVs():
- faceBitmask |= (1 << 3)
- self.faces.append(typeBitmask | faceBitmask)
- def _exportFaceVertexNormals(self, face):
- for i in range(face.polygonVertexCount()):
- self.faces.append(face.normalIndex(i) + self.normalOffset)
- def _exportNormals(self, mesh):
- for normal in mesh.getNormals():
- self.normals += [round(normal.x, FLOAT_PRECISION), round(normal.y, FLOAT_PRECISION), round(normal.z, FLOAT_PRECISION)]
- def _exportUVs(self, mesh):
- us, vs = mesh.getUVs()
- for i, u in enumerate(us):
- self.uvs.append(u)
- self.uvs.append(vs[i])
- def _getTypeBitmask(self):
- bitmask = 0
- if self.options['normals']:
- bitmask |= 32
- return bitmask
- def _exportMaterials(self):
- for mat in ls(type='lambert'):
- self.materials.append(self._exportMaterial(mat))
- def _exportMaterial(self, mat):
- result = {
- "DbgName": mat.name(),
- "blending": "NormalBlending",
- "colorDiffuse": map(lambda i: i * mat.getDiffuseCoeff(), mat.getColor().rgb),
- "colorAmbient": mat.getAmbientColor().rgb,
- "depthTest": True,
- "depthWrite": True,
- "shading": mat.__class__.__name__,
- "transparency": mat.getTransparency().a,
- "transparent": mat.getTransparency().a != 1.0,
- "vertexColors": False
- }
- if isinstance(mat, nodetypes.Phong):
- result["colorSpecular"] = mat.getSpecularColor().rgb
- result["specularCoef"] = mat.getCosPower()
- if self.options["specularMaps"]:
- self._exportSpecularMap(result, mat)
- if self.options["bumpMaps"]:
- self._exportBumpMap(result, mat)
- if self.options["diffuseMaps"]:
- self._exportDiffuseMap(result, mat)
- return result
- def _exportBumpMap(self, result, mat):
- for bump in mat.listConnections(type='bump2d'):
- for f in bump.listConnections(type='file'):
- result["mapNormalFactor"] = 1
- self._exportFile(result, f, "Normal")
- def _exportDiffuseMap(self, result, mat):
- for f in mat.attr('color').inputs():
- result["colorDiffuse"] = f.attr('defaultColor').get()
- self._exportFile(result, f, "Diffuse")
- def _exportSpecularMap(self, result, mat):
- for f in mat.attr('specularColor').inputs():
- result["colorSpecular"] = f.attr('defaultColor').get()
- self._exportFile(result, f, "Specular")
- def _exportFile(self, result, mapFile, mapType):
- src = mapFile.ftn.get()
- targetDir = os.path.dirname(self.path)
- fName = os.path.basename(src)
- if self.options['copyTextures']:
- shutil.copy2(src, os.path.join(targetDir, fName))
- result["map" + mapType] = fName
- result["map" + mapType + "Repeat"] = [1, 1]
- result["map" + mapType + "Wrap"] = ["repeat", "repeat"]
- result["map" + mapType + "Anistropy"] = 4
- def _exportBones(self):
- for joint in ls(type='joint'):
- if joint.getParent():
- parentIndex = self._indexOfJoint(joint.getParent().name())
- else:
- parentIndex = -1
- rotq = joint.getRotation(quaternion=True) * joint.getOrientation()
- pos = joint.getTranslation()
- self.bones.append({
- "parent": parentIndex,
- "name": joint.name(),
- "pos": self._roundPos(pos),
- "rotq": self._roundQuat(rotq)
- })
- def _indexOfJoint(self, name):
- if not hasattr(self, '_jointNames'):
- self._jointNames = dict([(joint.name(), i) for i, joint in enumerate(ls(type='joint'))])
- if name in self._jointNames:
- return self._jointNames[name]
- else:
- return -1
- def _exportKeyframeAnimations(self):
- hierarchy = []
- i = -1
- frameRate = FramesPerSecond(currentUnit(query=True, time=True)).value()
- for joint in ls(type='joint'):
- hierarchy.append({
- "parent": i,
- "keys": self._getKeyframes(joint, frameRate)
- })
- i += 1
- self.animations.append({
- "name": "skeletalAction.001",
- "length": (playbackOptions(maxTime=True, query=True) - playbackOptions(minTime=True, query=True)) / frameRate,
- "fps": 1,
- "hierarchy": hierarchy
- })
- def _getKeyframes(self, joint, frameRate):
- firstFrame = playbackOptions(minTime=True, query=True)
- lastFrame = playbackOptions(maxTime=True, query=True)
- frames = sorted(list(set(keyframe(joint, query=True) + [firstFrame, lastFrame])))
- keys = []
- print("joint " + joint.name() + " has " + str(len(frames)) + " keyframes")
- for frame in frames:
- self._goToFrame(frame)
- keys.append(self._getCurrentKeyframe(joint, frame, frameRate))
- return keys
- def _getCurrentKeyframe(self, joint, frame, frameRate):
- pos = joint.getTranslation()
- rot = joint.getRotation(quaternion=True) * joint.getOrientation()
- return {
- 'time': (frame - playbackOptions(minTime=True, query=True)) / frameRate,
- 'pos': self._roundPos(pos),
- 'rot': self._roundQuat(rot),
- 'scl': [1,1,1]
- }
- def _roundPos(self, pos):
- return map(lambda x: round(x, FLOAT_PRECISION), [pos.x, pos.y, pos.z])
- def _roundQuat(self, rot):
- return map(lambda x: round(x, FLOAT_PRECISION), [rot.x, rot.y, rot.z, rot.w])
- def _exportSkins(self):
- for mesh in self._allMeshes():
- print("exporting skins for mesh: " + mesh.name())
- skins = filter(lambda skin: mesh in skin.getOutputGeometry(), ls(type='skinCluster'))
- if len(skins) > 0:
- print("mesh has " + str(len(skins)) + " skins")
- skin = skins[0]
- joints = skin.influenceObjects()
- for weights in skin.getWeights(mesh.vtx):
- numWeights = 0
- for i in range(0, len(weights)):
- if weights[i] > 0:
- self.skinWeights.append(weights[i])
- self.skinIndices.append(self._indexOfJoint(joints[i].name()))
- numWeights += 1
- if numWeights > self.options["influencesPerVertex"]:
- raise Exception("More than " + str(self.options["influencesPerVertex"]) + " influences on a vertex in " + mesh.name() + ".")
- for i in range(0, self.options["influencesPerVertex"] - numWeights):
- self.skinWeights.append(0)
- self.skinIndices.append(0)
- else:
- print("mesh has no skins, appending 0")
- for i in range(0, len(mesh.getPoints()) * self.options["influencesPerVertex"]):
- self.skinWeights.append(0)
- self.skinIndices.append(0)
- class NullAnimCurve(object):
- def getValue(self, index):
- return 0.0
- class ThreeJsTranslator(MPxFileTranslator):
- def __init__(self):
- MPxFileTranslator.__init__(self)
- def haveWriteMethod(self):
- return True
- def filter(self):
- return '*.js'
- def defaultExtension(self):
- return 'js'
- def writer(self, fileObject, optionString, accessMode):
- path = fileObject.fullName()
- writer = ThreeJsWriter()
- writer.write(path, optionString, accessMode)
- def translatorCreator():
- return asMPxPtr(ThreeJsTranslator())
- def initializePlugin(mobject):
- mplugin = MFnPlugin(mobject)
- try:
- mplugin.registerFileTranslator(kPluginTranslatorTypeName, None, translatorCreator, kOptionScript, kDefaultOptionsString)
- except:
- sys.stderr.write('Failed to register translator: %s' % kPluginTranslatorTypeName)
- raise
- def uninitializePlugin(mobject):
- mplugin = MFnPlugin(mobject)
- try:
- mplugin.deregisterFileTranslator(kPluginTranslatorTypeName)
- except:
- sys.stderr.write('Failed to deregister translator: %s' % kPluginTranslatorTypeName)
- raise
- class FramesPerSecond(object):
- MAYA_VALUES = {
- 'game': 15,
- 'film': 24,
- 'pal': 25,
- 'ntsc': 30,
- 'show': 48,
- 'palf': 50,
- 'ntscf': 60
- }
- def __init__(self, fpsString):
- self.fpsString = fpsString
- def value(self):
- if self.fpsString in FramesPerSecond.MAYA_VALUES:
- return FramesPerSecond.MAYA_VALUES[self.fpsString]
- else:
- return int(filter(lambda c: c.isdigit(), self.fpsString))
|