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