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