7a648dc7d78ace9a343e6a002a15cde05e3af7ca
[blender.git] / release / scripts / obj_export.py
1 #!BPY
2
3 """
4 Name: 'Wavefront (.obj)...'
5 Blender: 232
6 Group: 'Export'
7 Tooltip: 'Save a Wavefront OBJ File'
8 """
9
10 __author__ = "Campbell Barton, Jiri Hnidek"
11 __url__ = ["blender", "elysiun"]
12 __version__ = "1.0"
13
14 __bpydoc__ = """\
15 This script is an exporter to OBJ file format.
16
17 Usage:
18
19 Run this script from "File->Export" menu to export all meshes.
20 """
21
22
23 # --------------------------------------------------------------------------
24 # OBJ Export v1.0 by Campbell Barton (AKA Ideasman)
25 # --------------------------------------------------------------------------
26 # ***** BEGIN GPL LICENSE BLOCK *****
27 #
28 # This program is free software; you can redistribute it and/or
29 # modify it under the terms of the GNU General Public License
30 # as published by the Free Software Foundation; either version 2
31 # of the License, or (at your option) any later version.
32 #
33 # This program is distributed in the hope that it will be useful,
34 # but WITHOUT ANY WARRANTY; without even the implied warranty of
35 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36 # GNU General Public License for more details.
37 #
38 # You should have received a copy of the GNU General Public License
39 # along with this program; if not, write to the Free Software Foundation,
40 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
41 #
42 # ***** END GPL LICENCE BLOCK *****
43 # --------------------------------------------------------------------------
44
45
46 import Blender
47 from Blender import Mesh, Scene, Window, sys, Image, Draw
48
49 #==================================================#
50 # New name based on old with a different extension #
51 #==================================================#
52 def newFName(ext):
53         return Blender.Get('filename')[: -len(Blender.Get('filename').split('.', -1)[-1]) ] + ext
54
55 # Returns a tuple - path,extension.
56 # 'hello.obj' >  ('hello', '.obj')
57 def splitExt(path):
58         dotidx = path.rfind('.')
59         if dotidx == -1:
60                 return path, ''
61         else:
62                 return path[:dotidx], path[dotidx:] 
63
64 def fixName(name):
65         if name == None:
66                 return 'None'
67         else:
68                 return name.replace(' ', '_')
69
70 # Used to add the scene name into the filename without using odd chars
71 def saneFilechars(name):
72         for ch in ' /\\~!@#$%^&*()+=[];\':",./<>?':
73                 name = name.replace(ch, '_')
74         return name
75
76 def sortPair(a,b):
77         return min(a,b), max(a,b)
78
79 def getMeshFromObject(object, name=None, mesh=None):
80         if mesh:
81                 mesh.verts = None # Clear the meshg
82         else:
83                 if not name:
84                         mesh = Mesh.New()
85                 else:
86                         mesh = Mesh.New(name)
87         
88         
89         type = object.getType()
90         dataname = object.getData(1)
91         
92         try:
93                 mesh.getFromObject(object.name) 
94         except:
95                 return None
96         
97         if type == 'Mesh':
98                 tempMe = Mesh.Get( dataname )
99                 mesh.materials = tempMe.materials
100                 mesh.degr = tempMe.degr
101                 mesh.mode = tempMe.mode
102         else:
103                 try:
104                         # Will only work for curves!!
105                         # Text- no material access in python interface.
106                         # Surf- no python interface
107                         # MBall- no material access in python interface.
108                         
109                         data = object.getData()
110                         materials = data.getMaterials()
111                         mesh.materials = materials
112                         print 'assigning materials for non mesh'
113                 except:
114                         print 'Cant assign materials to', type
115         
116         return mesh
117
118 global MTL_DICT
119
120 # A Dict of Materials
121 # (material.name, image.name):matname_imagename # matname_imagename has gaps removed.
122 MTL_DICT = {} 
123
124 def save_mtl(filename):
125         global MTL_DICT
126         
127         world = Blender.World.GetCurrent()
128         if world:
129                 worldAmb = world.getAmb()
130         else:
131                 worldAmb = (0,0,0) # Default value
132         
133         file = open(filename, "w")
134         file.write('# Blender MTL File: %s\n' % Blender.Get('filename').split('\\')[-1].split('/')[-1])
135         file.write('# Material Count: %i\n' % len(MTL_DICT))
136         # Write material/image combinations we have used.
137         for key, mtl_mat_name in MTL_DICT.iteritems():
138                 
139                 # Get the Blender data for the material and the image.
140                 # Having an image named None will make a bug, dont do it :)
141                 
142                 file.write('newmtl %s\n' % mtl_mat_name) # Define a new material: matname_imgname
143                 
144                 if key[0] == None:
145                         #write a dummy material here?
146                         file.write('Ns 0\n')
147                         file.write('Ka %s %s %s\n' %  tuple([round(c, 6) for c in worldAmb])  ) # Ambient, uses mirror colour,
148                         file.write('Kd 0.8 0.8 0.8\n')
149                         file.write('Ks 0.8 0.8 0.8\n')
150                         file.write('d 1\n') # No alpha
151                         file.write('illum 2\n') # light normaly 
152                         
153                 else:
154                         mat = Blender.Material.Get(key[0])
155                         file.write('Ns %s\n' % round((mat.getHardness()-1) * 1.9607843137254901 ) ) # Hardness, convert blenders 1-511 to MTL's 
156                         file.write('Ka %s %s %s\n' %  tuple([round(c*mat.getAmb(), 6) for c in worldAmb])  ) # Ambient, uses mirror colour,
157                         file.write('Kd %s %s %s\n' % tuple([round(c*mat.getRef(), 6) for c in mat.getRGBCol()]) ) # Diffuse
158                         file.write('Ks %s %s %s\n' % tuple([round(c*mat.getSpec(), 6) for c in mat.getSpecCol()]) ) # Specular
159                         file.write('Ni %s\n' % round(mat.getIOR(), 6)) # Refraction index
160                         file.write('d %s\n' % round(mat.getAlpha(), 6)) # Alpha (obj uses 'd' for dissolve)
161                         
162                         # 0 to disable lighting, 1 for ambient & diffuse only (specular color set to black), 2 for full lighting.
163                         if mat.getMode() & Blender.Material.Modes['SHADELESS']:
164                                 file.write('illum 0\n') # ignore lighting
165                         elif mat.getSpec() == 0:
166                                 file.write('illum 1\n') # no specular.
167                         else:
168                                 file.write('illum 2\n') # light normaly 
169                 
170                 
171                 # Write images!
172                 if key[1] != None:  # We have an image on the face!
173                         img = Image.Get(key[1])
174                         file.write('map_Kd %s\n' % img.filename.split('\\')[-1].split('/')[-1]) # Diffuse mapping image                 
175                 
176                 elif key[0] != None: # No face image. if we havea material search for MTex image.
177                         for mtex in mat.getTextures():
178                                 if mtex and mtex.tex.type == Blender.Texture.Types.IMAGE:
179                                         try:
180                                                 filename = mtex.tex.image.filename.split('\\')[-1].split('/')[-1]
181                                                 file.write('map_Kd %s\n' % filename) # Diffuse mapping image
182                                                 break
183                                         except:
184                                                 # Texture has no image though its an image type, best ignore.
185                                                 pass
186                 
187                 file.write('\n\n')
188         
189         file.close()
190
191 def copy_file(source, dest):
192         file = open(source, 'rb')
193         data = file.read()
194         file.close()
195         
196         file = open(dest, 'wb')
197         file.write(data)
198         file.close()
199
200
201 def copy_images(dest_dir):
202         if dest_dir[-1] != sys.sep:
203                 dest_dir += sys.sep
204         
205         # Get unique image names
206         uniqueImages = {}
207         for matname, imagename in MTL_DICT.iterkeys(): # Only use image name
208                 if imagename != None:
209                         uniqueImages[imagename] = None # Should use sets here. wait until Python 2.4 is default.
210         
211         # Now copy images
212         copyCount = 0
213         
214         for imageName in uniqueImages.iterkeys():
215                 print imageName
216                 bImage = Image.Get(imageName)
217                 image_path = sys.expandpath(bImage.filename)
218                 if sys.exists(image_path):
219                         # Make a name for the target path.
220                         dest_image_path = dest_dir + image_path.split('\\')[-1].split('/')[-1]
221                         if not sys.exists(dest_image_path): # Image isnt alredy there
222                                 print '\tCopying "%s" > "%s"' % (image_path, dest_image_path)
223                                 copy_file(image_path, dest_image_path)
224                                 copyCount+=1
225         print '\tCopied %d images' % copyCount
226         
227 def save_obj(filename, objects, EXPORT_EDGES=False, EXPORT_NORMALS=False, EXPORT_MTL=True, EXPORT_COPY_IMAGES=False, EXPORT_APPLY_MODIFIERS=True):
228         '''
229         Basic save function. The context and options must be alredy set
230         This can be accessed externaly
231         eg.
232         save_obj( 'c:\\test\\foobar.obj', Blender.Object.GetSelected() ) # Using default options.
233         '''
234         print 'OBJ Export path: "%s"' % filename
235         global MTL_DICT
236         temp_mesh_name = '~tmp-mesh'
237         time1 = sys.time()
238         scn = Scene.GetCurrent()
239
240         file = open(filename, "w")
241         
242         # Write Header
243         file.write('# Blender OBJ File: %s\n' % (Blender.Get('filename').split('/')[-1].split('\\')[-1] ))
244         file.write('# www.blender.org\n')
245
246         # Tell the obj file what material file to use.
247         mtlfilename = '%s.mtl' % '.'.join(filename.split('.')[:-1])
248         file.write('mtllib %s\n' % ( mtlfilename.split('\\')[-1].split('/')[-1] ))
249         
250         # Get the container mesh.
251         if EXPORT_APPLY_MODIFIERS:
252                 containerMesh = meshName = tempMesh = None
253                 for meshName in Blender.NMesh.GetNames():
254                         if meshName.startswith(temp_mesh_name):
255                                 tempMesh = Mesh.Get(meshName)
256                                 if not tempMesh.users:
257                                         containerMesh = tempMesh
258                 if not containerMesh:
259                         containerMesh = Mesh.New(temp_mesh_name)
260                 del meshName
261                 del tempMesh
262         
263         
264         
265         # Initialize totals, these are updated each object
266         totverts = totuvco = totno = 1
267         
268         globalUVCoords = {}
269         globalNormals = {}
270         
271         # Get all meshs
272         for ob in objects:
273                 
274                 # Will work for non meshes now! :)
275                 if EXPORT_APPLY_MODIFIERS or ob.getType() != 'Mesh':
276                         m = getMeshFromObject(ob, temp_mesh_name, containerMesh)
277                         if not m:
278                                 continue
279                 else: # We are a mesh. get the data.
280                         m = ob.getData(mesh=1)
281                 
282                 faces = [ f for f in m.faces ]
283                 if EXPORT_EDGES:
284                         edges = [ ed for ed in m.edges ]
285                 else:
286                         edges = []
287                         
288                 if not (len(faces)+len(edges)): # Make sure there is somthing to write
289                         continue # dont bother with this mesh.
290                 
291                 m.transform(ob.matrix)
292                 
293                 # # Crash Blender
294                 #materials = m.getMaterials(1) # 1 == will return None in the list.
295                 materials = m.materials
296                 
297                 
298                 if materials:
299                         materialNames = map(lambda mat: mat.name, materials) # Bug Blender, dosent account for null materials, still broken.    
300                 else:
301                         materialNames = []
302                 
303                 # Possible there null materials, will mess up indicies
304                 # but at least it will export, wait until Blender gets fixed.
305                 materialNames.extend((16-len(materialNames)) * [None])
306                 
307                 
308                 # Sort by Material, then images
309                 # so we dont over context switch in the obj file.
310                 if m.faceUV:
311                         faces.sort(lambda a,b: cmp((a.mat, a.image, a.smooth), (b.mat, b.image, b.smooth)))
312                 else:
313                         faces.sort(lambda a,b: cmp((a.mat, a.smooth), (b.mat, b.smooth)))
314                 
315                 
316                 # Set the default mat to no material and no image.
317                 contextMat = (0, 0) # Can never be this, so we will label a new material teh first chance we get.
318                 contextSmooth = None # Will either be true or false,  set bad to force initialization switch.
319                 
320                 file.write('o %s_%s\n' % (fixName(ob.name), fixName(m.name))) # Write Object name
321                 
322                 # Vert
323                 for v in m.verts:
324                         file.write('v %.6f %.6f %.6f\n' % tuple(v.co))
325                 
326                 # UV
327                 if m.faceUV:
328                         for f in faces:
329                                 for uvKey in f.uv:
330                                         uvKey = tuple(uvKey)
331                                         if not globalUVCoords.has_key(uvKey):
332                                                 globalUVCoords[uvKey] = totuvco
333                                                 totuvco +=1
334                                                 file.write('vt %.6f %.6f 0.0\n' % uvKey)
335                 
336                 # NORMAL, Smooth/Non smoothed.
337                 if EXPORT_NORMALS:
338                         for f in faces:
339                                 if f.smooth:
340                                         for v in f.v:
341                                                 noKey = tuple(v.no)
342                                                 if not globalNormals.has_key( noKey ):
343                                                         globalNormals[noKey] = totno
344                                                         totno +=1
345                                                         file.write('vn %.6f %.6f %.6f\n' % noKey)
346                                 else:
347                                         # Hard, 1 normal from the face.
348                                         noKey = tuple(f.no)
349                                         if not globalNormals.has_key( noKey ):
350                                                 globalNormals[noKey] = totno
351                                                 totno +=1
352                                                 file.write('vn %.6f %.6f %.6f\n' % noKey)
353                 
354                 
355                 uvIdx = 0
356                 for f in faces:
357                         
358                         # MAKE KEY
359                         if m.faceUV and f.image: # Object is always true.
360                                 key = materialNames[f.mat],  f.image.name
361                         else:
362                                 key = materialNames[f.mat],  None # No image, use None instead.
363                         
364                         # CHECK FOR CONTEXT SWITCH
365                         if key == contextMat:
366                                 pass # Context alredy switched, dont do anythoing
367                         elif key[0] == None and key[1] == None:
368                                 # Write a null material, since we know the context has changed.
369                                 file.write('usemtl (null)\n') # mat, image
370                                 
371                         else:
372                                 try: # Faster to try then 2x dict lookups.
373                                         
374                                         # We have the material, just need to write the context switch,
375                                         file.write('usemtl %s\n' % MTL_DICT[key]) # mat, image
376                                         
377                                 except KeyError:
378                                         # First add to global dict so we can export to mtl
379                                         # Then write mtl
380                                         
381                                         # Make a new names from the mat and image name,
382                                         # converting any spaces to underscores with fixName.
383                                         
384                                         # If none image dont bother adding it to the name
385                                         if key[1] == None:
386                                                 tmp_matname = MTL_DICT[key] ='%s' % fixName(key[0])
387                                                 file.write('usemtl %s\n' % tmp_matname) # mat, image
388                                                 
389                                         else:
390                                                 tmp_matname = MTL_DICT[key] = '%s_%s' % (fixName(key[0]), fixName(key[1]))
391                                                 file.write('usemtl %s\n' % tmp_matname) # mat, image
392                                 
393                         contextMat = key
394                         
395                         if f.smooth != contextSmooth:
396                                 if f.smooth:
397                                         file.write('s 1\n')
398                                 else:
399                                         file.write('s off\n')
400                                 contextSmooth = f.smooth
401                         
402                         file.write('f')
403                         if m.faceUV:
404                                 if EXPORT_NORMALS:
405                                         if f.smooth: # Smoothed, use vertex normals
406                                                 for vi, v in enumerate(f.v):
407                                                         file.write( ' %d/%d/%d' % (\
408                                                           v.index+totverts,\
409                                                           globalUVCoords[ tuple(f.uv[vi]) ],\
410                                                           globalNormals[ tuple(v.no) ])) # vert, uv, normal
411                                         else: # No smoothing, face normals
412                                                 no = globalNormals[ tuple(f.no) ]
413                                                 for vi, v in enumerate(f.v):
414                                                         file.write( ' %d/%d/%d' % (\
415                                                           v.index+totverts,\
416                                                           globalUVCoords[ tuple(f.uv[vi]) ],\
417                                                           no)) # vert, uv, normal
418                                 
419                                 else: # No Normals
420                                         for vi, v in enumerate(f.v):
421                                                 file.write( ' %d/%d' % (\
422                                                   v.index+totverts,\
423                                                   globalUVCoords[ tuple(f.uv[vi])])) # vert, uv
424                                         
425                                         
426                         else: # No UV's
427                                 if EXPORT_NORMALS:
428                                         if f.smooth: # Smoothed, use vertex normals
429                                                 for v in f.v:
430                                                         file.write( ' %d//%d' % (\
431                                                           v.index+totverts,\
432                                                           globalNormals[ tuple(v.no) ]))
433                                         else: # No smoothing, face normals
434                                                 no = globalNormals[ tuple(f.no) ]
435                                                 for v in f.v:
436                                                         file.write( ' %d//%d' % (\
437                                                           v.index+totverts,\
438                                                           no))
439                                 else: # No Normals
440                                         for v in f.v:
441                                                 file.write( ' %d' % (\
442                                                   v.index+totverts))
443                                         
444                         file.write('\n')
445                 
446                 # Write edges.
447                 if EXPORT_EDGES:
448                         edgeUsers = {}
449                         for f in faces:
450                                 for i in xrange(len(f.v)):
451                                         faceEdgeVKey = sortPair(f.v[i].index, f.v[i-1].index)
452                                         
453                                         # We dont realy need to keep count. Just that a face uses it 
454                                         # so dont export.
455                                         edgeUsers[faceEdgeVKey] = 1 
456                                 
457                         for ed in edges:
458                                 edgeVKey = sortPair(ed.v1.index, ed.v2.index)
459                                 if not edgeUsers.has_key(edgeVKey): # No users? Write the edge.
460                                         file.write('f %d %d\n' % (edgeVKey[0]+totverts, edgeVKey[1]+totverts))
461                 
462                 # Make the indicies global rather then per mesh
463                 totverts += len(m.verts)
464         file.close()
465         
466         
467         # Now we have all our materials, save them
468         if EXPORT_MTL:
469                 save_mtl(mtlfilename)
470         if EXPORT_COPY_IMAGES:
471                 dest_dir = filename
472                 # Remove chars until we are just the path.
473                 while dest_dir and dest_dir[-1] not in '\\/':
474                         dest_dir = dest_dir[:-1]
475                 if dest_dir:
476                         copy_images(dest_dir)
477                 else:
478                         print '\tError: "%s" could not be used as a base for an image path.' % filename
479         
480         print "OBJ Export time: %.2f" % (sys.time() - time1)
481         
482         
483         
484         
485         
486         
487
488 def save_obj_ui(filename):
489         
490         for s in Window.GetScreenInfo():
491                 Window.QHandle(s['id'])
492         
493         EXPORT_APPLY_MODIFIERS = Draw.Create(1)
494         EXPORT_SEL_ONLY = Draw.Create(0)
495         EXPORT_EDGES = Draw.Create(0)
496         EXPORT_NORMALS = Draw.Create(0)
497         EXPORT_MTL = Draw.Create(1)
498         EXPORT_ALL_SCENES = Draw.Create(0)
499         EXPORT_ANIMATION = Draw.Create(0)
500         EXPORT_COPY_IMAGES = Draw.Create(0)
501         
502         
503         # Get USER Options
504         pup_block = [\
505         ('Apply Modifiers', EXPORT_APPLY_MODIFIERS, 'Use transformed mesh data from each object. May break vert order for morph targets.'),\
506         ('Selection Only', EXPORT_SEL_ONLY, 'Only export objects in visible selection.'),\
507         ('Edges', EXPORT_EDGES, 'Edges not connected to faces.'),\
508         ('Normals', EXPORT_NORMALS, 'Export vertex normal data (Ignored on import).'),\
509         ('Materials', EXPORT_MTL, 'Write a seperate MTL file with the OBJ.'),\
510         ('All Scenes', EXPORT_ALL_SCENES, 'Each scene as a seperate OBJ file.'),\
511         ('Animation', EXPORT_ANIMATION, 'Each frame as a seperate OBJ file.'),\
512         ('Copy Images', EXPORT_COPY_IMAGES, 'Copy image files to the export directory, never everwrite.'),\
513         ]
514         
515         if not Draw.PupBlock('Export...', pup_block):
516                 return
517         
518         Window.WaitCursor(1)
519         
520         EXPORT_APPLY_MODIFIERS = EXPORT_APPLY_MODIFIERS.val
521         EXPORT_SEL_ONLY = EXPORT_SEL_ONLY.val
522         EXPORT_EDGES = EXPORT_EDGES.val
523         EXPORT_NORMALS = EXPORT_NORMALS.val
524         EXPORT_MTL = EXPORT_MTL.val
525         EXPORT_ALL_SCENES = EXPORT_ALL_SCENES.val
526         EXPORT_ANIMATION = EXPORT_ANIMATION.val
527         EXPORT_COPY_IMAGES = EXPORT_COPY_IMAGES.val
528         
529         
530         
531         base_name, ext = splitExt(filename)
532         context_name = [base_name, '', '', ext] # basename, scene_name, framenumber, extension
533         
534         # Use the options to export the data using save_obj()
535         # def save_obj(filename, objects, EXPORT_EDGES=False, EXPORT_NORMALS=False, EXPORT_MTL=True, EXPORT_COPY_IMAGES=False, EXPORT_APPLY_MODIFIERS=True):
536         orig_scene = Scene.GetCurrent()
537         if EXPORT_ALL_SCENES:
538                 export_scenes = Scene.Get()
539         else:
540                 export_scenes = [orig_scene]
541         
542         # Export all scenes.
543         for scn in export_scenes:
544                 scn.makeCurrent() # If alredy current, this is not slow.
545                 context = scn.getRenderingContext()
546                 orig_frame = Blender.Get('curframe')
547                 
548                 if EXPORT_ALL_SCENES: # Add scene name into the context_name
549                         context_name[1] = '_%s' % saneFilechars(scn.name) # WARNING, its possible that this could cause a collision. we could fix if were feeling parranoied.
550                 
551                 # Export an animation?
552                 if EXPORT_ANIMATION:
553                         scene_frames = range(context.startFrame(), context.endFrame()+1) # up to and including the end frame.
554                 else:
555                         scene_frames = [orig_frame] # Dont export an animation.
556                 
557                 # Loop through all frames in the scene and export.
558                 for frame in scene_frames:
559                         if EXPORT_ANIMATION: # Add frame to the filename.
560                                 context_name[2] = '_%.6d' % frame
561                         
562                         Blender.Set('curframe', frame)
563                         if EXPORT_SEL_ONLY:
564                                 export_objects = Object.GetSelected() # Export Context
565                         else:
566                                 export_objects = scn.getChildren()
567                         
568                         # EXPORTTHE FILE.
569                         save_obj(''.join(context_name), export_objects, EXPORT_EDGES, EXPORT_NORMALS, EXPORT_MTL, EXPORT_COPY_IMAGES, EXPORT_APPLY_MODIFIERS)
570                 
571                 Blender.Set('curframe', orig_frame)
572         
573         # Restore old active scene.
574         orig_scene.makeCurrent()
575         Window.WaitCursor(0)
576         
577         
578
579 Window.FileSelector(save_obj_ui, 'Export Wavefront OBJ', newFName('obj'))