rename vrml2 export operator.
[blender-addons.git] / io_scene_3ds / export_3ds.py
blobe7f624ac745e0d1a0db270c7d92980bf34db4dbc
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 # Script copyright (C) Bob Holcomb
22 # Contributors: Campbell Barton, Bob Holcomb, Richard Lärkäng, Damien McGinnes, Mark Stijnman
24 """
25 Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
26 from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
27 """
29 ######################################################
30 # Data Structures
31 ######################################################
33 #Some of the chunks that we will export
34 #----- Primary Chunk, at the beginning of each file
35 PRIMARY = 0x4D4D
37 #------ Main Chunks
38 OBJECTINFO = 0x3D3D # This gives the version of the mesh and is found right before the material and object information
39 VERSION = 0x0002 # This gives the version of the .3ds file
40 KFDATA = 0xB000 # This is the header for all of the key frame info
42 #------ sub defines of OBJECTINFO
43 MATERIAL = 45055 # 0xAFFF // This stored the texture info
44 OBJECT = 16384 # 0x4000 // This stores the faces, vertices, etc...
46 #>------ sub defines of MATERIAL
47 MATNAME = 0xA000 # This holds the material name
48 MATAMBIENT = 0xA010 # Ambient color of the object/material
49 MATDIFFUSE = 0xA020 # This holds the color of the object/material
50 MATSPECULAR = 0xA030 # SPecular color of the object/material
51 MATSHINESS = 0xA040 # ??
53 MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture
54 MAT_OPACMAP = 0xA210 # head for opacity map
55 MAT_BUMPMAP = 0xA230 # read for normal map
56 MAT_SPECMAP = 0xA204 # read for specularity map
58 #>------ sub defines of MAT_???MAP
59 MATMAPFILE = 0xA300 # This holds the file name of a texture
61 MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag
62 MAT_MAP_USCALE = 0xA354 # U axis scaling
63 MAT_MAP_VSCALE = 0xA356 # V axis scaling
64 MAT_MAP_UOFFSET = 0xA358 # U axis offset
65 MAT_MAP_VOFFSET = 0xA35A # V axis offset
66 MAT_MAP_ANG = 0xA35C # UV rotation around the z-axis in rad
68 RGB1 = 0x0011
69 RGB2 = 0x0012
71 #>------ sub defines of OBJECT
72 OBJECT_MESH = 0x4100 # This lets us know that we are reading a new object
73 OBJECT_LIGHT = 0x4600 # This lets un know we are reading a light object
74 OBJECT_CAMERA = 0x4700 # This lets un know we are reading a camera object
76 #>------ sub defines of CAMERA
77 OBJECT_CAM_RANGES = 0x4720 # The camera range values
79 #>------ sub defines of OBJECT_MESH
80 OBJECT_VERTICES = 0x4110 # The objects vertices
81 OBJECT_FACES = 0x4120 # The objects faces
82 OBJECT_MATERIAL = 0x4130 # This is found if the object has a material, either texture map or color
83 OBJECT_UV = 0x4140 # The UV texture coordinates
84 OBJECT_TRANS_MATRIX = 0x4160 # The Object Matrix
86 #>------ sub defines of KFDATA
87 KFDATA_KFHDR = 0xB00A
88 KFDATA_KFSEG = 0xB008
89 KFDATA_KFCURTIME = 0xB009
90 KFDATA_OBJECT_NODE_TAG = 0xB002
92 #>------ sub defines of OBJECT_NODE_TAG
93 OBJECT_NODE_ID = 0xB030
94 OBJECT_NODE_HDR = 0xB010
95 OBJECT_PIVOT = 0xB013
96 OBJECT_INSTANCE_NAME = 0xB011
97 POS_TRACK_TAG = 0xB020
98 ROT_TRACK_TAG = 0xB021
99 SCL_TRACK_TAG = 0xB022
101 import struct
103 # So 3ds max can open files, limit names to 12 in length
104 # this is verry annoying for filenames!
105 name_unique = [] # stores str, ascii only
106 name_mapping = {} # stores {orig: byte} mapping
109 def sane_name(name):
110 name_fixed = name_mapping.get(name)
111 if name_fixed is not None:
112 return name_fixed
114 # strip non ascii chars
115 new_name_clean = new_name = name.encode("ASCII", "replace").decode("ASCII")[:12]
116 i = 0
118 while new_name in name_unique:
119 new_name = new_name_clean + ".%.3d" % i
120 i += 1
122 # note, appending the 'str' version.
123 name_unique.append(new_name)
124 name_mapping[name] = new_name = new_name.encode("ASCII", "replace")
125 return new_name
128 def uv_key(uv):
129 return round(uv[0], 6), round(uv[1], 6)
131 # size defines:
132 SZ_SHORT = 2
133 SZ_INT = 4
134 SZ_FLOAT = 4
137 class _3ds_ushort(object):
138 """Class representing a short (2-byte integer) for a 3ds file.
139 *** This looks like an unsigned short H is unsigned from the struct docs - Cam***"""
140 __slots__ = ("value", )
142 def __init__(self, val=0):
143 self.value = val
145 def get_size(self):
146 return SZ_SHORT
148 def write(self, file):
149 file.write(struct.pack("<H", self.value))
151 def __str__(self):
152 return str(self.value)
155 class _3ds_uint(object):
156 """Class representing an int (4-byte integer) for a 3ds file."""
157 __slots__ = ("value", )
159 def __init__(self, val):
160 self.value = val
162 def get_size(self):
163 return SZ_INT
165 def write(self, file):
166 file.write(struct.pack("<I", self.value))
168 def __str__(self):
169 return str(self.value)
172 class _3ds_float(object):
173 """Class representing a 4-byte IEEE floating point number for a 3ds file."""
174 __slots__ = ("value", )
176 def __init__(self, val):
177 self.value = val
179 def get_size(self):
180 return SZ_FLOAT
182 def write(self, file):
183 file.write(struct.pack("<f", self.value))
185 def __str__(self):
186 return str(self.value)
189 class _3ds_string(object):
190 """Class representing a zero-terminated string for a 3ds file."""
191 __slots__ = ("value", )
193 def __init__(self, val):
194 assert(type(val) == bytes)
195 self.value = val
197 def get_size(self):
198 return (len(self.value) + 1)
200 def write(self, file):
201 binary_format = "<%ds" % (len(self.value) + 1)
202 file.write(struct.pack(binary_format, self.value))
204 def __str__(self):
205 return self.value
208 class _3ds_point_3d(object):
209 """Class representing a three-dimensional point for a 3ds file."""
210 __slots__ = "x", "y", "z"
212 def __init__(self, point):
213 self.x, self.y, self.z = point
215 def get_size(self):
216 return 3 * SZ_FLOAT
218 def write(self, file):
219 file.write(struct.pack('<3f', self.x, self.y, self.z))
221 def __str__(self):
222 return '(%f, %f, %f)' % (self.x, self.y, self.z)
224 # Used for writing a track
226 class _3ds_point_4d(object):
227 """Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
228 __slots__ = "x","y","z","w"
229 def __init__(self, point=(0.0,0.0,0.0,0.0)):
230 self.x, self.y, self.z, self.w = point
232 def get_size(self):
233 return 4*SZ_FLOAT
235 def write(self,file):
236 data=struct.pack('<4f', self.x, self.y, self.z, self.w)
237 file.write(data)
239 def __str__(self):
240 return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
244 class _3ds_point_uv(object):
245 """Class representing a UV-coordinate for a 3ds file."""
246 __slots__ = ("uv", )
248 def __init__(self, point):
249 self.uv = point
251 def get_size(self):
252 return 2 * SZ_FLOAT
254 def write(self, file):
255 data = struct.pack('<2f', self.uv[0], self.uv[1])
256 file.write(data)
258 def __str__(self):
259 return '(%g, %g)' % self.uv
262 class _3ds_rgb_color(object):
263 """Class representing a (24-bit) rgb color for a 3ds file."""
264 __slots__ = "r", "g", "b"
266 def __init__(self, col):
267 self.r, self.g, self.b = col
269 def get_size(self):
270 return 3
272 def write(self, file):
273 file.write(struct.pack('<3B', int(255 * self.r), int(255 * self.g), int(255 * self.b)))
275 def __str__(self):
276 return '{%f, %f, %f}' % (self.r, self.g, self.b)
279 class _3ds_face(object):
280 """Class representing a face for a 3ds file."""
281 __slots__ = ("vindex", )
283 def __init__(self, vindex):
284 self.vindex = vindex
286 def get_size(self):
287 return 4 * SZ_SHORT
289 # no need to validate every face vert. the oversized array will
290 # catch this problem
292 def write(self, file):
293 # The last zero is only used by 3d studio
294 file.write(struct.pack("<4H", self.vindex[0], self.vindex[1], self.vindex[2], 0))
296 def __str__(self):
297 return "[%d %d %d]" % (self.vindex[0], self.vindex[1], self.vindex[2])
300 class _3ds_array(object):
301 """Class representing an array of variables for a 3ds file.
303 Consists of a _3ds_ushort to indicate the number of items, followed by the items themselves.
305 __slots__ = "values", "size"
307 def __init__(self):
308 self.values = []
309 self.size = SZ_SHORT
311 # add an item:
312 def add(self, item):
313 self.values.append(item)
314 self.size += item.get_size()
316 def get_size(self):
317 return self.size
319 def validate(self):
320 return len(self.values) <= 65535
322 def write(self, file):
323 _3ds_ushort(len(self.values)).write(file)
324 for value in self.values:
325 value.write(file)
327 # To not overwhelm the output in a dump, a _3ds_array only
328 # outputs the number of items, not all of the actual items.
329 def __str__(self):
330 return '(%d items)' % len(self.values)
333 class _3ds_named_variable(object):
334 """Convenience class for named variables."""
336 __slots__ = "value", "name"
338 def __init__(self, name, val=None):
339 self.name = name
340 self.value = val
342 def get_size(self):
343 if self.value is None:
344 return 0
345 else:
346 return self.value.get_size()
348 def write(self, file):
349 if self.value is not None:
350 self.value.write(file)
352 def dump(self, indent):
353 if self.value is not None:
354 print(indent * " ",
355 self.name if self.name else "[unnamed]",
356 " = ",
357 self.value)
360 #the chunk class
361 class _3ds_chunk(object):
362 """Class representing a chunk in a 3ds file.
364 Chunks contain zero or more variables, followed by zero or more subchunks.
366 __slots__ = "ID", "size", "variables", "subchunks"
368 def __init__(self, chunk_id=0):
369 self.ID = _3ds_ushort(chunk_id)
370 self.size = _3ds_uint(0)
371 self.variables = []
372 self.subchunks = []
374 def add_variable(self, name, var):
375 """Add a named variable.
377 The name is mostly for debugging purposes."""
378 self.variables.append(_3ds_named_variable(name, var))
380 def add_subchunk(self, chunk):
381 """Add a subchunk."""
382 self.subchunks.append(chunk)
384 def get_size(self):
385 """Calculate the size of the chunk and return it.
387 The sizes of the variables and subchunks are used to determine this chunk\'s size."""
388 tmpsize = self.ID.get_size() + self.size.get_size()
389 for variable in self.variables:
390 tmpsize += variable.get_size()
391 for subchunk in self.subchunks:
392 tmpsize += subchunk.get_size()
393 self.size.value = tmpsize
394 return self.size.value
396 def validate(self):
397 for var in self.variables:
398 func = getattr(var.value, "validate", None)
399 if (func is not None) and not func():
400 return False
402 for chunk in self.subchunks:
403 func = getattr(chunk, "validate", None)
404 if (func is not None) and not func():
405 return False
407 return True
409 def write(self, file):
410 """Write the chunk to a file.
412 Uses the write function of the variables and the subchunks to do the actual work."""
413 #write header
414 self.ID.write(file)
415 self.size.write(file)
416 for variable in self.variables:
417 variable.write(file)
418 for subchunk in self.subchunks:
419 subchunk.write(file)
421 def dump(self, indent=0):
422 """Write the chunk to a file.
424 Dump is used for debugging purposes, to dump the contents of a chunk to the standard output.
425 Uses the dump function of the named variables and the subchunks to do the actual work."""
426 print(indent * " ",
427 "ID=%r" % hex(self.ID.value),
428 "size=%r" % self.get_size())
429 for variable in self.variables:
430 variable.dump(indent + 1)
431 for subchunk in self.subchunks:
432 subchunk.dump(indent + 1)
435 ######################################################
436 # EXPORT
437 ######################################################
439 def get_material_image_texslots(material):
440 # blender utility func.
441 if material:
442 return [s for s in material.texture_slots if s and s.texture.type == 'IMAGE' and s.texture.image]
444 return []
447 images = []
448 if material:
449 for mtex in material.getTextures():
450 if mtex and mtex.tex.type == Blender.Texture.Types.IMAGE:
451 image = mtex.tex.image
452 if image:
453 images.append(image) # maye want to include info like diffuse, spec here.
454 return images
458 def make_material_subchunk(chunk_id, color):
459 """Make a material subchunk.
461 Used for color subchunks, such as diffuse color or ambient color subchunks."""
462 mat_sub = _3ds_chunk(chunk_id)
463 col1 = _3ds_chunk(RGB1)
464 col1.add_variable("color1", _3ds_rgb_color(color))
465 mat_sub.add_subchunk(col1)
466 # optional:
467 #col2 = _3ds_chunk(RGB1)
468 #col2.add_variable("color2", _3ds_rgb_color(color))
469 #mat_sub.add_subchunk(col2)
470 return mat_sub
473 def make_material_texture_chunk(chunk_id, texslots, tess_uv_image=None):
474 """Make Material Map texture chunk given a seq. of `MaterialTextureSlot`'s
476 `tess_uv_image` is optionally used as image source if the slots are
477 empty. No additional filtering for mapping modes is done, all
478 slots are written "as is".
481 mat_sub = _3ds_chunk(chunk_id)
482 has_entry = False
484 import bpy
486 def add_texslot(texslot):
487 texture = texslot.texture
488 image = texture.image
490 filename = bpy.path.basename(image.filepath)
491 mat_sub_file = _3ds_chunk(MATMAPFILE)
492 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
493 mat_sub.add_subchunk(mat_sub_file)
495 maptile = 0
497 # no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
498 if texture.extension == 'REPEAT' and (texture.use_mirror_x and texture.repeat_x > 1) \
499 or (texture.use_mirror_y and texture.repeat_y > 1):
500 maptile |= 0x2
501 # CLIP maps to 3DS' decal flag
502 elif texture.extension == 'CLIP':
503 maptile |= 0x10
505 mat_sub_tile = _3ds_chunk(MAT_MAP_TILING)
506 mat_sub_tile.add_variable("maptiling", _3ds_ushort(maptile))
507 mat_sub.add_subchunk(mat_sub_tile)
509 mat_sub_uscale = _3ds_chunk(MAT_MAP_USCALE)
510 mat_sub_uscale.add_variable("mapuscale", _3ds_float(texslot.scale[0]))
511 mat_sub.add_subchunk(mat_sub_uscale)
513 mat_sub_vscale = _3ds_chunk(MAT_MAP_VSCALE)
514 mat_sub_vscale.add_variable("mapuscale", _3ds_float(texslot.scale[1]))
515 mat_sub.add_subchunk(mat_sub_vscale)
517 mat_sub_uoffset = _3ds_chunk(MAT_MAP_UOFFSET)
518 mat_sub_uoffset.add_variable("mapuoffset", _3ds_float(texslot.offset[0]))
519 mat_sub.add_subchunk(mat_sub_uoffset)
521 mat_sub_voffset = _3ds_chunk(MAT_MAP_VOFFSET)
522 mat_sub_voffset.add_variable("mapvoffset", _3ds_float(texslot.offset[1]))
523 mat_sub.add_subchunk(mat_sub_voffset)
525 # store all textures for this mapto in order. This at least is what
526 # the 3DS exporter did so far, afaik most readers will just skip
527 # over 2nd textures.
528 for slot in texslots:
529 add_texslot(slot)
530 has_entry = True
532 # image from tess. UV face - basically the code above should handle
533 # this already. No idea why its here so keep it :-)
534 if tess_uv_image and not has_entry:
535 has_entry = True
537 filename = bpy.path.basename(tess_uv_image.filepath)
538 mat_sub_file = _3ds_chunk(MATMAPFILE)
539 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
540 mat_sub.add_subchunk(mat_sub_file)
542 return mat_sub if has_entry else None
545 def make_material_chunk(material, image):
546 """Make a material chunk out of a blender material."""
547 material_chunk = _3ds_chunk(MATERIAL)
548 name = _3ds_chunk(MATNAME)
550 name_str = material.name if material else "None"
552 if image:
553 name_str += image.name
555 name.add_variable("name", _3ds_string(sane_name(name_str)))
556 material_chunk.add_subchunk(name)
558 if not material:
559 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0.0, 0.0, 0.0)))
560 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (0.8, 0.8, 0.8)))
561 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1.0, 1.0, 1.0)))
563 else:
564 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (material.ambient * material.diffuse_color)[:]))
565 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color[:]))
566 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
568 slots = get_material_image_texslots(material) # can be None
570 if slots:
572 spec = [s for s in slots if s.use_map_specular or s.use_map_color_spec]
573 matmap = make_material_texture_chunk(MAT_SPECMAP, spec)
574 if matmap:
575 material_chunk.add_subchunk(matmap)
577 alpha = [s for s in slots if s.use_map_alpha]
578 matmap = make_material_texture_chunk(MAT_OPACMAP, alpha)
579 if matmap:
580 material_chunk.add_subchunk(matmap)
582 normal = [s for s in slots if s.use_map_normal]
583 matmap = make_material_texture_chunk(MAT_BUMPMAP, normal)
584 if matmap:
585 material_chunk.add_subchunk(matmap)
587 # make sure no textures are lost. Everything that doesn't fit
588 # into a channel is exported as diffuse texture with a
589 # warning.
590 diffuse = []
591 for s in slots:
592 if s.use_map_color_diffuse:
593 diffuse.append(s)
594 elif not (s in normal or s in alpha or s in spec):
595 print('\nwarning: failed to map texture to 3DS map channel, assuming diffuse')
596 diffuse.append(s)
598 if diffuse:
599 matmap = make_material_texture_chunk(MAT_DIFFUSEMAP, diffuse, image)
600 if matmap:
601 material_chunk.add_subchunk(matmap)
603 return material_chunk
606 class tri_wrapper(object):
607 """Class representing a triangle.
609 Used when converting faces to triangles"""
611 __slots__ = "vertex_index", "mat", "image", "faceuvs", "offset"
613 def __init__(self, vindex=(0, 0, 0), mat=None, image=None, faceuvs=None):
614 self.vertex_index = vindex
615 self.mat = mat
616 self.image = image
617 self.faceuvs = faceuvs
618 self.offset = [0, 0, 0] # offset indices
621 def extract_triangles(mesh):
622 """Extract triangles from a mesh.
624 If the mesh contains quads, they will be split into triangles."""
625 tri_list = []
626 do_uv = bool(mesh.tessface_uv_textures)
628 img = None
629 for i, face in enumerate(mesh.tessfaces):
630 f_v = face.vertices
632 uf = mesh.tessface_uv_textures.active.data[i] if do_uv else None
634 if do_uv:
635 f_uv = uf.uv
636 img = uf.image if uf else None
637 if img is not None:
638 img = img.name
640 # if f_v[3] == 0:
641 if len(f_v) == 3:
642 new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
643 if (do_uv):
644 new_tri.faceuvs = uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
645 tri_list.append(new_tri)
647 else: # it's a quad
648 new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
649 new_tri_2 = tri_wrapper((f_v[0], f_v[2], f_v[3]), face.material_index, img)
651 if (do_uv):
652 new_tri.faceuvs = uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
653 new_tri_2.faceuvs = uv_key(f_uv[0]), uv_key(f_uv[2]), uv_key(f_uv[3])
655 tri_list.append(new_tri)
656 tri_list.append(new_tri_2)
658 return tri_list
661 def remove_face_uv(verts, tri_list):
662 """Remove face UV coordinates from a list of triangles.
664 Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
665 need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
666 there are multiple uv coordinates per vertex."""
668 # initialize a list of UniqueLists, one per vertex:
669 #uv_list = [UniqueList() for i in xrange(len(verts))]
670 unique_uvs = [{} for i in range(len(verts))]
672 # for each face uv coordinate, add it to the UniqueList of the vertex
673 for tri in tri_list:
674 for i in range(3):
675 # store the index into the UniqueList for future reference:
676 # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
678 context_uv_vert = unique_uvs[tri.vertex_index[i]]
679 uvkey = tri.faceuvs[i]
681 offset_index__uv_3ds = context_uv_vert.get(uvkey)
683 if not offset_index__uv_3ds:
684 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
686 tri.offset[i] = offset_index__uv_3ds[0]
688 # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
689 # only once.
691 # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
692 # faces refer to the new face indices:
693 vert_index = 0
694 vert_array = _3ds_array()
695 uv_array = _3ds_array()
696 index_list = []
697 for i, vert in enumerate(verts):
698 index_list.append(vert_index)
700 pt = _3ds_point_3d(vert.co) # reuse, should be ok
701 uvmap = [None] * len(unique_uvs[i])
702 for ii, uv_3ds in unique_uvs[i].values():
703 # add a vertex duplicate to the vertex_array for every uv associated with this vertex:
704 vert_array.add(pt)
705 # add the uv coordinate to the uv array:
706 # This for loop does not give uv's ordered by ii, so we create a new map
707 # and add the uv's later
708 # uv_array.add(uv_3ds)
709 uvmap[ii] = uv_3ds
711 # Add the uv's in the correct order
712 for uv_3ds in uvmap:
713 # add the uv coordinate to the uv array:
714 uv_array.add(uv_3ds)
716 vert_index += len(unique_uvs[i])
718 # Make sure the triangle vertex indices now refer to the new vertex list:
719 for tri in tri_list:
720 for i in range(3):
721 tri.offset[i] += index_list[tri.vertex_index[i]]
722 tri.vertex_index = tri.offset
724 return vert_array, uv_array, tri_list
727 def make_faces_chunk(tri_list, mesh, materialDict):
728 """Make a chunk for the faces.
730 Also adds subchunks assigning materials to all faces."""
732 materials = mesh.materials
733 if not materials:
734 mat = None
736 face_chunk = _3ds_chunk(OBJECT_FACES)
737 face_list = _3ds_array()
739 if mesh.tessface_uv_textures:
740 # Gather materials used in this mesh - mat/image pairs
741 unique_mats = {}
742 for i, tri in enumerate(tri_list):
744 face_list.add(_3ds_face(tri.vertex_index))
746 if materials:
747 mat = materials[tri.mat]
748 if mat:
749 mat = mat.name
751 img = tri.image
753 try:
754 context_mat_face_array = unique_mats[mat, img][1]
755 except:
756 name_str = mat if mat else "None"
757 if img:
758 name_str += img
760 context_mat_face_array = _3ds_array()
761 unique_mats[mat, img] = _3ds_string(sane_name(name_str)), context_mat_face_array
763 context_mat_face_array.add(_3ds_ushort(i))
764 # obj_material_faces[tri.mat].add(_3ds_ushort(i))
766 face_chunk.add_variable("faces", face_list)
767 for mat_name, mat_faces in unique_mats.values():
768 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
769 obj_material_chunk.add_variable("name", mat_name)
770 obj_material_chunk.add_variable("face_list", mat_faces)
771 face_chunk.add_subchunk(obj_material_chunk)
773 else:
775 obj_material_faces = []
776 obj_material_names = []
777 for m in materials:
778 if m:
779 obj_material_names.append(_3ds_string(sane_name(m.name)))
780 obj_material_faces.append(_3ds_array())
781 n_materials = len(obj_material_names)
783 for i, tri in enumerate(tri_list):
784 face_list.add(_3ds_face(tri.vertex_index))
785 if (tri.mat < n_materials):
786 obj_material_faces[tri.mat].add(_3ds_ushort(i))
788 face_chunk.add_variable("faces", face_list)
789 for i in range(n_materials):
790 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
791 obj_material_chunk.add_variable("name", obj_material_names[i])
792 obj_material_chunk.add_variable("face_list", obj_material_faces[i])
793 face_chunk.add_subchunk(obj_material_chunk)
795 return face_chunk
798 def make_vert_chunk(vert_array):
799 """Make a vertex chunk out of an array of vertices."""
800 vert_chunk = _3ds_chunk(OBJECT_VERTICES)
801 vert_chunk.add_variable("vertices", vert_array)
802 return vert_chunk
805 def make_uv_chunk(uv_array):
806 """Make a UV chunk out of an array of UVs."""
807 uv_chunk = _3ds_chunk(OBJECT_UV)
808 uv_chunk.add_variable("uv coords", uv_array)
809 return uv_chunk
812 def make_matrix_4x3_chunk(matrix):
813 matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
814 for vec in matrix.col:
815 for f in vec[:3]:
816 matrix_chunk.add_variable("matrix_f", _3ds_float(f))
817 return matrix_chunk
820 def make_mesh_chunk(mesh, matrix, materialDict):
821 """Make a chunk out of a Blender mesh."""
823 # Extract the triangles from the mesh:
824 tri_list = extract_triangles(mesh)
826 if mesh.tessface_uv_textures:
827 # Remove the face UVs and convert it to vertex UV:
828 vert_array, uv_array, tri_list = remove_face_uv(mesh.vertices, tri_list)
829 else:
830 # Add the vertices to the vertex array:
831 vert_array = _3ds_array()
832 for vert in mesh.vertices:
833 vert_array.add(_3ds_point_3d(vert.co))
834 # no UV at all:
835 uv_array = None
837 # create the chunk:
838 mesh_chunk = _3ds_chunk(OBJECT_MESH)
840 # add vertex chunk:
841 mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
842 # add faces chunk:
844 mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
846 # if available, add uv chunk:
847 if uv_array:
848 mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
850 mesh_chunk.add_subchunk(make_matrix_4x3_chunk(matrix))
852 return mesh_chunk
855 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
856 def make_kfdata(start=0, stop=0, curtime=0):
857 """Make the basic keyframe data chunk"""
858 kfdata = _3ds_chunk(KFDATA)
860 kfhdr = _3ds_chunk(KFDATA_KFHDR)
861 kfhdr.add_variable("revision", _3ds_ushort(0))
862 # Not really sure what filename is used for, but it seems it is usually used
863 # to identify the program that generated the .3ds:
864 kfhdr.add_variable("filename", _3ds_string("Blender"))
865 kfhdr.add_variable("animlen", _3ds_uint(stop-start))
867 kfseg = _3ds_chunk(KFDATA_KFSEG)
868 kfseg.add_variable("start", _3ds_uint(start))
869 kfseg.add_variable("stop", _3ds_uint(stop))
871 kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
872 kfcurtime.add_variable("curtime", _3ds_uint(curtime))
874 kfdata.add_subchunk(kfhdr)
875 kfdata.add_subchunk(kfseg)
876 kfdata.add_subchunk(kfcurtime)
877 return kfdata
879 def make_track_chunk(ID, obj):
880 """Make a chunk for track data.
882 Depending on the ID, this will construct a position, rotation or scale track."""
883 track_chunk = _3ds_chunk(ID)
884 track_chunk.add_variable("track_flags", _3ds_ushort())
885 track_chunk.add_variable("unknown", _3ds_uint())
886 track_chunk.add_variable("unknown", _3ds_uint())
887 track_chunk.add_variable("nkeys", _3ds_uint(1))
888 # Next section should be repeated for every keyframe, but for now, animation is not actually supported.
889 track_chunk.add_variable("tcb_frame", _3ds_uint(0))
890 track_chunk.add_variable("tcb_flags", _3ds_ushort())
891 if obj.type=='Empty':
892 if ID==POS_TRACK_TAG:
893 # position vector:
894 track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
895 elif ID==ROT_TRACK_TAG:
896 # rotation (quaternion, angle first, followed by axis):
897 q = obj.getEuler().to_quaternion() # XXX, todo!
898 track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
899 elif ID==SCL_TRACK_TAG:
900 # scale vector:
901 track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
902 else:
903 # meshes have their transformations applied before
904 # exporting, so write identity transforms here:
905 if ID==POS_TRACK_TAG:
906 # position vector:
907 track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
908 elif ID==ROT_TRACK_TAG:
909 # rotation (quaternion, angle first, followed by axis):
910 track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
911 elif ID==SCL_TRACK_TAG:
912 # scale vector:
913 track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
915 return track_chunk
917 def make_kf_obj_node(obj, name_to_id):
918 """Make a node chunk for a Blender object.
920 Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
921 Blender Empty objects are converted to dummy nodes."""
923 name = obj.name
924 # main object node chunk:
925 kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
926 # chunk for the object id:
927 obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
928 # object id is from the name_to_id dictionary:
929 obj_id_chunk.add_variable("node_id", _3ds_ushort(name_to_id[name]))
931 # object node header:
932 obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
933 # object name:
934 if obj.type == 'Empty':
935 # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk
936 # for their name (see below):
937 obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
938 else:
939 # Add the name:
940 obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
941 # Add Flag variables (not sure what they do):
942 obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0))
943 obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
945 # Check parent-child relationships:
946 parent = obj.parent
947 if (parent is None) or (parent.name not in name_to_id):
948 # If no parent, or the parents name is not in the name_to_id dictionary,
949 # parent id becomes -1:
950 obj_node_header_chunk.add_variable("parent", _3ds_ushort(-1))
951 else:
952 # Get the parent's id from the name_to_id dictionary:
953 obj_node_header_chunk.add_variable("parent", _3ds_ushort(name_to_id[parent.name]))
955 # Add pivot chunk:
956 obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
957 obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
958 kf_obj_node.add_subchunk(obj_pivot_chunk)
960 # add subchunks for object id and node header:
961 kf_obj_node.add_subchunk(obj_id_chunk)
962 kf_obj_node.add_subchunk(obj_node_header_chunk)
964 # Empty objects need to have an extra chunk for the instance name:
965 if obj.type == 'Empty':
966 obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
967 obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
968 kf_obj_node.add_subchunk(obj_instance_name_chunk)
970 # Add track chunks for position, rotation and scale:
971 kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
972 kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
973 kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
975 return kf_obj_node
979 def save(operator,
980 context, filepath="",
981 use_selection=True,
982 global_matrix=None,
985 import bpy
986 import mathutils
988 import time
989 from bpy_extras.io_utils import create_derived_objects, free_derived_objects
991 """Save the Blender scene to a 3ds file."""
993 # Time the export
994 time1 = time.clock()
995 #Blender.Window.WaitCursor(1)
997 if global_matrix is None:
998 global_matrix = mathutils.Matrix()
1000 if bpy.ops.object.mode_set.poll():
1001 bpy.ops.object.mode_set(mode='OBJECT')
1003 # Initialize the main chunk (primary):
1004 primary = _3ds_chunk(PRIMARY)
1005 # Add version chunk:
1006 version_chunk = _3ds_chunk(VERSION)
1007 version_chunk.add_variable("version", _3ds_uint(3))
1008 primary.add_subchunk(version_chunk)
1010 # init main object info chunk:
1011 object_info = _3ds_chunk(OBJECTINFO)
1013 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1014 # init main key frame data chunk:
1015 kfdata = make_kfdata()
1018 # Make a list of all materials used in the selected meshes (use a dictionary,
1019 # each material is added once):
1020 materialDict = {}
1021 mesh_objects = []
1023 scene = context.scene
1025 if use_selection:
1026 objects = (ob for ob in scene.objects if ob.is_visible(scene) and ob.select)
1027 else:
1028 objects = (ob for ob in scene.objects if ob.is_visible(scene))
1030 for ob in objects:
1031 # get derived objects
1032 free, derived = create_derived_objects(scene, ob)
1034 if derived is None:
1035 continue
1037 for ob_derived, mat in derived:
1038 if ob.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
1039 continue
1041 try:
1042 data = ob_derived.to_mesh(scene, True, 'PREVIEW')
1043 except:
1044 data = None
1046 if data:
1047 matrix = global_matrix * mat
1048 data.transform(matrix)
1049 mesh_objects.append((ob_derived, data, matrix))
1050 mat_ls = data.materials
1051 mat_ls_len = len(mat_ls)
1053 # get material/image tuples.
1054 if data.tessface_uv_textures:
1055 if not mat_ls:
1056 mat = mat_name = None
1058 for f, uf in zip(data.tessfaces, data.tessface_uv_textures.active.data):
1059 if mat_ls:
1060 mat_index = f.material_index
1061 if mat_index >= mat_ls_len:
1062 mat_index = f.mat = 0
1063 mat = mat_ls[mat_index]
1064 mat_name = None if mat is None else mat.name
1065 # else there already set to none
1067 img = uf.image
1068 img_name = None if img is None else img.name
1070 materialDict.setdefault((mat_name, img_name), (mat, img))
1072 else:
1073 for mat in mat_ls:
1074 if mat: # material may be None so check its not.
1075 materialDict.setdefault((mat.name, None), (mat, None))
1077 # Why 0 Why!
1078 for f in data.tessfaces:
1079 if f.material_index >= mat_ls_len:
1080 f.material_index = 0
1082 if free:
1083 free_derived_objects(ob)
1085 # Make material chunks for all materials used in the meshes:
1086 for mat_and_image in materialDict.values():
1087 object_info.add_subchunk(make_material_chunk(mat_and_image[0], mat_and_image[1]))
1089 # Give all objects a unique ID and build a dictionary from object name to object id:
1091 name_to_id = {}
1092 for ob, data in mesh_objects:
1093 name_to_id[ob.name]= len(name_to_id)
1094 #for ob in empty_objects:
1095 # name_to_id[ob.name]= len(name_to_id)
1098 # Create object chunks for all meshes:
1099 i = 0
1100 for ob, blender_mesh, matrix in mesh_objects:
1101 # create a new object chunk
1102 object_chunk = _3ds_chunk(OBJECT)
1104 # set the object name
1105 object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1107 # make a mesh chunk out of the mesh:
1108 object_chunk.add_subchunk(make_mesh_chunk(blender_mesh, matrix, materialDict))
1110 # ensure the mesh has no over sized arrays
1111 # skip ones that do!, otherwise we cant write since the array size wont
1112 # fit into USHORT.
1113 if object_chunk.validate():
1114 object_info.add_subchunk(object_chunk)
1115 else:
1116 operator.report({'WARNING'}, "Object %r can't be written into a 3DS file")
1118 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1119 # make a kf object node for the object:
1120 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1123 if not blender_mesh.users:
1124 bpy.data.meshes.remove(blender_mesh)
1125 #blender_mesh.vertices = None
1127 i += i
1129 # Create chunks for all empties:
1130 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1131 for ob in empty_objects:
1132 # Empties only require a kf object node:
1133 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1134 pass
1137 # Add main object info chunk to primary chunk:
1138 primary.add_subchunk(object_info)
1140 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1141 # Add main keyframe data chunk to primary chunk:
1142 primary.add_subchunk(kfdata)
1145 # At this point, the chunk hierarchy is completely built.
1147 # Check the size:
1148 primary.get_size()
1149 # Open the file for writing:
1150 file = open(filepath, 'wb')
1152 # Recursively write the chunks to file:
1153 primary.write(file)
1155 # Close the file:
1156 file.close()
1158 # Clear name mapping vars, could make locals too
1159 del name_unique[:]
1160 name_mapping.clear()
1162 # Debugging only: report the exporting time:
1163 #Blender.Window.WaitCursor(0)
1164 print("3ds export time: %.2f" % (time.clock() - time1))
1166 # Debugging only: dump the chunk hierarchy:
1167 #primary.dump()
1169 return {'FINISHED'}