Export_3ds: Added coat weight as reflection blur
[blender-addons.git] / io_scene_3ds / export_3ds.py
blob7021cf67b04304d5032f9d5ac32c8e240c178ff5
1 # SPDX-FileCopyrightText: 2005 Bob Holcomb
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
7 from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
8 """
10 import bpy
11 import time
12 import math
13 import struct
14 import mathutils
15 import bpy_extras
16 from bpy_extras import node_shader_utils
18 ###################
19 # Data Structures #
20 ###################
22 # Some of the chunks that we will export
23 # >----- Primary Chunk, at the beginning of each file
24 PRIMARY = 0x4D4D
26 # >----- Main Chunks
27 OBJECTINFO = 0x3D3D # Main mesh object chunk before material and object information
28 MESHVERSION = 0x3D3E # This gives the version of the mesh
29 VERSION = 0x0002 # This gives the version of the .3ds file
30 KFDATA = 0xB000 # This is the header for all of the keyframe info
32 # >----- sub defines of OBJECTINFO
33 BITMAP = 0x1100 # The background image name
34 USE_BITMAP = 0x1101 # The background image flag
35 SOLIDBACKGND = 0x1200 # The background color (RGB)
36 USE_SOLIDBGND = 0x1201 # The background color flag
37 VGRADIENT = 0x1300 # The background gradient colors
38 USE_VGRADIENT = 0x1301 # The background gradient flag
39 O_CONSTS = 0x1500 # The origin of the 3D cursor
40 AMBIENTLIGHT = 0x2100 # The color of the ambient light
41 FOG = 0x2200 # The fog atmosphere settings
42 USE_FOG = 0x2201 # The fog atmosphere flag
43 LAYER_FOG = 0x2302 # The fog layer atmosphere settings
44 USE_LAYER_FOG = 0x2303 # The fog layer atmosphere flag
45 MATERIAL = 45055 # 0xAFFF // This stored the texture info
46 OBJECT = 16384 # 0x4000 // This stores the faces, vertices, etc...
48 # >------ sub defines of MATERIAL
49 MATNAME = 0xA000 # This holds the material name
50 MATAMBIENT = 0xA010 # Ambient color of the object/material
51 MATDIFFUSE = 0xA020 # This holds the color of the object/material
52 MATSPECULAR = 0xA030 # Specular color of the object/material
53 MATSHINESS = 0xA040 # Specular intensity of the object/material (percent)
54 MATSHIN2 = 0xA041 # Reflection of the object/material (percent)
55 MATSHIN3 = 0xA042 # metallic/mirror of the object/material (percent)
56 MATTRANS = 0xA050 # Transparency value (100-OpacityValue) (percent)
57 MATXPFALL = 0xA052 # Transparency falloff ratio (percent)
58 MATREFBLUR = 0xA053 # Reflection blurring ratio (percent)
59 MATSELFILLUM = 0xA080 # # Material self illumination flag
60 MATSELFILPCT = 0xA084 # Self illumination strength (percent)
61 MATWIRE = 0xA085 # Material wireframe rendered flag
62 MATFACEMAP = 0xA088 # Face mapped textures flag
63 MATPHONGSOFT = 0xA08C # Phong soften material flag
64 MATWIREABS = 0xA08E # Wire size in units flag
65 MATWIRESIZE = 0xA087 # Rendered wire size in pixels
66 MATSHADING = 0xA100 # Material shading method
68 # >------ sub defines of MAT_MAP
69 MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture
70 MAT_SPECMAP = 0xA204 # head for specularity map
71 MAT_OPACMAP = 0xA210 # head for opacity map
72 MAT_REFLMAP = 0xA220 # head for reflect map
73 MAT_BUMPMAP = 0xA230 # head for normal map
74 MAT_BUMP_PERCENT = 0xA252 # Normalmap strength (percent)
75 MAT_TEX2MAP = 0xA33A # head for secondary texture
76 MAT_SHINMAP = 0xA33C # head for roughness map
77 MAT_SELFIMAP = 0xA33D # head for emission map
78 MAT_MAP_FILE = 0xA300 # This holds the file name of a texture
79 MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag
80 MAT_MAP_TEXBLUR = 0xA353 # Texture blurring factor
81 MAT_MAP_USCALE = 0xA354 # U axis scaling
82 MAT_MAP_VSCALE = 0xA356 # V axis scaling
83 MAT_MAP_UOFFSET = 0xA358 # U axis offset
84 MAT_MAP_VOFFSET = 0xA35A # V axis offset
85 MAT_MAP_ANG = 0xA35C # UV rotation around the z-axis in rad
86 MAP_COL1 = 0xA360 # Tint Color1
87 MAP_COL2 = 0xA362 # Tint Color2
88 MAP_RCOL = 0xA364 # Red tint
89 MAP_GCOL = 0xA366 # Green tint
90 MAP_BCOL = 0xA368 # Blue tint
92 RGB = 0x0010 # RGB float Color1
93 RGB1 = 0x0011 # RGB int Color1
94 RGBI = 0x0012 # RGB int Color2
95 RGBF = 0x0013 # RGB float Color2
96 PCT = 0x0030 # Percent chunk
97 PCTF = 0x0031 # Percent float
98 MASTERSCALE = 0x0100 # Master scale factor
100 # >------ sub defines of OBJECT
101 OBJECT_MESH = 0x4100 # This lets us know that we are reading a new object
102 OBJECT_LIGHT = 0x4600 # This lets us know we are reading a light object
103 OBJECT_CAMERA = 0x4700 # This lets us know we are reading a camera object
104 OBJECT_HIERARCHY = 0x4F00 # Hierarchy id of the object
105 OBJECT_PARENT = 0x4F10 # Parent id of the object
107 # >------ Sub defines of LIGHT
108 LIGHT_MULTIPLIER = 0x465B # The light energy factor
109 LIGHT_INNER_RANGE = 0x4659 # Light inner range value
110 LIGHT_OUTER_RANGE = 0x465A # Light outer range value
111 LIGHT_ATTENUATE = 0x4625 # Light attenuation flag
112 LIGHT_SPOTLIGHT = 0x4610 # The target of a spotlight
113 LIGHT_SPOT_ROLL = 0x4656 # Light spot roll angle
114 LIGHT_SPOT_SHADOWED = 0x4630 # Light spot shadow flag
115 LIGHT_SPOT_LSHADOW = 0x4641 # Light spot shadow parameters
116 LIGHT_SPOT_SEE_CONE = 0x4650 # Light spot show cone flag
117 LIGHT_SPOT_RECTANGLE = 0x4651 # Light spot rectangle flag
118 LIGHT_SPOT_OVERSHOOT = 0x4652 # Light spot overshoot flag
119 LIGHT_SPOT_PROJECTOR = 0x4653 # Light spot projection bitmap
120 LIGHT_SPOT_ASPECT = 0x4657 # Light spot aspect ratio
122 # >------ sub defines of CAMERA
123 OBJECT_CAM_RANGES = 0x4720 # The camera range values
125 # >------ sub defines of OBJECT_MESH
126 OBJECT_VERTICES = 0x4110 # The objects vertices
127 OBJECT_VERTFLAGS = 0x4111 # The objects vertex flags
128 OBJECT_FACES = 0x4120 # The objects faces
129 OBJECT_MATERIAL = 0x4130 # This is found if the object has a material, either texture map or color
130 OBJECT_UV = 0x4140 # The UV texture coordinates
131 OBJECT_SMOOTH = 0x4150 # The objects smooth groups
132 OBJECT_TRANS_MATRIX = 0x4160 # The Object Matrix
134 # >------ sub defines of KFDATA
135 AMBIENT_NODE_TAG = 0xB001 # Ambient node tag
136 OBJECT_NODE_TAG = 0xB002 # Object tree tag
137 CAMERA_NODE_TAG = 0xB003 # Camera object tag
138 TARGET_NODE_TAG = 0xB004 # Camera target tag
139 LIGHT_NODE_TAG = 0xB005 # Light object tag
140 LTARGET_NODE_TAG = 0xB006 # Light target tag
141 SPOT_NODE_TAG = 0xB007 # Spotlight tag
142 KFDATA_KFSEG = 0xB008 # Frame start & end
143 KFDATA_KFCURTIME = 0xB009 # Frame current
144 KFDATA_KFHDR = 0xB00A # Keyframe header
146 # >------ sub defines of OBJECT_NODE_TAG
147 OBJECT_NODE_ID = 0xB030 # Object hierachy ID
148 OBJECT_NODE_HDR = 0xB010 # Hierachy tree header
149 OBJECT_INSTANCE_NAME = 0xB011 # Object instance name
150 OBJECT_PARENT_NAME = 0x80F0 # Object parent name
151 OBJECT_PIVOT = 0xB013 # Object pivot position
152 OBJECT_BOUNDBOX = 0xB014 # Object boundbox
153 OBJECT_MORPH_SMOOTH = 0xB015 # Object smooth angle
154 POS_TRACK_TAG = 0xB020 # Position transform tag
155 ROT_TRACK_TAG = 0xB021 # Rotation transform tag
156 SCL_TRACK_TAG = 0xB022 # Scale transform tag
157 FOV_TRACK_TAG = 0xB023 # Field of view tag
158 ROLL_TRACK_TAG = 0xB024 # Roll transform tag
159 COL_TRACK_TAG = 0xB025 # Color transform tag
160 HOTSPOT_TRACK_TAG = 0xB027 # Hotspot transform tag
161 FALLOFF_TRACK_TAG = 0xB028 # Falloff transform tag
163 ROOT_OBJECT = 0xFFFF # Root object
166 # So 3ds max can open files, limit names to 12 in length
167 # this is very annoying for filenames!
168 name_unique = [] # stores str, ascii only
169 name_mapping = {} # stores {orig: byte} mapping
171 def sane_name(name):
172 name_fixed = name_mapping.get(name)
173 if name_fixed is not None:
174 return name_fixed
176 # Strip non ascii chars
177 new_name_clean = new_name = name.encode("ASCII", "replace").decode("ASCII")[:12]
178 i = 0
180 while new_name in name_unique:
181 new_name = new_name_clean + '.%.3d' % i
182 i += 1
184 # Note, appending the 'str' version
185 name_unique.append(new_name)
186 name_mapping[name] = new_name = new_name.encode("ASCII", "replace")
187 return new_name
190 def uv_key(uv):
191 return round(uv[0], 6), round(uv[1], 6)
193 # Size defines
194 SZ_SHORT = 2
195 SZ_INT = 4
196 SZ_FLOAT = 4
198 class _3ds_ushort(object):
199 """Class representing a short (2-byte integer) for a 3ds file."""
200 __slots__ = ("value", )
202 def __init__(self, val=0):
203 self.value = val
205 def get_size(self):
206 return SZ_SHORT
208 def write(self, file):
209 file.write(struct.pack('<H', self.value))
211 def __str__(self):
212 return str(self.value)
215 class _3ds_uint(object):
216 """Class representing an int (4-byte integer) for a 3ds file."""
217 __slots__ = ("value", )
219 def __init__(self, val):
220 self.value = val
222 def get_size(self):
223 return SZ_INT
225 def write(self, file):
226 file.write(struct.pack('<I', self.value))
228 def __str__(self):
229 return str(self.value)
232 class _3ds_float(object):
233 """Class representing a 4-byte IEEE floating point number for a 3ds file."""
234 __slots__ = ("value", )
236 def __init__(self, val):
237 self.value = val
239 def get_size(self):
240 return SZ_FLOAT
242 def write(self, file):
243 file.write(struct.pack('<f', self.value))
245 def __str__(self):
246 return str(self.value)
249 class _3ds_string(object):
250 """Class representing a zero-terminated string for a 3ds file."""
251 __slots__ = ("value", )
253 def __init__(self, val):
254 assert type(val) == bytes
255 self.value = val
257 def get_size(self):
258 return (len(self.value) + 1)
260 def write(self, file):
261 binary_format = '<%ds' % (len(self.value) + 1)
262 file.write(struct.pack(binary_format, self.value))
264 def __str__(self):
265 return str((self.value).decode("ASCII"))
268 class _3ds_point_3d(object):
269 """Class representing a three-dimensional point for a 3ds file."""
270 __slots__ = "x", "y", "z"
272 def __init__(self, point):
273 self.x, self.y, self.z = point
275 def get_size(self):
276 return 3 * SZ_FLOAT
278 def write(self, file):
279 file.write(struct.pack('<3f', self.x, self.y, self.z))
281 def __str__(self):
282 return '(%f, %f, %f)' % (self.x, self.y, self.z)
285 # Used for writing a track
286 class _3ds_point_4d(object):
287 """Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
288 __slots__ = "w", "x", "y", "z"
290 def __init__(self, point):
291 self.w, self.x, self.y, self.z = point
293 def get_size(self):
294 return 4 * SZ_FLOAT
296 def write(self,file):
297 data=struct.pack('<4f', self.w, self.x, self.y, self.z)
298 file.write(data)
300 def __str__(self):
301 return '(%f, %f, %f, %f)' % (self.w, self.x, self.y, self.z)
304 class _3ds_point_uv(object):
305 """Class representing a UV-coordinate for a 3ds file."""
306 __slots__ = ("uv", )
308 def __init__(self, point):
309 self.uv = point
311 def get_size(self):
312 return 2 * SZ_FLOAT
314 def write(self, file):
315 data = struct.pack('<2f', self.uv[0], self.uv[1])
316 file.write(data)
318 def __str__(self):
319 return '(%g, %g)' % self.uv
322 class _3ds_float_color(object):
323 """Class representing a rgb float color for a 3ds file."""
324 __slots__ = "r", "g", "b"
326 def __init__(self, col):
327 self.r, self.g, self.b = col
329 def get_size(self):
330 return 3 * SZ_FLOAT
332 def write(self, file):
333 file.write(struct.pack('<3f', self.r, self.g, self.b))
335 def __str__(self):
336 return '{%f, %f, %f}' % (self.r, self.g, self.b)
339 class _3ds_rgb_color(object):
340 """Class representing a (24-bit) rgb color for a 3ds file."""
341 __slots__ = "r", "g", "b"
343 def __init__(self, col):
344 self.r, self.g, self.b = col
346 def get_size(self):
347 return 3
349 def write(self, file):
350 file.write(struct.pack('<3B', int(255 * self.r), int(255 * self.g), int(255 * self.b)))
352 def __str__(self):
353 return '{%f, %f, %f}' % (self.r, self.g, self.b)
356 class _3ds_face(object):
357 """Class representing a face for a 3ds file."""
358 __slots__ = ("vindex", "flag", )
360 def __init__(self, vindex, flag):
361 self.vindex = vindex
362 self.flag = flag
364 def get_size(self):
365 return 4 * SZ_SHORT
367 # No need to validate every face vert, the oversized array will catch this problem
368 def write(self, file):
369 # The last short is used for face flags
370 file.write(struct.pack('<4H', self.vindex[0], self.vindex[1], self.vindex[2], self.flag))
372 def __str__(self):
373 return '[%d %d %d %d]' % (self.vindex[0], self.vindex[1], self.vindex[2], self.flag)
376 class _3ds_array(object):
377 """Class representing an array of variables for a 3ds file.
378 Consists of a _3ds_ushort to indicate the number of items, followed by the items themselves."""
379 __slots__ = "values", "size"
381 def __init__(self):
382 self.values = []
383 self.size = SZ_SHORT
385 # Add an item
386 def add(self, item):
387 self.values.append(item)
388 self.size += item.get_size()
390 def get_size(self):
391 return self.size
393 def validate(self):
394 return len(self.values) <= 65535
396 def write(self, file):
397 _3ds_ushort(len(self.values)).write(file)
398 for value in self.values:
399 value.write(file)
401 # To not overwhelm the output in a dump, a _3ds_array only
402 # outputs the number of items, not all of the actual items
403 def __str__(self):
404 return '(%d items)' % len(self.values)
407 class _3ds_named_variable(object):
408 """Convenience class for named variables."""
409 __slots__ = "value", "name"
411 def __init__(self, name, val=None):
412 self.name = name
413 self.value = val
415 def get_size(self):
416 if self.value is None:
417 return 0
418 else:
419 return self.value.get_size()
421 def write(self, file):
422 if self.value is not None:
423 self.value.write(file)
425 def dump(self, indent):
426 if self.value is not None:
427 print(indent * " ",
428 self.name if self.name else "[unnamed]",
429 " = ",
430 self.value)
433 # The chunk class
434 class _3ds_chunk(object):
435 """Class representing a chunk in a 3ds file.
436 Chunks contain zero or more variables, followed by zero or more subchunks."""
437 __slots__ = "ID", "size", "variables", "subchunks"
439 def __init__(self, chunk_id=0):
440 self.ID = _3ds_ushort(chunk_id)
441 self.size = _3ds_uint(0)
442 self.variables = []
443 self.subchunks = []
445 def add_variable(self, name, var):
446 """Add a named variable.
447 The name is mostly for debugging purposes."""
448 self.variables.append(_3ds_named_variable(name, var))
450 def add_subchunk(self, chunk):
451 """Add a subchunk."""
452 self.subchunks.append(chunk)
454 def get_size(self):
455 """Calculate the size of the chunk and return it.
456 The sizes of the variables and subchunks are used to determine this chunk's size."""
457 tmpsize = self.ID.get_size() + self.size.get_size()
458 for variable in self.variables:
459 tmpsize += variable.get_size()
460 for subchunk in self.subchunks:
461 tmpsize += subchunk.get_size()
462 self.size.value = tmpsize
463 return self.size.value
465 def validate(self):
466 for var in self.variables:
467 func = getattr(var.value, "validate", None)
468 if (func is not None) and not func():
469 return False
471 for chunk in self.subchunks:
472 func = getattr(chunk, "validate", None)
473 if (func is not None) and not func():
474 return False
476 return True
478 def write(self, file):
479 """Write the chunk to a file.
480 Uses the write function of the variables and the subchunks to do the actual work."""
482 # Write header
483 self.ID.write(file)
484 self.size.write(file)
485 for variable in self.variables:
486 variable.write(file)
487 for subchunk in self.subchunks:
488 subchunk.write(file)
490 def dump(self, indent=0):
491 """Write the chunk to a file.
492 Dump is used for debugging purposes, to dump the contents of a chunk to the standard output.
493 Uses the dump function of the named variables and the subchunks to do the actual work."""
494 print(indent * " ",
495 'ID=%r' % hex(self.ID.value),
496 'size=%r' % self.get_size())
497 for variable in self.variables:
498 variable.dump(indent + 1)
499 for subchunk in self.subchunks:
500 subchunk.dump(indent + 1)
503 #############
504 # MATERIALS #
505 #############
507 def get_material_image(material):
508 """ Get images from paint slots."""
509 if material:
510 pt = material.paint_active_slot
511 tex = material.texture_paint_images
512 if pt < len(tex):
513 slot = tex[pt]
514 if slot.type == 'IMAGE':
515 return slot
518 def get_uv_image(ma):
519 """ Get image from material wrapper."""
520 if ma and ma.use_nodes:
521 mat_wrap = node_shader_utils.PrincipledBSDFWrapper(ma)
522 mat_tex = mat_wrap.base_color_texture
523 if mat_tex and mat_tex.image is not None:
524 return mat_tex.image
525 else:
526 return get_material_image(ma)
529 def make_material_subchunk(chunk_id, color):
530 """Make a material subchunk.
531 Used for color subchunks, such as diffuse color or ambient color subchunks."""
532 mat_sub = _3ds_chunk(chunk_id)
533 col1 = _3ds_chunk(RGB1)
534 col1.add_variable("color1", _3ds_rgb_color(color))
535 mat_sub.add_subchunk(col1)
536 # Optional
537 # col2 = _3ds_chunk(RGBI)
538 # col2.add_variable("color2", _3ds_rgb_color(color))
539 # mat_sub.add_subchunk(col2)
540 return mat_sub
543 def make_percent_subchunk(chunk_id, percent):
544 """Make a percentage based subchunk."""
545 pct_sub = _3ds_chunk(chunk_id)
546 pcti = _3ds_chunk(PCT)
547 pcti.add_variable("percent", _3ds_ushort(int(round(percent * 100, 0))))
548 pct_sub.add_subchunk(pcti)
549 # Optional
550 # pctf = _3ds_chunk(PCTF)
551 # pctf.add_variable("pctfloat", _3ds_float(round(percent, 6)))
552 # pct_sub.add_subchunk(pctf)
553 return pct_sub
556 def make_texture_chunk(chunk_id, teximages, pct):
557 """Make Material Map texture chunk."""
558 # Add texture percentage value (100 = 1.0)
559 mat_sub = make_percent_subchunk(chunk_id, pct)
560 has_entry = False
562 def add_image(img, extension):
563 filename = bpy.path.basename(img.filepath)
564 mat_sub_file = _3ds_chunk(MAT_MAP_FILE)
565 mat_sub_tiling = _3ds_chunk(MAT_MAP_TILING)
566 mat_sub_file.add_variable("image", _3ds_string(sane_name(filename)))
567 mat_sub.add_subchunk(mat_sub_file)
569 tiling = 0
570 if extension == 'EXTEND': # decal flag
571 tiling |= 0x1
572 if extension == 'MIRROR': # mirror flag
573 tiling |= 0x2
574 if extension == 'CLIP': # no wrap
575 tiling |= 0x10
577 mat_sub_tiling.add_variable("tiling", _3ds_ushort(tiling))
578 mat_sub.add_subchunk(mat_sub_tiling)
580 for tex in teximages:
581 extend = tex.extension
582 add_image(tex.image, extend)
583 has_entry = True
585 return mat_sub if has_entry else None
588 def make_material_texture_chunk(chunk_id, texslots, pct):
589 """Make Material Map texture chunk given a seq. of MaterialTextureSlot's
590 Paint slots are optionally used as image source if no nodes are used."""
592 # Add texture percentage value
593 mat_sub = make_percent_subchunk(chunk_id, pct)
594 has_entry = False
596 def add_texslot(texslot):
597 image = texslot.image
598 socket = None
600 filename = bpy.path.basename(image.filepath)
601 mat_sub_file = _3ds_chunk(MAT_MAP_FILE)
602 mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
603 mat_sub.add_subchunk(mat_sub_file)
604 for link in texslot.socket_dst.links:
605 socket = link.from_socket.identifier
607 mat_sub_mapflags = _3ds_chunk(MAT_MAP_TILING)
608 """Control bit flags, where 0x1 activates decaling, 0x2 activates mirror,
609 0x8 activates inversion, 0x10 deactivates tiling, 0x20 activates summed area sampling,
610 0x40 activates alpha source, 0x80 activates tinting, 0x100 ignores alpha, 0x200 activates RGB tint.
611 Bits 0x80, 0x100, and 0x200 are only used with TEXMAP, TEX2MAP, and SPECMAP chunks.
612 0x40, when used with a TEXMAP, TEX2MAP, or SPECMAP chunk must be accompanied with a tint bit,
613 either 0x100 or 0x200, tintcolor will be processed if a tintflag was created."""
615 mapflags = 0
616 if texslot.extension == 'EXTEND':
617 mapflags |= 0x1
618 if texslot.extension == 'MIRROR':
619 mapflags |= 0x2
620 if texslot.extension == 'CLIP':
621 mapflags |= 0x10
623 if socket == 'Alpha':
624 mapflags |= 0x40
625 if texslot.socket_dst.identifier in {'Base Color', 'Specular Tint'}:
626 mapflags |= 0x80 if image.colorspace_settings.name == 'Non-Color' else 0x200
628 mat_sub_mapflags.add_variable("mapflags", _3ds_ushort(mapflags))
629 mat_sub.add_subchunk(mat_sub_mapflags)
631 mat_sub_texblur = _3ds_chunk(MAT_MAP_TEXBLUR) # Based on observation this is usually 1.0
632 mat_sub_texblur.add_variable("maptexblur", _3ds_float(1.0))
633 mat_sub.add_subchunk(mat_sub_texblur)
635 mat_sub_uscale = _3ds_chunk(MAT_MAP_USCALE)
636 mat_sub_uscale.add_variable("mapuscale", _3ds_float(round(texslot.scale[0], 6)))
637 mat_sub.add_subchunk(mat_sub_uscale)
639 mat_sub_vscale = _3ds_chunk(MAT_MAP_VSCALE)
640 mat_sub_vscale.add_variable("mapvscale", _3ds_float(round(texslot.scale[1], 6)))
641 mat_sub.add_subchunk(mat_sub_vscale)
643 mat_sub_uoffset = _3ds_chunk(MAT_MAP_UOFFSET)
644 mat_sub_uoffset.add_variable("mapuoffset", _3ds_float(round(texslot.translation[0], 6)))
645 mat_sub.add_subchunk(mat_sub_uoffset)
647 mat_sub_voffset = _3ds_chunk(MAT_MAP_VOFFSET)
648 mat_sub_voffset.add_variable("mapvoffset", _3ds_float(round(texslot.translation[1], 6)))
649 mat_sub.add_subchunk(mat_sub_voffset)
651 mat_sub_angle = _3ds_chunk(MAT_MAP_ANG)
652 mat_sub_angle.add_variable("mapangle", _3ds_float(round(texslot.rotation[2], 6)))
653 mat_sub.add_subchunk(mat_sub_angle)
655 if texslot.socket_dst.identifier in {'Base Color', 'Specular Tint'}: # Add tint color
656 tint = texslot.socket_dst.identifier == 'Base Color' and texslot.image.colorspace_settings.name == 'Non-Color'
657 if tint or texslot.socket_dst.identifier == 'Specular Tint':
658 tint1 = _3ds_chunk(MAP_COL1)
659 tint2 = _3ds_chunk(MAP_COL2)
660 tint1.add_variable("tint1", _3ds_rgb_color(texslot.node_dst.inputs['Coat Tint'].default_value[:3]))
661 tint2.add_variable("tint2", _3ds_rgb_color(texslot.node_dst.inputs['Sheen Tint'].default_value[:3]))
662 mat_sub.add_subchunk(tint1)
663 mat_sub.add_subchunk(tint2)
665 # Store all textures for this mapto in order. This at least is what the
666 # 3DS exporter did so far, afaik most readers will just skip over 2nd textures
667 for slot in texslots:
668 if slot.image is not None:
669 add_texslot(slot)
670 has_entry = True
672 return mat_sub if has_entry else None
675 def make_material_chunk(material, image):
676 """Make a material chunk out of a blender material.
677 Shading method is required for 3ds max, 0 for wireframe.
678 0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
680 material_chunk = _3ds_chunk(MATERIAL)
681 name = _3ds_chunk(MATNAME)
682 shading = _3ds_chunk(MATSHADING)
684 name_str = material.name if material else "None"
686 # if image:
687 # name_str += image.name
689 name.add_variable("name", _3ds_string(sane_name(name_str)))
690 material_chunk.add_subchunk(name)
692 if not material:
693 shading.add_variable("shading", _3ds_ushort(1)) # Flat shading
694 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0.0, 0.0, 0.0)))
695 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (0.8, 0.8, 0.8)))
696 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1.0, 1.0, 1.0)))
697 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 0.8))
698 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, 0.5))
699 material_chunk.add_subchunk(shading)
701 elif material and material.use_nodes:
702 wrap = node_shader_utils.PrincipledBSDFWrapper(material)
703 shading.add_variable("shading", _3ds_ushort(3)) # Phong shading
704 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, wrap.emission_color[:3]))
705 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, wrap.base_color[:3]))
706 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, wrap.specular_tint[:3]))
707 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 1 - wrap.roughness))
708 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, wrap.specular))
709 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, wrap.metallic))
710 material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
711 material_chunk.add_subchunk(make_percent_subchunk(MATXPFALL, wrap.transmission))
712 material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength))
713 material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value))
714 material_chunk.add_subchunk(shading)
716 primary_tex = False
717 mtype = 'MIX', 'MIX_RGB'
718 mtlks = material.node_tree.links
719 mxtex = [lk.from_node for lk in mtlks if lk.from_node.type == 'TEX_IMAGE' and lk.to_socket.identifier in {'Color2', 'B_Color'}]
720 mxpct = next((lk.from_node.inputs[0].default_value for lk in mtlks if lk.from_node.type in mtype and lk.to_node.type == 'BSDF_PRINCIPLED'), 0.5)
722 if wrap.base_color_texture:
723 color = [wrap.base_color_texture]
724 c_pct = 0.7 + sum(wrap.base_color[:]) * 0.1
725 matmap = make_material_texture_chunk(MAT_DIFFUSEMAP, color, c_pct)
726 if matmap:
727 material_chunk.add_subchunk(matmap)
728 primary_tex = True
730 if mxtex and not primary_tex:
731 material_chunk.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP, mxtex, mxpct))
732 primary_tex = True
734 if wrap.specular_tint_texture:
735 spec = [wrap.specular_tint_texture]
736 s_pct = material.specular_intensity
737 matmap = make_material_texture_chunk(MAT_SPECMAP, spec, s_pct)
738 if matmap:
739 material_chunk.add_subchunk(matmap)
741 if wrap.alpha_texture:
742 alpha = [wrap.alpha_texture]
743 a_pct = material.diffuse_color[3]
744 matmap = make_material_texture_chunk(MAT_OPACMAP, alpha, a_pct)
745 if matmap:
746 material_chunk.add_subchunk(matmap)
748 if wrap.metallic_texture:
749 metallic = [wrap.metallic_texture]
750 m_pct = material.metallic
751 matmap = make_material_texture_chunk(MAT_REFLMAP, metallic, m_pct)
752 if matmap:
753 material_chunk.add_subchunk(matmap)
755 if wrap.normalmap_texture:
756 normal = [wrap.normalmap_texture]
757 b_pct = wrap.normalmap_strength
758 matmap = make_material_texture_chunk(MAT_BUMPMAP, normal, b_pct)
759 if matmap:
760 material_chunk.add_subchunk(matmap)
761 material_chunk.add_subchunk(make_percent_subchunk(MAT_BUMP_PERCENT, b_pct))
763 if wrap.roughness_texture:
764 roughness = [wrap.roughness_texture]
765 r_pct = 1 - material.roughness
766 matmap = make_material_texture_chunk(MAT_SHINMAP, roughness, r_pct)
767 if matmap:
768 material_chunk.add_subchunk(matmap)
770 if wrap.emission_color_texture:
771 emission = [wrap.emission_color_texture]
772 e_pct = wrap.emission_strength
773 matmap = make_material_texture_chunk(MAT_SELFIMAP, emission, e_pct)
774 if matmap:
775 material_chunk.add_subchunk(matmap)
777 # Make sure no textures are lost. Everything that doesn't fit
778 # into a channel is exported as secondary texture
779 for link in mtlks:
780 mxsecondary = link.from_node if link.from_node.type == 'TEX_IMAGE' and link.to_socket.identifier in {'Color1', 'A_Color'} else False
781 if mxsecondary:
782 matmap = make_texture_chunk(MAT_TEX2MAP, [mxsecondary], 1 - mxpct)
783 if primary_tex and matmap:
784 material_chunk.add_subchunk(matmap)
786 else:
787 shading.add_variable("shading", _3ds_ushort(2)) # Gouraud shading
788 material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, material.line_color[:3]))
789 material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color[:3]))
790 material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
791 material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 1 - material.roughness))
792 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, material.specular_intensity))
793 material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, material.metallic))
794 material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - material.diffuse_color[3]))
795 material_chunk.add_subchunk(shading)
797 slots = [get_material_image(material)] # Can be None
799 if image:
800 material_chunk.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP, slots))
802 return material_chunk
805 #############
806 # MESH DATA #
807 #############
809 class tri_wrapper(object):
810 """Class representing a triangle.
811 Used when converting faces to triangles"""
813 __slots__ = "vertex_index", "ma", "image", "faceuvs", "offset", "flag", "group"
815 def __init__(self, vindex=(0, 0, 0), ma=None, image=None, faceuvs=None, flag=0, group=0):
816 self.vertex_index = vindex
817 self.ma = ma
818 self.image = image
819 self.faceuvs = faceuvs
820 self.offset = [0, 0, 0] # Offset indices
821 self.flag = flag
822 self.group = group
825 def extract_triangles(mesh):
826 """Extract triangles from a mesh."""
828 mesh.calc_loop_triangles()
829 (polygroup, count) = mesh.calc_smooth_groups(use_bitflags=True)
831 tri_list = []
832 do_uv = bool(mesh.uv_layers)
834 img = None
835 for i, face in enumerate(mesh.loop_triangles):
836 f_v = face.vertices
837 v1, v2, v3 = f_v
838 uf = mesh.uv_layers.active.data if do_uv else None
840 if do_uv:
841 f_uv = [uf[lp].uv for lp in face.loops]
842 for ma in mesh.materials:
843 img = get_uv_image(ma) if uf else None
844 if img is not None:
845 img = img.name
846 uv1, uv2, uv3 = f_uv
848 """Flag 0x1 sets CA edge visible, Flag 0x2 sets BC edge visible, Flag 0x4 sets AB edge visible
849 Flag 0x8 indicates a U axis texture wrap seam and Flag 0x10 indicates a V axis texture wrap seam
850 In Blender we use the edge CA, BC, and AB flags for sharp edges flags."""
851 a_b = mesh.edges[mesh.loops[face.loops[0]].edge_index]
852 b_c = mesh.edges[mesh.loops[face.loops[1]].edge_index]
853 c_a = mesh.edges[mesh.loops[face.loops[2]].edge_index]
855 if v3 == 0:
856 v1, v2, v3 = v3, v1, v2
857 a_b, b_c, c_a = c_a, a_b, b_c
858 if do_uv:
859 uv1, uv2, uv3 = uv3, uv1, uv2
861 faceflag = 0
862 if c_a.use_edge_sharp:
863 faceflag |= 0x1
864 if b_c.use_edge_sharp:
865 faceflag |= 0x2
866 if a_b.use_edge_sharp:
867 faceflag |= 0x4
869 smoothgroup = polygroup[face.polygon_index]
871 if len(f_v) == 3:
872 new_tri = tri_wrapper((v1, v2, v3), face.material_index, img)
873 if (do_uv):
874 new_tri.faceuvs = uv_key(uv1), uv_key(uv2), uv_key(uv3)
875 new_tri.flag = faceflag
876 new_tri.group = smoothgroup if face.use_smooth else 0
877 tri_list.append(new_tri)
879 return tri_list
882 def remove_face_uv(verts, tri_list):
883 """Remove face UV coordinates from a list of triangles.
884 Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
885 need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
886 there are multiple uv coordinates per vertex."""
888 # Initialize a list of UniqueUVs, one per vertex
889 unique_uvs = [{} for i in range(len(verts))]
891 # For each face uv coordinate, add it to the UniqueList of the vertex
892 for tri in tri_list:
893 for i in range(3):
894 # Store the index into the UniqueList for future reference
895 # offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
897 context_uv_vert = unique_uvs[tri.vertex_index[i]]
898 uvkey = tri.faceuvs[i]
899 offset_index__uv_3ds = context_uv_vert.get(uvkey)
901 if not offset_index__uv_3ds:
902 offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
904 tri.offset[i] = offset_index__uv_3ds[0]
906 # At this point each vertex has a UniqueList containing every uv coord associated with it only once
907 # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
908 # faces refer to the new face indices
909 vert_index = 0
910 vert_array = _3ds_array()
911 uv_array = _3ds_array()
912 index_list = []
913 for i, vert in enumerate(verts):
914 index_list.append(vert_index)
916 pt = _3ds_point_3d(vert.co) # reuse, should be ok
917 uvmap = [None] * len(unique_uvs[i])
918 for ii, uv_3ds in unique_uvs[i].values():
919 # Add a vertex duplicate to the vertex_array for every uv associated with this vertex
920 vert_array.add(pt)
921 # Add the uv coordinate to the uv array, this for loop does not give
922 # uv's ordered by ii, so we create a new map and add the uv's later
923 uvmap[ii] = uv_3ds
925 # Add uv's in the correct order and add coordinates to the uv array
926 for uv_3ds in uvmap:
927 uv_array.add(uv_3ds)
929 vert_index += len(unique_uvs[i])
931 # Make sure the triangle vertex indices now refer to the new vertex list
932 for tri in tri_list:
933 for i in range(3):
934 tri.offset[i] += index_list[tri.vertex_index[i]]
935 tri.vertex_index = tri.offset
937 return vert_array, uv_array, tri_list
940 def make_faces_chunk(tri_list, mesh, materialDict):
941 """Make a chunk for the faces.
942 Also adds subchunks assigning materials to all faces."""
943 do_smooth = False
944 use_smooth = [poly.use_smooth for poly in mesh.polygons]
945 if True in use_smooth:
946 do_smooth = True
948 materials = mesh.materials
949 if not materials:
950 ma = None
952 face_chunk = _3ds_chunk(OBJECT_FACES)
953 face_list = _3ds_array()
955 if mesh.uv_layers:
956 # Gather materials used in this mesh - mat/image pairs
957 unique_mats = {}
958 for i, tri in enumerate(tri_list):
959 face_list.add(_3ds_face(tri.vertex_index, tri.flag))
961 if materials:
962 ma = materials[tri.ma]
963 if ma:
964 ma = ma.name
966 img = tri.image
968 try:
969 context_face_array = unique_mats[ma, img][1]
970 except:
971 name_str = ma if ma else "None"
972 # if img:
973 # name_str += img
975 context_face_array = _3ds_array()
976 unique_mats[ma, img] = _3ds_string(sane_name(name_str)), context_face_array
978 context_face_array.add(_3ds_ushort(i))
980 face_chunk.add_variable("faces", face_list)
981 for ma_name, ma_faces in unique_mats.values():
982 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
983 obj_material_chunk.add_variable("name", ma_name)
984 obj_material_chunk.add_variable("face_list", ma_faces)
985 face_chunk.add_subchunk(obj_material_chunk)
987 else:
988 obj_material_faces = []
989 obj_material_names = []
990 for m in materials:
991 if m:
992 obj_material_names.append(_3ds_string(sane_name(m.name)))
993 obj_material_faces.append(_3ds_array())
994 n_materials = len(obj_material_names)
996 for i, tri in enumerate(tri_list):
997 face_list.add(_3ds_face(tri.vertex_index, tri.flag))
998 if (tri.ma < n_materials):
999 obj_material_faces[tri.ma].add(_3ds_ushort(i))
1001 face_chunk.add_variable("faces", face_list)
1002 for i in range(n_materials):
1003 obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
1004 obj_material_chunk.add_variable("name", obj_material_names[i])
1005 obj_material_chunk.add_variable("face_list", obj_material_faces[i])
1006 face_chunk.add_subchunk(obj_material_chunk)
1008 if do_smooth:
1009 obj_smooth_chunk = _3ds_chunk(OBJECT_SMOOTH)
1010 for i, tri in enumerate(tri_list):
1011 obj_smooth_chunk.add_variable("face_" + str(i), _3ds_uint(tri.group))
1012 face_chunk.add_subchunk(obj_smooth_chunk)
1014 return face_chunk
1017 def make_vert_chunk(vert_array):
1018 """Make a vertex chunk out of an array of vertices."""
1019 vert_chunk = _3ds_chunk(OBJECT_VERTICES)
1020 vert_chunk.add_variable("vertices", vert_array)
1021 return vert_chunk
1024 def make_uv_chunk(uv_array):
1025 """Make a UV chunk out of an array of UVs."""
1026 uv_chunk = _3ds_chunk(OBJECT_UV)
1027 uv_chunk.add_variable("uv coords", uv_array)
1028 return uv_chunk
1031 def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
1032 """Make a chunk out of a Blender mesh."""
1034 # Extract the triangles from the mesh
1035 tri_list = extract_triangles(mesh)
1037 if mesh.uv_layers:
1038 # Remove the face UVs and convert it to vertex UV
1039 vert_array, uv_array, tri_list = remove_face_uv(mesh.vertices, tri_list)
1040 else:
1041 # Add the vertices to the vertex array
1042 vert_array = _3ds_array()
1043 for vert in mesh.vertices:
1044 vert_array.add(_3ds_point_3d(vert.co))
1045 # No UV at all
1046 uv_array = None
1048 # Create the chunk
1049 mesh_chunk = _3ds_chunk(OBJECT_MESH)
1051 # Add vertex and faces chunk
1052 mesh_chunk.add_subchunk(make_vert_chunk(vert_array))
1053 mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh, materialDict))
1055 # If available, add uv chunk
1056 if uv_array:
1057 mesh_chunk.add_subchunk(make_uv_chunk(uv_array))
1059 # Create transformation matrix chunk
1060 matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
1061 obj_matrix = matrix.transposed().to_3x3()
1063 if ob.parent is None or (ob.parent.name not in translation):
1064 obj_translate = matrix.to_translation()
1066 else: # Calculate child matrix translation relative to parent
1067 obj_translate = translation[ob.name].cross(-1 * translation[ob.parent.name])
1069 matrix_chunk.add_variable("xx", _3ds_float(obj_matrix[0].to_tuple(6)[0]))
1070 matrix_chunk.add_variable("xy", _3ds_float(obj_matrix[0].to_tuple(6)[1]))
1071 matrix_chunk.add_variable("xz", _3ds_float(obj_matrix[0].to_tuple(6)[2]))
1072 matrix_chunk.add_variable("yx", _3ds_float(obj_matrix[1].to_tuple(6)[0]))
1073 matrix_chunk.add_variable("yy", _3ds_float(obj_matrix[1].to_tuple(6)[1]))
1074 matrix_chunk.add_variable("yz", _3ds_float(obj_matrix[1].to_tuple(6)[2]))
1075 matrix_chunk.add_variable("zx", _3ds_float(obj_matrix[2].to_tuple(6)[0]))
1076 matrix_chunk.add_variable("zy", _3ds_float(obj_matrix[2].to_tuple(6)[1]))
1077 matrix_chunk.add_variable("zz", _3ds_float(obj_matrix[2].to_tuple(6)[2]))
1078 matrix_chunk.add_variable("tx", _3ds_float(obj_translate.to_tuple(6)[0]))
1079 matrix_chunk.add_variable("ty", _3ds_float(obj_translate.to_tuple(6)[1]))
1080 matrix_chunk.add_variable("tz", _3ds_float(obj_translate.to_tuple(6)[2]))
1082 mesh_chunk.add_subchunk(matrix_chunk)
1084 return mesh_chunk
1087 def calc_target(posi, tilt=0.0, pan=0.0):
1088 """Calculate target position for cameras and spotlights."""
1089 adjacent = math.radians(90)
1090 turn = 0.0 if abs(pan) < adjacent else -0.0
1091 lean = 0.0 if abs(tilt) > adjacent else -0.0
1092 diagonal = math.sqrt(pow(posi.x ,2) + pow(posi.y ,2))
1093 target_x = math.copysign(posi.x + (posi.y * math.tan(pan)), pan)
1094 target_y = math.copysign(posi.y + (posi.x * math.tan(adjacent - pan)), turn)
1095 target_z = math.copysign(posi.z + diagonal * math.tan(adjacent - tilt), lean)
1097 return target_x, target_y, target_z
1100 #################
1101 # KEYFRAME DATA #
1102 #################
1104 def make_kfdata(revision, start=0, stop=100, curtime=0):
1105 """Make the basic keyframe data chunk."""
1106 kfdata = _3ds_chunk(KFDATA)
1108 kfhdr = _3ds_chunk(KFDATA_KFHDR)
1109 kfhdr.add_variable("revision", _3ds_ushort(revision))
1110 kfhdr.add_variable("filename", _3ds_string(b'Blender'))
1111 kfhdr.add_variable("animlen", _3ds_uint(stop - start))
1113 kfseg = _3ds_chunk(KFDATA_KFSEG)
1114 kfseg.add_variable("start", _3ds_uint(start))
1115 kfseg.add_variable("stop", _3ds_uint(stop))
1117 kfcurtime = _3ds_chunk(KFDATA_KFCURTIME)
1118 kfcurtime.add_variable("curtime", _3ds_uint(curtime))
1120 kfdata.add_subchunk(kfhdr)
1121 kfdata.add_subchunk(kfseg)
1122 kfdata.add_subchunk(kfcurtime)
1123 return kfdata
1126 def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
1127 """Make a chunk for track data. Depending on the ID, this will construct
1128 a position, rotation, scale, roll, color, fov, hotspot or falloff track."""
1129 track_chunk = _3ds_chunk(ID)
1131 if ID in {POS_TRACK_TAG, ROT_TRACK_TAG, SCL_TRACK_TAG, ROLL_TRACK_TAG} and ob.animation_data and ob.animation_data.action:
1132 action = ob.animation_data.action
1133 if action.fcurves:
1134 fcurves = action.fcurves
1135 fcurves.update()
1136 kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
1137 nkeys = len(kframes)
1138 if not 0 in kframes:
1139 kframes.append(0)
1140 nkeys += 1
1141 kframes = sorted(set(kframes))
1142 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1143 track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
1144 track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
1145 track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
1147 if ID == POS_TRACK_TAG: # Position
1148 for i, frame in enumerate(kframes):
1149 pos_track = [fc for fc in fcurves if fc is not None and fc.data_path == 'location']
1150 pos_x = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 0), ob_pos.x)
1151 pos_y = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 1), ob_pos.y)
1152 pos_z = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 2), ob_pos.z)
1153 pos = ob_size @ mathutils.Vector((pos_x, pos_y, pos_z))
1154 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1155 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1156 track_chunk.add_variable("position", _3ds_point_3d((pos.x, pos.y, pos.z)))
1158 elif ID == ROT_TRACK_TAG: # Rotation
1159 for i, frame in enumerate(kframes):
1160 rot_track = [fc for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
1161 rot_x = next((tc.evaluate(frame) for tc in rot_track if tc.array_index == 0), ob_rot.x)
1162 rot_y = next((tc.evaluate(frame) for tc in rot_track if tc.array_index == 1), ob_rot.y)
1163 rot_z = next((tc.evaluate(frame) for tc in rot_track if tc.array_index == 2), ob_rot.z)
1164 quat = mathutils.Euler((rot_x, rot_y, rot_z)).to_quaternion().inverted()
1165 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1166 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1167 track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis.x, quat.axis.y, quat.axis.z)))
1169 elif ID == SCL_TRACK_TAG: # Scale
1170 for i, frame in enumerate(kframes):
1171 scale_track = [fc for fc in fcurves if fc is not None and fc.data_path == 'scale']
1172 size_x = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 0), ob_size.x)
1173 size_y = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 1), ob_size.y)
1174 size_z = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 2), ob_size.z)
1175 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1176 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1177 track_chunk.add_variable("scale", _3ds_point_3d((size_x, size_y, size_z)))
1179 elif ID == ROLL_TRACK_TAG: # Roll
1180 for i, frame in enumerate(kframes):
1181 roll_track = [fc for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
1182 roll = next((tc.evaluate(frame) for tc in roll_track if tc.array_index == 1), ob_rot.y)
1183 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1184 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1185 track_chunk.add_variable("roll", _3ds_float(round(math.degrees(roll), 4)))
1187 elif ID in {COL_TRACK_TAG, FOV_TRACK_TAG, HOTSPOT_TRACK_TAG, FALLOFF_TRACK_TAG} and ob.data.animation_data and ob.data.animation_data.action:
1188 action = ob.data.animation_data.action
1189 if action.fcurves:
1190 fcurves = action.fcurves
1191 fcurves.update()
1192 kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
1193 nkeys = len(kframes)
1194 if not 0 in kframes:
1195 kframes.append(0)
1196 nkeys += 1
1197 kframes = sorted(set(kframes))
1198 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1199 track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
1200 track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
1201 track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
1203 if ID == COL_TRACK_TAG: # Color
1204 for i, frame in enumerate(kframes):
1205 color = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'color']
1206 if not color:
1207 color = ob.data.color[:3]
1208 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1209 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1210 track_chunk.add_variable("color", _3ds_float_color(color))
1212 elif ID == FOV_TRACK_TAG: # Field of view
1213 for i, frame in enumerate(kframes):
1214 lens = next((fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'lens'), ob.data.lens)
1215 fov = 2 * math.atan(ob.data.sensor_width / (2 * lens))
1216 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1217 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1218 track_chunk.add_variable("fov", _3ds_float(round(math.degrees(fov), 4)))
1220 elif ID == HOTSPOT_TRACK_TAG: # Hotspot
1221 for i, frame in enumerate(kframes):
1222 beamsize = next((fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'spot_size'), ob.data.spot_size)
1223 blend = next((fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'spot_blend'), ob.data.spot_blend)
1224 hot_spot = math.degrees(beamsize) - (blend * math.floor(math.degrees(beamsize)))
1225 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1226 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1227 track_chunk.add_variable("hotspot", _3ds_float(round(hot_spot, 4)))
1229 elif ID == FALLOFF_TRACK_TAG: # Falloff
1230 for i, frame in enumerate(kframes):
1231 fall_off = next((fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'spot_size'), ob.data.spot_size)
1232 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1233 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1234 track_chunk.add_variable("falloff", _3ds_float(round(math.degrees(fall_off), 4)))
1236 else:
1237 track_chunk.add_variable("track_flags", _3ds_ushort(0x40)) # Based on observation default flag is 0x40
1238 track_chunk.add_variable("frame_start", _3ds_uint(0))
1239 track_chunk.add_variable("frame_total", _3ds_uint(0))
1240 track_chunk.add_variable("nkeys", _3ds_uint(1))
1241 # Next section should be repeated for every keyframe, with no animation only one tag is needed
1242 track_chunk.add_variable("tcb_frame", _3ds_uint(0))
1243 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1245 # New method simply inserts the parameters
1246 if ID == POS_TRACK_TAG: # Position vector
1247 track_chunk.add_variable("position", _3ds_point_3d(ob_pos))
1249 elif ID == ROT_TRACK_TAG: # Rotation (angle first [radians], followed by axis)
1250 quat = ob_rot.to_quaternion().inverted()
1251 track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis.x, quat.axis.y, quat.axis.z)))
1253 elif ID == SCL_TRACK_TAG: # Scale vector
1254 track_chunk.add_variable("scale", _3ds_point_3d(ob_size))
1256 elif ID == ROLL_TRACK_TAG: # Roll angle
1257 track_chunk.add_variable("roll", _3ds_float(round(math.degrees(ob_rot.y), 4)))
1259 elif ID == COL_TRACK_TAG: # Color values
1260 track_chunk.add_variable("color", _3ds_float_color(ob.data.color[:3]))
1262 elif ID == FOV_TRACK_TAG: # Field of view
1263 track_chunk.add_variable("fov", _3ds_float(round(math.degrees(ob.data.angle), 4)))
1265 elif ID == HOTSPOT_TRACK_TAG: # Hotspot
1266 beam_angle = math.degrees(ob.data.spot_size)
1267 track_chunk.add_variable("hotspot", _3ds_float(round(beam_angle - (ob.data.spot_blend * math.floor(beam_angle)), 4)))
1269 elif ID == FALLOFF_TRACK_TAG: # Falloff
1270 track_chunk.add_variable("falloff", _3ds_float(round(math.degrees(ob.data.spot_size), 4)))
1272 return track_chunk
1275 def make_object_node(ob, translation, rotation, scale, name_id):
1276 """Make a node chunk for a Blender object. Takes Blender object as parameter.
1277 Blender Empty objects are converted to dummy nodes."""
1279 name = ob.name
1280 if ob.type == 'CAMERA':
1281 obj_node = _3ds_chunk(CAMERA_NODE_TAG)
1282 elif ob.type == 'LIGHT':
1283 obj_node = _3ds_chunk(LIGHT_NODE_TAG)
1284 if ob.data.type == 'SPOT':
1285 obj_node = _3ds_chunk(SPOT_NODE_TAG)
1286 else: # Main object node chunk
1287 obj_node = _3ds_chunk(OBJECT_NODE_TAG)
1289 # Chunk for the object ID from name_id dictionary:
1290 obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
1291 obj_id_chunk.add_variable("node_id", _3ds_ushort(name_id[name]))
1292 obj_node.add_subchunk(obj_id_chunk)
1294 # Object node header with object name
1295 obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
1296 parent = ob.parent
1298 if ob.type == 'EMPTY': # Forcing to use the real name for empties
1299 # Empties called $$$DUMMY and use OBJECT_INSTANCE_NAME chunk as name
1300 obj_node_header_chunk.add_variable("name", _3ds_string(b"$$$DUMMY"))
1301 obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0x4000))
1302 obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
1304 else: # Add flag variables - Based on observation flags1 is usually 0x0040 and 0x4000 for empty objects
1305 obj_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
1306 obj_node_header_chunk.add_variable("flags1", _3ds_ushort(0x0040))
1308 """Flags2 defines 0x01 for display path, 0x04 object frozen,
1309 0x10 for motion blur, 0x20 for material morph and bit 0x40 for mesh morph."""
1310 obj_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
1311 obj_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
1314 # COMMENTED OUT FOR 2.42 RELEASE!! CRASHES 3DS MAX
1315 # Check parent-child relationships:
1316 if parent is None or parent.name not in name_id:
1317 # If no parent, or parents name is not in dictionary, ID becomes -1:
1318 obj_node_header_chunk.add_variable("parent", _3ds_ushort(-1))
1319 else: # Get the parent's ID from the name_id dictionary:
1320 obj_node_header_chunk.add_variable("parent", _3ds_ushort(name_id[parent.name]))
1323 # Add subchunk for node header
1324 obj_node.add_subchunk(obj_node_header_chunk)
1326 # Alternatively use PARENT_NAME chunk for hierachy
1327 if parent is not None and (parent.name in name_id):
1328 obj_parent_name_chunk = _3ds_chunk(OBJECT_PARENT_NAME)
1329 obj_parent_name_chunk.add_variable("parent", _3ds_string(sane_name(parent.name)))
1330 obj_node.add_subchunk(obj_parent_name_chunk)
1332 # Empty objects need to have an extra chunk for the instance name
1333 if ob.type == 'EMPTY': # Will use a real object name for empties for now
1334 obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME)
1335 obj_instance_name_chunk.add_variable("name", _3ds_string(sane_name(name)))
1336 obj_node.add_subchunk(obj_instance_name_chunk)
1338 if ob.type in {'MESH', 'EMPTY'}: # Add a pivot point at the object center
1339 pivot_pos = (translation[name])
1340 obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT)
1341 obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(pivot_pos))
1342 obj_node.add_subchunk(obj_pivot_chunk)
1344 # Create a bounding box from quadrant diagonal
1345 obj_boundbox = _3ds_chunk(OBJECT_BOUNDBOX)
1346 obj_boundbox.add_variable("min", _3ds_point_3d(ob.bound_box[0]))
1347 obj_boundbox.add_variable("max", _3ds_point_3d(ob.bound_box[6]))
1348 obj_node.add_subchunk(obj_boundbox)
1350 # Add track chunks for position, rotation, size
1351 ob_scale = scale[name] # and collect masterscale
1352 if parent is None or (parent.name not in name_id):
1353 ob_pos = translation[name]
1354 ob_rot = rotation[name]
1355 ob_size = ob.scale
1357 else: # Calculate child position and rotation of the object center, no scale applied
1358 ob_pos = translation[name] - translation[parent.name]
1359 ob_rot = rotation[name].to_quaternion().cross(rotation[parent.name].to_quaternion().copy().inverted()).to_euler()
1360 ob_size = mathutils.Vector((1.0, 1.0, 1.0))
1362 obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, ob, ob_pos, ob_rot, ob_scale))
1364 if ob.type in {'MESH', 'EMPTY'}:
1365 obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1366 obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1367 if ob.type =='CAMERA':
1368 obj_node.add_subchunk(make_track_chunk(FOV_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1369 obj_node.add_subchunk(make_track_chunk(ROLL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1370 if ob.type =='LIGHT':
1371 obj_node.add_subchunk(make_track_chunk(COL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1372 if ob.type == 'LIGHT' and ob.data.type == 'SPOT':
1373 obj_node.add_subchunk(make_track_chunk(HOTSPOT_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1374 obj_node.add_subchunk(make_track_chunk(FALLOFF_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1375 obj_node.add_subchunk(make_track_chunk(ROLL_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
1377 return obj_node
1380 def make_target_node(ob, translation, rotation, scale, name_id):
1381 """Make a target chunk for light and camera objects."""
1383 name = ob.name
1384 name_id["ø " + name] = len(name_id)
1385 if ob.type == 'CAMERA': # Add camera target
1386 tar_node = _3ds_chunk(TARGET_NODE_TAG)
1387 elif ob.type == 'LIGHT': # Add spot target
1388 tar_node = _3ds_chunk(LTARGET_NODE_TAG)
1390 # Chunk for the object ID from name_id dictionary:
1391 tar_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
1392 tar_id_chunk.add_variable("node_id", _3ds_ushort(name_id[name]))
1393 tar_node.add_subchunk(tar_id_chunk)
1395 # Object node header with object name
1396 tar_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
1397 # Targets get the same name as the object, flags1 is usually 0x0010 and parent ROOT_OBJECT
1398 tar_node_header_chunk.add_variable("name", _3ds_string(sane_name(name)))
1399 tar_node_header_chunk.add_variable("flags1", _3ds_ushort(0x0010))
1400 tar_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
1401 tar_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
1403 # Add subchunk for node header
1404 tar_node.add_subchunk(tar_node_header_chunk)
1406 # Calculate target position
1407 ob_pos = translation[name]
1408 ob_rot = rotation[name]
1409 ob_scale = scale[name]
1410 target_pos = calc_target(ob_pos, ob_rot.x, ob_rot.z)
1412 # Add track chunks for target position
1413 track_chunk = _3ds_chunk(POS_TRACK_TAG)
1415 if ob.animation_data and ob.animation_data.action:
1416 action = ob.animation_data.action
1417 if action.fcurves:
1418 fcurves = action.fcurves
1419 fcurves.update()
1420 kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
1421 nkeys = len(kframes)
1422 if not 0 in kframes:
1423 kframes.append(0)
1424 nkeys += 1
1425 kframes = sorted(set(kframes))
1426 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1427 track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
1428 track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
1429 track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
1431 for i, frame in enumerate(kframes):
1432 loc_target = [fc for fc in fcurves if fc is not None and fc.data_path == 'location']
1433 loc_x = next((tc.evaluate(frame) for tc in loc_target if tc.array_index == 0), ob_pos.x)
1434 loc_y = next((tc.evaluate(frame) for tc in loc_target if tc.array_index == 1), ob_pos.y)
1435 loc_z = next((tc.evaluate(frame) for tc in loc_target if tc.array_index == 2), ob_pos.z)
1436 rot_target = [fc for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
1437 rot_x = next((tc.evaluate(frame) for tc in rot_target if tc.array_index == 0), ob_rot.x)
1438 rot_z = next((tc.evaluate(frame) for tc in rot_target if tc.array_index == 2), ob_rot.z)
1439 target_distance = ob_scale @ mathutils.Vector((loc_x, loc_y, loc_z))
1440 target_pos = calc_target(target_distance, rot_x, rot_z)
1441 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1442 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1443 track_chunk.add_variable("position", _3ds_point_3d(target_pos))
1445 else: # Track header
1446 track_chunk.add_variable("track_flags", _3ds_ushort(0x40)) # Based on observation default flag is 0x40
1447 track_chunk.add_variable("frame_start", _3ds_uint(0))
1448 track_chunk.add_variable("frame_total", _3ds_uint(0))
1449 track_chunk.add_variable("nkeys", _3ds_uint(1))
1450 # Keyframe header
1451 track_chunk.add_variable("tcb_frame", _3ds_uint(0))
1452 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1453 track_chunk.add_variable("position", _3ds_point_3d(target_pos))
1455 tar_node.add_subchunk(track_chunk)
1457 return tar_node
1460 def make_ambient_node(world):
1461 """Make an ambient node for the world color, if the color is animated."""
1463 amb_color = world.color[:3]
1464 amb_node = _3ds_chunk(AMBIENT_NODE_TAG)
1465 track_chunk = _3ds_chunk(COL_TRACK_TAG)
1467 # Chunk for the ambient ID is ROOT_OBJECT
1468 amb_id_chunk = _3ds_chunk(OBJECT_NODE_ID)
1469 amb_id_chunk.add_variable("node_id", _3ds_ushort(ROOT_OBJECT))
1470 amb_node.add_subchunk(amb_id_chunk)
1472 # Object node header, name is "$AMBIENT$" for ambient nodes
1473 amb_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR)
1474 amb_node_header_chunk.add_variable("name", _3ds_string(b"$AMBIENT$"))
1475 amb_node_header_chunk.add_variable("flags1", _3ds_ushort(0x4000)) # Flags1 0x4000 for empty objects
1476 amb_node_header_chunk.add_variable("flags2", _3ds_ushort(0))
1477 amb_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
1478 amb_node.add_subchunk(amb_node_header_chunk)
1480 if world.use_nodes and world.node_tree.animation_data.action:
1481 ambioutput = 'EMISSION' ,'MIX_SHADER', 'WORLD_OUTPUT'
1482 action = world.node_tree.animation_data.action
1483 links = world.node_tree.links
1484 ambilinks = [lk for lk in links if lk.from_node.type in {'EMISSION', 'RGB'} and lk.to_node.type in ambioutput]
1485 if ambilinks and action.fcurves:
1486 fcurves = action.fcurves
1487 fcurves.update()
1488 emission = next((lk.from_socket.node for lk in ambilinks if lk.to_node.type in ambioutput), False)
1489 ambinode = next((lk.from_socket.node for lk in ambilinks if lk.to_node.type == 'EMISSION'), emission)
1490 kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
1491 ambipath = ('nodes[\"RGB\"].outputs[0].default_value' if ambinode and ambinode.type == 'RGB' else
1492 'nodes[\"Emission\"].inputs[0].default_value')
1493 nkeys = len(kframes)
1494 if not 0 in kframes:
1495 kframes.append(0)
1496 nkeys = nkeys + 1
1497 kframes = sorted(set(kframes))
1498 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1499 track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
1500 track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
1501 track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
1503 for i, frame in enumerate(kframes):
1504 ambient = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == ambipath]
1505 if not ambient:
1506 ambient = amb_color
1507 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1508 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1509 track_chunk.add_variable("color", _3ds_float_color(ambient[:3]))
1511 elif world.animation_data.action:
1512 action = world.animation_data.action
1513 if action.fcurves:
1514 fcurves = action.fcurves
1515 fcurves.update()
1516 kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
1517 nkeys = len(kframes)
1518 if not 0 in kframes:
1519 kframes.append(0)
1520 nkeys += 1
1521 kframes = sorted(set(kframes))
1522 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1523 track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
1524 track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
1525 track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
1527 for i, frame in enumerate(kframes):
1528 ambient = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == 'color']
1529 if not ambient:
1530 ambient = amb_color
1531 track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
1532 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1533 track_chunk.add_variable("color", _3ds_float_color(ambient))
1535 else: # Track header
1536 track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
1537 track_chunk.add_variable("frame_start", _3ds_uint(0))
1538 track_chunk.add_variable("frame_total", _3ds_uint(0))
1539 track_chunk.add_variable("nkeys", _3ds_uint(1))
1540 # Keyframe header
1541 track_chunk.add_variable("tcb_frame", _3ds_uint(0))
1542 track_chunk.add_variable("tcb_flags", _3ds_ushort())
1543 track_chunk.add_variable("color", _3ds_float_color(amb_color))
1545 amb_node.add_subchunk(track_chunk)
1547 return amb_node
1550 ##########
1551 # EXPORT #
1552 ##########
1554 def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False, use_selection=False,
1555 object_filter=None, use_hierarchy=False, use_keyframes=False, global_matrix=None, use_cursor=False):
1556 """Save the Blender scene to a 3ds file."""
1558 # Time the export
1559 duration = time.time()
1560 context.window.cursor_set('WAIT')
1562 scene = context.scene
1563 layer = context.view_layer
1564 depsgraph = context.evaluated_depsgraph_get()
1565 world = scene.world
1567 unit_measure = 1.0
1568 if use_scene_unit:
1569 unit_length = scene.unit_settings.length_unit
1570 if unit_length == 'MILES':
1571 unit_measure = 0.000621371
1572 elif unit_length == 'KILOMETERS':
1573 unit_measure = 0.001
1574 elif unit_length == 'FEET':
1575 unit_measure = 3.280839895
1576 elif unit_length == 'INCHES':
1577 unit_measure = 39.37007874
1578 elif unit_length == 'CENTIMETERS':
1579 unit_measure = 100
1580 elif unit_length == 'MILLIMETERS':
1581 unit_measure = 1000
1582 elif unit_length == 'THOU':
1583 unit_measure = 39370.07874
1584 elif unit_length == 'MICROMETERS':
1585 unit_measure = 1000000
1587 mtx_scale = mathutils.Matrix.Scale((scale_factor * unit_measure),4)
1589 if global_matrix is None:
1590 global_matrix = mathutils.Matrix()
1592 if bpy.ops.object.mode_set.poll():
1593 bpy.ops.object.mode_set(mode='OBJECT')
1595 # Initialize the main chunk (primary)
1596 primary = _3ds_chunk(PRIMARY)
1598 # Add version chunk
1599 version_chunk = _3ds_chunk(VERSION)
1600 version_chunk.add_variable("version", _3ds_uint(3))
1601 primary.add_subchunk(version_chunk)
1603 # Init main object info chunk
1604 object_info = _3ds_chunk(OBJECTINFO)
1605 mesh_version = _3ds_chunk(MESHVERSION)
1606 mesh_version.add_variable("mesh", _3ds_uint(3))
1607 object_info.add_subchunk(mesh_version)
1609 # Init main keyframe data chunk
1610 if use_keyframes:
1611 revision = 0x0005
1612 stop = scene.frame_end
1613 start = scene.frame_start
1614 curtime = scene.frame_current
1615 kfdata = make_kfdata(revision, start, stop, curtime)
1617 # Make a list of all materials used in the selected meshes (use dictionary, each material is added once)
1618 materialDict = {}
1619 mesh_objects = []
1621 if use_selection:
1622 objects = [ob for ob in scene.objects if ob.type in object_filter and ob.visible_get(view_layer=layer) and ob.select_get(view_layer=layer)]
1623 else:
1624 objects = [ob for ob in scene.objects if ob.type in object_filter and ob.visible_get(view_layer=layer)]
1626 empty_objects = [ob for ob in objects if ob.type == 'EMPTY']
1627 light_objects = [ob for ob in objects if ob.type == 'LIGHT']
1628 camera_objects = [ob for ob in objects if ob.type == 'CAMERA']
1630 for ob in objects:
1631 # Get derived objects
1632 derived_dict = bpy_extras.io_utils.create_derived_objects(depsgraph, [ob])
1633 derived = derived_dict.get(ob)
1635 if derived is None:
1636 continue
1638 for ob_derived, mtx in derived:
1639 if ob.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
1640 continue
1642 try:
1643 data = ob_derived.to_mesh()
1644 except:
1645 data = None
1647 if data:
1648 matrix = global_matrix @ mtx
1649 data.transform(matrix)
1650 data.transform(mtx_scale)
1651 mesh_objects.append((ob_derived, data, matrix))
1652 ma_ls = data.materials
1653 ma_ls_len = len(ma_ls)
1655 # Get material/image tuples
1656 if data.uv_layers:
1657 if not ma_ls:
1658 ma = ma_name = None
1660 for f, uf in zip(data.polygons, data.uv_layers.active.data):
1661 if ma_ls:
1662 ma_index = f.material_index
1663 if ma_index >= ma_ls_len:
1664 ma_index = f.material_index = 0
1665 ma = ma_ls[ma_index]
1666 ma_name = None if ma is None else ma.name
1667 # Else there already set to none
1669 img = get_uv_image(ma)
1670 img_name = None if img is None else img.name
1672 materialDict.setdefault((ma_name, img_name), (ma, img))
1674 else:
1675 for ma in ma_ls:
1676 if ma: # Material may be None so check its not
1677 materialDict.setdefault((ma.name, None), (ma, None))
1679 # Why 0 Why!
1680 for f in data.polygons:
1681 if f.material_index >= ma_ls_len:
1682 f.material_index = 0
1685 # Make MATERIAL chunks for all materials used in the meshes
1686 for ma_image in materialDict.values():
1687 object_info.add_subchunk(make_material_chunk(ma_image[0], ma_image[1]))
1689 # Add MASTERSCALE element
1690 mscale = _3ds_chunk(MASTERSCALE)
1691 mscale.add_variable("scale", _3ds_float(1.0))
1692 object_info.add_subchunk(mscale)
1694 # Add 3D cursor location
1695 if use_cursor:
1696 cursor_chunk = _3ds_chunk(O_CONSTS)
1697 cursor_chunk.add_variable("cursor", _3ds_point_3d(scene.cursor.location))
1698 object_info.add_subchunk(cursor_chunk)
1700 # Add AMBIENT color
1701 if world is not None and 'WORLD' in object_filter:
1702 ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
1703 ambient_light = _3ds_chunk(RGB)
1704 ambient_light.add_variable("ambient", _3ds_float_color(world.color))
1705 ambient_chunk.add_subchunk(ambient_light)
1706 object_info.add_subchunk(ambient_chunk)
1708 # Add BACKGROUND and BITMAP
1709 if world.use_nodes:
1710 bgtype = 'BACKGROUND'
1711 ntree = world.node_tree.links
1712 background_color_chunk = _3ds_chunk(RGB)
1713 background_chunk = _3ds_chunk(SOLIDBACKGND)
1714 background_flag = _3ds_chunk(USE_SOLIDBGND)
1715 bgmixer = 'BACKGROUND', 'MIX', 'MIX_RGB'
1716 bgshade = 'ADD_SHADER', 'MIX_SHADER', 'OUTPUT_WORLD'
1717 bg_tex = 'TEX_IMAGE', 'TEX_ENVIRONMENT'
1718 bg_color = next((lk.from_node.inputs[0].default_value[:3] for lk in ntree if lk.from_node.type == bgtype and lk.to_node.type in bgshade), world.color)
1719 bg_mixer = next((lk.from_node.type for lk in ntree if lk.from_node.type in bgmixer and lk.to_node.type == bgtype), bgtype)
1720 bg_image = next((lk.from_node.image for lk in ntree if lk.from_node.type in bg_tex and lk.to_node.type == bg_mixer), False)
1721 gradient = next((lk.from_node.color_ramp.elements for lk in ntree if lk.from_node.type == 'VALTORGB' and lk.to_node.type in bgmixer), False)
1722 background_color_chunk.add_variable("color", _3ds_float_color(bg_color))
1723 background_chunk.add_subchunk(background_color_chunk)
1724 if bg_image and bg_image is not None:
1725 background_image = _3ds_chunk(BITMAP)
1726 background_flag = _3ds_chunk(USE_BITMAP)
1727 background_image.add_variable("image", _3ds_string(sane_name(bg_image.name)))
1728 object_info.add_subchunk(background_image)
1729 object_info.add_subchunk(background_chunk)
1731 # Add VGRADIENT chunk
1732 if gradient and len(gradient) >= 3:
1733 gradient_chunk = _3ds_chunk(VGRADIENT)
1734 background_flag = _3ds_chunk(USE_VGRADIENT)
1735 gradient_chunk.add_variable("midpoint", _3ds_float(gradient[1].position))
1736 gradient_topcolor_chunk = _3ds_chunk(RGB)
1737 gradient_topcolor_chunk.add_variable("color", _3ds_float_color(gradient[2].color[:3]))
1738 gradient_chunk.add_subchunk(gradient_topcolor_chunk)
1739 gradient_midcolor_chunk = _3ds_chunk(RGB)
1740 gradient_midcolor_chunk.add_variable("color", _3ds_float_color(gradient[1].color[:3]))
1741 gradient_chunk.add_subchunk(gradient_midcolor_chunk)
1742 gradient_lowcolor_chunk = _3ds_chunk(RGB)
1743 gradient_lowcolor_chunk.add_variable("color", _3ds_float_color(gradient[0].color[:3]))
1744 gradient_chunk.add_subchunk(gradient_lowcolor_chunk)
1745 object_info.add_subchunk(gradient_chunk)
1746 object_info.add_subchunk(background_flag)
1748 # Add FOG
1749 fognode = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_ABSORPTION' and lk.to_socket.node.type in bgshade), False)
1750 if fognode:
1751 fog_chunk = _3ds_chunk(FOG)
1752 fog_color_chunk = _3ds_chunk(RGB)
1753 use_fog_flag = _3ds_chunk(USE_FOG)
1754 fog_density = fognode.inputs['Density'].default_value * 100
1755 fog_color_chunk.add_variable("color", _3ds_float_color(fognode.inputs[0].default_value[:3]))
1756 fog_chunk.add_variable("nearplane", _3ds_float(world.mist_settings.start))
1757 fog_chunk.add_variable("nearfog", _3ds_float(fog_density * 0.5))
1758 fog_chunk.add_variable("farplane", _3ds_float(world.mist_settings.depth))
1759 fog_chunk.add_variable("farfog", _3ds_float(fog_density + fog_density * 0.5))
1760 fog_chunk.add_subchunk(fog_color_chunk)
1761 object_info.add_subchunk(fog_chunk)
1763 # Add LAYER FOG
1764 foglayer = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_SCATTER' and lk.to_socket.node.type in bgshade), False)
1765 if foglayer:
1766 layerfog_flag = 0
1767 if world.mist_settings.falloff == 'QUADRATIC':
1768 layerfog_flag |= 0x1
1769 if world.mist_settings.falloff == 'INVERSE_QUADRATIC':
1770 layerfog_flag |= 0x2
1771 layerfog_chunk = _3ds_chunk(LAYER_FOG)
1772 layerfog_color_chunk = _3ds_chunk(RGB)
1773 use_fog_flag = _3ds_chunk(USE_LAYER_FOG)
1774 layerfog_color_chunk.add_variable("color", _3ds_float_color(foglayer.inputs[0].default_value[:3]))
1775 layerfog_chunk.add_variable("lowZ", _3ds_float(world.mist_settings.start))
1776 layerfog_chunk.add_variable("highZ", _3ds_float(world.mist_settings.height))
1777 layerfog_chunk.add_variable("density", _3ds_float(foglayer.inputs[1].default_value))
1778 layerfog_chunk.add_variable("flags", _3ds_uint(layerfog_flag))
1779 layerfog_chunk.add_subchunk(layerfog_color_chunk)
1780 object_info.add_subchunk(layerfog_chunk)
1781 if fognode or foglayer and layer.use_pass_mist:
1782 object_info.add_subchunk(use_fog_flag)
1783 if use_keyframes and world.animation_data or (world.node_tree and world.node_tree.animation_data):
1784 kfdata.add_subchunk(make_ambient_node(world))
1786 # Collect translation for transformation matrix
1787 translation = {}
1788 rotation = {}
1789 scale = {}
1791 # Give all objects a unique ID and build a dictionary from object name to object id
1792 object_id = {}
1793 name_id = {}
1795 for ob, data, matrix in mesh_objects:
1796 translation[ob.name] = mtx_scale @ ob.location
1797 rotation[ob.name] = ob.rotation_euler
1798 scale[ob.name] = mtx_scale.copy()
1799 name_id[ob.name] = len(name_id)
1800 object_id[ob.name] = len(object_id)
1802 for ob in empty_objects:
1803 translation[ob.name] = mtx_scale @ ob.location
1804 rotation[ob.name] = ob.rotation_euler
1805 scale[ob.name] = mtx_scale.copy()
1806 name_id[ob.name] = len(name_id)
1808 for ob in light_objects:
1809 translation[ob.name] = mtx_scale @ ob.location
1810 rotation[ob.name] = ob.rotation_euler
1811 scale[ob.name] = mtx_scale.copy()
1812 name_id[ob.name] = len(name_id)
1813 object_id[ob.name] = len(object_id)
1815 for ob in camera_objects:
1816 translation[ob.name] = mtx_scale @ ob.location
1817 rotation[ob.name] = ob.rotation_euler
1818 scale[ob.name] = mtx_scale.copy()
1819 name_id[ob.name] = len(name_id)
1820 object_id[ob.name] = len(object_id)
1822 # Create object chunks for all meshes
1823 i = 0
1824 for ob, mesh, matrix in mesh_objects:
1825 object_chunk = _3ds_chunk(OBJECT)
1827 # Set the object name
1828 object_chunk.add_variable("name", _3ds_string(sane_name(ob.name)))
1830 # Make a mesh chunk out of the mesh
1831 object_chunk.add_subchunk(make_mesh_chunk(ob, mesh, matrix, materialDict, translation))
1833 # Add hierachy chunk with ID from object_id dictionary
1834 if use_hierarchy:
1835 obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
1836 obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
1838 # Add parent chunk if object has a parent
1839 if ob.parent is not None and (ob.parent.name in object_id):
1840 obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
1841 obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
1842 obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
1843 object_chunk.add_subchunk(obj_hierarchy_chunk)
1845 # ensure the mesh has no over sized arrays - skip ones that do!
1846 # Otherwise we cant write since the array size wont fit into USHORT
1847 if object_chunk.validate():
1848 object_info.add_subchunk(object_chunk)
1849 else:
1850 operator.report({'WARNING'}, "Object %r can't be written into a 3DS file")
1852 # Export object node
1853 if use_keyframes:
1854 kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
1856 i += i
1858 # Create chunks for all empties - only requires a object node
1859 if use_keyframes:
1860 for ob in empty_objects:
1861 kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
1863 # Create light object chunks
1864 for ob in light_objects:
1865 object_chunk = _3ds_chunk(OBJECT)
1866 obj_light_chunk = _3ds_chunk(OBJECT_LIGHT)
1867 color_float_chunk = _3ds_chunk(RGB)
1868 light_distance = translation[ob.name]
1869 light_attenuate = _3ds_chunk(LIGHT_ATTENUATE)
1870 light_inner_range = _3ds_chunk(LIGHT_INNER_RANGE)
1871 light_outer_range = _3ds_chunk(LIGHT_OUTER_RANGE)
1872 light_energy_factor = _3ds_chunk(LIGHT_MULTIPLIER)
1873 light_ratio = ob.data.energy if ob.data.type == 'SUN' else ob.data.energy * 0.001
1874 object_chunk.add_variable("light", _3ds_string(sane_name(ob.name)))
1875 obj_light_chunk.add_variable("location", _3ds_point_3d(light_distance))
1876 color_float_chunk.add_variable("color", _3ds_float_color(ob.data.color))
1877 light_outer_range.add_variable("distance", _3ds_float(ob.data.cutoff_distance))
1878 light_inner_range.add_variable("radius", _3ds_float(ob.data.shadow_soft_size * 100))
1879 light_energy_factor.add_variable("energy", _3ds_float(light_ratio))
1880 obj_light_chunk.add_subchunk(color_float_chunk)
1881 obj_light_chunk.add_subchunk(light_outer_range)
1882 obj_light_chunk.add_subchunk(light_inner_range)
1883 obj_light_chunk.add_subchunk(light_energy_factor)
1884 if ob.data.use_custom_distance:
1885 obj_light_chunk.add_subchunk(light_attenuate)
1887 if ob.data.type == 'SPOT':
1888 cone_angle = math.degrees(ob.data.spot_size)
1889 hot_spot = cone_angle - (ob.data.spot_blend * math.floor(cone_angle))
1890 spot_pos = calc_target(light_distance, rotation[ob.name].x, rotation[ob.name].z)
1891 spotlight_chunk = _3ds_chunk(LIGHT_SPOTLIGHT)
1892 spot_roll_chunk = _3ds_chunk(LIGHT_SPOT_ROLL)
1893 spotlight_chunk.add_variable("target", _3ds_point_3d(spot_pos))
1894 spotlight_chunk.add_variable("hotspot", _3ds_float(round(hot_spot, 4)))
1895 spotlight_chunk.add_variable("angle", _3ds_float(round(cone_angle, 4)))
1896 spot_roll_chunk.add_variable("roll", _3ds_float(round(rotation[ob.name].y, 6)))
1897 spotlight_chunk.add_subchunk(spot_roll_chunk)
1898 if ob.data.use_shadow:
1899 spot_shadow_flag = _3ds_chunk(LIGHT_SPOT_SHADOWED)
1900 spot_shadow_chunk = _3ds_chunk(LIGHT_SPOT_LSHADOW)
1901 spot_shadow_chunk.add_variable("bias", _3ds_float(round(ob.data.shadow_buffer_bias,4)))
1902 spot_shadow_chunk.add_variable("filter", _3ds_float(round((ob.data.shadow_buffer_clip_start * 10),4)))
1903 spot_shadow_chunk.add_variable("buffer", _3ds_ushort(0x200))
1904 spotlight_chunk.add_subchunk(spot_shadow_flag)
1905 spotlight_chunk.add_subchunk(spot_shadow_chunk)
1906 if ob.data.show_cone:
1907 spot_cone_chunk = _3ds_chunk(LIGHT_SPOT_SEE_CONE)
1908 spotlight_chunk.add_subchunk(spot_cone_chunk)
1909 if ob.data.use_square:
1910 spot_square_chunk = _3ds_chunk(LIGHT_SPOT_RECTANGLE)
1911 spotlight_chunk.add_subchunk(spot_square_chunk)
1912 if ob.scale.x and ob.scale.y != 0.0:
1913 spot_aspect_chunk = _3ds_chunk(LIGHT_SPOT_ASPECT)
1914 spot_aspect_chunk.add_variable("aspect", _3ds_float(round((ob.scale.x / ob.scale.y),4)))
1915 spotlight_chunk.add_subchunk(spot_aspect_chunk)
1916 if ob.data.use_nodes:
1917 links = ob.data.node_tree.links
1918 bptype = 'EMISSION'
1919 bpmix = 'MIX', 'MIX_RGB', 'EMISSION'
1920 bptex = 'TEX_IMAGE', 'TEX_ENVIRONMENT'
1921 bpout = 'ADD_SHADER', 'MIX_SHADER', 'OUTPUT_LIGHT'
1922 bshade = next((lk.from_node.type for lk in links if lk.from_node.type == bptype and lk.to_node.type in bpout), None)
1923 bpnode = next((lk.from_node.type for lk in links if lk.from_node.type in bpmix and lk.to_node.type == bshade), bshade)
1924 bitmap = next((lk.from_node.image for lk in links if lk.from_node.type in bptex and lk.to_node.type == bpnode), False)
1925 if bitmap and bitmap is not None:
1926 spot_projector_chunk = _3ds_chunk(LIGHT_SPOT_PROJECTOR)
1927 spot_projector_chunk.add_variable("image", _3ds_string(sane_name(bitmap.name)))
1928 spotlight_chunk.add_subchunk(spot_projector_chunk)
1929 obj_light_chunk.add_subchunk(spotlight_chunk)
1931 # Add light to object chunk
1932 object_chunk.add_subchunk(obj_light_chunk)
1934 # Add hierachy chunks with ID from object_id dictionary
1935 if use_hierarchy:
1936 obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
1937 obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
1938 obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
1939 if ob.parent is not None and (ob.parent.name in object_id):
1940 obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
1941 obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
1942 obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
1943 object_chunk.add_subchunk(obj_hierarchy_chunk)
1945 # Add light object and hierarchy chunks to object info
1946 object_info.add_subchunk(object_chunk)
1948 # Export light and spotlight target node
1949 if use_keyframes:
1950 kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
1951 if ob.data.type == 'SPOT':
1952 kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale, name_id))
1954 # Create camera object chunks
1955 for ob in camera_objects:
1956 object_chunk = _3ds_chunk(OBJECT)
1957 camera_chunk = _3ds_chunk(OBJECT_CAMERA)
1958 crange_chunk = _3ds_chunk(OBJECT_CAM_RANGES)
1959 camera_distance = translation[ob.name]
1960 camera_target = calc_target(camera_distance, rotation[ob.name].x, rotation[ob.name].z)
1961 object_chunk.add_variable("camera", _3ds_string(sane_name(ob.name)))
1962 camera_chunk.add_variable("location", _3ds_point_3d(camera_distance))
1963 camera_chunk.add_variable("target", _3ds_point_3d(camera_target))
1964 camera_chunk.add_variable("roll", _3ds_float(round(rotation[ob.name].y, 6)))
1965 camera_chunk.add_variable("lens", _3ds_float(ob.data.lens))
1966 crange_chunk.add_variable("clipstart", _3ds_float(ob.data.clip_start * 0.1))
1967 crange_chunk.add_variable("clipend", _3ds_float(ob.data.clip_end * 0.1))
1968 camera_chunk.add_subchunk(crange_chunk)
1969 object_chunk.add_subchunk(camera_chunk)
1971 # Add hierachy chunks with ID from object_id dictionary
1972 if use_hierarchy:
1973 obj_hierarchy_chunk = _3ds_chunk(OBJECT_HIERARCHY)
1974 obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
1975 obj_hierarchy_chunk.add_variable("hierarchy", _3ds_ushort(object_id[ob.name]))
1976 if ob.parent is not None and (ob.parent.name in object_id):
1977 obj_parent_chunk = _3ds_chunk(OBJECT_PARENT)
1978 obj_parent_chunk.add_variable("parent", _3ds_ushort(object_id[ob.parent.name]))
1979 obj_hierarchy_chunk.add_subchunk(obj_parent_chunk)
1980 object_chunk.add_subchunk(obj_hierarchy_chunk)
1982 # Add light object and hierarchy chunks to object info
1983 object_info.add_subchunk(object_chunk)
1985 # Export camera and target node
1986 if use_keyframes:
1987 kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
1988 kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale, name_id))
1990 # Add main object info chunk to primary chunk
1991 primary.add_subchunk(object_info)
1993 # Add main keyframe data chunk to primary chunk
1994 if use_keyframes:
1995 primary.add_subchunk(kfdata)
1997 # The chunk hierarchy is completely built, now check the size
1998 primary.get_size()
2000 # Open the file for writing
2001 file = open(filepath, 'wb')
2003 # Recursively write the chunks to file
2004 primary.write(file)
2006 # Close the file
2007 file.close()
2009 # Clear name mapping vars, could make locals too
2010 del name_unique[:]
2011 name_mapping.clear()
2013 # Debugging only: report the exporting time
2014 context.window.cursor_set('DEFAULT')
2015 print("3ds export time: %.2f" % (time.time() - duration))
2017 # Debugging only: dump the chunk hierarchy
2018 # primary.dump()
2020 return {'FINISHED'}