1 # SPDX-FileCopyrightText: 2005 Bob Holcomb
3 # SPDX-License-Identifier: GPL-2.0-or-later
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.
16 from bpy_extras
import node_shader_utils
22 # Some of the chunks that we will export
23 # >----- Primary Chunk, at the beginning of each file
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
172 name_fixed
= name_mapping
.get(name
)
173 if name_fixed
is not None:
176 # Strip non ascii chars
177 new_name_clean
= new_name
= name
.encode("ASCII", "replace").decode("ASCII")[:12]
180 while new_name
in name_unique
:
181 new_name
= new_name_clean
+ '.%.3d' % i
184 # Note, appending the 'str' version
185 name_unique
.append(new_name
)
186 name_mapping
[name
] = new_name
= new_name
.encode("ASCII", "replace")
191 return round(uv
[0], 6), round(uv
[1], 6)
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):
208 def write(self
, file):
209 file.write(struct
.pack('<H', self
.value
))
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
):
225 def write(self
, file):
226 file.write(struct
.pack('<I', self
.value
))
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
):
242 def write(self
, file):
243 file.write(struct
.pack('<f', self
.value
))
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
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
))
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
278 def write(self
, file):
279 file.write(struct
.pack('<3f', self
.x
, self
.y
, self
.z
))
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
296 def write(self
,file):
297 data
=struct
.pack('<4f', self
.w
, self
.x
, self
.y
, self
.z
)
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."""
308 def __init__(self
, point
):
314 def write(self
, file):
315 data
= struct
.pack('<2f', self
.uv
[0], self
.uv
[1])
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
332 def write(self
, file):
333 file.write(struct
.pack('<3f', self
.r
, self
.g
, self
.b
))
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
349 def write(self
, file):
350 file.write(struct
.pack('<3B', int(255 * self
.r
), int(255 * self
.g
), int(255 * self
.b
)))
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
):
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
))
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"
387 self
.values
.append(item
)
388 self
.size
+= item
.get_size()
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
:
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
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):
416 if self
.value
is None:
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:
428 self
.name
if self
.name
else "[unnamed]",
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)
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
)
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
466 for var
in self
.variables
:
467 func
= getattr(var
.value
, "validate", None)
468 if (func
is not None) and not func():
471 for chunk
in self
.subchunks
:
472 func
= getattr(chunk
, "validate", None)
473 if (func
is not None) and not func():
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."""
484 self
.size
.write(file)
485 for variable
in self
.variables
:
487 for subchunk
in self
.subchunks
:
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."""
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)
507 def get_material_image(material
):
508 """ Get images from paint slots."""
510 pt
= material
.paint_active_slot
511 tex
= material
.texture_paint_images
514 if slot
.type == 'IMAGE':
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:
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
)
537 # col2 = _3ds_chunk(RGBI)
538 # col2.add_variable("color2", _3ds_rgb_color(color))
539 # mat_sub.add_subchunk(col2)
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
)
550 # pctf = _3ds_chunk(PCTF)
551 # pctf.add_variable("pctfloat", _3ds_float(round(percent, 6)))
552 # pct_sub.add_subchunk(pctf)
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
)
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
)
570 if extension
== 'EXTEND': # decal flag
572 if extension
== 'MIRROR': # mirror flag
574 if extension
== 'CLIP': # no wrap
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
)
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
)
596 def add_texslot(texslot
):
597 image
= texslot
.image
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."""
616 if texslot
.extension
== 'EXTEND':
618 if texslot
.extension
== 'MIRROR':
620 if texslot
.extension
== 'CLIP':
623 if socket
== 'Alpha':
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:
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"
687 # name_str += image.name
689 name
.add_variable("name", _3ds_string(sane_name(name_str
)))
690 material_chunk
.add_subchunk(name
)
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
)
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
)
727 material_chunk
.add_subchunk(matmap
)
730 if mxtex
and not primary_tex
:
731 material_chunk
.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP
, mxtex
, mxpct
))
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
)
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
)
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
)
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
)
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
)
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
)
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
780 mxsecondary
= link
.from_node
if link
.from_node
.type == 'TEX_IMAGE' and link
.to_socket
.identifier
in {'Color1', 'A_Color'} else False
782 matmap
= make_texture_chunk(MAT_TEX2MAP
, [mxsecondary
], 1 - mxpct
)
783 if primary_tex
and matmap
:
784 material_chunk
.add_subchunk(matmap
)
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
800 material_chunk
.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP
, slots
))
802 return material_chunk
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
819 self
.faceuvs
= faceuvs
820 self
.offset
= [0, 0, 0] # Offset indices
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)
832 do_uv
= bool(mesh
.uv_layers
)
835 for i
, face
in enumerate(mesh
.loop_triangles
):
838 uf
= mesh
.uv_layers
.active
.data
if do_uv
else None
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
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
]
856 v1
, v2
, v3
= v3
, v1
, v2
857 a_b
, b_c
, c_a
= c_a
, a_b
, b_c
859 uv1
, uv2
, uv3
= uv3
, uv1
, uv2
862 if c_a
.use_edge_sharp
:
864 if b_c
.use_edge_sharp
:
866 if a_b
.use_edge_sharp
:
869 smoothgroup
= polygroup
[face
.polygon_index
]
872 new_tri
= tri_wrapper((v1
, v2
, v3
), face
.material_index
, img
)
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
)
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
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
910 vert_array
= _3ds_array()
911 uv_array
= _3ds_array()
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
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
925 # Add uv's in the correct order and add coordinates to the uv array
929 vert_index
+= len(unique_uvs
[i
])
931 # Make sure the triangle vertex indices now refer to the new vertex list
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."""
944 use_smooth
= [poly
.use_smooth
for poly
in mesh
.polygons
]
945 if True in use_smooth
:
948 materials
= mesh
.materials
952 face_chunk
= _3ds_chunk(OBJECT_FACES
)
953 face_list
= _3ds_array()
956 # Gather materials used in this mesh - mat/image pairs
958 for i
, tri
in enumerate(tri_list
):
959 face_list
.add(_3ds_face(tri
.vertex_index
, tri
.flag
))
962 ma
= materials
[tri
.ma
]
969 context_face_array
= unique_mats
[ma
, img
][1]
971 name_str
= ma
if ma
else "None"
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
)
988 obj_material_faces
= []
989 obj_material_names
= []
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
)
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
)
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
)
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
)
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
)
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
)
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
))
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
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
)
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
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
)
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
1134 fcurves
= action
.fcurves
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
:
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
1190 fcurves
= action
.fcurves
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
:
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']
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)))
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)))
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."""
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
)
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
]
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
))
1380 def make_target_node(ob
, translation
, rotation
, scale
, name_id
):
1381 """Make a target chunk for light and camera objects."""
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
1418 fcurves
= action
.fcurves
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
:
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))
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
)
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
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
:
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
]
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
1514 fcurves
= action
.fcurves
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
:
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']
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))
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
)
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."""
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()
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':
1580 elif unit_length
== 'MILLIMETERS':
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
)
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
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)
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
)]
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']
1631 # Get derived objects
1632 derived_dict
= bpy_extras
.io_utils
.create_derived_objects(depsgraph
, [ob
])
1633 derived
= derived_dict
.get(ob
)
1638 for ob_derived
, mtx
in derived
:
1639 if ob
.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
1643 data
= ob_derived
.to_mesh()
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
1660 for f
, uf
in zip(data
.polygons
, data
.uv_layers
.active
.data
):
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
))
1676 if ma
: # Material may be None so check its not
1677 materialDict
.setdefault((ma
.name
, None), (ma
, None))
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
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
)
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
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
)
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)
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
)
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)
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
1791 # Give all objects a unique ID and build a dictionary from object name to object 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
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
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
)
1850 operator
.report({'WARNING'}, "Object %r can't be written into a 3DS file")
1852 # Export object node
1854 kfdata
.add_subchunk(make_object_node(ob
, translation
, rotation
, scale
, name_id
))
1858 # Create chunks for all empties - only requires a object node
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
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
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
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
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
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
1995 primary
.add_subchunk(kfdata
)
1997 # The chunk hierarchy is completely built, now check the size
2000 # Open the file for writing
2001 file = open(filepath
, 'wb')
2003 # Recursively write the chunks to file
2009 # Clear name mapping vars, could make locals too
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