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