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