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