Sun Position: remove show_daylight_savings preference
[blender-addons.git] / io_scene_3ds / export_3ds.py
blobc9e731508f151c8883f508f17d66d3eaa28aba2d
1 # SPDX-License-Identifier: GPL-2.0-or-later
2 # Copyright 2005 Bob Holcomb
4 """
5 Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
6 from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
7 """
9 import bpy
10 import math
11 import struct
12 import mathutils
13 import bpy_extras
14 from bpy_extras import node_shader_utils
16 ######################################################
17 # Data Structures
18 ######################################################
20 # Some of the chunks that we will export
21 # ----- Primary Chunk, at the beginning of each file
22 PRIMARY = 0x4D4D
24 # ------ Main Chunks
25 VERSION = 0x0002 # This gives the version of the .3ds file
26 KFDATA = 0xB000 # This is the header for all of the key frame info
28 # ------ sub defines of OBJECTINFO
29 OBJECTINFO = 0x3D3D # Main mesh object chunk before the material and object information
30 MESHVERSION = 0x3D3E # This gives the version of the mesh
31 AMBIENTLIGHT = 0x2100 # The color of the ambient light
32 MATERIAL = 45055 # 0xAFFF // This stored the texture info
33 OBJECT = 16384 # 0x4000 // This stores the faces, vertices, etc...
35 # >------ sub defines of MATERIAL
36 MATNAME = 0xA000 # This holds the material name
37 MATAMBIENT = 0xA010 # Ambient color of the object/material
38 MATDIFFUSE = 0xA020 # This holds the color of the object/material
39 MATSPECULAR = 0xA030 # Specular color of the object/material
40 MATSHINESS = 0xA040 # Specular intensity of the object/material (percent)
41 MATSHIN2 = 0xA041 # Reflection of the object/material (percent)
42 MATSHIN3 = 0xA042 # metallic/mirror of the object/material (percent)
43 MATTRANS = 0xA050 # Transparency value (100-OpacityValue) (percent)
44 MATSELFILPCT = 0xA084 # Self illumination strength (percent)
45 MATSHADING = 0xA100 # Material shading method
47 MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture
48 MAT_SPECMAP = 0xA204 # head for specularity map
49 MAT_OPACMAP = 0xA210 # head for opacity map
50 MAT_REFLMAP = 0xA220 # head for reflect map
51 MAT_BUMPMAP = 0xA230 # head for normal map
52 MAT_BUMP_PERCENT = 0xA252 # Normalmap strength (percent)
53 MAT_TEX2MAP = 0xA33A # head for secondary texture
54 MAT_SHINMAP = 0xA33C # head for roughness map
55 MAT_SELFIMAP = 0xA33D # head for emission map
57 # >------ sub defines of MAT_MAP
58 MATMAPFILE = 0xA300 # This holds the file name of a texture
59 MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag
60 MAT_MAP_TEXBLUR = 0xA353 # Texture blurring factor
61 MAT_MAP_USCALE = 0xA354 # U axis scaling
62 MAT_MAP_VSCALE = 0xA356 # V axis scaling
63 MAT_MAP_UOFFSET = 0xA358 # U axis offset
64 MAT_MAP_VOFFSET = 0xA35A # V axis offset
65 MAT_MAP_ANG = 0xA35C # UV rotation around the z-axis in rad
66 MAP_COL1 = 0xA360 # Tint Color1
67 MAP_COL2 = 0xA362 # Tint Color2
68 MAP_RCOL = 0xA364 # Red tint
69 MAP_GCOL = 0xA366 # Green tint
70 MAP_BCOL = 0xA368 # Blue tint
72 RGB = 0x0010 # RGB float Color1
73 RGB1 = 0x0011 # RGB int Color1
74 RGBI = 0x0012 # RGB int Color2
75 RGBF = 0x0013 # RGB float Color2
76 PCT = 0x0030 # Percent chunk
77 PCTF = 0x0031 # Percent float
78 MASTERSCALE = 0x0100 # Master scale factor
80 # >------ sub defines of OBJECT
81 OBJECT_MESH = 0x4100 # This lets us know that we are reading a new object
82 OBJECT_LIGHT = 0x4600 # This lets us know we are reading a light object
83 OBJECT_CAMERA = 0x4700 # This lets us know we are reading a camera object
85 # >------ Sub defines of LIGHT
86 LIGHT_MULTIPLIER = 0x465B # The light energy factor
87 LIGHT_SPOTLIGHT = 0x4610 # The target of a spotlight
88 LIGHT_SPOTROLL = 0x4656 # The roll angle of the spot
90 # >------ sub defines of CAMERA
91 OBJECT_CAM_RANGES = 0x4720 # The camera range values
93 # >------ sub defines of OBJECT_MESH
94 OBJECT_VERTICES = 0x4110 # The objects vertices
95 OBJECT_VERTFLAGS = 0x4111 # The objects vertex flags
96 OBJECT_FACES = 0x4120 # The objects faces
97 OBJECT_MATERIAL = 0x4130 # This is found if the object has a material, either texture map or color
98 OBJECT_UV = 0x4140 # The UV texture coordinates
99 OBJECT_SMOOTH = 0x4150 # The objects smooth groups
100 OBJECT_TRANS_MATRIX = 0x4160 # The Object Matrix
102 # >------ sub defines of KFDATA
103 KFDATA_KFHDR = 0xB00A
104 KFDATA_KFSEG = 0xB008
105 KFDATA_KFCURTIME = 0xB009
106 KFDATA_OBJECT_NODE_TAG = 0xB002
108 # >------ sub defines of OBJECT_NODE_TAG
109 OBJECT_NODE_ID = 0xB030
110 OBJECT_NODE_HDR = 0xB010
111 OBJECT_PIVOT = 0xB013
112 OBJECT_INSTANCE_NAME = 0xB011
113 POS_TRACK_TAG = 0xB020
114 ROT_TRACK_TAG = 0xB021
115 SCL_TRACK_TAG = 0xB022
118 # So 3ds max can open files, limit names to 12 in length
119 # this is very annoying for filenames!
120 name_unique = [] # stores str, ascii only
121 name_mapping = {} # stores {orig: byte} mapping
124 def sane_name(name):
125 name_fixed = name_mapping.get(name)
126 if name_fixed is not None:
127 return name_fixed
129 # strip non ascii chars
130 new_name_clean = new_name = name.encode("ASCII", "replace").decode("ASCII")[:12]
131 i = 0
133 while new_name in name_unique:
134 new_name = new_name_clean + ".%.3d" % i
135 i += 1
137 # note, appending the 'str' version.
138 name_unique.append(new_name)
139 name_mapping[name] = new_name = new_name.encode("ASCII", "replace")
140 return new_name
143 def uv_key(uv):
144 return round(uv[0], 6), round(uv[1], 6)
147 # size defines:
148 SZ_SHORT = 2
149 SZ_INT = 4
150 SZ_FLOAT = 4
153 class _3ds_ushort(object):
154 """Class representing a short (2-byte integer) for a 3ds file.
155 *** This looks like an unsigned short H is unsigned from the struct docs - Cam***"""
156 __slots__ = ("value", )
158 def __init__(self, val=0):
159 self.value = val
161 def get_size(self):
162 return SZ_SHORT
164 def write(self, file):
165 file.write(struct.pack("<H", self.value))
167 def __str__(self):
168 return str(self.value)
171 class _3ds_uint(object):
172 """Class representing an int (4-byte integer) for a 3ds file."""
173 __slots__ = ("value", )
175 def __init__(self, val):
176 self.value = val
178 def get_size(self):
179 return SZ_INT
181 def write(self, file):
182 file.write(struct.pack("<I", self.value))
184 def __str__(self):
185 return str(self.value)
188 class _3ds_float(object):
189 """Class representing a 4-byte IEEE floating point number for a 3ds file."""
190 __slots__ = ("value", )
192 def __init__(self, val):
193 self.value = val
195 def get_size(self):
196 return SZ_FLOAT
198 def write(self, file):
199 file.write(struct.pack("<f", self.value))
201 def __str__(self):
202 return str(self.value)
205 class _3ds_string(object):
206 """Class representing a zero-terminated string for a 3ds file."""
207 __slots__ = ("value", )
209 def __init__(self, val):
210 assert type(val) == bytes
211 self.value = val
213 def get_size(self):
214 return (len(self.value) + 1)
216 def write(self, file):
217 binary_format = "<%ds" % (len(self.value) + 1)
218 file.write(struct.pack(binary_format, self.value))
220 def __str__(self):
221 return str(self.value)
224 class _3ds_point_3d(object):
225 """Class representing a three-dimensional point for a 3ds file."""
226 __slots__ = "x", "y", "z"
228 def __init__(self, point):
229 self.x, self.y, self.z = point
231 def get_size(self):
232 return 3 * SZ_FLOAT
234 def write(self, file):
235 file.write(struct.pack('<3f', self.x, self.y, self.z))
237 def __str__(self):
238 return '(%f, %f, %f)' % (self.x, self.y, self.z)
241 # Used for writing a track
243 class _3ds_point_4d(object):
244 """Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
245 __slots__ = "x","y","z","w"
246 def __init__(self, point=(0.0,0.0,0.0,0.0)):
247 self.x, self.y, self.z, self.w = point
249 def get_size(self):
250 return 4*SZ_FLOAT
252 def write(self,file):
253 data=struct.pack('<4f', self.x, self.y, self.z, self.w)
254 file.write(data)
256 def __str__(self):
257 return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
261 class _3ds_point_uv(object):
262 """Class representing a UV-coordinate for a 3ds file."""
263 __slots__ = ("uv", )
265 def __init__(self, point):
266 self.uv = point
268 def get_size(self):
269 return 2 * SZ_FLOAT
271 def write(self, file):
272 data = struct.pack('<2f', self.uv[0], self.uv[1])
273 file.write(data)
275 def __str__(self):
276 return '(%g, %g)' % self.uv
279 class _3ds_float_color(object):
280 """Class representing a rgb float color for a 3ds file."""
281 __slots__ = "r", "g", "b"
283 def __init__(self, col):
284 self.r, self.g, self.b = col
286 def get_size(self):
287 return 3 * SZ_FLOAT
289 def write(self, file):
290 file.write(struct.pack('3f', self.r, self.g, self.b))
292 def __str__(self):
293 return '{%f, %f, %f}' % (self.r, self.g, self.b)
296 class _3ds_rgb_color(object):
297 """Class representing a (24-bit) rgb color for a 3ds file."""
298 __slots__ = "r", "g", "b"
300 def __init__(self, col):
301 self.r, self.g, self.b = col
303 def get_size(self):
304 return 3
306 def write(self, file):
307 file.write(struct.pack('<3B', int(255 * self.r), int(255 * self.g), int(255 * self.b)))
309 def __str__(self):
310 return '{%f, %f, %f}' % (self.r, self.g, self.b)
313 class _3ds_face(object):
314 """Class representing a face for a 3ds file."""
315 __slots__ = ("vindex", "flag")
317 def __init__(self, vindex, flag):
318 self.vindex = vindex
319 self.flag = flag
321 def get_size(self):
322 return 4 * SZ_SHORT
324 # no need to validate every face vert. the oversized array will
325 # catch this problem
327 def write(self, file):
328 # The last short is used for face flags
329 file.write(struct.pack("<4H", self.vindex[0], self.vindex[1], self.vindex[2], self.flag))
331 def __str__(self):
332 return "[%d %d %d %d]" % (self.vindex[0], self.vindex[1], self.vindex[2], self.flag)
335 class _3ds_array(object):
336 """Class representing an array of variables for a 3ds file.
338 Consists of a _3ds_ushort to indicate the number of items, followed by the items themselves.
340 __slots__ = "values", "size"
342 def __init__(self):
343 self.values = []
344 self.size = SZ_SHORT
346 # add an item:
347 def add(self, item):
348 self.values.append(item)
349 self.size += item.get_size()
351 def get_size(self):
352 return self.size
354 def validate(self):
355 return len(self.values) <= 65535
357 def write(self, file):
358 _3ds_ushort(len(self.values)).write(file)
359 for value in self.values:
360 value.write(file)
362 # To not overwhelm the output in a dump, a _3ds_array only
363 # outputs the number of items, not all of the actual items.
364 def __str__(self):
365 return '(%d items)' % len(self.values)
368 class _3ds_named_variable(object):
369 """Convenience class for named variables."""
371 __slots__ = "value", "name"
373 def __init__(self, name, val=None):
374 self.name = name
375 self.value = val
377 def get_size(self):
378 if self.value is None:
379 return 0
380 else:
381 return self.value.get_size()
383 def write(self, file):
384 if self.value is not None:
385 self.value.write(file)
387 def dump(self, indent):
388 if self.value is not None:
389 print(indent * " ",
390 self.name if self.name else "[unnamed]",
391 " = ",
392 self.value)
395 # the chunk class
396 class _3ds_chunk(object):
397 """Class representing a chunk in a 3ds file.
399 Chunks contain zero or more variables, followed by zero or more subchunks.
401 __slots__ = "ID", "size", "variables", "subchunks"
403 def __init__(self, chunk_id=0):
404 self.ID = _3ds_ushort(chunk_id)
405 self.size = _3ds_uint(0)
406 self.variables = []
407 self.subchunks = []
409 def add_variable(self, name, var):
410 """Add a named variable.
412 The name is mostly for debugging purposes."""
413 self.variables.append(_3ds_named_variable(name, var))
415 def add_subchunk(self, chunk):
416 """Add a subchunk."""
417 self.subchunks.append(chunk)
419 def get_size(self):
420 """Calculate the size of the chunk and return it.
422 The sizes of the variables and subchunks are used to determine this chunk\'s size."""
423 tmpsize = self.ID.get_size() + self.size.get_size()
424 for variable in self.variables:
425 tmpsize += variable.get_size()
426 for subchunk in self.subchunks:
427 tmpsize += subchunk.get_size()
428 self.size.value = tmpsize
429 return self.size.value
431 def validate(self):
432 for var in self.variables:
433 func = getattr(var.value, "validate", None)
434 if (func is not None) and not func():
435 return False
437 for chunk in self.subchunks:
438 func = getattr(chunk, "validate", None)
439 if (func is not None) and not func():
440 return False
442 return True
444 def write(self, file):
445 """Write the chunk to a file.
447 Uses the write function of the variables and the subchunks to do the actual work."""
448 # write header
449 self.ID.write(file)
450 self.size.write(file)
451 for variable in self.variables:
452 variable.write(file)
453 for subchunk in self.subchunks:
454 subchunk.write(file)
456 def dump(self, indent=0):
457 """Write the chunk to a file.
459 Dump is used for debugging purposes, to dump the contents of a chunk to the standard output.
460 Uses the dump function of the named variables and the subchunks to do the actual work."""
461 print(indent * " ",
462 "ID=%r" % hex(self.ID.value),
463 "size=%r" % self.get_size())
464 for variable in self.variables:
465 variable.dump(indent + 1)
466 for subchunk in self.subchunks:
467 subchunk.dump(indent + 1)
470 ######################################################
471 # EXPORT
472 ######################################################
474 def get_material_image(material):
475 """ Get images from paint slots."""
476 if material:
477 pt = material.paint_active_slot
478 tex = material.texture_paint_images
479 if pt < len(tex):
480 slot = tex[pt]
481 if slot.type == 'IMAGE':
482 return slot
485 def get_uv_image(ma):
486 """ Get image from material wrapper."""
487 if ma and ma.use_nodes:
488 ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma)
489 ma_tex = ma_wrap.base_color_texture
490 if ma_tex and ma_tex.image is not None:
491 return ma_tex.image
492 else:
493 return get_material_image(ma)
496 def make_material_subchunk(chunk_id, color):
497 """Make a material subchunk.
499 Used for color subchunks, such as diffuse color or ambient color subchunks."""
500 mat_sub = _3ds_chunk(chunk_id)
501 col1 = _3ds_chunk(RGB1)
502 col1.add_variable("color1", _3ds_rgb_color(color))
503 mat_sub.add_subchunk(col1)
504 # optional:
505 # col2 = _3ds_chunk(RGBI)
506 # col2.add_variable("color2", _3ds_rgb_color(color))
507 # mat_sub.add_subchunk(col2)
508 return mat_sub
511 def make_percent_subchunk(chunk_id, percent):
512 """Make a percentage based subchunk."""
513 pct_sub = _3ds_chunk(chunk_id)
514 pcti = _3ds_chunk(PCT)
515 pcti.add_variable("percent", _3ds_ushort(int(round(percent * 100, 0))))
516 pct_sub.add_subchunk(pcti)
517 return pct_sub
520 def make_texture_chunk(chunk_id, images):
521 """Make Material Map texture chunk."""
522 # Add texture percentage value (100 = 1.0)
523 ma_sub = make_percent_subchunk(chunk_id, 1)
524 has_entry = False
526 def add_image(img):
527 filename = bpy.path.basename(image.filepath)
528 ma_sub_file = _3ds_chunk(MATMAPFILE)
529 ma_sub_file.add_variable("image", _3ds_string(sane_name(filename)))
530 ma_sub.add_subchunk(ma_sub_file)
532 for image in images:
533 add_image(image)
534 has_entry = True
536 return ma_sub if has_entry else None
539 def make_material_texture_chunk(chunk_id, texslots, pct):
540 """Make Material Map texture chunk given a seq. of `MaterialTextureSlot`'s
541 Paint slots are optionally used as image source if no nodes are
542 used. No additional filtering for mapping modes is done, all
543 slots are written "as is"."""
544 # Add texture percentage value
545 mat_sub = make_percent_subchunk(chunk_id, pct)
546 has_entry = False
548 def add_texslot(texslot):
549 image = texslot.image
551 filename = bpy.path.basename(image.filepath)
552 mat_sub_file = _3ds_chunk(MATMAPFILE)
553 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
554 mat_sub.add_subchunk(mat_sub_file)
555 for link in texslot.socket_dst.links:
556 socket = link.from_socket.identifier
558 maptile = 0
560 # no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
561 if texslot.extension == 'EXTEND':
562 maptile |= 0x1
563 # CLIP maps to 3DS' decal flag
564 elif texslot.extension == 'CLIP':
565 maptile |= 0x10
567 mat_sub_tile = _3ds_chunk(MAT_MAP_TILING)
568 mat_sub_tile.add_variable("tiling", _3ds_ushort(maptile))
569 mat_sub.add_subchunk(mat_sub_tile)
571 if socket == 'Alpha':
572 mat_sub_alpha = _3ds_chunk(MAP_TILING)
573 alphaflag = 0x40 # summed area sampling 0x20
574 mat_sub_alpha.add_variable("alpha", _3ds_ushort(alphaflag))
575 mat_sub.add_subchunk(mat_sub_alpha)
576 if texslot.socket_dst.identifier in {'Base Color', 'Specular'}:
577 mat_sub_tint = _3ds_chunk(MAP_TILING) # RGB tint 0x200
578 tint = 0x80 if texslot.image.colorspace_settings.name == 'Non-Color' else 0x200
579 mat_sub_tint.add_variable("tint", _3ds_ushort(tint))
580 mat_sub.add_subchunk(mat_sub_tint)
582 mat_sub_texblur = _3ds_chunk(MAT_MAP_TEXBLUR) # Based on observation this is usually 1.0
583 mat_sub_texblur.add_variable("maptexblur", _3ds_float(1.0))
584 mat_sub.add_subchunk(mat_sub_texblur)
586 mat_sub_uscale = _3ds_chunk(MAT_MAP_USCALE)
587 mat_sub_uscale.add_variable("mapuscale", _3ds_float(round(texslot.scale[0], 6)))
588 mat_sub.add_subchunk(mat_sub_uscale)
590 mat_sub_vscale = _3ds_chunk(MAT_MAP_VSCALE)
591 mat_sub_vscale.add_variable("mapvscale", _3ds_float(round(texslot.scale[1], 6)))
592 mat_sub.add_subchunk(mat_sub_vscale)
594 mat_sub_uoffset = _3ds_chunk(MAT_MAP_UOFFSET)
595 mat_sub_uoffset.add_variable("mapuoffset", _3ds_float(round(texslot.translation[0], 6)))
596 mat_sub.add_subchunk(mat_sub_uoffset)
598 mat_sub_voffset = _3ds_chunk(MAT_MAP_VOFFSET)
599 mat_sub_voffset.add_variable("mapvoffset", _3ds_float(round(texslot.translation[1], 6)))
600 mat_sub.add_subchunk(mat_sub_voffset)
602 mat_sub_angle = _3ds_chunk(MAT_MAP_ANG)
603 mat_sub_angle.add_variable("mapangle", _3ds_float(round(texslot.rotation[2], 6)))
604 mat_sub.add_subchunk(mat_sub_angle)
606 if texslot.socket_dst.identifier in {'Base Color', 'Specular'}:
607 rgb = _3ds_chunk(MAP_COL1) # Add tint color
608 base = texslot.owner_shader.material.diffuse_color[:3]
609 spec = texslot.owner_shader.material.specular_color[:]
610 rgb.add_variable("mapcolor", _3ds_rgb_color(spec if texslot.socket_dst.identifier == 'Specular' else base))
611 mat_sub.add_subchunk(rgb)
613 # store all textures for this mapto in order. This at least is what
614 # the 3DS exporter did so far, afaik most readers will just skip
615 # over 2nd textures.
616 for slot in texslots:
617 if slot.image is not None:
618 add_texslot(slot)
619 has_entry = True
621 return mat_sub if has_entry else None
624 def make_material_chunk(material, image):
625 """Make a material chunk out of a blender material.
626 Shading method is required for 3ds max, 0 for wireframe.
627 0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
628 material_chunk = _3ds_chunk(MATERIAL)
629 name = _3ds_chunk(MATNAME)
630 shading = _3ds_chunk(MATSHADING)
632 name_str = material.name if material else "None"
634 # if image:
635 # name_str += image.name
637 name.add_variable("name", _3ds_string(sane_name(name_str)))
638 material_chunk.add_subchunk(name)
640 if not material:
641 shading.add_variable("shading", _3ds_ushort(1)) # Flat shading
642 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0.0, 0.0, 0.0)))
643 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (0.8, 0.8, 0.8)))
644 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1.0, 1.0, 1.0)))
645 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 0.8))
646 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, 1))
647 material_chunk.add_subchunk(shading)
649 elif material and material.use_nodes:
650 wrap = node_shader_utils.PrincipledBSDFWrapper(material)
651 shading.add_variable("shading", _3ds_ushort(3)) # Phong shading
652 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, wrap.emission_color[:3]))
653 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, wrap.base_color[:3]))
654 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
655 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 1 - wrap.roughness))
656 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, wrap.specular))
657 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, wrap.metallic))
658 material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
659 material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength))
660 material_chunk.add_subchunk(shading)
662 primary_tex = False
664 if wrap.base_color_texture:
665 d_pct = 0.7 + sum(wrap.base_color[:]) * 0.1
666 color = [wrap.base_color_texture]
667 matmap = make_material_texture_chunk(MAT_DIFFUSEMAP, color, d_pct)
668 if matmap:
669 material_chunk.add_subchunk(matmap)
670 primary_tex = True
672 if wrap.specular_texture:
673 spec = [wrap.specular_texture]
674 s_pct = material.specular_intensity
675 matmap = make_material_texture_chunk(MAT_SPECMAP, spec, s_pct)
676 if matmap:
677 material_chunk.add_subchunk(matmap)
679 if wrap.alpha_texture:
680 alpha = [wrap.alpha_texture]
681 a_pct = material.diffuse_color[3]
682 matmap = make_material_texture_chunk(MAT_OPACMAP, alpha, a_pct)
683 if matmap:
684 material_chunk.add_subchunk(matmap)
686 if wrap.metallic_texture:
687 metallic = [wrap.metallic_texture]
688 m_pct = material.metallic
689 matmap = make_material_texture_chunk(MAT_REFLMAP, metallic, m_pct)
690 if matmap:
691 material_chunk.add_subchunk(matmap)
693 if wrap.normalmap_texture:
694 normal = [wrap.normalmap_texture]
695 b_pct = wrap.normalmap_strength
696 matmap = make_material_texture_chunk(MAT_BUMPMAP, normal, b_pct)
697 if matmap:
698 material_chunk.add_subchunk(matmap)
699 material_chunk.add_subchunk(make_percent_subchunk(MAT_BUMP_PERCENT, b_pct))
701 if wrap.roughness_texture:
702 roughness = [wrap.roughness_texture]
703 r_pct = 1 - material.roughness
704 matmap = make_material_texture_chunk(MAT_SHINMAP, roughness, r_pct)
705 if matmap:
706 material_chunk.add_subchunk(matmap)
708 if wrap.emission_color_texture:
709 e_pct = wrap.emission_strength
710 emission = [wrap.emission_color_texture]
711 matmap = make_material_texture_chunk(MAT_SELFIMAP, emission, e_pct)
712 if matmap:
713 material_chunk.add_subchunk(matmap)
715 # make sure no textures are lost. Everything that doesn't fit
716 # into a channel is exported as secondary texture
717 diffuse = []
719 for link in wrap.material.node_tree.links:
720 if link.from_node.type == 'TEX_IMAGE' and link.to_node.type == 'MIX_RGB':
721 diffuse = [link.from_node.image]
723 if diffuse:
724 if not primary_tex:
725 matmap = make_texture_chunk(MAT_DIFFUSEMAP, diffuse)
726 else:
727 matmap = make_texture_chunk(MAT_TEX2MAP, diffuse)
728 if matmap:
729 material_chunk.add_subchunk(matmap)
731 else:
732 shading.add_variable("shading", _3ds_ushort(2)) # Gouraud shading
733 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, material.line_color[:3]))
734 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color[:3]))
735 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
736 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 1 - material.roughness))
737 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, material.specular_intensity))
738 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, material.metallic))
739 material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - material.diffuse_color[3]))
740 material_chunk.add_subchunk(shading)
742 slots = [get_material_image(material)] # can be None
744 if image:
745 material_chunk.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP, slots))
747 return material_chunk
750 class tri_wrapper(object):
751 """Class representing a triangle.
752 Used when converting faces to triangles"""
754 __slots__ = "vertex_index", "ma", "image", "faceuvs", "offset", "flag", "group"
756 def __init__(self, vindex=(0, 0, 0), ma=None, image=None, faceuvs=None, flag=0, group=0):
757 self.vertex_index = vindex
758 self.ma = ma
759 self.image = image
760 self.faceuvs = faceuvs
761 self.offset = [0, 0, 0] # offset indices
762 self.flag = flag
763 self.group = group
766 def extract_triangles(mesh):
767 """Extract triangles from a mesh."""
769 mesh.calc_loop_triangles()
770 (polygroup, count) = mesh.calc_smooth_groups(use_bitflags=True)
772 tri_list = []
773 do_uv = bool(mesh.uv_layers)
775 img = None
776 for i, face in enumerate(mesh.loop_triangles):
777 f_v = face.vertices
778 v1, v2, v3 = f_v
779 uf = mesh.uv_layers.active.data if do_uv else None
781 if do_uv:
782 f_uv = [uf[lp].uv for lp in face.loops]
783 for ma in mesh.materials:
784 img = get_uv_image(ma) if uf else None
785 if img is not None:
786 img = img.name
787 uv1, uv2, uv3 = f_uv
789 """Flag 0x1 sets CA edge visible, Flag 0x2 sets BC edge visible, Flag 0x4 sets AB edge visible
790 Flag 0x8 indicates a U axis texture wrap seam and Flag 0x10 indicates a V axis texture wrap seam
791 In Blender we use the edge CA, BC, and AB flags for sharp edges flags"""
792 a_b = mesh.edges[mesh.loops[face.loops[0]].edge_index]
793 b_c = mesh.edges[mesh.loops[face.loops[1]].edge_index]
794 c_a = mesh.edges[mesh.loops[face.loops[2]].edge_index]
796 if v3 == 0:
797 v1, v2, v3 = v3, v1, v2
798 a_b, b_c, c_a = c_a, a_b, b_c
799 if do_uv:
800 uv1, uv2, uv3 = uv3, uv1, uv2
802 faceflag = 0
803 if c_a.use_edge_sharp:
804 faceflag = faceflag + 0x1
805 if b_c.use_edge_sharp:
806 faceflag = faceflag + 0x2
807 if a_b.use_edge_sharp:
808 faceflag = faceflag + 0x4
810 smoothgroup = polygroup[face.polygon_index]
812 if len(f_v) == 3:
813 new_tri = tri_wrapper((v1, v2, v3), face.material_index, img)
814 if (do_uv):
815 new_tri.faceuvs = uv_key(uv1), uv_key(uv2), uv_key(uv3)
816 new_tri.flag = faceflag
817 new_tri.group = smoothgroup if face.use_smooth else 0
818 tri_list.append(new_tri)
820 return tri_list
823 def remove_face_uv(verts, tri_list):
824 """Remove face UV coordinates from a list of triangles.
825 Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
826 need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
827 there are multiple uv coordinates per vertex."""
829 # initialize a list of UniqueLists, one per vertex:
830 # uv_list = [UniqueList() for i in xrange(len(verts))]
831 unique_uvs = [{} for i in range(len(verts))]
833 # for each face uv coordinate, add it to the UniqueList of the vertex
834 for tri in tri_list:
835 for i in range(3):
836 # store the index into the UniqueList for future reference:
837 # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
839 context_uv_vert = unique_uvs[tri.vertex_index[i]]
840 uvkey = tri.faceuvs[i]
842 offset_index__uv_3ds = context_uv_vert.get(uvkey)
844 if not offset_index__uv_3ds:
845 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
847 tri.offset[i] = offset_index__uv_3ds[0]
849 # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
850 # only once.
852 # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
853 # faces refer to the new face indices:
854 vert_index = 0
855 vert_array = _3ds_array()
856 uv_array = _3ds_array()
857 index_list = []
858 for i, vert in enumerate(verts):
859 index_list.append(vert_index)
861 pt = _3ds_point_3d(vert.co) # reuse, should be ok
862 uvmap = [None] * len(unique_uvs[i])
863 for ii, uv_3ds in unique_uvs[i].values():
864 # add a vertex duplicate to the vertex_array for every uv associated with this vertex:
865 vert_array.add(pt)
866 # add the uv coordinate to the uv array:
867 # This for loop does not give uv's ordered by ii, so we create a new map
868 # and add the uv's later
869 # uv_array.add(uv_3ds)
870 uvmap[ii] = uv_3ds
872 # Add the uv's in the correct order
873 for uv_3ds in uvmap:
874 # add the uv coordinate to the uv array:
875 uv_array.add(uv_3ds)
877 vert_index += len(unique_uvs[i])
879 # Make sure the triangle vertex indices now refer to the new vertex list:
880 for tri in tri_list:
881 for i in range(3):
882 tri.offset[i] += index_list[tri.vertex_index[i]]
883 tri.vertex_index = tri.offset
885 return vert_array, uv_array, tri_list
888 def make_faces_chunk(tri_list, mesh, materialDict):
889 """Make a chunk for the faces.
890 Also adds subchunks assigning materials to all faces."""
891 do_smooth = False
892 use_smooth = [poly.use_smooth for poly in mesh.polygons]
893 if True in use_smooth:
894 do_smooth = True
896 materials = mesh.materials
897 if not materials:
898 ma = None
900 face_chunk = _3ds_chunk(OBJECT_FACES)
901 face_list = _3ds_array()
903 if mesh.uv_layers:
904 # Gather materials used in this mesh - mat/image pairs
905 unique_mats = {}
906 for i, tri in enumerate(tri_list):
907 face_list.add(_3ds_face(tri.vertex_index, tri.flag))
909 if materials:
910 ma = materials[tri.ma]
911 if ma:
912 ma = ma.name
914 img = tri.image
916 try:
917 context_face_array = unique_mats[ma, img][1]
918 except:
919 name_str = ma if ma else "None"
920 # if img:
921 # name_str += img
923 context_face_array = _3ds_array()
924 unique_mats[ma, img] = _3ds_string(sane_name(name_str)), context_face_array
926 context_face_array.add(_3ds_ushort(i))
927 # obj_material_faces[tri.ma].add(_3ds_ushort(i))
929 face_chunk.add_variable("faces", face_list)
930 for ma_name, ma_faces in unique_mats.values():
931 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
932 obj_material_chunk.add_variable("name", ma_name)
933 obj_material_chunk.add_variable("face_list", ma_faces)
934 face_chunk.add_subchunk(obj_material_chunk)
936 else:
937 obj_material_faces = []
938 obj_material_names = []
939 for m in materials:
940 if m:
941 obj_material_names.append(_3ds_string(sane_name(m.name)))
942 obj_material_faces.append(_3ds_array())
943 n_materials = len(obj_material_names)
945 for i, tri in enumerate(tri_list):
946 face_list.add(_3ds_face(tri.vertex_index, tri.flag))
947 if (tri.ma < n_materials):
948 obj_material_faces[tri.ma].add(_3ds_ushort(i))
950 face_chunk.add_variable("faces", face_list)
951 for i in range(n_materials):
952 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
953 obj_material_chunk.add_variable("name", obj_material_names[i])
954 obj_material_chunk.add_variable("face_list", obj_material_faces[i])
955 face_chunk.add_subchunk(obj_material_chunk)
957 if do_smooth:
958 obj_smooth_chunk = _3ds_chunk(OBJECT_SMOOTH)
959 for i, tri in enumerate(tri_list):
960 obj_smooth_chunk.add_variable("face_" + str(i), _3ds_uint(tri.group))
961 face_chunk.add_subchunk(obj_smooth_chunk)
963 return face_chunk
966 def make_vert_chunk(vert_array):
967 """Make a vertex chunk out of an array of vertices."""
968 vert_chunk = _3ds_chunk(OBJECT_VERTICES)
969 vert_chunk.add_variable("vertices", vert_array)
970 return vert_chunk
973 def make_uv_chunk(uv_array):
974 """Make a UV chunk out of an array of UVs."""
975 uv_chunk = _3ds_chunk(OBJECT_UV)
976 uv_chunk.add_variable("uv coords", uv_array)
977 return uv_chunk
981 def make_matrix_4x3_chunk(matrix):
982 matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
983 for vec in matrix.col:
984 for f in vec[:3]:
985 matrix_chunk.add_variable("matrix_f", _3ds_float(f))
986 return matrix_chunk
990 def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
991 """Make a chunk out of a Blender mesh."""
993 # Extract the triangles from the mesh:
994 tri_list = extract_triangles(mesh)
996 if mesh.uv_layers:
997 # Remove the face UVs and convert it to vertex UV:
998 vert_array, uv_array, tri_list = remove_face_uv(mesh.vertices, tri_list)
999 else:
1000 # Add the vertices to the vertex array:
1001 vert_array = _3ds_array()
1002 for vert in mesh.vertices:
1003 vert_array.add(_3ds_point_3d(vert.co))
1004 # no UV at all:
1005 uv_array = None
1007 # create the chunk:
1008 mesh_chunk = _3ds_chunk(OBJECT_MESH)
1010 # add vertex chunk:
1011 mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
1013 # add faces chunk:
1014 mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
1016 # if available, add uv chunk:
1017 if uv_array:
1018 mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
1020 # mesh_chunk.add_subchunk(make_matrix_4x3_chunk(matrix))
1022 # create transformation matrix chunk
1023 matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
1024 obj_matrix = matrix.transposed().to_3x3()
1026 if ob.parent is None:
1027 obj_translate = translation[ob.name]
1029 else: # Calculate child matrix translation relative to parent
1030 obj_translate = translation[ob.name].cross(-1 * translation[ob.parent.name])
1032 matrix_chunk.add_variable("xx", _3ds_float(obj_matrix[0].to_tuple(6)[0]))
1033 matrix_chunk.add_variable("xy", _3ds_float(obj_matrix[0].to_tuple(6)[1]))
1034 matrix_chunk.add_variable("xz", _3ds_float(obj_matrix[0].to_tuple(6)[2]))
1035 matrix_chunk.add_variable("yx", _3ds_float(obj_matrix[1].to_tuple(6)[0]))
1036 matrix_chunk.add_variable("yy", _3ds_float(obj_matrix[1].to_tuple(6)[1]))
1037 matrix_chunk.add_variable("yz", _3ds_float(obj_matrix[1].to_tuple(6)[2]))
1038 matrix_chunk.add_variable("zx", _3ds_float(obj_matrix[2].to_tuple(6)[0]))
1039 matrix_chunk.add_variable("zy", _3ds_float(obj_matrix[2].to_tuple(6)[1]))
1040 matrix_chunk.add_variable("zz", _3ds_float(obj_matrix[2].to_tuple(6)[2]))
1041 matrix_chunk.add_variable("tx", _3ds_float(obj_translate.to_tuple(6)[0]))
1042 matrix_chunk.add_variable("ty", _3ds_float(obj_translate.to_tuple(6)[1]))
1043 matrix_chunk.add_variable("tz", _3ds_float(obj_translate.to_tuple(6)[2]))
1045 mesh_chunk.add_subchunk(matrix_chunk)
1047 return mesh_chunk
1050 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1051 def make_kfdata(start=0, stop=0, curtime=0):
1052 """Make the basic keyframe data chunk"""
1053 kfdata = _3ds_chunk(KFDATA)
1055 kfhdr = _3ds_chunk(KFDATA_KFHDR)
1056 kfhdr.add_variable("revision", _3ds_ushort(0))
1057 # Not really sure what filename is used for, but it seems it is usually used
1058 # to identify the program that generated the .3ds:
1059 kfhdr.add_variable("filename", _3ds_string("Blender"))
1060 kfhdr.add_variable("animlen", _3ds_uint(stop-start))
1062 kfseg = _3ds_chunk(KFDATA_KFSEG)
1063 kfseg.add_variable("start", _3ds_uint(start))
1064 kfseg.add_variable("stop", _3ds_uint(stop))
1066 kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
1067 kfcurtime.add_variable("curtime", _3ds_uint(curtime))
1069 kfdata.add_subchunk(kfhdr)
1070 kfdata.add_subchunk(kfseg)
1071 kfdata.add_subchunk(kfcurtime)
1072 return kfdata
1074 def make_track_chunk(ID, obj):
1075 """Make a chunk for track data.
1077 Depending on the ID, this will construct a position, rotation or scale track."""
1078 track_chunk = _3ds_chunk(ID)
1079 track_chunk.add_variable("track_flags", _3ds_ushort())
1080 track_chunk.add_variable("unknown", _3ds_uint())
1081 track_chunk.add_variable("unknown", _3ds_uint())
1082 track_chunk.add_variable("nkeys", _3ds_uint(1))
1083 # Next section should be repeated for every keyframe, but for now, animation is not actually supported.
1084 track_chunk.add_variable("tcb_frame", _3ds_uint(0))
1085 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1086 if obj.type=='Empty':
1087 if ID==POS_TRACK_TAG:
1088 # position vector:
1089 track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation()))
1090 elif ID==ROT_TRACK_TAG:
1091 # rotation (quaternion, angle first, followed by axis):
1092 q = obj.getEuler().to_quaternion() # XXX, todo!
1093 track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2])))
1094 elif ID==SCL_TRACK_TAG:
1095 # scale vector:
1096 track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize()))
1097 else:
1098 # meshes have their transformations applied before
1099 # exporting, so write identity transforms here:
1100 if ID==POS_TRACK_TAG:
1101 # position vector:
1102 track_chunk.add_variable("position", _3ds_point_3d((0.0,0.0,0.0)))
1103 elif ID==ROT_TRACK_TAG:
1104 # rotation (quaternion, angle first, followed by axis):
1105 track_chunk.add_variable("rotation", _3ds_point_4d((0.0, 1.0, 0.0, 0.0)))
1106 elif ID==SCL_TRACK_TAG:
1107 # scale vector:
1108 track_chunk.add_variable("scale", _3ds_point_3d((1.0, 1.0, 1.0)))
1110 return track_chunk
1112 def make_kf_obj_node(obj, name_to_id):
1113 """Make a node chunk for a Blender object.
1115 Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id.
1116 Blender Empty objects are converted to dummy nodes."""
1118 name = obj.name
1119 # main object node chunk:
1120 kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG)
1121 # chunk for the object id:
1122 obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
1123 # object id is from the name_to_id dictionary:
1124 obj_id_chunk.add_variable("node_id", _3ds_ushort(name_to_id[name]))
1126 # object node header:
1127 obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
1128 # object name:
1129 if obj.type == 'Empty':
1130 # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk
1131 # for their name (see below):
1132 obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY"))
1133 else:
1134 # Add the name:
1135 obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
1136 # Add Flag variables (not sure what they do):
1137 obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0))
1138 obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
1140 # Check parent-child relationships:
1141 parent = obj.parent
1142 if (parent is None) or (parent.name not in name_to_id):
1143 # If no parent, or the parents name is not in the name_to_id dictionary,
1144 # parent id becomes -1:
1145 obj_node_header_chunk.add_variable("parent", _3ds_ushort(-1))
1146 else:
1147 # Get the parent's id from the name_to_id dictionary:
1148 obj_node_header_chunk.add_variable("parent", _3ds_ushort(name_to_id[parent.name]))
1150 # Add pivot chunk:
1151 obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
1152 obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation()))
1153 kf_obj_node.add_subchunk(obj_pivot_chunk)
1155 # add subchunks for object id and node header:
1156 kf_obj_node.add_subchunk(obj_id_chunk)
1157 kf_obj_node.add_subchunk(obj_node_header_chunk)
1159 # Empty objects need to have an extra chunk for the instance name:
1160 if obj.type == 'Empty':
1161 obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
1162 obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
1163 kf_obj_node.add_subchunk(obj_instance_name_chunk)
1165 # Add track chunks for position, rotation and scale:
1166 kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj))
1167 kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj))
1168 kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj))
1170 return kf_obj_node
1174 def save(operator,
1175 context, filepath="",
1176 use_selection=True,
1177 global_matrix=None,
1180 import time
1181 # from bpy_extras.io_utils import create_derived_objects, free_derived_objects
1183 """Save the Blender scene to a 3ds file."""
1185 # Time the export
1186 duration = time.time()
1187 # Blender.Window.WaitCursor(1)
1189 if global_matrix is None:
1190 global_matrix = mathutils.Matrix()
1192 if bpy.ops.object.mode_set.poll():
1193 bpy.ops.object.mode_set(mode='OBJECT')
1195 scene = context.scene
1196 layer = context.view_layer
1197 depsgraph = context.evaluated_depsgraph_get()
1199 # Initialize the main chunk (primary):
1200 primary = _3ds_chunk(PRIMARY)
1201 # Add version chunk:
1202 version_chunk = _3ds_chunk(VERSION)
1203 version_chunk.add_variable("version", _3ds_uint(3))
1204 primary.add_subchunk(version_chunk)
1206 # Init main object info chunk:
1207 object_info = _3ds_chunk(OBJECTINFO)
1208 mesh_version = _3ds_chunk(MESHVERSION)
1209 mesh_version.add_variable("mesh", _3ds_uint(3))
1210 object_info.add_subchunk(mesh_version)
1212 # Add MASTERSCALE element
1213 mscale = _3ds_chunk(MASTERSCALE)
1214 mscale.add_variable("scale", _3ds_float(1))
1215 object_info.add_subchunk(mscale)
1217 # Add AMBIENT color
1218 if scene.world is not None:
1219 ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
1220 ambient_light = _3ds_chunk(RGB)
1221 ambient_light.add_variable("ambient", _3ds_float_color(scene.world.color))
1222 ambient_chunk.add_subchunk(ambient_light)
1223 object_info.add_subchunk(ambient_chunk)
1225 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1226 # init main key frame data chunk:
1227 kfdata = make_kfdata()
1230 # Make a list of all materials used in the selected meshes (use a dictionary,
1231 # each material is added once):
1232 materialDict = {}
1233 mesh_objects = []
1235 if use_selection:
1236 objects = [ob for ob in scene.objects if not ob.hide_viewport and ob.select_get(view_layer=layer)]
1237 else:
1238 objects = [ob for ob in scene.objects if not ob.hide_viewport]
1240 light_objects = [ob for ob in objects if ob.type == 'LIGHT']
1241 camera_objects = [ob for ob in objects if ob.type == 'CAMERA']
1243 for ob in objects:
1244 # get derived objects
1245 # free, derived = create_derived_objects(scene, ob)
1246 derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
1247 derived = derived_dict.get(ob)
1249 if derived is None:
1250 continue
1252 for ob_derived, mtx in derived:
1253 if ob.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
1254 continue
1256 try:
1257 data = ob_derived.to_mesh()
1258 except:
1259 data = None
1261 if data:
1262 matrix = global_matrix @ mtx
1263 data.transform(matrix)
1264 mesh_objects.append((ob_derived, data, matrix))
1265 ma_ls = data.materials
1266 ma_ls_len = len(ma_ls)
1268 # get material/image tuples.
1269 if data.uv_layers:
1270 if not ma_ls:
1271 ma = ma_name = None
1273 for f, uf in zip(data.polygons, data.uv_layers.active.data):
1274 if ma_ls:
1275 ma_index = f.material_index
1276 if ma_index >= ma_ls_len:
1277 ma_index = f.material_index = 0
1278 ma = ma_ls[ma_index]
1279 ma_name = None if ma is None else ma.name
1280 # else there already set to none
1282 img = get_uv_image(ma)
1283 img_name = None if img is None else img.name
1285 materialDict.setdefault((ma_name, img_name), (ma, img))
1287 else:
1288 for ma in ma_ls:
1289 if ma: # material may be None so check its not.
1290 materialDict.setdefault((ma.name, None), (ma, None))
1292 # Why 0 Why!
1293 for f in data.polygons:
1294 if f.material_index >= ma_ls_len:
1295 f.material_index = 0
1297 # ob_derived_eval.to_mesh_clear()
1299 # if free:
1300 # free_derived_objects(ob)
1302 # Make material chunks for all materials used in the meshes:
1303 for ma_image in materialDict.values():
1304 object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
1306 # Give all objects a unique ID and build a dictionary from object name to object id:
1307 translation = {} # collect translation for transformation matrix
1308 # name_to_id = {}
1309 for ob, data, matrix in mesh_objects:
1310 translation[ob.name] = ob.location
1311 # name_to_id[ob.name]= len(name_to_id)
1313 #for ob in empty_objects:
1314 # name_to_id[ob.name]= len(name_to_id)
1317 # Create object chunks for all meshes:
1318 i = 0
1319 for ob, mesh, matrix in mesh_objects:
1320 # create a new object chunk
1321 object_chunk = _3ds_chunk(OBJECT)
1323 # set the object name
1324 object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1326 # make a mesh chunk out of the mesh:
1327 object_chunk.add_subchunk(make_mesh_chunk(ob, mesh, matrix, materialDict, translation))
1329 # ensure the mesh has no over sized arrays
1330 # skip ones that do!, otherwise we cant write since the array size wont
1331 # fit into USHORT.
1332 if object_chunk.validate():
1333 object_info.add_subchunk(object_chunk)
1334 else:
1335 operator.report({'WARNING'}, "Object %r can't be written into a 3DS file")
1337 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1338 # make a kf object node for the object:
1339 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1342 # if not blender_mesh.users:
1343 # bpy.data.meshes.remove(blender_mesh)
1344 # blender_mesh.vertices = None
1346 i += i
1348 # Create chunks for all empties:
1349 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1350 for ob in empty_objects:
1351 # Empties only require a kf object node:
1352 kfdata.add_subchunk(make_kf_obj_node(ob, name_to_id))
1353 pass
1356 # Create light object chunks
1357 for ob in light_objects:
1358 object_chunk = _3ds_chunk(OBJECT)
1359 light_chunk = _3ds_chunk(OBJECT_LIGHT)
1360 color_float_chunk = _3ds_chunk(RGB)
1361 energy_factor = _3ds_chunk(LIGHT_MULTIPLIER)
1362 object_chunk.add_variable("light", _3ds_string(sane_name(ob.name)))
1363 light_chunk.add_variable("location", _3ds_point_3d(ob.location))
1364 color_float_chunk.add_variable("color", _3ds_float_color(ob.data.color))
1365 energy_factor.add_variable("energy", _3ds_float(ob.data.energy * .001))
1366 light_chunk.add_subchunk(color_float_chunk)
1367 light_chunk.add_subchunk(energy_factor)
1369 if ob.data.type == 'SPOT':
1370 cone_angle = math.degrees(ob.data.spot_size)
1371 hotspot = cone_angle - (ob.data.spot_blend * math.floor(cone_angle))
1372 hypo = math.copysign(math.sqrt(pow(ob.location[0], 2) + pow(ob.location[1], 2)), ob.location[1])
1373 pos_x = ob.location[0] + (ob.location[1] * math.tan(ob.rotation_euler[2]))
1374 pos_y = ob.location[1] + (ob.location[0] * math.tan(math.radians(90) - ob.rotation_euler[2]))
1375 pos_z = hypo * math.tan(math.radians(90) - ob.rotation_euler[0])
1376 spotlight_chunk = _3ds_chunk(LIGHT_SPOTLIGHT)
1377 spot_roll_chunk = _3ds_chunk(LIGHT_SPOTROLL)
1378 spotlight_chunk.add_variable("target", _3ds_point_3d((pos_x, pos_y, pos_z)))
1379 spotlight_chunk.add_variable("hotspot", _3ds_float(round(hotspot, 4)))
1380 spotlight_chunk.add_variable("angle", _3ds_float(round(cone_angle, 4)))
1381 spot_roll_chunk.add_variable("roll", _3ds_float(round(ob.rotation_euler[1], 6)))
1382 spotlight_chunk.add_subchunk(spot_roll_chunk)
1383 light_chunk.add_subchunk(spotlight_chunk)
1385 # Add light to object info
1386 object_chunk.add_subchunk(light_chunk)
1387 object_info.add_subchunk(object_chunk)
1389 # Create camera object chunks
1390 for ob in camera_objects:
1391 object_chunk = _3ds_chunk(OBJECT)
1392 camera_chunk = _3ds_chunk(OBJECT_CAMERA)
1393 diagonal = math.copysign(math.sqrt(pow(ob.location[0], 2) + pow(ob.location[1], 2)), ob.location[1])
1394 focus_x = ob.location[0] + (ob.location[1] * math.tan(ob.rotation_euler[2]))
1395 focus_y = ob.location[1] + (ob.location[0] * math.tan(math.radians(90) - ob.rotation_euler[2]))
1396 focus_z = diagonal * math.tan(math.radians(90) - ob.rotation_euler[0])
1397 object_chunk.add_variable("camera", _3ds_string(sane_name(ob.name)))
1398 camera_chunk.add_variable("location", _3ds_point_3d(ob.location))
1399 camera_chunk.add_variable("target", _3ds_point_3d((focus_x, focus_y, focus_z)))
1400 camera_chunk.add_variable("roll", _3ds_float(round(ob.rotation_euler[1], 6)))
1401 camera_chunk.add_variable("lens", _3ds_float(ob.data.lens))
1402 object_chunk.add_subchunk(camera_chunk)
1403 object_info.add_subchunk(object_chunk)
1405 # Add main object info chunk to primary chunk:
1406 primary.add_subchunk(object_info)
1408 ''' # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1409 # Add main keyframe data chunk to primary chunk:
1410 primary.add_subchunk(kfdata)
1413 # At this point, the chunk hierarchy is completely built.
1415 # Check the size:
1416 primary.get_size()
1417 # Open the file for writing:
1418 file = open(filepath, 'wb')
1420 # Recursively write the chunks to file:
1421 primary.write(file)
1423 # Close the file:
1424 file.close()
1426 # Clear name mapping vars, could make locals too
1427 del name_unique[:]
1428 name_mapping.clear()
1430 # Debugging only: report the exporting time:
1431 # Blender.Window.WaitCursor(0)
1432 print("3ds export time: %.2f" % (time.time() - duration))
1434 # Debugging only: dump the chunk hierarchy:
1435 # primary.dump()
1437 return {'FINISHED'}