diff --git a/io_scene_3ds/export_3ds.py b/io_scene_3ds/export_3ds.py index 7b37db23ac88fa311151ebd5403b035a3664b53b..40fdeda364844c11665e24f695182a78264fc957 100644 --- a/io_scene_3ds/export_3ds.py +++ b/io_scene_3ds/export_3ds.py @@ -49,8 +49,21 @@ 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 # ?? -MATMAP = 0xA200 # This is a header for a new material -MATMAPFILE = 0xA300 # This holds the file name of the texture + +MAT_DIFFUSEMAP = 0xA200 # This is a header for a new diffuse texture +MAT_OPACMAP = 0xA210 # head for opacity map +MAT_BUMPMAP = 0xA230 # read for normal map +MAT_SPECMAP = 0xA204 # read for specularity map + +#>------ sub defines of MAT_???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_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 RGB1 = 0x0011 RGB2 = 0x0012 @@ -423,10 +436,10 @@ class _3ds_chunk(object): # EXPORT ###################################################### -def get_material_images(material): +def get_material_image_texslots(material): # blender utility func. if material: - return [s.texture.image for s in material.texture_slots if s and s.texture.type == 'IMAGE' and s.texture.image] + return [s for s in material.texture_slots if s and s.texture.type == 'IMAGE' and s.texture.image] return [] # images = [] @@ -454,22 +467,76 @@ def make_material_subchunk(chunk_id, color): return mat_sub -def make_material_texture_chunk(chunk_id, images): - """Make Material Map texture chunk +def make_material_texture_chunk(chunk_id, texslots, tess_uv_image=None): + """Make Material Map texture chunk given a seq. of `MaterialTextureSlot`'s + + `tess_uv_image` is optionally used as image source if the slots are + empty. No additional filtering for mapping modes is done, all + slots are written "as is". """ + mat_sub = _3ds_chunk(chunk_id) + has_entry = False + + import bpy + + def add_texslot(texslot): + texture = texslot.texture + image = texture.image - def add_image(image): - import bpy 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 image in images: - add_image(image) + maptile = 0 + + # no perfect mapping for mirror modes - 3DS only has uniform mirror w. repeat=2 + if texture.extension == 'REPEAT' and (texture.use_mirror_x and texture.repeat_x > 1) \ + or (texture.use_mirror_y and texture.repeat_y > 1): + maptile |= 0x2 + # CLIP maps to 3DS' decal flag + elif texture.extension == 'CLIP': + maptile |= 0x10 + + mat_sub_tile = _3ds_chunk(MAT_MAP_TILING) + mat_sub_tile.add_variable("maptiling", _3ds_ushort(maptile)) + mat_sub.add_subchunk(mat_sub_tile) + + mat_sub_uscale = _3ds_chunk(MAT_MAP_USCALE) + mat_sub_uscale.add_variable("mapuscale", _3ds_float(texslot.scale[0])) + mat_sub.add_subchunk(mat_sub_uscale) + + mat_sub_vscale = _3ds_chunk(MAT_MAP_VSCALE) + mat_sub_vscale.add_variable("mapuscale", _3ds_float(texslot.scale[1])) + mat_sub.add_subchunk(mat_sub_vscale) + + mat_sub_uoffset = _3ds_chunk(MAT_MAP_UOFFSET) + mat_sub_uoffset.add_variable("mapuoffset", _3ds_float(texslot.offset[0])) + mat_sub.add_subchunk(mat_sub_uoffset) + + mat_sub_voffset = _3ds_chunk(MAT_MAP_VOFFSET) + mat_sub_voffset.add_variable("mapvoffset", _3ds_float(texslot.offset[1])) + mat_sub.add_subchunk(mat_sub_voffset) + + # 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: + add_texslot(slot) + has_entry = True + + # image from tess. UV face - basically the code above should handle + # this already. No idea why its here so keep it :-) + if tess_uv_image and not has_entry: + has_entry = True + + filename = bpy.path.basename(tess_uv_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) - return mat_sub + return mat_sub if has_entry else None def make_material_chunk(material, image): @@ -495,12 +562,40 @@ def make_material_chunk(material, image): material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, material.diffuse_color[:])) material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, material.specular_color[:])) - images = get_material_images(material) # can be None - if image: - images.append(image) - - if images: - material_chunk.add_subchunk(make_material_texture_chunk(MATMAP, images)) + slots = get_material_image_texslots(material) # can be None + + if slots: + + spec = [s for s in slots if s.use_map_specular or s.use_map_color_spec] + matmap = make_material_texture_chunk(MAT_SPECMAP, spec) + if matmap: + material_chunk.add_subchunk(matmap) + + alpha = [s for s in slots if s.use_map_alpha] + matmap = make_material_texture_chunk(MAT_OPACMAP, alpha) + if matmap: + material_chunk.add_subchunk(matmap) + + normal = [s for s in slots if s.use_map_normal] + matmap = make_material_texture_chunk(MAT_BUMPMAP, normal) + if matmap: + material_chunk.add_subchunk(matmap) + + # make sure no textures are lost. Everything that doesn't fit + # into a channel is exported as diffuse texture with a + # warning. + diffuse = [] + for s in slots: + if s.use_map_color_diffuse: + diffuse.append(s) + elif not (s in normal or s in alpha or s in spec): + print('\nwarning: failed to map texture to 3DS map channel, assuming diffuse') + diffuse.append(s) + + if diffuse: + matmap = make_material_texture_chunk(MAT_DIFFUSEMAP, diffuse, image) + if matmap: + material_chunk.add_subchunk(matmap) return material_chunk diff --git a/io_scene_3ds/import_3ds.py b/io_scene_3ds/import_3ds.py index 32a41b1a782e2a9ba28209d553f13391d77c4f97..338bf02a0cee0cb6c9b25db6277c8d25d0d43058 100644 --- a/io_scene_3ds/import_3ds.py +++ b/io_scene_3ds/import_3ds.py @@ -66,6 +66,13 @@ MAT_REFLECTION_MAP = 0xA220 # This is a header for a new reflection map MAT_BUMP_MAP = 0xA230 # This is a header for a new bump map MAT_MAP_FILEPATH = 0xA300 # This holds the file name of the texture +MAT_MAP_TILING = 0xa351 # 2nd bit (from LSB) is mirror UV flag +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 + MAT_FLOAT_COLOR = 0x0010 # color defined as 3 floats MAT_24BIT_COLOR = 0x0011 # color defined as 3 bytes @@ -211,7 +218,7 @@ def skip_to_end(file, skip_chunk): skip_chunk.bytes_read += buffer_size -def add_texture_to_material(image, texture, material, mapto): +def add_texture_to_material(image, texture, scale, offset, extension, material, mapto): #print('assigning %s to %s' % (texture, material)) if mapto not in {'COLOR', 'SPECULARITY', 'ALPHA', 'NORMAL'}: @@ -226,6 +233,18 @@ def add_texture_to_material(image, texture, material, mapto): mtex.texture_coords = 'UV' mtex.use_map_color_diffuse = False + mtex.scale = (scale[0], scale[1], 1.0) + mtex.offset = (offset[0], offset[1], 0.0) + + texture.extension = 'REPEAT' + if extension == 'mirror': + # 3DS mirror flag can be emulated by these settings (at least so it seems) + texture.repeat_x = texture.repeat_y = 2 + texture.use_mirror_x = texture.use_mirror_y = True + elif extension == 'decal': + # 3DS' decal mode maps best to Blenders CLIP + texture.extension = 'CLIP' + if mapto == 'COLOR': mtex.use_map_color_diffuse = True elif mapto == 'SPECULARITY': @@ -255,6 +274,7 @@ def process_next_chunk(file, previous_chunk, importedObjects, IMAGE_SEARCH): # TEXMODE = Mesh.FaceModes['TEX'] # Localspace variable names, faster. + STRUCT_SIZE_FLOAT = struct.calcsize('f') STRUCT_SIZE_2FLOAT = struct.calcsize('2f') STRUCT_SIZE_3FLOAT = struct.calcsize('3f') STRUCT_SIZE_4FLOAT = struct.calcsize('4f') @@ -330,7 +350,7 @@ def process_next_chunk(file, previous_chunk, importedObjects, IMAGE_SEARCH): uvl[pl.loop_start].uv = contextMeshUV[v1 * 2: (v1 * 2) + 2] uvl[pl.loop_start + 1].uv = contextMeshUV[v2 * 2: (v2 * 2) + 2] - uvl[pl.loop_start + 2].uv = contextMeshUV[v3 * 2: ( v3 * 2) + 2] + uvl[pl.loop_start + 2].uv = contextMeshUV[v3 * 2: (v3 * 2) + 2] # always a tri bmesh.validate() @@ -352,10 +372,20 @@ def process_next_chunk(file, previous_chunk, importedObjects, IMAGE_SEARCH): CreateBlenderObject = False def read_float_color(temp_chunk): - temp_data = file.read(struct.calcsize('3f')) - temp_chunk.bytes_read += 12 + temp_data = file.read(STRUCT_SIZE_3FLOAT) + temp_chunk.bytes_read += STRUCT_SIZE_3FLOAT return [float(col) for col in struct.unpack('<3f', temp_data)] + def read_float(temp_chunk): + temp_data = file.read(STRUCT_SIZE_FLOAT) + temp_chunk.bytes_read += STRUCT_SIZE_FLOAT + return struct.unpack('<f', temp_data)[0] + + def read_short(temp_chunk): + temp_data = file.read(STRUCT_SIZE_UNSIGNED_SHORT) + temp_chunk.bytes_read += STRUCT_SIZE_UNSIGNED_SHORT + return struct.unpack('<H', temp_data)[0] + def read_byte_color(temp_chunk): temp_data = file.read(struct.calcsize('3B')) temp_chunk.bytes_read += 3 @@ -364,24 +394,46 @@ def process_next_chunk(file, previous_chunk, importedObjects, IMAGE_SEARCH): def read_texture(new_chunk, temp_chunk, name, mapto): new_texture = bpy.data.textures.new(name, type='IMAGE') - img = None + u_scale, v_scale, u_offset, v_offset = 1.0, 1.0, 0.0, 0.0 + mirror = False + extension = 'wrap' while (new_chunk.bytes_read < new_chunk.length): #print 'MAT_TEXTURE_MAP..while', new_chunk.bytes_read, new_chunk.length read_chunk(file, temp_chunk) if temp_chunk.ID == MAT_MAP_FILEPATH: texture_name, read_str_len = read_string(file) + img = TEXTURE_DICT[contextMaterial.name] = load_image(texture_name, dirname) - new_chunk.bytes_read += read_str_len # plus one for the null character that gets removed + temp_chunk.bytes_read += read_str_len # plus one for the null character that gets removed - else: - skip_to_end(file, temp_chunk) + elif temp_chunk.ID == MAT_MAP_USCALE: + u_scale = read_float(temp_chunk) + elif temp_chunk.ID == MAT_MAP_VSCALE: + v_scale = read_float(temp_chunk) + + elif temp_chunk.ID == MAT_MAP_UOFFSET: + u_offset = read_float(temp_chunk) + elif temp_chunk.ID == MAT_MAP_VOFFSET: + v_offset = read_float(temp_chunk) + + elif temp_chunk.ID == MAT_MAP_TILING: + tiling = read_short(temp_chunk) + if tiling & 0x2: + extension = 'mirror' + elif tiling & 0x10: + extension = 'decal' + + elif temp_chunk.ID == MAT_MAP_ANG: + print("\nwarning: ignoring UV rotation") + skip_to_end(file, temp_chunk) new_chunk.bytes_read += temp_chunk.bytes_read # add the map to the material in the right channel if img: - add_texture_to_material(img, new_texture, contextMaterial, mapto) + add_texture_to_material(img, new_texture, (u_scale, v_scale), + (u_offset, v_offset), extension, contextMaterial, mapto) dirname = os.path.dirname(file.name)