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