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 #####
21 # Script copyright (C) Bob Holcomb
22 # Contributors: Campbell Barton, Bob Holcomb, Richard Lärkäng, Damien McGinnes, Mark Stijnman
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.
29 ######################################################
31 ######################################################
33 #Some of the chunks that we will export
34 #----- Primary Chunk, at the beginning of each file
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
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
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
96 OBJECT_INSTANCE_NAME
= 0xB011
97 POS_TRACK_TAG
= 0xB020
98 ROT_TRACK_TAG
= 0xB021
99 SCL_TRACK_TAG
= 0xB022
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
110 name_fixed
= name_mapping
.get(name
)
111 if name_fixed
is not None:
114 # strip non ascii chars
115 new_name_clean
= new_name
= name
.encode("ASCII", "replace").decode("ASCII")[:12]
118 while new_name
in name_unique
:
119 new_name
= new_name_clean
+ ".%.3d" % i
122 # note, appending the 'str' version.
123 name_unique
.append(new_name
)
124 name_mapping
[name
] = new_name
= new_name
.encode("ASCII", "replace")
129 return round(uv
[0], 6), round(uv
[1], 6)
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):
148 def write(self
, file):
149 file.write(struct
.pack("<H", self
.value
))
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
):
165 def write(self
, file):
166 file.write(struct
.pack("<I", self
.value
))
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
):
182 def write(self
, file):
183 file.write(struct
.pack("<f", self
.value
))
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
)
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
))
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
218 def write(self
, file):
219 file.write(struct
.pack('<3f', self
.x
, self
.y
, self
.z
))
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
235 def write(self,file):
236 data=struct.pack('<4f', self.x, self.y, self.z, self.w)
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."""
248 def __init__(self
, point
):
254 def write(self
, file):
255 data
= struct
.pack('<2f', self
.uv
[0], self
.uv
[1])
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
272 def write(self
, file):
273 file.write(struct
.pack('<3B', int(255 * self
.r
), int(255 * self
.g
), int(255 * self
.b
)))
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
):
289 # no need to validate every face vert. the oversized array will
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))
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"
313 self
.values
.append(item
)
314 self
.size
+= item
.get_size()
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
:
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.
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):
343 if self
.value
is None:
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:
355 self
.name
if self
.name
else "[unnamed]",
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)
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
)
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
397 for var
in self
.variables
:
398 func
= getattr(var
.value
, "validate", None)
399 if (func
is not None) and not func():
402 for chunk
in self
.subchunks
:
403 func
= getattr(chunk
, "validate", None)
404 if (func
is not None) and not func():
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."""
415 self
.size
.write(file)
416 for variable
in self
.variables
:
418 for subchunk
in self
.subchunks
:
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."""
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 ######################################################
437 ######################################################
439 def get_material_image_texslots(material
):
440 # blender utility func.
442 return [s
for s
in material
.texture_slots
if s
and s
.texture
.type == 'IMAGE' and s
.texture
.image
]
449 for mtex in material.getTextures():
450 if mtex and mtex.tex.type == Blender.Texture.Types.IMAGE:
451 image = mtex.tex.image
453 images.append(image) # maye want to include info like diffuse, spec here.
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
)
467 #col2 = _3ds_chunk(RGB1)
468 #col2.add_variable("color2", _3ds_rgb_color(color))
469 #mat_sub.add_subchunk(col2)
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
)
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
)
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):
501 # CLIP maps to 3DS' decal flag
502 elif texture
.extension
== 'CLIP':
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
528 for slot
in texslots
:
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
:
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"
553 name_str
+= image
.name
555 name
.add_variable("name", _3ds_string(sane_name(name_str
)))
556 material_chunk
.add_subchunk(name
)
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)))
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
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
)
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
)
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
)
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
592 if s
.use_map_color_diffuse
:
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')
599 matmap
= make_material_texture_chunk(MAT_DIFFUSEMAP
, diffuse
, image
)
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
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."""
626 do_uv
= bool(mesh
.tessface_uv_textures
)
629 for i
, face
in enumerate(mesh
.tessfaces
):
632 uf
= mesh
.tessface_uv_textures
.active
.data
[i
] if do_uv
else None
636 img
= uf
.image
if uf
else None
642 new_tri
= tri_wrapper((f_v
[0], f_v
[1], f_v
[2]), face
.material_index
, img
)
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
)
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
)
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
)
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
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
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:
694 vert_array
= _3ds_array()
695 uv_array
= _3ds_array()
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:
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)
711 # Add the uv's in the correct order
713 # add the uv coordinate to the uv array:
716 vert_index
+= len(unique_uvs
[i
])
718 # Make sure the triangle vertex indices now refer to the new vertex list:
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
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
742 for i
, tri
in enumerate(tri_list
):
744 face_list
.add(_3ds_face(tri
.vertex_index
))
747 mat
= materials
[tri
.mat
]
754 context_mat_face_array
= unique_mats
[mat
, img
][1]
756 name_str
= mat
if mat
else "None"
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
)
775 obj_material_faces
= []
776 obj_material_names
= []
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
)
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
)
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
)
812 def make_matrix_4x3_chunk(matrix
):
813 matrix_chunk
= _3ds_chunk(OBJECT_TRANS_MATRIX
)
814 for vec
in matrix
.col
:
816 matrix_chunk
.add_variable("matrix_f", _3ds_float(f
))
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
)
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
))
838 mesh_chunk
= _3ds_chunk(OBJECT_MESH
)
841 mesh_chunk
.add_subchunk(make_vert_chunk(vert_array
))
844 mesh_chunk
.add_subchunk(make_faces_chunk(tri_list
, mesh
, materialDict
))
846 # if available, add uv chunk:
848 mesh_chunk
.add_subchunk(make_uv_chunk(uv_array
))
850 mesh_chunk
.add_subchunk(make_matrix_4x3_chunk(matrix
))
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)
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:
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:
901 track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
903 # meshes have their transformations applied before
904 # exporting, so write identity transforms here:
905 if ID==POS_TRACK_TAG:
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:
913 track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
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."""
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)
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"))
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:
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))
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]))
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))
980 context
, filepath
="",
989 from bpy_extras
.io_utils
import create_derived_objects
, free_derived_objects
991 """Save the Blender scene to a 3ds file."""
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):
1023 scene
= context
.scene
1026 objects
= (ob
for ob
in scene
.objects
if ob
.is_visible(scene
) and ob
.select
)
1028 objects
= (ob
for ob
in scene
.objects
if ob
.is_visible(scene
))
1031 # get derived objects
1032 free
, derived
= create_derived_objects(scene
, ob
)
1037 for ob_derived
, mat
in derived
:
1038 if ob
.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
1042 data
= ob_derived
.to_mesh(scene
, True, 'PREVIEW')
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
:
1056 mat
= mat_name
= None
1058 for f
, uf
in zip(data
.tessfaces
, data
.tessface_uv_textures
.active
.data
):
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
1068 img_name
= None if img
is None else img
.name
1070 materialDict
.setdefault((mat_name
, img_name
), (mat
, img
))
1074 if mat
: # material may be None so check its not.
1075 materialDict
.setdefault((mat
.name
, None), (mat
, None))
1078 for f
in data
.tessfaces
:
1079 if f
.material_index
>= mat_ls_len
:
1080 f
.material_index
= 0
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:
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:
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
1113 if object_chunk
.validate():
1114 object_info
.add_subchunk(object_chunk
)
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
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))
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.
1149 # Open the file for writing:
1150 file = open(filepath
, 'wb')
1152 # Recursively write the chunks to file:
1158 # Clear name mapping vars, could make locals too
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: