change python scripts so modules which register with blender have a register() functi...
[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(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()
83         return True, [(dob.object, dob.matrix) for dob in ob.dupli_list]
84     else:
85         return False, [(ob, ob.matrix)]
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.filename)
498 #               filename = image.filename.split('\\')[-1].split('/')[-1]
499         mat_sub_file = _3ds_chunk(MATMAPFILE)
500         mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
501         mat_sub.add_subchunk(mat_sub_file)
502
503     for image in images:
504         add_image(image)
505
506     return mat_sub
507
508 def make_material_chunk(material, image):
509     '''Make a material chunk out of a blender material.'''
510     material_chunk = _3ds_chunk(MATERIAL)
511     name = _3ds_chunk(MATNAME)
512
513     if material:        name_str = material.name
514     else:                       name_str = 'None'
515     if image:   name_str += image.name
516
517     name.add_variable("name", _3ds_string(sane_name(name_str)))
518     material_chunk.add_subchunk(name)
519
520     if not material:
521         material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0,0,0) ))
522         material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (.8, .8, .8) ))
523         material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1,1,1) ))
524
525     else:
526         material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.ambient for a in material.diffuse_color] ))
527 #               material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, [a*material.amb for a in material.rgbCol] ))
528         material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color))
529 #               material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.rgbCol))
530         material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color))
531 #               material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specCol))
532
533         images = get_material_images(material) # can be None
534         if image: images.append(image)
535
536         if images:
537             material_chunk.add_subchunk(make_material_texture_chunk(MATMAP, images))
538
539     return material_chunk
540
541 class tri_wrapper(object):
542     '''Class representing a triangle.
543
544     Used when converting faces to triangles'''
545
546     __slots__ = 'vertex_index', 'mat', 'image', 'faceuvs', 'offset'
547     def __init__(self, vindex=(0,0,0), mat=None, image=None, faceuvs=None):
548         self.vertex_index= vindex
549         self.mat= mat
550         self.image= image
551         self.faceuvs= faceuvs
552         self.offset= [0, 0, 0] # offset indicies
553
554
555 def extract_triangles(mesh):
556     '''Extract triangles from a mesh.
557
558     If the mesh contains quads, they will be split into triangles.'''
559     tri_list = []
560     do_uv = len(mesh.uv_textures)
561 #       do_uv = mesh.faceUV
562
563 #       if not do_uv:
564 #               face_uv = None
565
566     img = None
567     for i, face in enumerate(mesh.faces):
568         f_v = face.verts
569 #               f_v = face.v
570
571         uf = mesh.active_uv_texture.data[i] if do_uv else None
572
573         if do_uv:
574             f_uv = uf.uv
575             # f_uv =  (uf.uv1, uf.uv2, uf.uv3, uf.uv4) if face.verts[3] else (uf.uv1, uf.uv2, uf.uv3)
576 #                       f_uv = face.uv
577             img = uf.image if uf else None
578 #                       img = face.image
579             if img: img = img.name
580
581         # if f_v[3] == 0:
582         if len(f_v)==3:
583             new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
584 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
585             if (do_uv): new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
586             tri_list.append(new_tri)
587
588         else: #it's a quad
589             new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
590 #                       new_tri = tri_wrapper((f_v[0].index, f_v[1].index, f_v[2].index), face.mat, img)
591             new_tri_2 = tri_wrapper((f_v[0], f_v[2], f_v[3]), face.material_index, img)
592 #                       new_tri_2 = tri_wrapper((f_v[0].index, f_v[2].index, f_v[3].index), face.mat, img)
593
594             if (do_uv):
595                 new_tri.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
596                 new_tri_2.faceuvs= uv_key(f_uv[0]), uv_key(f_uv[2]), uv_key(f_uv[3])
597
598             tri_list.append( new_tri )
599             tri_list.append( new_tri_2 )
600
601     return tri_list
602
603
604 def remove_face_uv(verts, tri_list):
605     '''Remove face UV coordinates from a list of triangles.
606
607     Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
608     need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
609     there are multiple uv coordinates per vertex.'''
610
611     # initialize a list of UniqueLists, one per vertex:
612     #uv_list = [UniqueList() for i in xrange(len(verts))]
613     unique_uvs= [{} for i in range(len(verts))]
614
615     # for each face uv coordinate, add it to the UniqueList of the vertex
616     for tri in tri_list:
617         for i in range(3):
618             # store the index into the UniqueList for future reference:
619             # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
620
621             context_uv_vert= unique_uvs[tri.vertex_index[i]]
622             uvkey= tri.faceuvs[i]
623
624             offset_index__uv_3ds = context_uv_vert.get(uvkey)
625
626             if not offset_index__uv_3ds:
627                 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
628
629             tri.offset[i] = offset_index__uv_3ds[0]
630
631
632
633     # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
634     # only once.
635
636     # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
637     # faces refer to the new face indices:
638     vert_index = 0
639     vert_array = _3ds_array()
640     uv_array = _3ds_array()
641     index_list = []
642     for i,vert in enumerate(verts):
643         index_list.append(vert_index)
644
645         pt = _3ds_point_3d(vert.co) # reuse, should be ok
646         uvmap = [None] * len(unique_uvs[i])
647         for ii, uv_3ds in unique_uvs[i].values():
648             # add a vertex duplicate to the vertex_array for every uv associated with this vertex:
649             vert_array.add(pt)
650             # add the uv coordinate to the uv array:
651             # This for loop does not give uv's ordered by ii, so we create a new map
652             # and add the uv's later
653             # uv_array.add(uv_3ds)
654             uvmap[ii] = uv_3ds
655
656         # Add the uv's in the correct order
657         for uv_3ds in uvmap:
658             # add the uv coordinate to the uv array:
659             uv_array.add(uv_3ds)
660
661         vert_index += len(unique_uvs[i])
662
663     # Make sure the triangle vertex indices now refer to the new vertex list:
664     for tri in tri_list:
665         for i in range(3):
666             tri.offset[i]+=index_list[tri.vertex_index[i]]
667         tri.vertex_index= tri.offset
668
669     return vert_array, uv_array, tri_list
670
671 def make_faces_chunk(tri_list, mesh, materialDict):
672     '''Make a chunk for the faces.
673
674     Also adds subchunks assigning materials to all faces.'''
675
676     materials = mesh.materials
677     if not materials:
678         mat = None
679
680     face_chunk = _3ds_chunk(OBJECT_FACES)
681     face_list = _3ds_array()
682
683
684     if len(mesh.uv_textures):
685 #       if mesh.faceUV:
686         # Gather materials used in this mesh - mat/image pairs
687         unique_mats = {}
688         for i,tri in enumerate(tri_list):
689
690             face_list.add(_3ds_face(tri.vertex_index))
691
692             if materials:
693                 mat = materials[tri.mat]
694                 if mat: mat = mat.name
695
696             img = tri.image
697
698             try:
699                 context_mat_face_array = unique_mats[mat, img][1]
700             except:
701
702                 if mat: name_str = mat
703                 else:   name_str = 'None'
704                 if img: name_str += img
705
706                 context_mat_face_array = _3ds_array()
707                 unique_mats[mat, img] = _3ds_string(sane_name(name_str)), context_mat_face_array
708
709
710             context_mat_face_array.add(_3ds_short(i))
711             # obj_material_faces[tri.mat].add(_3ds_short(i))
712
713         face_chunk.add_variable("faces", face_list)
714         for mat_name, mat_faces in unique_mats.values():
715             obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
716             obj_material_chunk.add_variable("name", mat_name)
717             obj_material_chunk.add_variable("face_list", mat_faces)
718             face_chunk.add_subchunk(obj_material_chunk)
719
720     else:
721
722         obj_material_faces=[]
723         obj_material_names=[]
724         for m in materials:
725             if m:
726                 obj_material_names.append(_3ds_string(sane_name(m.name)))
727                 obj_material_faces.append(_3ds_array())
728         n_materials = len(obj_material_names)
729
730         for i,tri in enumerate(tri_list):
731             face_list.add(_3ds_face(tri.vertex_index))
732             if (tri.mat < n_materials):
733                 obj_material_faces[tri.mat].add(_3ds_short(i))
734
735         face_chunk.add_variable("faces", face_list)
736         for i in range(n_materials):
737             obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL)
738             obj_material_chunk.add_variable("name", obj_material_names[i])
739             obj_material_chunk.add_variable("face_list", obj_material_faces[i])
740             face_chunk.add_subchunk(obj_material_chunk)
741
742     return face_chunk
743
744 def make_vert_chunk(vert_array):
745     '''Make a vertex chunk out of an array of vertices.'''
746     vert_chunk = _3ds_chunk(OBJECT_VERTICES)
747     vert_chunk.add_variable("vertices",vert_array)
748     return vert_chunk
749
750 def make_uv_chunk(uv_array):
751     '''Make a UV chunk out of an array of UVs.'''
752     uv_chunk = _3ds_chunk(OBJECT_UV)
753     uv_chunk.add_variable("uv coords", uv_array)
754     return uv_chunk
755
756 def make_mesh_chunk(mesh, materialDict):
757     '''Make a chunk out of a Blender mesh.'''
758
759     # Extract the triangles from the mesh:
760     tri_list = extract_triangles(mesh)
761
762     if len(mesh.uv_textures):
763 #       if mesh.faceUV:
764         # Remove the face UVs and convert it to vertex UV:
765         vert_array, uv_array, tri_list = remove_face_uv(mesh.verts, tri_list)
766     else:
767         # Add the vertices to the vertex array:
768         vert_array = _3ds_array()
769         for vert in mesh.verts:
770             vert_array.add(_3ds_point_3d(vert.co))
771         # If the mesh has vertex UVs, create an array of UVs:
772         if len(mesh.sticky):
773 #               if mesh.vertexUV:
774             uv_array = _3ds_array()
775             for uv in mesh.sticky:
776 #                       for vert in mesh.verts:
777                 uv_array.add(_3ds_point_uv(uv.co))
778 #                               uv_array.add(_3ds_point_uv(vert.uvco))
779         else:
780             # no UV at all:
781             uv_array = None
782
783     # create the chunk:
784     mesh_chunk = _3ds_chunk(OBJECT_MESH)
785
786     # add vertex chunk:
787     mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
788     # add faces chunk:
789
790     mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
791
792     # if available, add uv chunk:
793     if uv_array:
794         mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
795
796     return mesh_chunk
797
798 """ # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
799 def make_kfdata(start=0, stop=0, curtime=0):
800     '''Make the basic keyframe data chunk'''
801     kfdata = _3ds_chunk(KFDATA)
802
803     kfhdr = _3ds_chunk(KFDATA_KFHDR)
804     kfhdr.add_variable("revision", _3ds_short(0))
805     # Not really sure what filename is used for, but it seems it is usually used
806     # to identify the program that generated the .3ds:
807     kfhdr.add_variable("filename", _3ds_string("Blender"))
808     kfhdr.add_variable("animlen", _3ds_int(stop-start))
809
810     kfseg = _3ds_chunk(KFDATA_KFSEG)
811     kfseg.add_variable("start", _3ds_int(start))
812     kfseg.add_variable("stop", _3ds_int(stop))
813
814     kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
815     kfcurtime.add_variable("curtime", _3ds_int(curtime))
816
817     kfdata.add_subchunk(kfhdr)
818     kfdata.add_subchunk(kfseg)
819     kfdata.add_subchunk(kfcurtime)
820     return kfdata
821 """
822
823 """
824 def make_track_chunk(ID, obj):
825     '''Make a chunk for track data.
826
827     Depending on the ID, this will construct a position, rotation or scale track.'''
828     track_chunk = _3ds_chunk(ID)
829     track_chunk.add_variable("track_flags", _3ds_short())
830     track_chunk.add_variable("unknown", _3ds_int())
831     track_chunk.add_variable("unknown", _3ds_int())
832     track_chunk.add_variable("nkeys", _3ds_int(1))
833     # Next section should be repeated for every keyframe, but for now, animation is not actually supported.
834     track_chunk.add_variable("tcb_frame", _3ds_int(0))
835     track_chunk.add_variable("tcb_flags", _3ds_short())
836     if obj.type=='Empty':
837         if ID==POS_TRACK_TAG:
838             # position vector:
839             track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
840         elif ID==ROT_TRACK_TAG:
841             # rotation (quaternion, angle first, followed by axis):
842             q = obj.getEuler().to_quat()
843             track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
844         elif ID==SCL_TRACK_TAG:
845             # scale vector:
846             track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
847     else:
848         # meshes have their transformations applied before
849         # exporting, so write identity transforms here:
850         if ID==POS_TRACK_TAG:
851             # position vector:
852             track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
853         elif ID==ROT_TRACK_TAG:
854             # rotation (quaternion, angle first, followed by axis):
855             track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
856         elif ID==SCL_TRACK_TAG:
857             # scale vector:
858             track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
859
860     return track_chunk
861 """
862
863 """
864 def make_kf_obj_node(obj, name_to_id):
865     '''Make a node chunk for a Blender object.
866
867     Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
868     Blender Empty objects are converted to dummy nodes.'''
869
870     name = obj.name
871     # main object node chunk:
872     kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
873     # chunk for the object id:
874     obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
875     # object id is from the name_to_id dictionary:
876     obj_id_chunk.add_variable("node_id", _3ds_short(name_to_id[name]))
877
878     # object node header:
879     obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
880     # object name:
881     if obj.type == 'Empty':
882         # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk
883         # for their name (see below):
884         obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
885     else:
886         # Add the name:
887         obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
888     # Add Flag variables (not sure what they do):
889     obj_node_header_chunk.add_variable("flags1", _3ds_short(0))
890     obj_node_header_chunk.add_variable("flags2", _3ds_short(0))
891
892     # Check parent-child relationships:
893     parent = obj.parent
894     if (parent == None) or (parent.name not in name_to_id):
895         # If no parent, or the parents name is not in the name_to_id dictionary,
896         # parent id becomes -1:
897         obj_node_header_chunk.add_variable("parent", _3ds_short(-1))
898     else:
899         # Get the parent's id from the name_to_id dictionary:
900         obj_node_header_chunk.add_variable("parent", _3ds_short(name_to_id[parent.name]))
901
902     # Add pivot chunk:
903     obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
904     obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
905     kf_obj_node.add_subchunk(obj_pivot_chunk)
906
907     # add subchunks for object id and node header:
908     kf_obj_node.add_subchunk(obj_id_chunk)
909     kf_obj_node.add_subchunk(obj_node_header_chunk)
910
911     # Empty objects need to have an extra chunk for the instance name:
912     if obj.type == 'Empty':
913         obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
914         obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
915         kf_obj_node.add_subchunk(obj_instance_name_chunk)
916
917     # Add track chunks for position, rotation and scale:
918     kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
919     kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
920     kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
921
922     return kf_obj_node
923 """
924
925 # import BPyMessages
926 def save_3ds(filename, context):
927     '''Save the Blender scene to a 3ds file.'''
928     # Time the export
929
930     if not filename.lower().endswith('.3ds'):
931         filename += '.3ds'
932
933     # XXX
934 #       if not BPyMessages.Warning_SaveOver(filename):
935 #               return
936
937     # XXX
938     time1 = time.clock()
939 #       time1= Blender.sys.time()
940 #       Blender.Window.WaitCursor(1)
941
942     sce = context.scene
943 #       sce= bpy.data.scenes.active
944
945     # Initialize the main chunk (primary):
946     primary = _3ds_chunk(PRIMARY)
947     # Add version chunk:
948     version_chunk = _3ds_chunk(VERSION)
949     version_chunk.add_variable("version", _3ds_int(3))
950     primary.add_subchunk(version_chunk)
951
952     # init main object info chunk:
953     object_info = _3ds_chunk(OBJECTINFO)
954
955     ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
956     # init main key frame data chunk:
957     kfdata = make_kfdata()
958     '''
959
960     # Get all the supported objects selected in this scene:
961     # ob_sel= list(sce.objects.context)
962     # mesh_objects = [ (ob, me) for ob in ob_sel   for me in (BPyMesh.getMeshFromObject(ob, None, True, False, sce),) if me ]
963     # empty_objects = [ ob for ob in ob_sel if ob.type == 'Empty' ]
964
965     # Make a list of all materials used in the selected meshes (use a dictionary,
966     # each material is added once):
967     materialDict = {}
968     mesh_objects = []
969     for ob in [ob for ob in context.scene.objects if ob.is_visible()]:
970 #       for ob in sce.objects.context:
971
972         # get derived objects
973         free, derived = create_derived_objects(ob)
974
975         if derived == None: continue
976
977         for ob_derived, mat in derived:
978 #               for ob_derived, mat in getDerivedObjects(ob, False):
979
980             if ob.type not in ('MESH', 'CURVE', 'SURFACE', 'TEXT', 'META'):
981                 continue
982
983             data = ob_derived.create_mesh(True, 'PREVIEW')
984 #                       data = getMeshFromObject(ob_derived, None, True, False, sce)
985             if data:
986                 data.transform(mat)
987 #                               data.transform(mat, recalc_normals=False)
988                 mesh_objects.append((ob_derived, data))
989                 mat_ls = data.materials
990                 mat_ls_len = len(mat_ls)
991
992                 # get material/image tuples.
993                 if len(data.uv_textures):
994 #                               if data.faceUV:
995                     if not mat_ls:
996                         mat = mat_name = None
997
998                     for f, uf in zip(data.faces, data.active_uv_texture.data):
999                         if mat_ls:
1000                             mat_index = f.material_index
1001 #                                                       mat_index = f.mat
1002                             if mat_index >= mat_ls_len:
1003                                 mat_index = f.mat = 0
1004                             mat = mat_ls[mat_index]
1005                             if mat:     mat_name = mat.name
1006                             else:       mat_name = None
1007                         # else there alredy set to none
1008
1009                         img = uf.image
1010 #                                               img = f.image
1011                         if img: img_name = img.name
1012                         else:   img_name = None
1013
1014                         materialDict.setdefault((mat_name, img_name), (mat, img) )
1015
1016
1017                 else:
1018                     for mat in mat_ls:
1019                         if mat: # material may be None so check its not.
1020                             materialDict.setdefault((mat.name, None), (mat, None) )
1021
1022                     # Why 0 Why!
1023                     for f in data.faces:
1024                         if f.material_index >= mat_ls_len:
1025 #                                               if f.mat >= mat_ls_len:
1026                             f.material_index = 0
1027                             # f.mat = 0
1028
1029         if free:
1030             free_derived_objects(ob)
1031
1032
1033     # Make material chunks for all materials used in the meshes:
1034     for mat_and_image in materialDict.values():
1035         object_info.add_subchunk(make_material_chunk(mat_and_image[0], mat_and_image[1]))
1036
1037     # Give all objects a unique ID and build a dictionary from object name to object id:
1038     """
1039     name_to_id = {}
1040     for ob, data in mesh_objects:
1041         name_to_id[ob.name]= len(name_to_id)
1042     #for ob in empty_objects:
1043     #   name_to_id[ob.name]= len(name_to_id)
1044     """
1045
1046     # Create object chunks for all meshes:
1047     i = 0
1048     for ob, blender_mesh in mesh_objects:
1049         # create a new object chunk
1050         object_chunk = _3ds_chunk(OBJECT)
1051
1052         # set the object name
1053         object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1054
1055         # make a mesh chunk out of the mesh:
1056         object_chunk.add_subchunk(make_mesh_chunk(blender_mesh, materialDict))
1057         object_info.add_subchunk(object_chunk)
1058
1059         ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1060         # make a kf object node for the object:
1061         kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1062         '''
1063         if not blender_mesh.users:
1064             bpy.data.meshes.remove(blender_mesh)
1065 #               blender_mesh.verts = None
1066
1067         i+=i
1068
1069     # Create chunks for all empties:
1070     ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1071     for ob in empty_objects:
1072         # Empties only require a kf object node:
1073         kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1074         pass
1075     '''
1076
1077     # Add main object info chunk to primary chunk:
1078     primary.add_subchunk(object_info)
1079
1080     ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1081     # Add main keyframe data chunk to primary chunk:
1082     primary.add_subchunk(kfdata)
1083     '''
1084
1085     # At this point, the chunk hierarchy is completely built.
1086
1087     # Check the size:
1088     primary.get_size()
1089     # Open the file for writing:
1090     file = open( filename, 'wb' )
1091
1092     # Recursively write the chunks to file:
1093     primary.write(file)
1094
1095     # Close the file:
1096     file.close()
1097
1098     # Debugging only: report the exporting time:
1099 #       Blender.Window.WaitCursor(0)
1100     print("3ds export time: %.2f" % (time.clock() - time1))
1101 #       print("3ds export time: %.2f" % (Blender.sys.time() - time1))
1102
1103     # Debugging only: dump the chunk hierarchy:
1104     #primary.dump()
1105
1106
1107 # if __name__=='__main__':
1108 #     if struct:
1109 #         Blender.Window.FileSelector(save_3ds, "Export 3DS", Blender.sys.makename(ext='.3ds'))
1110 #     else:
1111 #         Blender.Draw.PupMenu("Error%t|This script requires a full python installation")
1112 # # save_3ds('/test_b.3ds')
1113 from bpy.props import *
1114 class Export3DS(bpy.types.Operator):
1115     '''Export to 3DS file format (.3ds).'''
1116     bl_idname = "export.autodesk_3ds"
1117     bl_label = 'Export 3DS'
1118
1119     # List of operator properties, the attributes will be assigned
1120     # to the class instance from the operator settings before calling.
1121
1122
1123     # filename = StringProperty(name="File Name", description="File name used for exporting the 3DS file", maxlen= 1024, default= ""),
1124     path = StringProperty(name="File Path", description="File path used for exporting the 3DS file", maxlen= 1024, default= "")
1125     check_existing = BoolProperty(name="Check Existing", description="Check and warn on overwriting existing files", default=True, options={'HIDDEN'})
1126
1127     def execute(self, context):
1128         save_3ds(self.properties.path, 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     def poll(self, context): # Poll isnt working yet
1137         return context.active_object != None
1138
1139
1140 # Add to a menu
1141 def menu_func(self, context):
1142     default_path = bpy.data.filename.replace(".blend", ".3ds")
1143     self.layout.operator(Export3DS.bl_idname, text="Autodesk 3DS...").path = default_path
1144
1145
1146 def register():
1147     bpy.types.register(Export3DS)
1148     bpy.types.INFO_MT_file_export.append(menu_func)
1149
1150 def unregister():
1151     bpy.types.unregister(Export3DS)
1152     bpy.types.INFO_MT_file_export.remove(menu_func)