6135375d18530edd9ccfbb04d209fe19dddf4769
[blender.git] / release / scripts / io / export_obj.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 """
22 Name: 'Wavefront (.obj)...'
23 Blender: 248
24 Group: 'Export'
25 Tooltip: 'Save a Wavefront OBJ File'
26 """
27
28 __author__ = "Campbell Barton, Jiri Hnidek, Paolo Ciccone"
29 __url__ = ['http://wiki.blender.org/index.php/Scripts/Manual/Export/wavefront_obj', 'www.blender.org', 'blenderartists.org']
30 __version__ = "1.21"
31
32 __bpydoc__ = """\
33 This script is an exporter to OBJ file format.
34
35 Usage:
36
37 Select the objects you wish to export and run this script from "File->Export" menu.
38 Selecting the default options from the popup box will be good in most cases.
39 All objects that can be represented as a mesh (mesh, curve, metaball, surface, text3d)
40 will be exported as mesh data.
41 """
42
43 # import math and other in functions that use them for the sake of fast Blender startup
44 # import math
45 import os
46 import time
47
48 import bpy
49 import Mathutils
50
51
52 # Returns a tuple - path,extension.
53 # 'hello.obj' >  ('hello', '.obj')
54 def splitExt(path):
55     dotidx = path.rfind('.')
56     if dotidx == -1:
57         return path, ''
58     else:
59         return path[:dotidx], path[dotidx:]
60
61 def fixName(name):
62     if name == None:
63         return 'None'
64     else:
65         return name.replace(' ', '_')
66
67 # A Dict of Materials
68 # (material.name, image.name):matname_imagename # matname_imagename has gaps removed.
69 MTL_DICT = {}
70
71 def write_mtl(scene, filename, copy_images):
72
73     world = scene.world
74     worldAmb = world.ambient_color
75
76     dest_dir = os.path.dirname(filename)
77
78     def copy_image(image):
79         rel = image.get_export_path(dest_dir, True)
80
81         if copy_images:
82             abspath = image.get_export_path(dest_dir, False)
83             if not os.path.exists(abs_path):
84                 shutil.copy(image.get_abs_filename(), abs_path)
85
86         return rel
87
88
89     file = open(filename, "w")
90     # XXX
91 #       file.write('# Blender3D MTL File: %s\n' % Blender.Get('filename').split('\\')[-1].split('/')[-1])
92     file.write('# Material Count: %i\n' % len(MTL_DICT))
93     # Write material/image combinations we have used.
94     for key, (mtl_mat_name, mat, img) in MTL_DICT.items():
95
96         # Get the Blender data for the material and the image.
97         # Having an image named None will make a bug, dont do it :)
98
99         file.write('newmtl %s\n' % mtl_mat_name) # Define a new material: matname_imgname
100
101         if mat:
102             file.write('Ns %.6f\n' % ((mat.specular_hardness-1) * 1.9607843137254901) ) # Hardness, convert blenders 1-511 to MTL's
103             file.write('Ka %.6f %.6f %.6f\n' %  tuple([c*mat.ambient for c in worldAmb])  ) # Ambient, uses mirror colour,
104             file.write('Kd %.6f %.6f %.6f\n' % tuple([c*mat.diffuse_intensity for c in mat.diffuse_color]) ) # Diffuse
105             file.write('Ks %.6f %.6f %.6f\n' % tuple([c*mat.specular_intensity for c in mat.specular_color]) ) # Specular
106             if hasattr(mat, "ior"):
107                 file.write('Ni %.6f\n' % mat.ior) # Refraction index
108             else:
109                 file.write('Ni %.6f\n' % 1.0)
110             file.write('d %.6f\n' % mat.alpha) # Alpha (obj uses 'd' for dissolve)
111
112             # 0 to disable lighting, 1 for ambient & diffuse only (specular color set to black), 2 for full lighting.
113             if mat.shadeless:
114                 file.write('illum 0\n') # ignore lighting
115             elif mat.specular_intensity == 0:
116                 file.write('illum 1\n') # no specular.
117             else:
118                 file.write('illum 2\n') # light normaly
119
120         else:
121             #write a dummy material here?
122             file.write('Ns 0\n')
123             file.write('Ka %.6f %.6f %.6f\n' %  tuple([c for c in worldAmb])  ) # Ambient, uses mirror colour,
124             file.write('Kd 0.8 0.8 0.8\n')
125             file.write('Ks 0.8 0.8 0.8\n')
126             file.write('d 1\n') # No alpha
127             file.write('illum 2\n') # light normaly
128
129         # Write images!
130         if img:  # We have an image on the face!
131             # write relative image path
132             rel = copy_image(img)
133             file.write('map_Kd %s\n' % rel) # Diffuse mapping image
134 #                       file.write('map_Kd %s\n' % img.filename.split('\\')[-1].split('/')[-1]) # Diffuse mapping image
135
136         elif mat: # No face image. if we havea material search for MTex image.
137             for mtex in mat.textures:
138                 if mtex and mtex.texture.type == 'IMAGE':
139                     try:
140                         filename = copy_image(mtex.texture.image)
141 #                                               filename = mtex.texture.image.filename.split('\\')[-1].split('/')[-1]
142                         file.write('map_Kd %s\n' % filename) # Diffuse mapping image
143                         break
144                     except:
145                         # Texture has no image though its an image type, best ignore.
146                         pass
147
148         file.write('\n\n')
149
150     file.close()
151
152 # XXX not used
153 def copy_file(source, dest):
154     file = open(source, 'rb')
155     data = file.read()
156     file.close()
157
158     file = open(dest, 'wb')
159     file.write(data)
160     file.close()
161
162
163 # XXX not used
164 def copy_images(dest_dir):
165     if dest_dir[-1] != os.sep:
166         dest_dir += os.sep
167 #       if dest_dir[-1] != sys.sep:
168 #               dest_dir += sys.sep
169
170     # Get unique image names
171     uniqueImages = {}
172     for matname, mat, image in MTL_DICT.values(): # Only use image name
173         # Get Texface images
174         if image:
175             uniqueImages[image] = image # Should use sets here. wait until Python 2.4 is default.
176
177         # Get MTex images
178         if mat:
179             for mtex in mat.textures:
180                 if mtex and mtex.texture.type == 'IMAGE':
181                     image_tex = mtex.texture.image
182                     if image_tex:
183                         try:
184                             uniqueImages[image_tex] = image_tex
185                         except:
186                             pass
187
188     # Now copy images
189     copyCount = 0
190
191 #       for bImage in uniqueImages.values():
192 #               image_path = bpy.utils.expandpath(bImage.filename)
193 #               if bpy.sys.exists(image_path):
194 #                       # Make a name for the target path.
195 #                       dest_image_path = dest_dir + image_path.split('\\')[-1].split('/')[-1]
196 #                       if not bpy.utils.exists(dest_image_path): # Image isnt alredy there
197 #                               print('\tCopying "%s" > "%s"' % (image_path, dest_image_path))
198 #                               copy_file(image_path, dest_image_path)
199 #                               copyCount+=1
200
201 #       paths= bpy.util.copy_images(uniqueImages.values(), dest_dir)
202
203     print('\tCopied %d images' % copyCount)
204 #       print('\tCopied %d images' % copyCount)
205
206 # XXX not converted
207 def test_nurbs_compat(ob):
208     if ob.type != 'Curve':
209         return False
210
211     for nu in ob.data:
212         if (not nu.knotsV) and nu.type != 1: # not a surface and not bezier
213             return True
214
215     return False
216
217
218 # XXX not converted
219 def write_nurb(file, ob, ob_mat):
220     tot_verts = 0
221     cu = ob.data
222
223     # use negative indices
224     Vector = Blender.Mathutils.Vector
225     for nu in cu:
226
227         if nu.type==0:          DEG_ORDER_U = 1
228         else:                           DEG_ORDER_U = nu.orderU-1  # Tested to be correct
229
230         if nu.type==1:
231             print("\tWarning, bezier curve:", ob.name, "only poly and nurbs curves supported")
232             continue
233
234         if nu.knotsV:
235             print("\tWarning, surface:", ob.name, "only poly and nurbs curves supported")
236             continue
237
238         if len(nu) <= DEG_ORDER_U:
239             print("\tWarning, orderU is lower then vert count, skipping:", ob.name)
240             continue
241
242         pt_num = 0
243         do_closed = (nu.flagU & 1)
244         do_endpoints = (do_closed==0) and (nu.flagU & 2)
245
246         for pt in nu:
247             pt = Vector(pt[0], pt[1], pt[2]) * ob_mat
248             file.write('v %.6f %.6f %.6f\n' % (pt[0], pt[1], pt[2]))
249             pt_num += 1
250         tot_verts += pt_num
251
252         file.write('g %s\n' % (fixName(ob.name))) # fixName(ob.getData(1)) could use the data name too
253         file.write('cstype bspline\n') # not ideal, hard coded
254         file.write('deg %d\n' % DEG_ORDER_U) # not used for curves but most files have it still
255
256         curve_ls = [-(i+1) for i in range(pt_num)]
257
258         # 'curv' keyword
259         if do_closed:
260             if DEG_ORDER_U == 1:
261                 pt_num += 1
262                 curve_ls.append(-1)
263             else:
264                 pt_num += DEG_ORDER_U
265                 curve_ls = curve_ls + curve_ls[0:DEG_ORDER_U]
266
267         file.write('curv 0.0 1.0 %s\n' % (' '.join( [str(i) for i in curve_ls] ))) # Blender has no U and V values for the curve
268
269         # 'parm' keyword
270         tot_parm = (DEG_ORDER_U + 1) + pt_num
271         tot_parm_div = float(tot_parm-1)
272         parm_ls = [(i/tot_parm_div) for i in range(tot_parm)]
273
274         if do_endpoints: # end points, force param
275             for i in range(DEG_ORDER_U+1):
276                 parm_ls[i] = 0.0
277                 parm_ls[-(1+i)] = 1.0
278
279         file.write('parm u %s\n' % ' '.join( [str(i) for i in parm_ls] ))
280
281         file.write('end\n')
282
283     return tot_verts
284
285 def write(filename, objects, scene,
286           EXPORT_TRI=False,
287           EXPORT_EDGES=False,
288           EXPORT_NORMALS=False,
289           EXPORT_NORMALS_HQ=False,
290           EXPORT_UV=True,
291           EXPORT_MTL=True,
292           EXPORT_COPY_IMAGES=False,
293           EXPORT_APPLY_MODIFIERS=True,
294           EXPORT_ROTX90=True,
295           EXPORT_BLEN_OBS=True,
296           EXPORT_GROUP_BY_OB=False,
297           EXPORT_GROUP_BY_MAT=False,
298           EXPORT_KEEP_VERT_ORDER=False,
299           EXPORT_POLYGROUPS=False,
300           EXPORT_CURVE_AS_NURBS=True):
301     '''
302     Basic write function. The context and options must be alredy set
303     This can be accessed externaly
304     eg.
305     write( 'c:\\test\\foobar.obj', Blender.Object.GetSelected() ) # Using default options.
306     '''
307
308     # XXX
309     import math
310
311     def veckey3d(v):
312         return round(v.x, 6), round(v.y, 6), round(v.z, 6)
313
314     def veckey2d(v):
315         return round(v[0], 6), round(v[1], 6)
316         # return round(v.x, 6), round(v.y, 6)
317
318     def findVertexGroupName(face, vWeightMap):
319         """
320         Searches the vertexDict to see what groups is assigned to a given face.
321         We use a frequency system in order to sort out the name because a given vetex can
322         belong to two or more groups at the same time. To find the right name for the face
323         we list all the possible vertex group names with their frequency and then sort by
324         frequency in descend order. The top element is the one shared by the highest number
325         of vertices is the face's group
326         """
327         weightDict = {}
328         for vert_index in face.verts:
329 #               for vert in face:
330             vWeights = vWeightMap[vert_index]
331 #                       vWeights = vWeightMap[vert]
332             for vGroupName, weight in vWeights:
333                 weightDict[vGroupName] = weightDict.get(vGroupName, 0) + weight
334
335         if weightDict:
336             alist = [(weight,vGroupName) for vGroupName, weight in weightDict.items()] # sort least to greatest amount of weight
337             alist.sort()
338             return(alist[-1][1]) # highest value last
339         else:
340             return '(null)'
341
342     # TODO: implement this in C? dunno how it should be called...
343     def getVertsFromGroup(me, group_index):
344         ret = []
345
346         for i, v in enumerate(me.verts):
347             for g in v.groups:
348                 if g.group == group_index:
349                     ret.append((i, g.weight))
350
351         return ret
352
353
354     print('OBJ Export path: "%s"' % filename)
355     temp_mesh_name = '~tmp-mesh'
356
357     time1 = time.clock()
358 #       time1 = sys.time()
359 #       scn = Scene.GetCurrent()
360
361     file = open(filename, "w")
362
363     # Write Header
364     file.write('# Blender3D v%s OBJ File: %s\n' % (bpy.version_string, bpy.data.filename.split('/')[-1].split('\\')[-1] ))
365     file.write('# www.blender3d.org\n')
366
367     # Tell the obj file what material file to use.
368     if EXPORT_MTL:
369         mtlfilename = '%s.mtl' % '.'.join(filename.split('.')[:-1])
370         file.write('mtllib %s\n' % ( mtlfilename.split('\\')[-1].split('/')[-1] ))
371
372     if EXPORT_ROTX90:
373         mat_xrot90= Mathutils.RotationMatrix(-math.pi/2, 4, 'x')
374
375     # Initialize totals, these are updated each object
376     totverts = totuvco = totno = 1
377
378     face_vert_index = 1
379
380     globalNormals = {}
381
382     # Get all meshes
383     for ob_main in objects:
384
385         # ignore dupli children
386         if ob_main.parent and ob_main.parent.dupli_type != 'NONE':
387             # XXX
388             print(ob_main.name, 'is a dupli child - ignoring')
389             continue
390
391         obs = []
392         if ob_main.dupli_type != 'NONE':
393             # XXX
394             print('creating dupli_list on', ob_main.name)
395             ob_main.create_dupli_list()
396
397             obs = [(dob.object, dob.matrix) for dob in ob_main.dupli_list]
398
399             # XXX debug print
400             print(ob_main.name, 'has', len(obs), 'dupli children')
401         else:
402             obs = [(ob_main, ob_main.matrix)]
403
404         for ob, ob_mat in obs:
405
406             # XXX postponed
407 #                       # Nurbs curve support
408 #                       if EXPORT_CURVE_AS_NURBS and test_nurbs_compat(ob):
409 #                               if EXPORT_ROTX90:
410 #                                       ob_mat = ob_mat * mat_xrot90
411
412 #                               totverts += write_nurb(file, ob, ob_mat)
413
414 #                               continue
415 #                       end nurbs
416
417             if ob.type != 'MESH':
418                 continue
419
420             me = ob.create_mesh(EXPORT_APPLY_MODIFIERS, 'PREVIEW')
421
422             if EXPORT_ROTX90:
423                 me.transform(mat_xrot90 * ob_mat)
424             else:
425                 me.transform(ob_mat)
426
427 #                       # Will work for non meshes now! :)
428 #                       me= BPyMesh.getMeshFromObject(ob, containerMesh, EXPORT_APPLY_MODIFIERS, EXPORT_POLYGROUPS, scn)
429 #                       if not me:
430 #                               continue
431
432             if EXPORT_UV:
433                 faceuv = len(me.uv_textures) > 0
434             else:
435                 faceuv = False
436
437             # XXX - todo, find a better way to do triangulation
438             # ...removed convert_to_triface because it relies on editmesh
439             '''
440             # We have a valid mesh
441             if EXPORT_TRI and me.faces:
442                 # Add a dummy object to it.
443                 has_quads = False
444                 for f in me.faces:
445                     if f.verts[3] != 0:
446                         has_quads = True
447                         break
448
449                 if has_quads:
450                     newob = bpy.data.add_object('MESH', 'temp_object')
451                     newob.data = me
452                     # if we forget to set Object.data - crash
453                     scene.objects.link(newob)
454                     newob.convert_to_triface(scene)
455                     # mesh will still be there
456                     scene.objects.unlink(newob)
457             '''
458
459             # Make our own list so it can be sorted to reduce context switching
460             face_index_pairs = [ (face, index) for index, face in enumerate(me.faces)]
461             # faces = [ f for f in me.faces ]
462
463             if EXPORT_EDGES:
464                 edges = me.edges
465             else:
466                 edges = []
467
468             if not (len(face_index_pairs)+len(edges)+len(me.verts)): # Make sure there is somthing to write
469
470                 # clean up
471                 bpy.data.remove_mesh(me)
472
473                 continue # dont bother with this mesh.
474
475             # XXX
476             # High Quality Normals
477             if EXPORT_NORMALS and face_index_pairs:
478                 me.calc_normals()
479 #                               if EXPORT_NORMALS_HQ:
480 #                                       BPyMesh.meshCalcNormals(me)
481 #                               else:
482 #                                       # transforming normals is incorrect
483 #                                       # when the matrix is scaled,
484 #                                       # better to recalculate them
485 #                                       me.calcNormals()
486
487             materials = me.materials
488
489             materialNames = []
490             materialItems = [m for m in materials]
491             if materials:
492                 for mat in materials:
493                     if mat: # !=None
494                         materialNames.append(mat.name)
495                     else:
496                         materialNames.append(None)
497                 # Cant use LC because some materials are None.
498                 # materialNames = map(lambda mat: mat.name, materials) # Bug Blender, dosent account for null materials, still broken.
499
500             # Possible there null materials, will mess up indicies
501             # but at least it will export, wait until Blender gets fixed.
502             materialNames.extend((16-len(materialNames)) * [None])
503             materialItems.extend((16-len(materialItems)) * [None])
504
505             # Sort by Material, then images
506             # so we dont over context switch in the obj file.
507             if EXPORT_KEEP_VERT_ORDER:
508                 pass
509             elif faceuv:
510                 # XXX update
511                 tface = me.active_uv_texture.data
512
513                 # exception only raised if Python 2.3 or lower...
514                 try:
515                     face_index_pairs.sort(key = lambda a: (a[0].material_index, tface[a[1]].image, a[0].smooth))
516                 except:
517                     face_index_pairs.sort(lambda a,b: cmp((a[0].material_index, tface[a[1]].image, a[0].smooth),
518                                                               (b[0].material_index, tface[b[1]].image, b[0].smooth)))
519             elif len(materials) > 1:
520                 try:
521                     face_index_pairs.sort(key = lambda a: (a[0].material_index, a[0].smooth))
522                 except:
523                     face_index_pairs.sort(lambda a,b: cmp((a[0].material_index, a[0].smooth),
524                                                               (b[0].material_index, b[0].smooth)))
525             else:
526                 # no materials
527                 try:
528                     face_index_pairs.sort(key = lambda a: a[0].smooth)
529                 except:
530                     face_index_pairs.sort(lambda a,b: cmp(a[0].smooth, b[0].smooth))
531 #                       if EXPORT_KEEP_VERT_ORDER:
532 #                               pass
533 #                       elif faceuv:
534 #                               try:    faces.sort(key = lambda a: (a.mat, a.image, a.smooth))
535 #                               except: faces.sort(lambda a,b: cmp((a.mat, a.image, a.smooth), (b.mat, b.image, b.smooth)))
536 #                       elif len(materials) > 1:
537 #                               try:    faces.sort(key = lambda a: (a.mat, a.smooth))
538 #                               except: faces.sort(lambda a,b: cmp((a.mat, a.smooth), (b.mat, b.smooth)))
539 #                       else:
540 #                               # no materials
541 #                               try:    faces.sort(key = lambda a: a.smooth)
542 #                               except: faces.sort(lambda a,b: cmp(a.smooth, b.smooth))
543
544             faces = [pair[0] for pair in face_index_pairs]
545
546             # Set the default mat to no material and no image.
547             contextMat = (0, 0) # Can never be this, so we will label a new material teh first chance we get.
548             contextSmooth = None # Will either be true or false,  set bad to force initialization switch.
549
550             if EXPORT_BLEN_OBS or EXPORT_GROUP_BY_OB:
551                 name1 = ob.name
552                 name2 = ob.data.name
553                 if name1 == name2:
554                     obnamestring = fixName(name1)
555                 else:
556                     obnamestring = '%s_%s' % (fixName(name1), fixName(name2))
557
558                 if EXPORT_BLEN_OBS:
559                     file.write('o %s\n' % obnamestring) # Write Object name
560                 else: # if EXPORT_GROUP_BY_OB:
561                     file.write('g %s\n' % obnamestring)
562
563
564             # Vert
565             for v in me.verts:
566                 file.write('v %.6f %.6f %.6f\n' % tuple(v.co))
567
568             # UV
569             if faceuv:
570                 uv_face_mapping = [[0,0,0,0] for f in faces] # a bit of a waste for tri's :/
571
572                 uv_dict = {} # could use a set() here
573                 uv_layer = me.active_uv_texture
574                 for f, f_index in face_index_pairs:
575
576                     tface = uv_layer.data[f_index]
577
578                     uvs = tface.uv
579                     # uvs = [tface.uv1, tface.uv2, tface.uv3]
580
581                     # # add another UV if it's a quad
582                     # if len(f.verts) == 4:
583                     #   uvs.append(tface.uv4)
584
585                     for uv_index, uv in enumerate(uvs):
586                         uvkey = veckey2d(uv)
587                         try:
588                             uv_face_mapping[f_index][uv_index] = uv_dict[uvkey]
589                         except:
590                             uv_face_mapping[f_index][uv_index] = uv_dict[uvkey] = len(uv_dict)
591                             file.write('vt %.6f %.6f\n' % tuple(uv))
592
593 #                               uv_dict = {} # could use a set() here
594 #                               for f_index, f in enumerate(faces):
595
596 #                                       for uv_index, uv in enumerate(f.uv):
597 #                                               uvkey = veckey2d(uv)
598 #                                               try:
599 #                                                       uv_face_mapping[f_index][uv_index] = uv_dict[uvkey]
600 #                                               except:
601 #                                                       uv_face_mapping[f_index][uv_index] = uv_dict[uvkey] = len(uv_dict)
602 #                                                       file.write('vt %.6f %.6f\n' % tuple(uv))
603
604                 uv_unique_count = len(uv_dict)
605 #                               del uv, uvkey, uv_dict, f_index, uv_index
606                 # Only need uv_unique_count and uv_face_mapping
607
608             # NORMAL, Smooth/Non smoothed.
609             if EXPORT_NORMALS:
610                 for f in faces:
611                     if f.smooth:
612                         for v in f:
613                             noKey = veckey3d(v.normal)
614                             if noKey not in globalNormals:
615                                 globalNormals[noKey] = totno
616                                 totno +=1
617                                 file.write('vn %.6f %.6f %.6f\n' % noKey)
618                     else:
619                         # Hard, 1 normal from the face.
620                         noKey = veckey3d(f.normal)
621                         if noKey not in globalNormals:
622                             globalNormals[noKey] = totno
623                             totno +=1
624                             file.write('vn %.6f %.6f %.6f\n' % noKey)
625
626             if not faceuv:
627                 f_image = None
628
629             # XXX
630             if EXPORT_POLYGROUPS:
631                 # Retrieve the list of vertex groups
632 #                               vertGroupNames = me.getVertGroupNames()
633
634                 currentVGroup = ''
635                 # Create a dictionary keyed by face id and listing, for each vertex, the vertex groups it belongs to
636                 vgroupsMap = [[] for _i in range(len(me.verts))]
637 #                               vgroupsMap = [[] for _i in xrange(len(me.verts))]
638                 for g in ob.vertex_groups:
639 #                               for vertexGroupName in vertGroupNames:
640                     for vIdx, vWeight in getVertsFromGroup(me, g.index):
641 #                                       for vIdx, vWeight in me.getVertsFromGroup(vertexGroupName, 1):
642                         vgroupsMap[vIdx].append((g.name, vWeight))
643
644             for f_index, f in enumerate(faces):
645                 f_v = [{"index": index, "vertex": me.verts[index]} for index in f.verts]
646
647                 # if f.verts[3] == 0:
648                 #       f_v.pop()
649
650 #                               f_v= f.v
651                 f_smooth= f.smooth
652                 f_mat = min(f.material_index, len(materialNames)-1)
653 #                               f_mat = min(f.mat, len(materialNames)-1)
654                 if faceuv:
655
656                     tface = me.active_uv_texture.data[face_index_pairs[f_index][1]]
657
658                     f_image = tface.image
659                     f_uv = tface.uv
660                     # f_uv= [tface.uv1, tface.uv2, tface.uv3]
661                     # if len(f.verts) == 4:
662                     #   f_uv.append(tface.uv4)
663 #                                       f_image = f.image
664 #                                       f_uv= f.uv
665
666                 # MAKE KEY
667                 if faceuv and f_image: # Object is always true.
668                     key = materialNames[f_mat],  f_image.name
669                 else:
670                     key = materialNames[f_mat],  None # No image, use None instead.
671
672                 # Write the vertex group
673                 if EXPORT_POLYGROUPS:
674                     if len(ob.vertex_groups):
675                         # find what vertext group the face belongs to
676                         theVGroup = findVertexGroupName(f,vgroupsMap)
677                         if      theVGroup != currentVGroup:
678                             currentVGroup = theVGroup
679                             file.write('g %s\n' % theVGroup)
680 #                               # Write the vertex group
681 #                               if EXPORT_POLYGROUPS:
682 #                                       if vertGroupNames:
683 #                                               # find what vertext group the face belongs to
684 #                                               theVGroup = findVertexGroupName(f,vgroupsMap)
685 #                                               if      theVGroup != currentVGroup:
686 #                                                       currentVGroup = theVGroup
687 #                                                       file.write('g %s\n' % theVGroup)
688
689                 # CHECK FOR CONTEXT SWITCH
690                 if key == contextMat:
691                     pass # Context alredy switched, dont do anything
692                 else:
693                     if key[0] == None and key[1] == None:
694                         # Write a null material, since we know the context has changed.
695                         if EXPORT_GROUP_BY_MAT:
696                             # can be mat_image or (null)
697                             file.write('g %s_%s\n' % (fixName(ob.name), fixName(ob.data.name)) ) # can be mat_image or (null)
698                         file.write('usemtl (null)\n') # mat, image
699
700                     else:
701                         mat_data= MTL_DICT.get(key)
702                         if not mat_data:
703                             # First add to global dict so we can export to mtl
704                             # Then write mtl
705
706                             # Make a new names from the mat and image name,
707                             # converting any spaces to underscores with fixName.
708
709                             # If none image dont bother adding it to the name
710                             if key[1] == None:
711                                 mat_data = MTL_DICT[key] = ('%s'%fixName(key[0])), materialItems[f_mat], f_image
712                             else:
713                                 mat_data = MTL_DICT[key] = ('%s_%s' % (fixName(key[0]), fixName(key[1]))), materialItems[f_mat], f_image
714
715                         if EXPORT_GROUP_BY_MAT:
716                             file.write('g %s_%s_%s\n' % (fixName(ob.name), fixName(ob.data.name), mat_data[0]) ) # can be mat_image or (null)
717
718                         file.write('usemtl %s\n' % mat_data[0]) # can be mat_image or (null)
719
720                 contextMat = key
721                 if f_smooth != contextSmooth:
722                     if f_smooth: # on now off
723                         file.write('s 1\n')
724                         contextSmooth = f_smooth
725                     else: # was off now on
726                         file.write('s off\n')
727                         contextSmooth = f_smooth
728
729                 file.write('f')
730                 if faceuv:
731                     if EXPORT_NORMALS:
732                         if f_smooth: # Smoothed, use vertex normals
733                             for vi, v in enumerate(f_v):
734                                 file.write( ' %d/%d/%d' % \
735                                                 (v["index"] + totverts,
736                                                  totuvco + uv_face_mapping[f_index][vi],
737                                                  globalNormals[ veckey3d(v["vertex"].normal) ]) ) # vert, uv, normal
738
739                         else: # No smoothing, face normals
740                             no = globalNormals[ veckey3d(f.normal) ]
741                             for vi, v in enumerate(f_v):
742                                 file.write( ' %d/%d/%d' % \
743                                                 (v["index"] + totverts,
744                                                  totuvco + uv_face_mapping[f_index][vi],
745                                                  no) ) # vert, uv, normal
746                     else: # No Normals
747                         for vi, v in enumerate(f_v):
748                             file.write( ' %d/%d' % (\
749                               v["index"] + totverts,\
750                               totuvco + uv_face_mapping[f_index][vi])) # vert, uv
751
752                     face_vert_index += len(f_v)
753
754                 else: # No UV's
755                     if EXPORT_NORMALS:
756                         if f_smooth: # Smoothed, use vertex normals
757                             for v in f_v:
758                                 file.write( ' %d//%d' %
759                                             (v["index"] + totverts, globalNormals[ veckey3d(v["vertex"].normal) ]) )
760                         else: # No smoothing, face normals
761                             no = globalNormals[ veckey3d(f.normal) ]
762                             for v in f_v:
763                                 file.write( ' %d//%d' % (v["index"] + totverts, no) )
764                     else: # No Normals
765                         for v in f_v:
766                             file.write( ' %d' % (v["index"] + totverts) )
767
768                 file.write('\n')
769
770             # Write edges.
771             if EXPORT_EDGES:
772                 for ed in edges:
773                     if ed.loose:
774                         file.write('f %d %d\n' % (ed.verts[0] + totverts, ed.verts[1] + totverts))
775
776             # Make the indicies global rather then per mesh
777             totverts += len(me.verts)
778             if faceuv:
779                 totuvco += uv_unique_count
780
781             # clean up
782             bpy.data.remove_mesh(me)
783
784         if ob_main.dupli_type != 'NONE':
785             ob_main.free_dupli_list()
786
787     file.close()
788
789
790     # Now we have all our materials, save them
791     if EXPORT_MTL:
792         write_mtl(scene, mtlfilename, EXPORT_COPY_IMAGES)
793 #       if EXPORT_COPY_IMAGES:
794 #               dest_dir = os.path.basename(filename)
795 # #             dest_dir = filename
796 # #             # Remove chars until we are just the path.
797 # #             while dest_dir and dest_dir[-1] not in '\\/':
798 # #                     dest_dir = dest_dir[:-1]
799 #               if dest_dir:
800 #                       copy_images(dest_dir)
801 #               else:
802 #                       print('\tError: "%s" could not be used as a base for an image path.' % filename)
803
804     print("OBJ Export time: %.2f" % (time.clock() - time1))
805 #       print "OBJ Export time: %.2f" % (sys.time() - time1)
806
807 def do_export(filename, context,
808               EXPORT_APPLY_MODIFIERS = True, # not used
809               EXPORT_ROTX90 = True, # wrong
810               EXPORT_TRI = False, # ok
811               EXPORT_EDGES = False,
812               EXPORT_NORMALS = False, # not yet
813               EXPORT_NORMALS_HQ = False, # not yet
814               EXPORT_UV = True, # ok
815               EXPORT_MTL = True,
816               EXPORT_SEL_ONLY = True, # ok
817               EXPORT_ALL_SCENES = False, # XXX not working atm
818               EXPORT_ANIMATION = False,
819               EXPORT_COPY_IMAGES = False,
820               EXPORT_BLEN_OBS = True,
821               EXPORT_GROUP_BY_OB = False,
822               EXPORT_GROUP_BY_MAT = False,
823               EXPORT_KEEP_VERT_ORDER = False,
824               EXPORT_POLYGROUPS = False,
825               EXPORT_CURVE_AS_NURBS = True):
826     #   Window.EditMode(0)
827     #   Window.WaitCursor(1)
828
829     base_name, ext = splitExt(filename)
830     context_name = [base_name, '', '', ext] # Base name, scene name, frame number, extension
831
832     orig_scene = context.scene
833
834 #       if EXPORT_ALL_SCENES:
835 #               export_scenes = bpy.data.scenes
836 #       else:
837 #               export_scenes = [orig_scene]
838
839     # XXX only exporting one scene atm since changing
840     # current scene is not possible.
841     # Brecht says that ideally in 2.5 we won't need such a function,
842     # allowing multiple scenes open at once.
843     export_scenes = [orig_scene]
844
845     # Export all scenes.
846     for scn in export_scenes:
847         #               scn.makeCurrent() # If already current, this is not slow.
848         #               context = scn.getRenderingContext()
849         orig_frame = scn.current_frame
850
851         if EXPORT_ALL_SCENES: # Add scene name into the context_name
852             context_name[1] = '_%s' % bpy.utils.clean_name(scn.name) # WARNING, its possible that this could cause a collision. we could fix if were feeling parranoied.
853
854         # Export an animation?
855         if EXPORT_ANIMATION:
856             scene_frames = range(scn.start_frame, context.end_frame+1) # Up to and including the end frame.
857         else:
858             scene_frames = [orig_frame] # Dont export an animation.
859
860         # Loop through all frames in the scene and export.
861         for frame in scene_frames:
862             if EXPORT_ANIMATION: # Add frame to the filename.
863                 context_name[2] = '_%.6d' % frame
864
865             scn.current_frame = frame
866             if EXPORT_SEL_ONLY:
867                 export_objects = context.selected_objects
868             else:
869                 export_objects = scn.objects
870
871             full_path= ''.join(context_name)
872
873             # erm... bit of a problem here, this can overwrite files when exporting frames. not too bad.
874             # EXPORT THE FILE.
875             write(full_path, export_objects, scn,
876                   EXPORT_TRI, EXPORT_EDGES, EXPORT_NORMALS,
877                   EXPORT_NORMALS_HQ, EXPORT_UV, EXPORT_MTL,
878                   EXPORT_COPY_IMAGES, EXPORT_APPLY_MODIFIERS,
879                   EXPORT_ROTX90, EXPORT_BLEN_OBS,
880                   EXPORT_GROUP_BY_OB, EXPORT_GROUP_BY_MAT, EXPORT_KEEP_VERT_ORDER,
881                   EXPORT_POLYGROUPS, EXPORT_CURVE_AS_NURBS)
882
883
884         scn.current_frame = orig_frame
885
886     # Restore old active scene.
887 #       orig_scene.makeCurrent()
888 #       Window.WaitCursor(0)
889
890
891 '''
892 Currently the exporter lacks these features:
893 * nurbs
894 * multiple scene export (only active scene is written)
895 * particles
896 '''
897
898 from bpy.props import *
899
900 class ExportOBJ(bpy.types.Operator):
901     '''Save a Wavefront OBJ File'''
902
903     bl_idname = "export.obj"
904     bl_label = 'Export OBJ'
905
906     # List of operator properties, the attributes will be assigned
907     # to the class instance from the operator settings before calling.
908
909     path = StringProperty(name="File Path", description="File path used for exporting the OBJ file", maxlen= 1024, default= "")
910
911     # context group
912     use_selection = BoolProperty(name="Selection Only", description="", default= False)
913     use_all_scenes = BoolProperty(name="All Scenes", description="", default= False)
914     use_animation = BoolProperty(name="All Animation", description="", default= False)
915
916     # object group
917     use_modifiers = BoolProperty(name="Apply Modifiers", description="", default= True)
918     use_rotate90 = BoolProperty(name="Rotate X90", description="", default= True)
919
920     # extra data group
921     use_edges = BoolProperty(name="Edges", description="", default= True)
922     use_normals = BoolProperty(name="Normals", description="", default= False)
923     use_hq_normals = BoolProperty(name="High Quality Normals", description="", default= True)
924     use_uvs = BoolProperty(name="UVs", description="", default= True)
925     use_materials = BoolProperty(name="Materials", description="", default= True)
926     copy_images = BoolProperty(name="Copy Images", description="", default= False)
927     use_triangles = BoolProperty(name="Triangulate", description="", default= False)
928     use_vertex_groups = BoolProperty(name="Polygroups", description="", default= False)
929     use_nurbs = BoolProperty(name="Nurbs", description="", default= False)
930
931     # grouping group
932     use_blen_objects = BoolProperty(name="Objects as OBJ Objects", description="", default= True)
933     group_by_object = BoolProperty(name="Objects as OBJ Groups ", description="", default= False)
934     group_by_material = BoolProperty(name="Material Groups", description="", default= False)
935     keep_vertex_order = BoolProperty(name="Keep Vertex Order", description="", default= False)
936
937
938     def execute(self, context):
939
940         do_export(self.properties.path, context,
941                   EXPORT_TRI=self.properties.use_triangles,
942                   EXPORT_EDGES=self.properties.use_edges,
943                   EXPORT_NORMALS=self.properties.use_normals,
944                   EXPORT_NORMALS_HQ=self.properties.use_hq_normals,
945                   EXPORT_UV=self.properties.use_uvs,
946                   EXPORT_MTL=self.properties.use_materials,
947                   EXPORT_COPY_IMAGES=self.properties.copy_images,
948                   EXPORT_APPLY_MODIFIERS=self.properties.use_modifiers,
949                   EXPORT_ROTX90=self.properties.use_rotate90,
950                   EXPORT_BLEN_OBS=self.properties.use_blen_objects,
951                   EXPORT_GROUP_BY_OB=self.properties.group_by_object,
952                   EXPORT_GROUP_BY_MAT=self.properties.group_by_material,
953                   EXPORT_KEEP_VERT_ORDER=self.properties.keep_vertex_order,
954                   EXPORT_POLYGROUPS=self.properties.use_vertex_groups,
955                   EXPORT_CURVE_AS_NURBS=self.properties.use_nurbs,
956                   EXPORT_SEL_ONLY=self.properties.use_selection,
957                   EXPORT_ALL_SCENES=self.properties.use_all_scenes)
958
959         return {'FINISHED'}
960
961     def invoke(self, context, event):
962         wm = context.manager
963         wm.add_fileselect(self)
964         return {'RUNNING_MODAL'}
965
966 bpy.types.register(ExportOBJ)
967
968 def menu_func(self, context):
969     default_path = bpy.data.filename.replace(".blend", ".obj")
970     self.layout.operator(ExportOBJ.bl_idname, text="Wavefront (.obj)...").path = default_path
971
972 menu_item = bpy.types.INFO_MT_file_export.append(menu_func)
973
974 if __name__ == "__main__":
975     bpy.ops.EXPORT_OT_obj(filename="/tmp/test.obj")
976
977 # CONVERSION ISSUES
978 # - matrix problem
979 # - duplis - only tested dupliverts
980 # - NURBS - needs API additions
981 # - all scenes export
982 # + normals calculation
983