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