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