Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# Script copyright (C) Bob Holcomb
# Contributors: Campbell Barton, Bob Holcomb, Richard Lärkäng, Damien McGinnes, Mark Stijnman, Sebastian Sille
"""
Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information
from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode.
"""
import bpy
import math
import struct
import mathutils
import bpy_extras
from bpy_extras import node_shader_utils
######################################################
# Data Structures
######################################################
# Some of the chunks that we will export
# ----- Primary Chunk, at the beginning of each file
VERSION = 0x0002 # This gives the version of the .3ds file
KFDATA = 0xB000 # This is the header for all of the key frame info
OBJECTINFO = 0x3D3D # Main mesh object chunk before the material and object information
MESHVERSION = 0x3D3E # This gives the version of the mesh
AMBIENTLIGHT = 0x2100 # The color of the ambient light
MATERIAL = 45055 # 0xAFFF // This stored the texture info
OBJECT = 16384 # 0x4000 // This stores the faces, vertices, etc...
MATNAME = 0xA000 # This holds the material name
MATAMBIENT = 0xA010 # Ambient color of the object/material
MATDIFFUSE = 0xA020 # This holds the color of the object/material
MATSPECULAR = 0xA030 # Specular color of the object/material
MATSHINESS = 0xA040 # Specular intensity of the object/material (percent)
MATSHIN2 = 0xA041 # Reflection of the object/material (percent)
MATSHIN3 = 0xA042 # metallic/mirror of the object/material (percent)
MATTRANS = 0xA050 # Transparency value (100-OpacityValue) (percent)
MATSELFILPCT = 0xA084 # Self illumination strength (percent)
MATSHADING = 0xA100 # Material shading method
MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture
MAT_SPECMAP = 0xA204 # head for specularity map
MAT_OPACMAP = 0xA210 # head for opacity map
MAT_REFLMAP = 0xA220 # head for reflect map
MAT_BUMPMAP = 0xA230 # head for normal map
MAT_BUMP_PERCENT = 0xA252 # Normalmap strength (percent)
MAT_TEX2MAP = 0xA33A # head for secondary texture
MAT_SHINMAP = 0xA33C # head for roughness map
MAT_SELFIMAP = 0xA33D # head for emission map
MATMAPFILE = 0xA300 # This holds the file name of a texture
MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag
MAT_MAP_TEXBLUR = 0xA353 # Texture blurring factor
MAT_MAP_USCALE = 0xA354 # U axis scaling
MAT_MAP_VSCALE = 0xA356 # V axis scaling
MAT_MAP_UOFFSET = 0xA358 # U axis offset
MAT_MAP_VOFFSET = 0xA35A # V axis offset
MAT_MAP_ANG = 0xA35C # UV rotation around the z-axis in rad
MAP_COL1 = 0xA360 # Tint Color1
MAP_COL2 = 0xA362 # Tint Color2
MAP_RCOL = 0xA364 # Red tint
MAP_GCOL = 0xA366 # Green tint
MAP_BCOL = 0xA368 # Blue tint
RGB = 0x0010 # RGB float
RGB1 = 0x0011 # RGB Color1
RGB2 = 0x0012 # RGB Color2
PCT = 0x0030 # Percent chunk
MASTERSCALE = 0x0100 # Master scale factor
OBJECT_MESH = 0x4100 # This lets us know that we are reading a new object
OBJECT_LIGHT = 0x4600 # This lets us know we are reading a light object
OBJECT_CAMERA = 0x4700 # This lets us know we are reading a camera object
LIGHT_MULTIPLIER = 0x465B # The light energy factor
LIGHT_SPOTLIGHT = 0x4610 # The target of a spotlight
LIGHT_SPOTROLL = 0x4656 # The roll angle of the spot
OBJECT_CAM_RANGES = 0x4720 # The camera range values
OBJECT_VERTICES = 0x4110 # The objects vertices
OBJECT_FACES = 0x4120 # The objects faces
OBJECT_MATERIAL = 0x4130 # This is found if the object has a material, either texture map or color
OBJECT_UV = 0x4140 # The UV texture coordinates
OBJECT_SMOOTH = 0x4150 # The objects smooth groups
OBJECT_TRANS_MATRIX = 0x4160 # The Object Matrix
KFDATA_KFHDR = 0xB00A
KFDATA_KFSEG = 0xB008
KFDATA_KFCURTIME = 0xB009
KFDATA_OBJECT_NODE_TAG = 0xB002
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
OBJECT_NODE_ID = 0xB030
OBJECT_NODE_HDR = 0xB010
OBJECT_PIVOT = 0xB013
OBJECT_INSTANCE_NAME = 0xB011
POS_TRACK_TAG = 0xB020
ROT_TRACK_TAG = 0xB021
SCL_TRACK_TAG = 0xB022
# So 3ds max can open files, limit names to 12 in length
# this is very annoying for filenames!
name_unique = [] # stores str, ascii only
name_mapping = {} # stores {orig: byte} mapping
def sane_name(name):
name_fixed = name_mapping.get(name)
if name_fixed is not None:
return name_fixed
# strip non ascii chars
new_name_clean = new_name = name.encode("ASCII", "replace").decode("ASCII")[:12]
i = 0
while new_name in name_unique:
new_name = new_name_clean + ".%.3d" % i
i += 1
# note, appending the 'str' version.
name_unique.append(new_name)
name_mapping[name] = new_name = new_name.encode("ASCII", "replace")
return new_name
def uv_key(uv):
return round(uv[0], 6), round(uv[1], 6)
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# size defines:
SZ_SHORT = 2
SZ_INT = 4
SZ_FLOAT = 4
class _3ds_ushort(object):
"""Class representing a short (2-byte integer) for a 3ds file.
*** This looks like an unsigned short H is unsigned from the struct docs - Cam***"""
__slots__ = ("value", )
def __init__(self, val=0):
self.value = val
def get_size(self):
return SZ_SHORT
def write(self, file):
file.write(struct.pack("<H", self.value))
def __str__(self):
return str(self.value)
class _3ds_uint(object):
"""Class representing an int (4-byte integer) for a 3ds file."""
__slots__ = ("value", )
def __init__(self, val):
self.value = val
def get_size(self):
return SZ_INT
def write(self, file):
file.write(struct.pack("<I", self.value))
def __str__(self):
return str(self.value)
class _3ds_float(object):
"""Class representing a 4-byte IEEE floating point number for a 3ds file."""
__slots__ = ("value", )
def __init__(self, val):
self.value = val
def get_size(self):
return SZ_FLOAT
def write(self, file):
file.write(struct.pack("<f", self.value))
def __str__(self):
return str(self.value)
class _3ds_string(object):
"""Class representing a zero-terminated string for a 3ds file."""
__slots__ = ("value", )
def __init__(self, val):
assert(type(val) == bytes)
self.value = val
def get_size(self):
return (len(self.value) + 1)
def write(self, file):
binary_format = "<%ds" % (len(self.value) + 1)
file.write(struct.pack(binary_format, self.value))
def __str__(self):
return str(self.value)
class _3ds_point_3d(object):
"""Class representing a three-dimensional point for a 3ds file."""
__slots__ = "x", "y", "z"
def __init__(self, point):
self.x, self.y, self.z = point
def get_size(self):
return 3 * SZ_FLOAT
def write(self, file):
file.write(struct.pack('<3f', self.x, self.y, self.z))
def __str__(self):
return '(%f, %f, %f)' % (self.x, self.y, self.z)
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# Used for writing a track
'''
class _3ds_point_4d(object):
"""Class representing a four-dimensional point for a 3ds file, for instance a quaternion."""
__slots__ = "x","y","z","w"
def __init__(self, point=(0.0,0.0,0.0,0.0)):
self.x, self.y, self.z, self.w = point
def get_size(self):
return 4*SZ_FLOAT
def write(self,file):
data=struct.pack('<4f', self.x, self.y, self.z, self.w)
file.write(data)
def __str__(self):
return '(%f, %f, %f, %f)' % (self.x, self.y, self.z, self.w)
'''
class _3ds_point_uv(object):
"""Class representing a UV-coordinate for a 3ds file."""
__slots__ = ("uv", )
def __init__(self, point):
self.uv = point
def get_size(self):
return 2 * SZ_FLOAT
def write(self, file):
data = struct.pack('<2f', self.uv[0], self.uv[1])
file.write(data)
def __str__(self):
return '(%g, %g)' % self.uv
class _3ds_float_color(object):
"""Class representing a rgb float color for a 3ds file."""
__slots__ = "r", "g", "b"
def __init__(self, col):
self.r, self.g, self.b = col
def get_size(self):
return 3 * SZ_FLOAT
def write(self, file):
file.write(struct.pack('3f', self.r, self.g, self.b))
def __str__(self):
return '{%f, %f, %f}' % (self.r, self.g, self.b)
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
class _3ds_rgb_color(object):
"""Class representing a (24-bit) rgb color for a 3ds file."""
__slots__ = "r", "g", "b"
def __init__(self, col):
self.r, self.g, self.b = col
def get_size(self):
return 3
def write(self, file):
file.write(struct.pack('<3B', int(255 * self.r), int(255 * self.g), int(255 * self.b)))
def __str__(self):
return '{%f, %f, %f}' % (self.r, self.g, self.b)
class _3ds_face(object):
"""Class representing a face for a 3ds file."""
__slots__ = ("vindex", )
def __init__(self, vindex):
self.vindex = vindex
def get_size(self):
return 4 * SZ_SHORT
# no need to validate every face vert. the oversized array will
# catch this problem
def write(self, file):
# The last zero is only used by 3d studio
file.write(struct.pack("<4H", self.vindex[0], self.vindex[1], self.vindex[2], 0))
def __str__(self):
return "[%d %d %d]" % (self.vindex[0], self.vindex[1], self.vindex[2])
class _3ds_array(object):
"""Class representing an array of variables for a 3ds file.
Consists of a _3ds_ushort to indicate the number of items, followed by the items themselves.
"""
__slots__ = "values", "size"
def __init__(self):
self.values = []
self.size = SZ_SHORT
# add an item:
def add(self, item):
self.values.append(item)
self.size += item.get_size()
def get_size(self):
return self.size
def validate(self):
return len(self.values) <= 65535
def write(self, file):
_3ds_ushort(len(self.values)).write(file)
for value in self.values:
value.write(file)
# To not overwhelm the output in a dump, a _3ds_array only
# outputs the number of items, not all of the actual items.
def __str__(self):
return '(%d items)' % len(self.values)
class _3ds_named_variable(object):
"""Convenience class for named variables."""
__slots__ = "value", "name"
def __init__(self, name, val=None):
self.name = name
self.value = val
def get_size(self):
if self.value is None:
return 0
else:
return self.value.get_size()
def write(self, file):
if self.value is not None:
self.value.write(file)
def dump(self, indent):
if self.value is not None:
print(indent * " ",
self.name if self.name else "[unnamed]",
" = ",
self.value)
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
class _3ds_chunk(object):
"""Class representing a chunk in a 3ds file.
Chunks contain zero or more variables, followed by zero or more subchunks.
"""
__slots__ = "ID", "size", "variables", "subchunks"
def __init__(self, chunk_id=0):
self.ID = _3ds_ushort(chunk_id)
self.size = _3ds_uint(0)
self.variables = []
self.subchunks = []
def add_variable(self, name, var):
"""Add a named variable.
The name is mostly for debugging purposes."""
self.variables.append(_3ds_named_variable(name, var))
def add_subchunk(self, chunk):
"""Add a subchunk."""
self.subchunks.append(chunk)
def get_size(self):
"""Calculate the size of the chunk and return it.
The sizes of the variables and subchunks are used to determine this chunk\'s size."""
tmpsize = self.ID.get_size() + self.size.get_size()
for variable in self.variables:
tmpsize += variable.get_size()
for subchunk in self.subchunks:
tmpsize += subchunk.get_size()
self.size.value = tmpsize
return self.size.value
def validate(self):
for var in self.variables:
func = getattr(var.value, "validate", None)
if (func is not None) and not func():
return False
for chunk in self.subchunks:
func = getattr(chunk, "validate", None)
if (func is not None) and not func():
return False
return True
def write(self, file):
"""Write the chunk to a file.
Uses the write function of the variables and the subchunks to do the actual work."""
self.ID.write(file)
self.size.write(file)
for variable in self.variables:
variable.write(file)
for subchunk in self.subchunks:
subchunk.write(file)
def dump(self, indent=0):
"""Write the chunk to a file.
Dump is used for debugging purposes, to dump the contents of a chunk to the standard output.
Uses the dump function of the named variables and the subchunks to do the actual work."""
print(indent * " ",
"ID=%r" % hex(self.ID.value),
"size=%r" % self.get_size())
for variable in self.variables:
variable.dump(indent + 1)
for subchunk in self.subchunks:
subchunk.dump(indent + 1)
######################################################
# EXPORT
######################################################
def get_material_image(material):
""" Get images from paint slots."""
pt = material.paint_active_slot
tex = material.texture_paint_images
if pt < len(tex):
slot = tex[pt]
if slot.type == 'IMAGE':
return slot
def get_uv_image(ma):
""" Get image from material wrapper."""
if ma and ma.use_nodes:
ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma)
ma_tex = ma_wrap.base_color_texture
if ma_tex and ma_tex.image is not None:
return ma_tex.image
else:
return get_material_image(ma)
def make_material_subchunk(chunk_id, color):
"""Make a material subchunk.
Used for color subchunks, such as diffuse color or ambient color subchunks."""
mat_sub = _3ds_chunk(chunk_id)
col1 = _3ds_chunk(RGB1)
col1.add_variable("color1", _3ds_rgb_color(color))
mat_sub.add_subchunk(col1)
# optional:
#col2 = _3ds_chunk(RGB1)
#col2.add_variable("color2", _3ds_rgb_color(color))
def make_percent_subchunk(chunk_id, percent):
"""Make a percentage based subchunk."""
pct_sub = _3ds_chunk(chunk_id)
pcti = _3ds_chunk(PCT)
pcti.add_variable("percent", _3ds_ushort(int(round(percent * 100, 0))))
pct_sub.add_subchunk(pcti)
return pct_sub
def make_texture_chunk(chunk_id, images):
"""Make Material Map texture chunk."""
# Add texture percentage value (100 = 1.0)
ma_sub = make_percent_subchunk(chunk_id, 1)
has_entry = False
def add_image(img):
filename = bpy.path.basename(image.filepath)
ma_sub_file = _3ds_chunk(MATMAPFILE)
ma_sub_file.add_variable("image", _3ds_string(sane_name(filename)))
ma_sub.add_subchunk(ma_sub_file)
for image in images:
add_image(image)
has_entry = True
return ma_sub if has_entry else None
def make_material_texture_chunk(chunk_id, texslots, pct):
"""Make Material Map texture chunk given a seq. of `MaterialTextureSlot`'s
Paint slots are optionally used as image source if no nodes are
used. No additional filtering for mapping modes is done, all
slots are written "as is"."""
# Add texture percentage value
mat_sub = make_percent_subchunk(chunk_id, pct)
has_entry = False
def add_texslot(texslot):
image = texslot.image
filename = bpy.path.basename(image.filepath)
mat_sub_file = _3ds_chunk(MATMAPFILE)
mat_sub_file.add_variable("mapfile", _3ds_string(sane_name(filename)))
mat_sub.add_subchunk(mat_sub_file)
for link in texslot.socket_dst.links:
socket = link.from_socket.identifier
maptile = 0
# no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2
if texslot.extension == 'EXTEND':
maptile |= 0x1
elif texslot.extension == 'CLIP':
maptile |= 0x10
mat_sub_tile = _3ds_chunk(MAT_MAP_TILING)
mat_sub_tile.add_variable("tiling", _3ds_ushort(maptile))
mat_sub.add_subchunk(mat_sub_tile)
mat_sub_alpha = _3ds_chunk(MAP_TILING)
alphaflag = 0x40 # summed area sampling 0x20
mat_sub_alpha.add_variable("alpha", _3ds_ushort(alphaflag))
mat_sub.add_subchunk(mat_sub_alpha)
if texslot.socket_dst.identifier in {'Base Color', 'Specular'}:
mat_sub_tint = _3ds_chunk(MAP_TILING) # RGB tint 0x200
tint = 0x80 if texslot.image.colorspace_settings.name == 'Non-Color' else 0x200
mat_sub_tint.add_variable("tint", _3ds_ushort(tint))
mat_sub.add_subchunk(mat_sub_tint)
mat_sub_texblur = _3ds_chunk(MAT_MAP_TEXBLUR) # Based on observation this is usually 1.0
mat_sub_texblur.add_variable("maptexblur", _3ds_float(1.0))
mat_sub.add_subchunk(mat_sub_texblur)
mat_sub_uscale = _3ds_chunk(MAT_MAP_USCALE)
mat_sub_uscale.add_variable("mapuscale", _3ds_float(round(texslot.scale[0], 6)))
mat_sub.add_subchunk(mat_sub_uscale)
mat_sub_vscale = _3ds_chunk(MAT_MAP_VSCALE)
mat_sub_vscale.add_variable("mapvscale", _3ds_float(round(texslot.scale[1], 6)))
mat_sub.add_subchunk(mat_sub_vscale)
mat_sub_uoffset = _3ds_chunk(MAT_MAP_UOFFSET)
mat_sub_uoffset.add_variable("mapuoffset", _3ds_float(round(texslot.translation[0], 6)))
mat_sub.add_subchunk(mat_sub_uoffset)
mat_sub_voffset = _3ds_chunk(MAT_MAP_VOFFSET)
mat_sub_voffset.add_variable("mapvoffset", _3ds_float(round(texslot.translation[1], 6)))
mat_sub.add_subchunk(mat_sub_voffset)
mat_sub_angle = _3ds_chunk(MAT_MAP_ANG)
mat_sub_angle.add_variable("mapangle", _3ds_float(round(texslot.rotation[2], 6)))
mat_sub.add_subchunk(mat_sub_angle)
if texslot.socket_dst.identifier in {'Base Color', 'Specular'}:
base = texslot.owner_shader.material.diffuse_color[:3]
spec = texslot.owner_shader.material.specular_color[:]
rgb.add_variable("mapcolor", _3ds_rgb_color(spec if texslot.socket_dst.identifier == 'Specular' else base))
mat_sub.add_subchunk(rgb)
# store all textures for this mapto in order. This at least is what
# the 3DS exporter did so far, afaik most readers will just skip
# over 2nd textures.
for slot in texslots:
if slot.image is not None:
add_texslot(slot)
has_entry = True
return mat_sub if has_entry else None
def make_material_chunk(material, image):
"""Make a material chunk out of a blender material.
Shading method is required for 3ds max, 0 for wireframe.
0x1 for flat, 0x2 for gouraud, 0x3 for phong and 0x4 for metal."""
material_chunk = _3ds_chunk(MATERIAL)
name = _3ds_chunk(MATNAME)
shading = _3ds_chunk(MATSHADING)
name_str = material.name if material else "None"
if image:
name_str += image.name
name.add_variable("name", _3ds_string(sane_name(name_str)))
material_chunk.add_subchunk(name)
if not material:
shading.add_variable("shading", _3ds_ushort(1)) # Flat shading
material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, (0.0, 0.0, 0.0)))
material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (0.8, 0.8, 0.8)))
material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1.0, 1.0, 1.0)))
material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, .2))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, 1))
material_chunk.add_subchunk(shading)
elif material and material.use_nodes:
wrap = node_shader_utils.PrincipledBSDFWrapper(material)
shading.add_variable("shading", _3ds_ushort(3)) # Phong shading
material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, wrap.emission_color[:3]))
material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, wrap.base_color[:3]))
material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, wrap.roughness))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, wrap.specular))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, wrap.metallic))
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
material_chunk.add_subchunk(shading)
if wrap.base_color_texture:
d_pct = 0.7 + sum(wrap.base_color[:]) * 0.1
color = [wrap.base_color_texture]
matmap = make_material_texture_chunk(MAT_DIFFUSEMAP, color, d_pct)
if matmap:
material_chunk.add_subchunk(matmap)
if wrap.specular_texture:
spec = [wrap.specular_texture]
s_pct = material.specular_intensity
matmap = make_material_texture_chunk(MAT_SPECMAP, spec, s_pct)
if matmap:
material_chunk.add_subchunk(matmap)
if wrap.alpha_texture:
alpha = [wrap.alpha_texture]
a_pct = material.diffuse_color[3]
matmap = make_material_texture_chunk(MAT_OPACMAP, alpha, a_pct)
if matmap:
material_chunk.add_subchunk(matmap)
if wrap.metallic_texture:
metallic = [wrap.metallic_texture]
m_pct = material.metallic
matmap = make_material_texture_chunk(MAT_REFLMAP, metallic, m_pct)
if matmap:
material_chunk.add_subchunk(matmap)
if wrap.normalmap_texture:
normal = [wrap.normalmap_texture]
bump = wrap.normalmap_strength
b_pct = min(bump, 1)
bumpval = min(999, (bump * 100)) # 3ds max bump = 999
strength = _3ds_chunk(MAT_BUMP_PERCENT)
strength.add_variable("bump_pct", _3ds_ushort(int(bumpval)))
matmap = make_material_texture_chunk(MAT_BUMPMAP, normal, b_pct)
if matmap:
material_chunk.add_subchunk(matmap)
material_chunk.add_subchunk(strength)
if wrap.roughness_texture:
roughness = [wrap.roughness_texture]
r_pct = material.roughness
matmap = make_material_texture_chunk(MAT_SHINMAP, roughness, r_pct)
if matmap:
material_chunk.add_subchunk(matmap)
if wrap.emission_color_texture:
emission = [wrap.emission_color_texture]
matmap = make_material_texture_chunk(MAT_SELFIMAP, emission, e_pct)
if matmap:
material_chunk.add_subchunk(matmap)
# make sure no textures are lost. Everything that doesn't fit
# into a channel is exported as secondary texture
diffuse = []
for link in wrap.material.node_tree.links:
if link.from_node.type == 'TEX_IMAGE' and link.to_node.type != 'BSDF_PRINCIPLED':
diffuse = [link.from_node.image] if not wrap.normalmap_texture else None
if diffuse:
matmap = make_texture_chunk(MAT_TEX2MAP, diffuse)
if matmap:
material_chunk.add_subchunk(matmap)
else:
shading.add_variable("shading", _3ds_ushort(2)) # Gouraud shading
material_chunk.add_subchunk(make_material_subchunk(MATAMBIENT, material.line_color[:3]))
material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color[:3]))
material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:]))
material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, material.roughness))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, material.specular_intensity))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN3, material.metallic))
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - material.diffuse_color[3]))
material_chunk.add_subchunk(shading)
slots = [get_material_image(material)] # can be None
if image:
material_chunk.add_subchunk(make_texture_chunk(MAT_DIFFUSEMAP, slots))
return material_chunk
class tri_wrapper(object):
"""Class representing a triangle.
Used when converting faces to triangles"""
__slots__ = "vertex_index", "ma", "image", "faceuvs", "offset", "group"
def __init__(self, vindex=(0, 0, 0), ma=None, image=None, faceuvs=None, group=0):
self.image = image
self.faceuvs = faceuvs
self.offset = [0, 0, 0] # offset indices
def extract_triangles(mesh):
"""Extract triangles from a mesh."""
mesh.calc_loop_triangles()
(polygroup, count) = mesh.calc_smooth_groups(use_bitflags=True)
do_uv = bool(mesh.uv_layers)
for i, face in enumerate(mesh.loop_triangles):
uf = mesh.uv_layers.active.data if do_uv else None
f_uv = [uf[lp].uv for lp in face.loops]
for ma in mesh.materials:
img = get_uv_image(ma) if uf else None
if img is not None:
img = img.name
smoothgroup = polygroup[face.polygon_index]
new_tri = tri_wrapper((f_v[0], f_v[1], f_v[2]), face.material_index, img)
if (do_uv):
new_tri.faceuvs = uv_key(f_uv[0]), uv_key(f_uv[1]), uv_key(f_uv[2])
new_tri.group = smoothgroup if face.use_smooth else 0
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
tri_list.append(new_tri)
return tri_list
def remove_face_uv(verts, tri_list):
"""Remove face UV coordinates from a list of triangles.
Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates
need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when
there are multiple uv coordinates per vertex."""
# initialize a list of UniqueLists, one per vertex:
#uv_list = [UniqueList() for i in xrange(len(verts))]
unique_uvs = [{} for i in range(len(verts))]
# for each face uv coordinate, add it to the UniqueList of the vertex
for tri in tri_list:
for i in range(3):
# store the index into the UniqueList for future reference:
# offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.faceuvs[i])))
context_uv_vert = unique_uvs[tri.vertex_index[i]]
uvkey = tri.faceuvs[i]
offset_index__uv_3ds = context_uv_vert.get(uvkey)
if not offset_index__uv_3ds:
offset_index__uv_3ds = context_uv_vert[uvkey] = len(context_uv_vert), _3ds_point_uv(uvkey)
tri.offset[i] = offset_index__uv_3ds[0]
# At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it
# only once.
# Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the
# faces refer to the new face indices:
vert_index = 0
vert_array = _3ds_array()
uv_array = _3ds_array()
index_list = []
for i, vert in enumerate(verts):
index_list.append(vert_index)
pt = _3ds_point_3d(vert.co) # reuse, should be ok
uvmap = [None] * len(unique_uvs[i])
for ii, uv_3ds in unique_uvs[i].values():
# add a vertex duplicate to the vertex_array for every uv associated with this vertex:
vert_array.add(pt)
# add the uv coordinate to the uv array:
# This for loop does not give uv's ordered by ii, so we create a new map
# and add the uv's later
# uv_array.add(uv_3ds)
uvmap[ii] = uv_3ds
# Add the uv's in the correct order
for uv_3ds in uvmap:
# add the uv coordinate to the uv array:
uv_array.add(uv_3ds)
vert_index += len(unique_uvs[i])
# Make sure the triangle vertex indices now refer to the new vertex list:
for tri in tri_list:
for i in range(3):
tri.offset[i] += index_list[tri.vertex_index[i]]
tri.vertex_index = tri.offset
return vert_array, uv_array, tri_list
def make_faces_chunk(tri_list, mesh, materialDict):
"""Make a chunk for the faces.
Also adds subchunks assigning materials to all faces."""
do_smooth = False
use_smooth = [poly.use_smooth for poly in mesh.polygons]
if True in use_smooth:
do_smooth = True
materials = mesh.materials
if not materials:
face_chunk = _3ds_chunk(OBJECT_FACES)
face_list = _3ds_array()
# Gather materials used in this mesh - mat/image pairs
unique_mats = {}
for i, tri in enumerate(tri_list):
face_list.add(_3ds_face(tri.vertex_index))
if materials:
ma = materials[tri.ma]
if ma:
ma = ma.name
context_face_array = unique_mats[ma, img][1]
name_str = ma if ma else "None"
context_face_array = _3ds_array()
unique_mats[ma, img] = _3ds_string(sane_name(name_str)), context_face_array
context_face_array.add(_3ds_ushort(i))
# obj_material_faces[tri.ma].add(_3ds_ushort(i))
face_chunk.add_variable("faces", face_list)
for ma_name, ma_faces in unique_mats.values():
obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
obj_material_chunk.add_variable("name", ma_name)
obj_material_chunk.add_variable("face_list", ma_faces)
face_chunk.add_subchunk(obj_material_chunk)
else:
obj_material_faces = []
obj_material_names = []
for m in materials:
if m:
obj_material_names.append(_3ds_string(sane_name(m.name)))
obj_material_faces.append(_3ds_array())
n_materials = len(obj_material_names)
for i, tri in enumerate(tri_list):
face_list.add(_3ds_face(tri.vertex_index))
if (tri.ma < n_materials):
obj_material_faces[tri.ma].add(_3ds_ushort(i))
face_chunk.add_variable("faces", face_list)
for i in range(n_materials):
obj_material_chunk = _3ds_chunk(OBJECT_MATERIAL)
obj_material_chunk.add_variable("name", obj_material_names[i])
obj_material_chunk.add_variable("face_list", obj_material_faces[i])
face_chunk.add_subchunk(obj_material_chunk)
if do_smooth:
obj_smooth_chunk = _3ds_chunk(OBJECT_SMOOTH)
for i, tri in enumerate(tri_list):
obj_smooth_chunk.add_variable("face_" + str(i), _3ds_uint(tri.group))
face_chunk.add_subchunk(obj_smooth_chunk)
return face_chunk
def make_vert_chunk(vert_array):
"""Make a vertex chunk out of an array of vertices."""
vert_chunk = _3ds_chunk(OBJECT_VERTICES)
vert_chunk.add_variable("vertices", vert_array)
return vert_chunk
def make_uv_chunk(uv_array):
"""Make a UV chunk out of an array of UVs."""
uv_chunk = _3ds_chunk(OBJECT_UV)
uv_chunk.add_variable("uv coords", uv_array)
return uv_chunk
def make_matrix_4x3_chunk(matrix):
matrix_chunk = _3ds_chunk(OBJECT_TRANS_MATRIX)
for vec in matrix.col:
for f in vec[:3]:
matrix_chunk.add_variable("matrix_f", _3ds_float(f))
return matrix_chunk
def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
"""Make a chunk out of a Blender mesh."""
# Extract the triangles from the mesh:
tri_list = extract_triangles(mesh)
# Remove the face UVs and convert it to vertex UV:
vert_array, uv_array, tri_list = remove_face_uv(mesh.vertices, tri_list)
else:
# Add the vertices to the vertex array:
vert_array = _3ds_array()
for vert in mesh.vertices:
vert_array.add(_3ds_point_3d(vert.co))
# no UV at all:
uv_array = None
# create the chunk: