threeJsFileTranslator.py 16 KB

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