threeJsFileTranslator.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. __author__ = 'Chris Lewis'
  2. __version__ = '0.1.0'
  3. __email__ = '[email protected]'
  4. import sys
  5. import json
  6. import maya.cmds as mc
  7. from maya.OpenMaya import *
  8. from maya.OpenMayaMPx import *
  9. kPluginTranslatorTypeName = 'Three.js'
  10. kOptionScript = 'ThreeJsExportScript'
  11. kDefaultOptionsString = '0'
  12. FLOAT_PRECISION = 8
  13. # adds decimal precision to JSON encoding
  14. class DecimalEncoder(json.JSONEncoder):
  15. def _iterencode(self, o, markers=None):
  16. if isinstance(o, float):
  17. s = str(o)
  18. if '.' in s and len(s[s.index('.'):]) > FLOAT_PRECISION - 1:
  19. s = '%.{0}f'.format(FLOAT_PRECISION) % o
  20. while '.' in s and s[-1] == '0':
  21. s = s[:-1] # this actually removes the last "0" from the string
  22. if s[-1] == '.': # added this test to avoid leaving "0." instead of "0.0",
  23. s += '0' # which would throw an error while loading the file
  24. return (s for s in [s])
  25. return super(DecimalEncoder, self)._iterencode(o, markers)
  26. class ThreeJsError(Exception):
  27. pass
  28. class ThreeJsWriter(object):
  29. def __init__(self):
  30. self.componentKeys = ['vertices', 'normals', 'colors', 'uvs', 'materials', 'faces']
  31. def _parseOptions(self, optionsString):
  32. self.options = dict([(x, False) for x in self.componentKeys])
  33. optionsString = optionsString[2:] # trim off the "0;" that Maya adds to the options string
  34. for option in optionsString.split(' '):
  35. self.options[option] = True
  36. def _updateOffsets(self):
  37. for key in self.componentKeys:
  38. if key == 'uvs':
  39. continue
  40. self.offsets[key] = len(getattr(self, key))
  41. for i in range(len(self.uvs)):
  42. self.offsets['uvs'][i] = len(self.uvs[i])
  43. def _getTypeBitmask(self, options):
  44. bitmask = 0
  45. if options['materials']:
  46. bitmask |= 2
  47. if options['uvs']:
  48. bitmask |= 8
  49. if options['normals']:
  50. bitmask |= 32
  51. if options['colors']:
  52. bitmask |= 128
  53. return bitmask
  54. def _exportMesh(self, dagPath, component):
  55. mesh = MFnMesh(dagPath)
  56. options = self.options.copy()
  57. self._updateOffsets()
  58. # export vertex data
  59. if options['vertices']:
  60. try:
  61. iterVerts = MItMeshVertex(dagPath, component)
  62. while not iterVerts.isDone():
  63. point = iterVerts.position(MSpace.kWorld)
  64. self.vertices += [point.x, point.y, point.z]
  65. iterVerts.next()
  66. except:
  67. options['vertices'] = False
  68. # export material data
  69. # TODO: actually parse material data
  70. materialIndices = MIntArray()
  71. if options['materials']:
  72. try:
  73. shaders = MObjectArray()
  74. mesh.getConnectedShaders(0, shaders, materialIndices)
  75. while len(self.materials) < shaders.length():
  76. self.materials.append({}) # placeholder material definition
  77. except:
  78. self.materials = [{}]
  79. # export uv data
  80. if options['uvs']:
  81. try:
  82. uvLayers = []
  83. mesh.getUVSetNames(uvLayers)
  84. while len(uvLayers) > len(self.uvs):
  85. self.uvs.append([])
  86. self.offsets['uvs'].append(0)
  87. for i, layer in enumerate(uvLayers):
  88. uList = MFloatArray()
  89. vList = MFloatArray()
  90. mesh.getUVs(uList, vList, layer)
  91. for j in xrange(uList.length()):
  92. self.uvs[i] += [uList[j], vList[j]]
  93. except:
  94. options['uvs'] = False
  95. # export normal data
  96. if options['normals']:
  97. try:
  98. normals = MFloatVectorArray()
  99. mesh.getNormals(normals, MSpace.kWorld)
  100. for i in xrange(normals.length()):
  101. point = normals[i]
  102. self.normals += [point.x, point.y, point.z]
  103. except:
  104. options['normals'] = False
  105. # export color data
  106. if options['colors']:
  107. try:
  108. colors = MColorArray()
  109. mesh.getColors(colors)
  110. for i in xrange(colors.length()):
  111. color = colors[i]
  112. # uncolored vertices are set to (-1, -1, -1). Clamps colors to (0, 0, 0).
  113. self.colors += [max(color.r, 0), max(color.g, 0), max(color.b, 0)]
  114. except:
  115. options['colors'] = False
  116. # export face data
  117. if not options['vertices']:
  118. return
  119. bitmask = self._getTypeBitmask(options)
  120. iterPolys = MItMeshPolygon(dagPath, component)
  121. while not iterPolys.isDone():
  122. self.faces.append(bitmask)
  123. # export face vertices
  124. verts = MIntArray()
  125. iterPolys.getVertices(verts)
  126. for i in xrange(verts.length()):
  127. self.faces.append(verts[i] + self.offsets['vertices'])
  128. # export face vertex materials
  129. if options['materials']:
  130. if materialIndices.length():
  131. self.faces.append(materialIndices[iterPolys.index()])
  132. # export face vertex uvs
  133. if options['uvs']:
  134. util = MScriptUtil()
  135. uvPtr = util.asIntPtr()
  136. for i, layer in enumerate(uvLayers):
  137. for j in xrange(verts.length()):
  138. iterPolys.getUVIndex(j, uvPtr, layer)
  139. uvIndex = util.getInt(uvPtr)
  140. self.faces.append(uvIndex + self.offsets['uvs'][i])
  141. # export face vertex normals
  142. if options['normals']:
  143. for i in xrange(3):
  144. normalIndex = iterPolys.normalIndex(i)
  145. self.faces.append(normalIndex + self.offsets['normals'])
  146. # export face vertex colors
  147. if options['colors']:
  148. colors = MIntArray()
  149. iterPolys.getColorIndices(colors)
  150. for i in xrange(colors.length()):
  151. self.faces.append(colors[i] + self.offsets['colors'])
  152. iterPolys.next()
  153. def _getMeshes(self, nodes):
  154. meshes = []
  155. for node in nodes:
  156. if mc.nodeType(node) == 'mesh':
  157. meshes.append(node)
  158. else:
  159. for child in mc.listRelatives(node, s=1):
  160. if mc.nodeType(child) == 'mesh':
  161. meshes.append(child)
  162. return meshes
  163. def _exportMeshes(self):
  164. # export all
  165. if self.accessMode == MPxFileTranslator.kExportAccessMode:
  166. mc.select(self._getMeshes(mc.ls(typ='mesh')))
  167. # export selection
  168. elif self.accessMode == MPxFileTranslator.kExportActiveAccessMode:
  169. mc.select(self._getMeshes(mc.ls(sl=1)))
  170. else:
  171. raise ThreeJsError('Unsupported access mode: {0}'.format(self.accessMode))
  172. dups = [mc.duplicate(mesh)[0] for mesh in mc.ls(sl=1)]
  173. combined = mc.polyUnite(dups, mergeUVSets=1, ch=0) if len(dups) > 1 else dups[0]
  174. mc.polyTriangulate(combined)
  175. mc.select(combined)
  176. sel = MSelectionList()
  177. MGlobal.getActiveSelectionList(sel)
  178. mDag = MDagPath()
  179. mComp = MObject()
  180. sel.getDagPath(0, mDag, mComp)
  181. self._exportMesh(mDag, mComp)
  182. mc.delete(combined)
  183. def write(self, path, optionString, accessMode):
  184. self.path = path
  185. self._parseOptions(optionString)
  186. self.accessMode = accessMode
  187. self.root = dict(metadata=dict(formatVersion=3))
  188. self.offsets = dict()
  189. for key in self.componentKeys:
  190. setattr(self, key, [])
  191. self.offsets[key] = 0
  192. self.offsets['uvs'] = []
  193. self.uvs = []
  194. self._exportMeshes()
  195. # add the component buffers to the root JSON object
  196. for key in self.componentKeys:
  197. buffer_ = getattr(self, key)
  198. if buffer_:
  199. self.root[key] = buffer_
  200. # materials are required for parsing
  201. if not self.root.has_key('materials'):
  202. self.root['materials'] = [{}]
  203. # write the file
  204. with file(self.path, 'w') as f:
  205. f.write(json.dumps(self.root, separators=(',',':'), cls=DecimalEncoder))
  206. class ThreeJsTranslator(MPxFileTranslator):
  207. def __init__(self):
  208. MPxFileTranslator.__init__(self)
  209. def haveWriteMethod(self):
  210. return True
  211. def filter(self):
  212. return '*.js'
  213. def defaultExtension(self):
  214. return 'js'
  215. def writer(self, fileObject, optionString, accessMode):
  216. path = fileObject.fullName()
  217. writer = ThreeJsWriter()
  218. writer.write(path, optionString, accessMode)
  219. def translatorCreator():
  220. return asMPxPtr(ThreeJsTranslator())
  221. def initializePlugin(mobject):
  222. mplugin = MFnPlugin(mobject)
  223. try:
  224. mplugin.registerFileTranslator(kPluginTranslatorTypeName, None, translatorCreator, kOptionScript, kDefaultOptionsString)
  225. except:
  226. sys.stderr.write('Failed to register translator: %s' % kPluginTranslatorTypeName)
  227. raise
  228. def uninitializePlugin(mobject):
  229. mplugin = MFnPlugin(mobject)
  230. try:
  231. mplugin.deregisterFileTranslator(kPluginTranslatorTypeName)
  232. except:
  233. sys.stderr.write('Failed to deregister translator: %s' % kPluginTranslatorTypeName)
  234. raise