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