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