threeJsFileTranslator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. __author__ = 'Sean Griffin'
  2. __version__ = '1.0.0'
  3. __email__ = '[email protected]'
  4. import sys
  5. import os.path
  6. import json
  7. import shutil
  8. from pymel.core import *
  9. from maya.OpenMaya import *
  10. from maya.OpenMayaMPx import *
  11. kPluginTranslatorTypeName = 'Three.js'
  12. kOptionScript = 'ThreeJsExportScript'
  13. kDefaultOptionsString = '0'
  14. FLOAT_PRECISION = 8
  15. class ThreeJsWriter(object):
  16. def __init__(self):
  17. self.componentKeys = ['vertices', 'normals', 'colors', 'uvs', 'faces',
  18. 'materials', 'diffuseMaps', 'specularMaps', 'bumpMaps', 'copyTextures',
  19. 'bones', 'skeletalAnim', 'bakeAnimations', 'prettyOutput']
  20. def write(self, path, optionString, accessMode):
  21. self.path = path
  22. self._parseOptions(optionString)
  23. self.verticeOffset = 0
  24. self.uvOffset = 0
  25. self.vertices = []
  26. self.materials = []
  27. self.faces = []
  28. self.normals = []
  29. self.uvs = []
  30. self.morphTargets = []
  31. self.bones = []
  32. self.animations = []
  33. self.skinIndices = []
  34. self.skinWeights = []
  35. if self.options["bakeAnimations"]:
  36. print("exporting animations")
  37. self._exportAnimations()
  38. self._goToFrame(self.options["startFrame"])
  39. if self.options["materials"]:
  40. print("exporting materials")
  41. self._exportMaterials()
  42. if self.options["bones"]:
  43. print("exporting bones")
  44. select(map(lambda m: m.getParent(), ls(type='mesh')))
  45. runtime.GoToBindPose()
  46. self._exportBones()
  47. print("exporting skins")
  48. self._exportSkins()
  49. print("exporting meshes")
  50. self._exportMeshes()
  51. if self.options["skeletalAnim"]:
  52. print("exporting keyframe animations")
  53. self._exportKeyframeAnimations()
  54. print("writing file")
  55. output = {
  56. 'metadata': {
  57. 'formatVersion': 3.1,
  58. 'generatedBy': 'Maya Exporter'
  59. },
  60. 'vertices': self.vertices,
  61. 'uvs': [self.uvs],
  62. 'faces': self.faces,
  63. 'normals': self.normals,
  64. 'materials': self.materials,
  65. }
  66. if self.options['bakeAnimations']:
  67. output['morphTargets'] = self.morphTargets
  68. if self.options['bones']:
  69. output['bones'] = self.bones
  70. output['skinIndices'] = self.skinIndices
  71. output['skinWeights'] = self.skinWeights
  72. output['influencesPerVertex'] = self.options["influencesPerVertex"]
  73. if self.options['skeletalAnim']:
  74. output['animations'] = self.animations
  75. with file(path, 'w') as f:
  76. if self.options['prettyOutput']:
  77. f.write(json.dumps(output, sort_keys=True, indent=4, separators=(',', ': ')))
  78. else:
  79. f.write(json.dumps(output, separators=(",",":")))
  80. def _allMeshes(self):
  81. if not hasattr(self, '__allMeshes'):
  82. self.__allMeshes = filter(lambda m: len(m.listConnections()) > 0, ls(type='mesh'))
  83. return self.__allMeshes
  84. def _parseOptions(self, optionsString):
  85. self.options = dict([(x, False) for x in self.componentKeys])
  86. for key in self.componentKeys:
  87. self.options[key] = key in optionsString
  88. if self.options["bones"]:
  89. boneOptionsString = optionsString[optionsString.find("bones"):]
  90. boneOptions = boneOptionsString.split(' ')
  91. self.options["influencesPerVertex"] = int(boneOptions[1])
  92. if self.options["bakeAnimations"]:
  93. bakeAnimOptionsString = optionsString[optionsString.find("bakeAnimations"):]
  94. bakeAnimOptions = bakeAnimOptionsString.split(' ')
  95. self.options["startFrame"] = int(bakeAnimOptions[1])
  96. self.options["endFrame"] = int(bakeAnimOptions[2])
  97. self.options["stepFrame"] = int(bakeAnimOptions[3])
  98. def _exportMeshes(self):
  99. if self.options['vertices']:
  100. self._exportVertices()
  101. for mesh in self._allMeshes():
  102. self._exportMesh(mesh)
  103. def _exportMesh(self, mesh):
  104. print("Exporting " + mesh.name())
  105. if self.options['faces']:
  106. print("Exporting faces")
  107. self._exportFaces(mesh)
  108. self.verticeOffset += len(mesh.getPoints())
  109. self.uvOffset += mesh.numUVs()
  110. if self.options['normals']:
  111. print("Exporting normals")
  112. self._exportNormals(mesh)
  113. if self.options['uvs']:
  114. print("Exporting UVs")
  115. self._exportUVs(mesh)
  116. def _getMaterialIndex(self, face, mesh):
  117. if not hasattr(self, '_materialIndices'):
  118. self._materialIndices = dict([(mat['DbgName'], i) for i, mat in enumerate(self.materials)])
  119. if self.options['materials']:
  120. for engine in mesh.listConnections(type='shadingEngine'):
  121. if sets(engine, isMember=face):
  122. for material in engine.listConnections(type='lambert'):
  123. if self._materialIndices.has_key(material.name()):
  124. return self._materialIndices[material.name()]
  125. return -1
  126. def _exportVertices(self):
  127. self.vertices += self._getVertices()
  128. def _exportAnimations(self):
  129. for frame in self._framesToExport():
  130. self._exportAnimationForFrame(frame)
  131. def _framesToExport(self):
  132. return range(self.options["startFrame"], self.options["endFrame"], self.options["stepFrame"])
  133. def _exportAnimationForFrame(self, frame):
  134. print("exporting frame " + str(frame))
  135. self._goToFrame(frame)
  136. self.morphTargets.append({
  137. 'name': "frame_" + str(frame),
  138. 'vertices': self._getVertices()
  139. })
  140. def _getVertices(self):
  141. 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)]]
  142. def _goToFrame(self, frame):
  143. currentTime(frame)
  144. def _exportFaces(self, mesh):
  145. typeBitmask = self._getTypeBitmask()
  146. for face in mesh.faces:
  147. materialIndex = self._getMaterialIndex(face, mesh)
  148. hasMaterial = materialIndex != -1
  149. self._exportFaceBitmask(face, typeBitmask, hasMaterial=hasMaterial)
  150. self.faces += map(lambda x: x + self.verticeOffset, face.getVertices())
  151. if self.options['materials']:
  152. if hasMaterial:
  153. self.faces.append(materialIndex)
  154. if self.options['uvs'] and face.hasUVs():
  155. self.faces += map(lambda v: face.getUVIndex(v) + self.uvOffset, range(face.polygonVertexCount()))
  156. if self.options['normals']:
  157. self._exportFaceVertexNormals(face)
  158. def _exportFaceBitmask(self, face, typeBitmask, hasMaterial=True):
  159. if face.polygonVertexCount() == 4:
  160. faceBitmask = 1
  161. else:
  162. faceBitmask = 0
  163. if hasMaterial:
  164. faceBitmask |= (1 << 1)
  165. if self.options['uvs'] and face.hasUVs():
  166. faceBitmask |= (1 << 3)
  167. self.faces.append(typeBitmask | faceBitmask)
  168. def _exportFaceVertexNormals(self, face):
  169. for i in range(face.polygonVertexCount()):
  170. self.faces.append(face.normalIndex(i))
  171. def _exportNormals(self, mesh):
  172. for normal in mesh.getNormals():
  173. self.normals += [round(normal.x, FLOAT_PRECISION), round(normal.y, FLOAT_PRECISION), round(normal.z, FLOAT_PRECISION)]
  174. def _exportUVs(self, mesh):
  175. us, vs = mesh.getUVs()
  176. for i, u in enumerate(us):
  177. self.uvs.append(u)
  178. self.uvs.append(vs[i])
  179. def _getTypeBitmask(self):
  180. bitmask = 0
  181. if self.options['normals']:
  182. bitmask |= 32
  183. return bitmask
  184. def _exportMaterials(self):
  185. for mat in ls(type='lambert'):
  186. self.materials.append(self._exportMaterial(mat))
  187. def _exportMaterial(self, mat):
  188. result = {
  189. "DbgName": mat.name(),
  190. "blending": "NormalBlending",
  191. "colorDiffuse": map(lambda i: i * mat.getDiffuseCoeff(), mat.getColor().rgb),
  192. "colorAmbient": mat.getAmbientColor().rgb,
  193. "depthTest": True,
  194. "depthWrite": True,
  195. "shading": mat.__class__.__name__,
  196. "transparency": mat.getTransparency().a,
  197. "transparent": mat.getTransparency().a != 1.0,
  198. "vertexColors": False
  199. }
  200. if isinstance(mat, nodetypes.Phong):
  201. result["colorSpecular"] = mat.getSpecularColor().rgb
  202. result["specularCoef"] = mat.getCosPower()
  203. if self.options["specularMaps"]:
  204. self._exportSpecularMap(result, mat)
  205. if self.options["bumpMaps"]:
  206. self._exportBumpMap(result, mat)
  207. if self.options["diffuseMaps"]:
  208. self._exportDiffuseMap(result, mat)
  209. return result
  210. def _exportBumpMap(self, result, mat):
  211. for bump in mat.listConnections(type='bump2d'):
  212. for f in bump.listConnections(type='file'):
  213. result["mapNormalFactor"] = 1
  214. self._exportFile(result, f, "Normal")
  215. def _exportDiffuseMap(self, result, mat):
  216. for f in mat.attr('color').inputs():
  217. result["colorDiffuse"] = f.attr('defaultColor').get()
  218. self._exportFile(result, f, "Diffuse")
  219. def _exportSpecularMap(self, result, mat):
  220. for f in mat.attr('specularColor').inputs():
  221. result["colorSpecular"] = f.attr('defaultColor').get()
  222. self._exportFile(result, f, "Specular")
  223. def _exportFile(self, result, mapFile, mapType):
  224. fName = os.path.basename(mapFile.ftn.get())
  225. if self.options['copyTextures']:
  226. shutil.copy2(mapFile.ftn.get(), os.path.dirname(self.path) + "/" + fName)
  227. result["map" + mapType] = fName
  228. result["map" + mapType + "Repeat"] = [1, 1]
  229. result["map" + mapType + "Wrap"] = ["repeat", "repeat"]
  230. result["map" + mapType + "Anistropy"] = 4
  231. def _exportBones(self):
  232. for joint in ls(type='joint'):
  233. if joint.getParent():
  234. parentIndex = self._indexOfJoint(joint.getParent().name())
  235. else:
  236. parentIndex = -1
  237. rotq = joint.getRotation(quaternion=True) * joint.getOrientation()
  238. pos = joint.getTranslation()
  239. self.bones.append({
  240. "parent": parentIndex,
  241. "name": joint.name(),
  242. "pos": self._roundPos(pos),
  243. "rotq": self._roundQuat(rotq)
  244. })
  245. def _indexOfJoint(self, name):
  246. if not hasattr(self, '_jointNames'):
  247. self._jointNames = dict([(joint.name(), i) for i, joint in enumerate(ls(type='joint'))])
  248. if name in self._jointNames:
  249. return self._jointNames[name]
  250. else:
  251. return -1
  252. def _exportKeyframeAnimations(self):
  253. hierarchy = []
  254. i = -1
  255. frameRate = FramesPerSecond(currentUnit(query=True, time=True)).value()
  256. for joint in ls(type='joint'):
  257. hierarchy.append({
  258. "parent": i,
  259. "keys": self._getKeyframes(joint, frameRate)
  260. })
  261. i += 1
  262. self.animations.append({
  263. "name": "skeletalAction.001",
  264. "length": (playbackOptions(maxTime=True, query=True) - playbackOptions(minTime=True, query=True)) / frameRate,
  265. "fps": 1,
  266. "hierarchy": hierarchy
  267. })
  268. def _getKeyframes(self, joint, frameRate):
  269. firstFrame = playbackOptions(minTime=True, query=True)
  270. lastFrame = playbackOptions(maxTime=True, query=True)
  271. frames = sorted(list(set(keyframe(joint, query=True) + [firstFrame, lastFrame])))
  272. keys = []
  273. print("joint " + joint.name() + " has " + str(len(frames)) + " keyframes")
  274. for frame in frames:
  275. self._goToFrame(frame)
  276. keys.append(self._getCurrentKeyframe(joint, frame, frameRate))
  277. return keys
  278. def _getCurrentKeyframe(self, joint, frame, frameRate):
  279. pos = joint.getTranslation()
  280. rot = joint.getRotation(quaternion=True) * joint.getOrientation()
  281. return {
  282. 'time': (frame - playbackOptions(minTime=True, query=True)) / frameRate,
  283. 'pos': self._roundPos(pos),
  284. 'rot': self._roundQuat(rot),
  285. 'scl': [1,1,1]
  286. }
  287. def _roundPos(self, pos):
  288. return map(lambda x: round(x, FLOAT_PRECISION), [pos.x, pos.y, pos.z])
  289. def _roundQuat(self, rot):
  290. return map(lambda x: round(x, FLOAT_PRECISION), [rot.x, rot.y, rot.z, rot.w])
  291. def _exportSkins(self):
  292. for mesh in self._allMeshes():
  293. print("exporting skins for mesh: " + mesh.name())
  294. skins = filter(lambda skin: mesh in skin.getOutputGeometry(), ls(type='skinCluster'))
  295. if len(skins) > 0:
  296. print("mesh has " + str(len(skins)) + " skins")
  297. skin = skins[0]
  298. joints = skin.influenceObjects()
  299. for weights in skin.getWeights(mesh.vtx):
  300. numWeights = 0
  301. for i in range(0, len(weights)):
  302. if weights[i] > 0:
  303. self.skinWeights.append(weights[i])
  304. self.skinIndices.append(self._indexOfJoint(joints[i].name()))
  305. numWeights += 1
  306. if numWeights > self.options["influencesPerVertex"]:
  307. raise Exception("More than " + str(self.options["influencesPerVertex"]) + " influences on a vertex in " + mesh.name() + ".")
  308. for i in range(0, self.options["influencesPerVertex"] - numWeights):
  309. self.skinWeights.append(0)
  310. self.skinIndices.append(0)
  311. else:
  312. print("mesh has no skins, appending 0")
  313. for i in range(0, len(mesh.getPoints()) * self.options["influencesPerVertex"]):
  314. self.skinWeights.append(0)
  315. self.skinIndices.append(0)
  316. class NullAnimCurve(object):
  317. def getValue(self, index):
  318. return 0.0
  319. class ThreeJsTranslator(MPxFileTranslator):
  320. def __init__(self):
  321. MPxFileTranslator.__init__(self)
  322. def haveWriteMethod(self):
  323. return True
  324. def filter(self):
  325. return '*.js'
  326. def defaultExtension(self):
  327. return 'js'
  328. def writer(self, fileObject, optionString, accessMode):
  329. path = fileObject.fullName()
  330. writer = ThreeJsWriter()
  331. writer.write(path, optionString, accessMode)
  332. def translatorCreator():
  333. return asMPxPtr(ThreeJsTranslator())
  334. def initializePlugin(mobject):
  335. mplugin = MFnPlugin(mobject)
  336. try:
  337. mplugin.registerFileTranslator(kPluginTranslatorTypeName, None, translatorCreator, kOptionScript, kDefaultOptionsString)
  338. except:
  339. sys.stderr.write('Failed to register translator: %s' % kPluginTranslatorTypeName)
  340. raise
  341. def uninitializePlugin(mobject):
  342. mplugin = MFnPlugin(mobject)
  343. try:
  344. mplugin.deregisterFileTranslator(kPluginTranslatorTypeName)
  345. except:
  346. sys.stderr.write('Failed to deregister translator: %s' % kPluginTranslatorTypeName)
  347. raise
  348. class FramesPerSecond(object):
  349. MAYA_VALUES = {
  350. 'game': 15,
  351. 'film': 24,
  352. 'pal': 25,
  353. 'ntsc': 30,
  354. 'show': 48,
  355. 'palf': 50,
  356. 'ntscf': 60
  357. }
  358. def __init__(self, fpsString):
  359. self.fpsString = fpsString
  360. def value(self):
  361. if self.fpsString in FramesPerSecond.MAYA_VALUES:
  362. return FramesPerSecond.MAYA_VALUES[self.fpsString]
  363. else:
  364. return int(filter(lambda c: c.isdigit(), self.fpsString))