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