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