change blender python interface for classes not to ise __idname__ rather bl_idname...
[blender-staging.git] / release / scripts / io / export_3ds.py
1 # coding: utf-8
2 __author__ = ["Campbell Barton", "Bob Holcomb", "Richard Lärkäng", "Damien McGinnes", "Mark Stijnman"]
3 __url__ = ("blenderartists.org", "www.blender.org", "www.gametutorials.com", "lib3ds.sourceforge.net/")
4 __version__ = "0.90a"
5 __bpydoc__ = """\
6
7 3ds Exporter
8
9 This script Exports a 3ds file.
10
11 Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
12 from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
13 """
14
15 # ***** BEGIN GPL LICENSE BLOCK *****
16 #
17 # Script copyright (C) Bob Holcomb 
18 #
19 # This program is free software; you can redistribute it and/or
20 # modify it under the terms of the GNU General Public License
21 # as published by the Free Software Foundation; either version 2
22 # of the License, or (at your option) any later version.
23 #
24 # This program is distributed in the hope that it will be useful,
25 # but WITHOUT ANY WARRANTY; without even the implied warranty of
26 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27 # GNU General Public License for more details.
28 #
29 # You should have received a copy of the GNU General Public License
30 # along with this program; if not, write to the Free Software Foundation,
31 # Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
32 #
33 # ***** END GPL LICENCE BLOCK *****
34 # --------------------------------------------------------------------------
35
36
37 ######################################################
38 # Importing modules
39 ######################################################
40
41 import struct
42 import os
43 import time
44
45 import bpy
46
47 # import Blender
48 # from BPyMesh import getMeshFromObject
49 # from BPyObject import getDerivedObjects
50 # try: 
51 #     import struct
52 # except: 
53 #     struct = None
54
55 # also used by X3D exporter
56 # return a tuple (free, object list), free is True if memory should be freed later with free_derived_objects()
57 def create_derived_objects(ob):
58         if ob.parent and ob.parent.dupli_type != 'NONE':
59                 return False, None
60
61         if ob.dupli_type != 'NONE':
62                 ob.create_dupli_list()
63                 return True, [(dob.object, dob.matrix) for dob in ob.dupli_list]
64         else:
65                 return False, [(ob, ob.matrix)]
66
67 # also used by X3D exporter
68 def free_derived_objects(ob):
69         ob.free_dupli_list()
70
71 # So 3ds max can open files, limit names to 12 in length
72 # this is verry annoying for filenames!
73 name_unique = []
74 name_mapping = {}
75 def sane_name(name):
76         name_fixed = name_mapping.get(name)
77         if name_fixed != None:
78                 return name_fixed
79         
80         if len(name) > 12:
81                 new_name = name[:12]
82         else:
83                 new_name = name
84         
85         i = 0
86         
87         while new_name in name_unique:
88                 new_name = new_name[:-4] + '.%.3d' % i
89                 i+=1
90         
91         name_unique.append(new_name)
92         name_mapping[name] = new_name
93         return new_name
94
95 ######################################################
96 # Data Structures
97 ######################################################
98
99 #Some of the chunks that we will export
100 #----- Primary Chunk, at the beginning of each file
101 PRIMARY= int("0x4D4D",16)
102
103 #------ Main Chunks
104 OBJECTINFO   =      int("0x3D3D",16);      #This gives the version of the mesh and is found right before the material and object information
105 VERSION      =      int("0x0002",16);      #This gives the version of the .3ds file
106 KFDATA       =      int("0xB000",16);      #This is the header for all of the key frame info
107
108 #------ sub defines of OBJECTINFO
109 MATERIAL=45055          #0xAFFF                         // This stored the texture info
110 OBJECT=16384            #0x4000                         // This stores the faces, vertices, etc...
111
112 #>------ sub defines of MATERIAL
113 MATNAME    =      int("0xA000",16);      # This holds the material name
114 MATAMBIENT   =      int("0xA010",16);      # Ambient color of the object/material
115 MATDIFFUSE   =      int("0xA020",16);      # This holds the color of the object/material
116 MATSPECULAR   =      int("0xA030",16);      # SPecular color of the object/material
117 MATSHINESS   =      int("0xA040",16);      # ??
118 MATMAP       =      int("0xA200",16);      # This is a header for a new material
119 MATMAPFILE    =      int("0xA300",16);      # This holds the file name of the texture
120
121 RGB1=   int("0x0011",16)
122 RGB2=   int("0x0012",16)
123
124 #>------ sub defines of OBJECT
125 OBJECT_MESH  =      int("0x4100",16);      # This lets us know that we are reading a new object
126 OBJECT_LIGHT =      int("0x4600",16);      # This lets un know we are reading a light object
127 OBJECT_CAMERA=      int("0x4700",16);      # This lets un know we are reading a camera object
128
129 #>------ sub defines of CAMERA
130 OBJECT_CAM_RANGES=   int("0x4720",16);      # The camera range values
131
132 #>------ sub defines of OBJECT_MESH
133 OBJECT_VERTICES =   int("0x4110",16);      # The objects vertices
134 OBJECT_FACES    =   int("0x4120",16);      # The objects faces
135 OBJECT_MATERIAL =   int("0x4130",16);      # This is found if the object has a material, either texture map or color
136 OBJECT_UV       =   int("0x4140",16);      # The UV texture coordinates
137 OBJECT_TRANS_MATRIX  =   int("0x4160",16); # The Object Matrix
138
139 #>------ sub defines of KFDATA
140 KFDATA_KFHDR            = int("0xB00A",16);
141 KFDATA_KFSEG            = int("0xB008",16);
142 KFDATA_KFCURTIME        = int("0xB009",16);
143 KFDATA_OBJECT_NODE_TAG  = int("0xB002",16);
144
145 #>------ sub defines of OBJECT_NODE_TAG
146 OBJECT_NODE_ID          = int("0xB030",16);
147 OBJECT_NODE_HDR         = int("0xB010",16);
148 OBJECT_PIVOT            = int("0xB013",16);
149 OBJECT_INSTANCE_NAME    = int("0xB011",16);
150 POS_TRACK_TAG                   = int("0xB020",16);
151 ROT_TRACK_TAG                   = int("0xB021",16);
152 SCL_TRACK_TAG                   = int("0xB022",16);
153
154 def uv_key(uv):
155         return round(uv[0], 6), round(uv[1], 6)
156 #       return round(uv.x, 6), round(uv.y, 6)
157
158 # size defines: 
159 SZ_SHORT = 2
160 SZ_INT   = 4
161 SZ_FLOAT = 4
162
163 class _3ds_short(object):
164         '''Class representing a short (2-byte integer) for a 3ds file.
165         *** This looks like an unsigned short H is unsigned from the struct docs - Cam***'''
166         __slots__ = 'value'
167         def __init__(self, val=0):
168                 self.value=val
169         
170         def get_size(self):
171                 return SZ_SHORT
172
173         def write(self,file):
174                 file.write(struct.pack("<H", self.value))
175                 
176         def __str__(self):
177                 return str(self.value)
178
179 class _3ds_int(object):
180         '''Class representing an int (4-byte integer) for a 3ds file.'''
181         __slots__ = 'value'
182         def __init__(self, val=0):
183                 self.value=val
184         
185         def get_size(self):
186                 return SZ_INT
187
188         def write(self,file):
189                 file.write(struct.pack("<I", self.value))
190         
191         def __str__(self):
192                 return str(self.value)
193
194 class _3ds_float(object):
195         '''Class representing a 4-byte IEEE floating point number for a 3ds file.'''
196         __slots__ = 'value'
197         def __init__(self, val=0.0):
198                 self.value=val
199         
200         def get_size(self):
201                 return SZ_FLOAT
202
203         def write(self,file):
204                 file.write(struct.pack("<f", self.value))
205         
206         def __str__(self):
207                 return str(self.value)
208
209
210 class _3ds_string(object):
211         '''Class representing a zero-terminated string for a 3ds file.'''
212         __slots__ = 'value'
213         def __init__(self, val=""):
214                 self.value=val
215         
216         def get_size(self):
217                 return (len(self.value)+1)
218
219         def write(self,file):
220                 binary_format = "<%ds" % (len(self.value)+1)
221                 file.write(struct.pack(binary_format, self.value))
222         
223         def __str__(self):
224                 return self.value
225
226 class _3ds_point_3d(object):
227         '''Class representing a three-dimensional point for a 3ds file.'''
228         __slots__ = 'x','y','z'
229         def __init__(self, point=(0.0,0.0,0.0)):
230                 self.x, self.y, self.z = point
231                 
232         def get_size(self):
233                 return 3*SZ_FLOAT
234
235         def write(self,file):
236                 file.write(struct.pack('<3f', self.x, self.y, self.z))
237         
238         def __str__(self):
239                 return '(%f, %f, %f)' % (self.x, self.y, self.z)
240                 
241 # Used for writing a track
242 """
243 class _3ds_point_4d(object):
244         '''Class representing a four-dimensional point for a 3ds file, for instance a quaternion.'''
245         __slots__ = 'x','y','z','w'
246         def __init__(self, point=(0.0,0.0,0.0,0.0)):
247                 self.x, self.y, self.z, self.w = point  
248         
249         def get_size(self):
250                 return 4*SZ_FLOAT
251
252         def write(self,file):
253                 data=struct.pack('<4f', self.x, self.y, self.z, self.w)
254                 file.write(data)
255
256         def __str__(self):
257                 return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
258 """
259
260 class _3ds_point_uv(object):
261         '''Class representing a UV-coordinate for a 3ds file.'''
262         __slots__ = 'uv'
263         def __init__(self, point=(0.0,0.0)):
264                 self.uv = point
265         
266         def __cmp__(self, other):
267                 return cmp(self.uv,other.uv)    
268         
269         def get_size(self):
270                 return 2*SZ_FLOAT
271         
272         def write(self,file):
273                 data=struct.pack('<2f', self.uv[0], self.uv[1])
274                 file.write(data)
275         
276         def __str__(self):
277                 return '(%g, %g)' % self.uv
278
279 class _3ds_rgb_color(object):
280         '''Class representing a (24-bit) rgb color for a 3ds file.'''
281         __slots__ = 'r','g','b'
282         def __init__(self, col=(0,0,0)):
283                 self.r, self.g, self.b = col
284         
285         def get_size(self):
286                 return 3
287         
288         def write(self,file):
289                 file.write( struct.pack('<3B', int(255*self.r), int(255*self.g), int(255*self.b) ) )
290 #               file.write( struct.pack('<3c', chr(int(255*self.r)), chr(int(255*self.g)), chr(int(255*self.b)) ) )
291         
292         def __str__(self):
293                 return '{%f, %f, %f}' % (self.r, self.g, self.b)
294
295 class _3ds_face(object):
296         '''Class representing a face for a 3ds file.'''
297         __slots__ = 'vindex'
298         def __init__(self, vindex):
299                 self.vindex = vindex
300         
301         def get_size(self):
302                 return 4*SZ_SHORT
303         
304         def write(self,file):
305                 # The last zero is only used by 3d studio
306                 file.write(struct.pack("<4H", self.vindex[0],self.vindex[1], self.vindex[2], 0))
307         
308         def __str__(self):
309                 return '[%d %d %d]' % (self.vindex[0],self.vindex[1], self.vindex[2])
310
311 class _3ds_array(object):
312         '''Class representing an array of variables for a 3ds file.
313
314         Consists of a _3ds_short to indicate the number of items, followed by the items themselves.
315         '''
316         __slots__ = 'values', 'size'
317         def __init__(self):
318                 self.values=[]
319                 self.size=SZ_SHORT
320         
321         # add an item:
322         def add(self,item):
323                 self.values.append(item)
324                 self.size+=item.get_size()
325         
326         def get_size(self):
327                 return self.size
328         
329         def write(self,file):
330                 _3ds_short(len(self.values)).write(file)
331                 #_3ds_int(len(self.values)).write(file)
332                 for value in self.values:
333                         value.write(file)
334         
335         # To not overwhelm the output in a dump, a _3ds_array only
336         # outputs the number of items, not all of the actual items. 
337         def __str__(self):
338                 return '(%d items)' % len(self.values)
339
340 class _3ds_named_variable(object):
341         '''Convenience class for named variables.'''
342         
343         __slots__ = 'value', 'name'
344         def __init__(self, name, val=None):
345                 self.name=name
346                 self.value=val
347         
348         def get_size(self):
349                 if (self.value==None): 
350                         return 0
351                 else:
352                         return self.value.get_size()
353         
354         def write(self, file):
355                 if (self.value!=None): 
356                         self.value.write(file)
357         
358         def dump(self,indent):
359                 if (self.value!=None):
360                         spaces=""
361                         for i in range(indent):
362                                 spaces+="  ";
363                         if (self.name!=""):
364                                 print(spaces, self.name, " = ", self.value)
365                         else:
366                                 print(spaces, "[unnamed]", " = ", self.value)
367
368
369 #the chunk class
370 class _3ds_chunk(object):
371         '''Class representing a chunk in a 3ds file.
372
373         Chunks contain zero or more variables, followed by zero or more subchunks.
374         '''
375         __slots__ = 'ID', 'size', 'variables', 'subchunks'
376         def __init__(self, id=0):
377                 self.ID=_3ds_short(id)
378                 self.size=_3ds_int(0)
379                 self.variables=[]
380                 self.subchunks=[]
381         
382         def set_ID(id):
383                 self.ID=_3ds_short(id)
384         
385         def add_variable(self, name, var):
386                 '''Add a named variable. 
387                 
388                 The name is mostly for debugging purposes.'''
389                 self.variables.append(_3ds_named_variable(name,var))
390         
391         def add_subchunk(self, chunk):
392                 '''Add a subchunk.'''
393                 self.subchunks.append(chunk)
394
395         def get_size(self):
396                 '''Calculate the size of the chunk and return it.
397                 
398                 The sizes of the variables and subchunks are used to determine this chunk\'s size.'''
399                 tmpsize=self.ID.get_size()+self.size.get_size()
400                 for variable in self.variables:
401                         tmpsize+=variable.get_size()
402                 for subchunk in self.subchunks:
403                         tmpsize+=subchunk.get_size()
404                 self.size.value=tmpsize
405                 return self.size.value
406
407         def write(self, file):
408                 '''Write the chunk to a file.
409                 
410                 Uses the write function of the variables and the subchunks to do the actual work.'''
411                 #write header
412                 self.ID.write(file)
413                 self.size.write(file)
414                 for variable in self.variables:
415                         variable.write(file)
416                 for subchunk in self.subchunks:
417                         subchunk.write(file)
418                 
419                 
420         def dump(self, indent=0):
421                 '''Write the chunk to a file.
422                 
423                 Dump is used for debugging purposes, to dump the contents of a chunk to the standard output. 
424                 Uses the dump function of the named variables and the subchunks to do the actual work.'''
425                 spaces=""
426                 for i in range(indent):
427                         spaces+="  ";
428                 print(spaces, "ID=", hex(self.ID.value), "size=", self.get_size())
429                 for variable in self.variables:
430                         variable.dump(indent+1)
431                 for subchunk in self.subchunks:
432                         subchunk.dump(indent+1)
433
434
435
436 ######################################################
437 # EXPORT
438 ######################################################
439
440 def get_material_images(material):
441         # blender utility func.
442         if material:
443                 return [s.texture.image for s in material.textures if s and s.texture.type == 'IMAGE' and s.texture.image]
444
445         return []
446 #       images = []
447 #       if material:
448 #               for mtex in material.getTextures():
449 #                       if mtex and mtex.tex.type == Blender.Texture.Types.IMAGE:
450 #                               image = mtex.tex.image
451 #                               if image:
452 #                                       images.append(image) # maye want to include info like diffuse, spec here.
453 #       return images
454
455
456 def make_material_subchunk(id, color):
457         '''Make a material subchunk.
458         
459         Used for color subchunks, such as diffuse color or ambient color subchunks.'''
460         mat_sub = _3ds_chunk(id)
461         col1 = _3ds_chunk(RGB1)
462         col1.add_variable("color1", _3ds_rgb_color(color));
463         mat_sub.add_subchunk(col1)
464 # optional:
465 #       col2 = _3ds_chunk(RGB1)
466 #       col2.add_variable("color2", _3ds_rgb_color(color));
467 #       mat_sub.add_subchunk(col2)
468         return mat_sub
469
470
471 def make_material_texture_chunk(id, images):
472         """Make Material Map texture chunk
473         """
474         mat_sub = _3ds_chunk(id)
475         
476         def add_image(img):
477                 filename = os.path.basename(image.filename)
478 #               filename = image.filename.split('\\')[-1].split('/')[-1]
479                 mat_sub_file = _3ds_chunk(MATMAPFILE)
480                 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
481                 mat_sub.add_subchunk(mat_sub_file)
482         
483         for image in images:
484                 add_image(image)
485         
486         return mat_sub
487
488 def make_material_chunk(material, image):
489         '''Make a material chunk out of a blender material.'''
490         material_chunk = _3ds_chunk(MATERIAL)
491         name = _3ds_chunk(MATNAME)
492         
493         if material:    name_str = material.name
494         else:                   name_str = 'None'
495         if image:       name_str += image.name
496                 
497         name.add_variable("name", _3ds_string(sane_name(name_str)))
498         material_chunk.add_subchunk(name)
499         
500         if not material:
501                 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0,0,0) ))
502                 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (.8, .8, .8) ))
503                 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1,1,1) ))
504         
505         else:
506                 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.ambient for a in material.diffuse_color] ))
507 #               material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.amb for a in material.rgbCol] ))
508                 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color))
509 #               material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.rgbCol))
510                 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color))
511 #               material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specCol))
512                 
513                 images = get_material_images(material) # can be None
514                 if image: images.append(image)
515                 
516                 if images:
517                         material_chunk.add_subchunk(make_material_texture_chunk(MATMAP, images))
518         
519         return material_chunk
520
521 class tri_wrapper(object):
522         '''Class representing a triangle.
523         
524         Used when converting faces to triangles'''
525         
526         __slots__ = 'vertex_index', 'mat', 'image', 'faceuvs', 'offset'
527         def __init__(self, vindex=(0,0,0), mat=None, image=None, faceuvs=None):
528                 self.vertex_index= vindex
529                 self.mat= mat
530                 self.image= image
531                 self.faceuvs= faceuvs
532                 self.offset= [0, 0, 0] # offset indicies
533
534
535 def extract_triangles(mesh):
536         '''Extract triangles from a mesh.
537         
538         If the mesh contains quads, they will be split into triangles.'''
539         tri_list = []
540         do_uv = len(mesh.uv_textures)
541 #       do_uv = mesh.faceUV
542         
543 #       if not do_uv:
544 #               face_uv = None
545         
546         img = None
547         for i, face in enumerate(mesh.faces):
548                 f_v = face.verts
549 #               f_v = face.v
550
551                 uf = mesh.active_uv_texture.data[i] if do_uv else None
552                 
553                 if do_uv:
554                         f_uv = uf.uv
555                         # f_uv =  (uf.uv1, uf.uv2, uf.uv3, uf.uv4) if face.verts[3] else (uf.uv1, uf.uv2, uf.uv3)
556 #                       f_uv = face.uv
557                         img = uf.image if uf else None
558 #                       img = face.image
559                         if img: img = img.name
560                 
561                 # if f_v[3] == 0:
562                 if len(f_v)==3:
563                         new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
564 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
565                         if (do_uv): new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
566                         tri_list.append(new_tri)
567                 
568                 else: #it's a quad
569                         new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
570 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
571                         new_tri_2 = tri_wrapper((f_v[0], f_v[2], f_v[3]), face.material_index, img)
572 #                       new_tri_2 = tri_wrapper((f_v[0].index, f_v[2].index, f_v[3].index), face.mat, img)
573                         
574                         if (do_uv):
575                                 new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
576                                 new_tri_2.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[2]), uv_key(f_uv[3])
577                         
578                         tri_list.append( new_tri )
579                         tri_list.append( new_tri_2 )
580                 
581         return tri_list
582         
583         
584 def remove_face_uv(verts, tri_list):
585         '''Remove face UV coordinates from a list of triangles.
586                 
587         Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
588         need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
589         there are multiple uv coordinates per vertex.'''
590         
591         # initialize a list of UniqueLists, one per vertex:
592         #uv_list = [UniqueList() for i in xrange(len(verts))]
593         unique_uvs= [{} for i in range(len(verts))]
594         
595         # for each face uv coordinate, add it to the UniqueList of the vertex
596         for tri in tri_list:
597                 for i in range(3):
598                         # store the index into the UniqueList for future reference:
599                         # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
600                         
601                         context_uv_vert= unique_uvs[tri.vertex_index[i]]
602                         uvkey= tri.faceuvs[i]
603                         
604                         offset_index__uv_3ds = context_uv_vert.get(uvkey)
605                         
606                         if not offset_index__uv_3ds:                            
607                                 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
608                         
609                         tri.offset[i] = offset_index__uv_3ds[0]
610                         
611                         
612                 
613         # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
614         # only once.
615         
616         # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
617         # faces refer to the new face indices:
618         vert_index = 0
619         vert_array = _3ds_array()
620         uv_array = _3ds_array()
621         index_list = []
622         for i,vert in enumerate(verts):
623                 index_list.append(vert_index)
624                 
625                 pt = _3ds_point_3d(vert.co) # reuse, should be ok
626                 uvmap = [None] * len(unique_uvs[i])
627                 for ii, uv_3ds in unique_uvs[i].values():
628                         # add a vertex duplicate to the vertex_array for every uv associated with this vertex:
629                         vert_array.add(pt)
630                         # add the uv coordinate to the uv array:
631                         # This for loop does not give uv's ordered by ii, so we create a new map
632                         # and add the uv's later
633                         # uv_array.add(uv_3ds)
634                         uvmap[ii] = uv_3ds
635                 
636                 # Add the uv's in the correct order
637                 for uv_3ds in uvmap:
638                         # add the uv coordinate to the uv array:
639                         uv_array.add(uv_3ds)
640                 
641                 vert_index += len(unique_uvs[i])
642         
643         # Make sure the triangle vertex indices now refer to the new vertex list:
644         for tri in tri_list:
645                 for i in range(3):
646                         tri.offset[i]+=index_list[tri.vertex_index[i]]
647                 tri.vertex_index= tri.offset
648         
649         return vert_array, uv_array, tri_list
650
651 def make_faces_chunk(tri_list, mesh, materialDict):
652         '''Make a chunk for the faces.
653         
654         Also adds subchunks assigning materials to all faces.'''
655         
656         materials = mesh.materials
657         if not materials:
658                 mat = None
659         
660         face_chunk = _3ds_chunk(OBJECT_FACES)
661         face_list = _3ds_array()
662         
663         
664         if len(mesh.uv_textures):
665 #       if mesh.faceUV:
666                 # Gather materials used in this mesh - mat/image pairs
667                 unique_mats = {}
668                 for i,tri in enumerate(tri_list):
669                         
670                         face_list.add(_3ds_face(tri.vertex_index))
671                         
672                         if materials:
673                                 mat = materials[tri.mat]
674                                 if mat: mat = mat.name
675                         
676                         img = tri.image
677                         
678                         try:
679                                 context_mat_face_array = unique_mats[mat, img][1]
680                         except:
681                                 
682                                 if mat: name_str = mat
683                                 else:   name_str = 'None'
684                                 if img: name_str += img
685                                 
686                                 context_mat_face_array = _3ds_array()
687                                 unique_mats[mat, img] = _3ds_string(sane_name(name_str)), context_mat_face_array
688                                 
689                         
690                         context_mat_face_array.add(_3ds_short(i))
691                         # obj_material_faces[tri.mat].add(_3ds_short(i))
692                 
693                 face_chunk.add_variable("faces", face_list)
694                 for mat_name, mat_faces in unique_mats.values():
695                         obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
696                         obj_material_chunk.add_variable("name", mat_name)
697                         obj_material_chunk.add_variable("face_list", mat_faces)
698                         face_chunk.add_subchunk(obj_material_chunk)
699                         
700         else:
701                 
702                 obj_material_faces=[]
703                 obj_material_names=[]
704                 for m in materials:
705                         if m:
706                                 obj_material_names.append(_3ds_string(sane_name(m.name)))
707                                 obj_material_faces.append(_3ds_array())
708                 n_materials = len(obj_material_names)
709                 
710                 for i,tri in enumerate(tri_list):
711                         face_list.add(_3ds_face(tri.vertex_index))
712                         if (tri.mat < n_materials):
713                                 obj_material_faces[tri.mat].add(_3ds_short(i))
714                 
715                 face_chunk.add_variable("faces", face_list)
716                 for i in range(n_materials):
717                         obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
718                         obj_material_chunk.add_variable("name", obj_material_names[i])
719                         obj_material_chunk.add_variable("face_list", obj_material_faces[i])
720                         face_chunk.add_subchunk(obj_material_chunk)
721         
722         return face_chunk
723
724 def make_vert_chunk(vert_array):
725         '''Make a vertex chunk out of an array of vertices.'''
726         vert_chunk = _3ds_chunk(OBJECT_VERTICES)
727         vert_chunk.add_variable("vertices",vert_array)
728         return vert_chunk
729
730 def make_uv_chunk(uv_array):
731         '''Make a UV chunk out of an array of UVs.'''
732         uv_chunk = _3ds_chunk(OBJECT_UV)
733         uv_chunk.add_variable("uv coords", uv_array)
734         return uv_chunk
735
736 def make_mesh_chunk(mesh, materialDict):
737         '''Make a chunk out of a Blender mesh.'''
738         
739         # Extract the triangles from the mesh:
740         tri_list = extract_triangles(mesh)
741         
742         if len(mesh.uv_textures):
743 #       if mesh.faceUV:
744                 # Remove the face UVs and convert it to vertex UV:
745                 vert_array, uv_array, tri_list = remove_face_uv(mesh.verts, tri_list)
746         else:
747                 # Add the vertices to the vertex array:
748                 vert_array = _3ds_array()
749                 for vert in mesh.verts:
750                         vert_array.add(_3ds_point_3d(vert.co))
751                 # If the mesh has vertex UVs, create an array of UVs:
752                 if len(mesh.sticky):
753 #               if mesh.vertexUV:
754                         uv_array = _3ds_array()
755                         for uv in mesh.sticky:
756 #                       for vert in mesh.verts:
757                                 uv_array.add(_3ds_point_uv(uv.co))
758 #                               uv_array.add(_3ds_point_uv(vert.uvco))
759                 else:
760                         # no UV at all:
761                         uv_array = None
762
763         # create the chunk:
764         mesh_chunk = _3ds_chunk(OBJECT_MESH)
765         
766         # add vertex chunk:
767         mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
768         # add faces chunk:
769         
770         mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
771         
772         # if available, add uv chunk:
773         if uv_array:
774                 mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
775         
776         return mesh_chunk
777
778 """ # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
779 def make_kfdata(start=0, stop=0, curtime=0):
780         '''Make the basic keyframe data chunk'''
781         kfdata = _3ds_chunk(KFDATA)
782         
783         kfhdr = _3ds_chunk(KFDATA_KFHDR)
784         kfhdr.add_variable("revision", _3ds_short(0))
785         # Not really sure what filename is used for, but it seems it is usually used
786         # to identify the program that generated the .3ds:
787         kfhdr.add_variable("filename", _3ds_string("Blender"))
788         kfhdr.add_variable("animlen", _3ds_int(stop-start))
789         
790         kfseg = _3ds_chunk(KFDATA_KFSEG)
791         kfseg.add_variable("start", _3ds_int(start))
792         kfseg.add_variable("stop", _3ds_int(stop))
793         
794         kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
795         kfcurtime.add_variable("curtime", _3ds_int(curtime))
796         
797         kfdata.add_subchunk(kfhdr)
798         kfdata.add_subchunk(kfseg)
799         kfdata.add_subchunk(kfcurtime)
800         return kfdata
801 """
802
803 """
804 def make_track_chunk(ID, obj):
805         '''Make a chunk for track data.
806         
807         Depending on the ID, this will construct a position, rotation or scale track.'''
808         track_chunk = _3ds_chunk(ID)
809         track_chunk.add_variable("track_flags", _3ds_short())
810         track_chunk.add_variable("unknown", _3ds_int())
811         track_chunk.add_variable("unknown", _3ds_int())
812         track_chunk.add_variable("nkeys", _3ds_int(1))
813         # Next section should be repeated for every keyframe, but for now, animation is not actually supported.
814         track_chunk.add_variable("tcb_frame", _3ds_int(0))
815         track_chunk.add_variable("tcb_flags", _3ds_short())
816         if obj.type=='Empty':
817                 if ID==POS_TRACK_TAG:
818                         # position vector:
819                         track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
820                 elif ID==ROT_TRACK_TAG:
821                         # rotation (quaternion, angle first, followed by axis):
822                         q = obj.getEuler().toQuat()
823                         track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
824                 elif ID==SCL_TRACK_TAG:
825                         # scale vector:
826                         track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
827         else:
828                 # meshes have their transformations applied before 
829                 # exporting, so write identity transforms here:
830                 if ID==POS_TRACK_TAG:
831                         # position vector:
832                         track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
833                 elif ID==ROT_TRACK_TAG:
834                         # rotation (quaternion, angle first, followed by axis):
835                         track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
836                 elif ID==SCL_TRACK_TAG:
837                         # scale vector:
838                         track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
839         
840         return track_chunk
841 """
842
843 """
844 def make_kf_obj_node(obj, name_to_id):
845         '''Make a node chunk for a Blender object.
846         
847         Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
848         Blender Empty objects are converted to dummy nodes.'''
849         
850         name = obj.name
851         # main object node chunk:
852         kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
853         # chunk for the object id: 
854         obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
855         # object id is from the name_to_id dictionary:
856         obj_id_chunk.add_variable("node_id", _3ds_short(name_to_id[name]))
857         
858         # object node header:
859         obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
860         # object name:
861         if obj.type == 'Empty':
862                 # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk 
863                 # for their name (see below):
864                 obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
865         else:
866                 # Add the name:
867                 obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
868         # Add Flag variables (not sure what they do):
869         obj_node_header_chunk.add_variable("flags1", _3ds_short(0))
870         obj_node_header_chunk.add_variable("flags2", _3ds_short(0))
871         
872         # Check parent-child relationships:
873         parent = obj.parent
874         if (parent == None) or (parent.name not in name_to_id):
875                 # If no parent, or the parents name is not in the name_to_id dictionary,
876                 # parent id becomes -1:
877                 obj_node_header_chunk.add_variable("parent", _3ds_short(-1))
878         else:
879                 # Get the parent's id from the name_to_id dictionary:
880                 obj_node_header_chunk.add_variable("parent", _3ds_short(name_to_id[parent.name]))
881         
882         # Add pivot chunk:
883         obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
884         obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
885         kf_obj_node.add_subchunk(obj_pivot_chunk)
886         
887         # add subchunks for object id and node header:
888         kf_obj_node.add_subchunk(obj_id_chunk)
889         kf_obj_node.add_subchunk(obj_node_header_chunk)
890
891         # Empty objects need to have an extra chunk for the instance name:
892         if obj.type == 'Empty':
893                 obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
894                 obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
895                 kf_obj_node.add_subchunk(obj_instance_name_chunk)
896         
897         # Add track chunks for position, rotation and scale:
898         kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
899         kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
900         kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
901
902         return kf_obj_node
903 """
904
905 # import BPyMessages
906 def save_3ds(filename, context):
907         '''Save the Blender scene to a 3ds file.'''
908         # Time the export
909         
910         if not filename.lower().endswith('.3ds'):
911                 filename += '.3ds'
912         
913         # XXX
914 #       if not BPyMessages.Warning_SaveOver(filename):
915 #               return
916         
917         # XXX
918         time1 = time.clock()
919 #       time1= Blender.sys.time()
920 #       Blender.Window.WaitCursor(1)
921
922         sce = context.scene
923 #       sce= bpy.data.scenes.active
924         
925         # Initialize the main chunk (primary):
926         primary = _3ds_chunk(PRIMARY)
927         # Add version chunk:
928         version_chunk = _3ds_chunk(VERSION)
929         version_chunk.add_variable("version", _3ds_int(3))
930         primary.add_subchunk(version_chunk)
931         
932         # init main object info chunk:
933         object_info = _3ds_chunk(OBJECTINFO)
934         
935         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
936         # init main key frame data chunk:
937         kfdata = make_kfdata()
938         '''
939         
940         # Get all the supported objects selected in this scene:
941         # ob_sel= list(sce.objects.context)
942         # mesh_objects = [ (ob, me) for ob in ob_sel   for me in (BPyMesh.getMeshFromObject(ob, None, True, False, sce),) if me ]
943         # empty_objects = [ ob for ob in ob_sel if ob.type == 'Empty' ]
944         
945         # Make a list of all materials used in the selected meshes (use a dictionary,
946         # each material is added once):
947         materialDict = {}
948         mesh_objects = []
949         for ob in [ob for ob in context.scene.objects if ob.is_visible()]:
950 #       for ob in sce.objects.context:
951
952                 # get derived objects
953                 free, derived = create_derived_objects(ob)
954
955                 if derived == None: continue
956
957                 for ob_derived, mat in derived:
958 #               for ob_derived, mat in getDerivedObjects(ob, False):
959
960                         if ob.type not in ('MESH', 'CURVE', 'SURFACE', 'TEXT', 'META'):
961                                 continue
962
963                         data = ob_derived.create_mesh(True, 'PREVIEW')
964 #                       data = getMeshFromObject(ob_derived, None, True, False, sce)
965                         if data:
966                                 data.transform(mat)
967 #                               data.transform(mat, recalc_normals=False)
968                                 mesh_objects.append((ob_derived, data))
969                                 mat_ls = data.materials
970                                 mat_ls_len = len(mat_ls)
971
972                                 # get material/image tuples.
973                                 if len(data.uv_textures):
974 #                               if data.faceUV:
975                                         if not mat_ls:
976                                                 mat = mat_name = None
977                                         
978                                         for f, uf in zip(data.faces, data.active_uv_texture.data):
979                                                 if mat_ls:
980                                                         mat_index = f.material_index
981 #                                                       mat_index = f.mat
982                                                         if mat_index >= mat_ls_len:
983                                                                 mat_index = f.mat = 0
984                                                         mat = mat_ls[mat_index]
985                                                         if mat: mat_name = mat.name
986                                                         else:   mat_name = None
987                                                 # else there alredy set to none
988                                                         
989                                                 img = uf.image
990 #                                               img = f.image
991                                                 if img: img_name = img.name
992                                                 else:   img_name = None
993                                                 
994                                                 materialDict.setdefault((mat_name, img_name), (mat, img) )
995                                                 
996                                         
997                                 else:
998                                         for mat in mat_ls:
999                                                 if mat: # material may be None so check its not.
1000                                                         materialDict.setdefault((mat.name, None), (mat, None) )
1001                                         
1002                                         # Why 0 Why!
1003                                         for f in data.faces:
1004                                                 if f.material_index >= mat_ls_len:
1005 #                                               if f.mat >= mat_ls_len:
1006                                                         f.material_index = 0
1007                                                         # f.mat = 0
1008
1009                 if free:
1010                         free_derived_objects(ob)
1011
1012         
1013         # Make material chunks for all materials used in the meshes:
1014         for mat_and_image in materialDict.values():
1015                 object_info.add_subchunk(make_material_chunk(mat_and_image[0], mat_and_image[1]))
1016         
1017         # Give all objects a unique ID and build a dictionary from object name to object id:
1018         """
1019         name_to_id = {}
1020         for ob, data in mesh_objects:
1021                 name_to_id[ob.name]= len(name_to_id)
1022         #for ob in empty_objects:
1023         #       name_to_id[ob.name]= len(name_to_id)
1024         """
1025         
1026         # Create object chunks for all meshes:
1027         i = 0
1028         for ob, blender_mesh in mesh_objects:
1029                 # create a new object chunk
1030                 object_chunk = _3ds_chunk(OBJECT)
1031                 
1032                 # set the object name
1033                 object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1034                 
1035                 # make a mesh chunk out of the mesh:
1036                 object_chunk.add_subchunk(make_mesh_chunk(blender_mesh, materialDict))
1037                 object_info.add_subchunk(object_chunk)
1038                 
1039                 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1040                 # make a kf object node for the object:
1041                 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1042                 '''
1043 #               if not blender_mesh.users:
1044                 bpy.data.remove_mesh(blender_mesh)
1045 #               blender_mesh.verts = None
1046
1047                 i+=i
1048
1049         # Create chunks for all empties:
1050         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1051         for ob in empty_objects:
1052                 # Empties only require a kf object node:
1053                 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1054                 pass
1055         '''
1056         
1057         # Add main object info chunk to primary chunk:
1058         primary.add_subchunk(object_info)
1059         
1060         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1061         # Add main keyframe data chunk to primary chunk:
1062         primary.add_subchunk(kfdata)
1063         '''
1064         
1065         # At this point, the chunk hierarchy is completely built.
1066         
1067         # Check the size:
1068         primary.get_size()
1069         # Open the file for writing:
1070         file = open( filename, 'wb' )
1071         
1072         # Recursively write the chunks to file:
1073         primary.write(file)
1074         
1075         # Close the file:
1076         file.close()
1077         
1078         # Debugging only: report the exporting time:
1079 #       Blender.Window.WaitCursor(0)
1080         print("3ds export time: %.2f" % (time.clock() - time1))
1081 #       print("3ds export time: %.2f" % (Blender.sys.time() - time1))
1082         
1083         # Debugging only: dump the chunk hierarchy:
1084         #primary.dump()
1085
1086
1087 # if __name__=='__main__':
1088 #     if struct:
1089 #         Blender.Window.FileSelector(save_3ds, "Export 3DS", Blender.sys.makename(ext='.3ds'))
1090 #     else:
1091 #         Blender.Draw.PupMenu("Error%t|This script requires a full python installation")
1092 # # save_3ds('/test_b.3ds')
1093
1094 class EXPORT_OT_autodesk_3ds(bpy.types.Operator):
1095         '''Export to 3DS file format (.3ds).'''
1096         bl_idname = "export.autodesk_3ds"
1097         bl_label = 'Export 3DS'
1098         
1099         # List of operator properties, the attributes will be assigned
1100         # to the class instance from the operator settings before calling.
1101
1102         bl_props = [
1103                 # bpy.props.StringProperty(attr="filename", name="File Name", description="File name used for exporting the 3DS file", maxlen= 1024, default= ""),
1104                 bpy.props.StringProperty(attr="path", name="File Path", description="File path used for exporting the 3DS file", maxlen= 1024, default= ""),
1105         ]
1106         
1107         def execute(self, context):
1108                 save_3ds(self.path, context)
1109                 return ('FINISHED',)
1110         
1111         def invoke(self, context, event):
1112                 wm = context.manager
1113                 wm.add_fileselect(self.__operator__)
1114                 return ('RUNNING_MODAL',)
1115         
1116         def poll(self, context): # Poll isnt working yet
1117                 print("Poll")
1118                 return context.active_object != None
1119
1120 bpy.ops.add(EXPORT_OT_autodesk_3ds)
1121
1122 # Add to a menu
1123 import dynamic_menu
1124 menu_func = lambda self, context: self.layout.itemO("export.autodesk_3ds", text="Autodesk 3DS...")
1125 menu_item = dynamic_menu.add(bpy.types.INFO_MT_file_export, menu_func)