# SPDX-FileCopyrightText: 2023-2024 Sebastian Schrand # 2017-2022 Jens M. Plonka # 2005-2018 Philippe Lagadec # # SPDX-License-Identifier: GPL-2.0-or-later # Import is based on using information from `olefile` IO source-code # and the FreeCAD Autodesk 3DS Max importer ImportMAX. # # `olefile` (formerly OleFileIO_PL) is copyright Philippe Lagadec. # (https://www.decalage.info) # # ImportMAX is copyright Jens M. Plonka. # (https://www.github.com/jmplonka/Importer3D) import io import os import re import sys import bpy import math import zlib import array import struct import mathutils from pathlib import Path from bpy_extras.image_utils import load_image from bpy_extras.node_shader_utils import PrincipledBSDFWrapper ################### # DATA STRUCTURES # ################### MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' WORD_CLSID = "00020900-0000-0000-C000-000000000046" MIN_FILE_SIZE = 1536 UNKNOWN_SIZE = 0x7FFFFFFF MAXFILE_SIZE = 0x7FFFFFFFFFFFFFFF MAXREGSECT = 0xFFFFFFFA # (-6) maximum SECT DIFSECT = 0xFFFFFFFC # (-4) denotes a DIFAT sector in a FAT FATSECT = 0xFFFFFFFD # (-3) denotes a FAT sector in a FAT ENDOFCHAIN = 0xFFFFFFFE # (-2) end of a virtual stream chain FREESECT = 0xFFFFFFFF # (-1) unallocated sector MAX_STREAM = 2 # element is a stream object ROOT_STORE = 5 # element is a root storage TYP_VALUE = {0x100, 0x2513} TYP_REFS = {0x1040, 0x2034, 0x2035} TYP_LINK = {0x1020, 0x1030, 0x1050, 0x1080, 0x3002, 0x4003} TYP_NAME = {0x340, 0x456, 0x962, 0x10A0, 0x1010, 0x1230, 0x4001} TYP_ARRAY = {0x96A, 0x96B, 0x96C, 0x2501, 0x2503, 0x2504, 0x2505, 0x2511} UNPACK_BOX_DATA = struct.Struct('= MIN_FILE_SIZE: header = filename[:len(MAGIC)] else: with open(filename, 'rb') as fp: header = fp.read(len(MAGIC)) if header == MAGIC: return True else: return False class MaxStream(io.BytesIO): """Returns an instance of the BytesIO class as read-only file object.""" def __init__(self, fp, sect, size, offset, sectorsize, fat, filesize): if size == UNKNOWN_SIZE: size = len(fat) * sectorsize nb_sectors = (size + (sectorsize - 1)) // sectorsize data = [] for i in range(nb_sectors): try: fp.seek(offset + sectorsize * sect) except: break sector_data = fp.read(sectorsize) data.append(sector_data) try: sect = fat[sect] & FREESECT except IndexError: break data = b"".join(data) if len(data) >= size: data = data[:size] self.size = size else: self.size = len(data) io.BytesIO.__init__(self, data) class MaxFileDirEntry: """Directory Entry for a stream or storage.""" STRUCT_DIRENTRY = '<64sHBBIII16sIQQIII' DIRENTRY_SIZE = 128 assert struct.calcsize(STRUCT_DIRENTRY) == DIRENTRY_SIZE def __init__(self, entry, sid, maxfile): self.sid = sid self.maxfile = maxfile self.kids = [] self.kids_dict = {} self.used = False ( self.name_raw, self.namelength, self.entry_type, self.color, self.sid_left, self.sid_right, self.sid_child, clsid, self.dwUserFlags, self.createTime, self.modifyTime, self.isectStart, self.sizeLow, self.sizeHigh ) = struct.unpack(MaxFileDirEntry.STRUCT_DIRENTRY, entry) if self.namelength > 64: self.namelength = 64 self.name_utf16 = self.name_raw[:(self.namelength - 2)] self.name = maxfile._decode_utf16_str(self.name_utf16) # print('DirEntry SID=%d: %s' % (self.sid, repr(self.name))) if maxfile.sectorsize == 512: self.size = self.sizeLow else: self.size = self.sizeLow + (int(self.sizeHigh) << 32) self.clsid = _clsid(clsid) self.is_minifat = False if self.entry_type in (ROOT_STORE, MAX_STREAM) and self.size > 0: if self.size < maxfile.minisectorcutoff \ and self.entry_type == MAX_STREAM: # only streams can be in MiniFAT self.is_minifat = True else: self.is_minifat = False maxfile._check_duplicate_stream(self.isectStart, self.is_minifat) self.sect_chain = None def build_sect_chain(self, maxfile): if self.sect_chain: return if self.entry_type not in (ROOT_STORE, MAX_STREAM) or self.size == 0: return self.sect_chain = list() if self.is_minifat and not maxfile.minifat: maxfile.loadminifat() next_sect = self.isectStart while next_sect != ENDOFCHAIN: self.sect_chain.append(next_sect) if self.is_minifat: next_sect = maxfile.minifat[next_sect] else: next_sect = maxfile.fat[next_sect] def build_storage_tree(self): if self.sid_child != FREESECT: self.append_kids(self.sid_child) self.kids.sort() def append_kids(self, child_sid): if child_sid == FREESECT: return else: child = self.maxfile._load_direntry(child_sid) if child.used: return child.used = True self.append_kids(child.sid_left) name_lower = child.name.lower() self.kids.append(child) self.kids_dict[name_lower] = child self.append_kids(child.sid_right) child.build_storage_tree() def __eq__(self, other): return self.name == other.name def __lt__(self, other): return self.name < other.name def __ne__(self, other): return not self.__eq__(other) def __le__(self, other): return self.__eq__(other) or self.__lt__(other) class ImportMaxFile: """Representing an interface for importing .max files.""" def __init__(self, filename=None): self._filesize = None self.byte_order = None self.directory_fp = None self.direntries = None self.dll_version = None self.fat = None self.first_difat_sector = None self.first_dir_sector = None self.first_mini_fat_sector = None self.fp = None self.header_clsid = None self.header_signature = None self.mini_sector_shift = None self.mini_sector_size = None self.mini_stream_cutoff_size = None self.minifat = None self.minifatsect = None self.minisectorcutoff = None self.minisectorsize = None self.ministream = None self.minor_version = None self.nb_sect = None self.num_difat_sectors = None self.num_dir_sectors = None self.num_fat_sectors = None self.num_mini_fat_sectors = None self.reserved1 = None self.reserved2 = None self.root = None self.sector_shift = None self.sector_size = None self.transaction_signature_number = None if filename: self.open(filename) def __enter__(self): return self def __exit__(self, *args): self.close() def _decode_utf16_str(self, utf16_str, errors='replace'): unicode_str = utf16_str.decode('UTF-16LE', errors) return unicode_str def open(self, filename): if hasattr(filename, 'read'): self.fp = filename elif isinstance(filename, bytes) and len(filename) >= MIN_FILE_SIZE: self.fp = io.BytesIO(filename) else: self.fp = open(filename, 'rb') filesize = 0 self.fp.seek(0, os.SEEK_END) try: filesize = self.fp.tell() finally: self.fp.seek(0) self._filesize = filesize self._used_streams_fat = [] self._used_streams_minifat = [] header = self.fp.read(512) fmt_header = '<8s16sHHHHHHLLLLLLLLLL' header_size = struct.calcsize(fmt_header) header1 = header[:header_size] ( self.header_signature, self.header_clsid, self.minor_version, self.dll_version, self.byte_order, self.sector_shift, self.mini_sector_shift, self.reserved1, self.reserved2, self.num_dir_sectors, self.num_fat_sectors, self.first_dir_sector, self.transaction_signature_number, self.mini_stream_cutoff_size, self.first_mini_fat_sector, self.num_mini_fat_sectors, self.first_difat_sector, self.num_difat_sectors ) = struct.unpack(fmt_header, header1) self.sector_size = 2**self.sector_shift self.mini_sector_size = 2**self.mini_sector_shift if self.mini_stream_cutoff_size != 0x1000: self.mini_stream_cutoff_size = 0x1000 self.nb_sect = ((filesize + self.sector_size - 1) // self.sector_size) - 1 # file clsid self.header_clsid = _clsid(header[8:24]) self.sectorsize = self.sector_size # i16(header, 30) self.minisectorsize = self.mini_sector_size # i16(header, 32) self.minisectorcutoff = self.mini_stream_cutoff_size # i32(header, 56) self._check_duplicate_stream(self.first_dir_sector) if self.num_mini_fat_sectors: self._check_duplicate_stream(self.first_mini_fat_sector) if self.num_difat_sectors: self._check_duplicate_stream(self.first_difat_sector) # Load file allocation tables self.loadfat(header) self.loaddirectory(self.first_dir_sector) self.minifatsect = self.first_mini_fat_sector def close(self): self.fp.close() def _check_duplicate_stream(self, first_sect, minifat=False): if minifat: used_streams = self._used_streams_minifat else: if first_sect in (DIFSECT, FATSECT, ENDOFCHAIN, FREESECT): return used_streams = self._used_streams_fat if first_sect in used_streams: pass else: used_streams.append(first_sect) def sector_array(self, sect): ary = array.array('I', sect) if sys.byteorder == 'big': ary.byteswap() return ary def loadfat_sect(self, sect): if isinstance(sect, array.array): fat1 = sect else: fat1 = self.sector_array(sect) isect = None for isect in fat1: isect = isect & FREESECT if isect == ENDOFCHAIN or isect == FREESECT: break sector = self.getsect(isect) nextfat = self.sector_array(sector) self.fat = self.fat + nextfat return isect def loadfat(self, header): sect = header[76:512] self.fat = array.array('I') self.loadfat_sect(sect) if self.num_difat_sectors != 0: nb_difat_sectors = (self.sectorsize // 4) - 1 nb_difat = (self.num_fat_sectors - 109 + nb_difat_sectors - 1) // nb_difat_sectors isect_difat = self.first_difat_sector for i in range(nb_difat): sector_difat = self.getsect(isect_difat) difat = self.sector_array(sector_difat) self.loadfat_sect(difat[:nb_difat_sectors]) isect_difat = difat[nb_difat_sectors] if len(self.fat) > self.nb_sect: self.fat = self.fat[:self.nb_sect] def loadminifat(self): stream_size = self.num_mini_fat_sectors * self.sector_size nb_minisectors = (self.root.size + self.mini_sector_size - 1) // self.mini_sector_size used_size = nb_minisectors * 4 sect = self._open(self.minifatsect, stream_size, force_FAT=True).read() self.minifat = self.sector_array(sect) self.minifat = self.minifat[:nb_minisectors] def getsect(self, sect): try: self.fp.seek(self.sectorsize * (sect + 1)) except: print('IndexError: Sector index out of range') sector = self.fp.read(self.sectorsize) return sector def loaddirectory(self, sect): self.directory_fp = self._open(sect, force_FAT=True) max_entries = self.directory_fp.size // 128 self.direntries = [None] * max_entries root_entry = self._load_direntry(0) self.root = self.direntries[0] self.root.build_storage_tree() def _load_direntry(self, sid): if self.direntries[sid] is not None: return self.direntries[sid] self.directory_fp.seek(sid * 128) entry = self.directory_fp.read(128) self.direntries[sid] = MaxFileDirEntry(entry, sid, self) return self.direntries[sid] def _open(self, start, size=UNKNOWN_SIZE, force_FAT=False): if size < self.minisectorcutoff and not force_FAT: if not self.ministream: self.loadminifat() size_ministream = self.root.size self.ministream = self._open(self.root.isectStart, size_ministream, force_FAT=True) return MaxStream(fp=self.ministream, sect=start, size=size, offset=0, sectorsize=self.minisectorsize, fat=self.minifat, filesize=self.ministream.size) else: return MaxStream(fp=self.fp, sect=start, size=size, offset=self.sectorsize, sectorsize=self.sectorsize, fat=self.fat, filesize=self._filesize) def _find(self, filename): if isinstance(filename, str): filename = filename.split('/') node = self.root for name in filename: for kid in node.kids: if kid.name.lower() == name.lower(): break node = kid return node.sid def openstream(self, filename): sid = self._find(filename) entry = self.direntries[sid] return self._open(entry.isectStart, entry.size) ################### # DATA PROCESSING # ################### class MaxChunk(object): """Representing a chunk of a .max file.""" __slots__ = "superid", "types", "level", "number", "size", "data" def __init__(self, superid, types, level, number, size, data=None): self.superid = superid self.types = types self.data = data self.number = 0 self.level = 0 self.size = 0 def __str__(self): return "%s[%4x]%04X:%s" % ("" * self.level, self.number, self.types, self.data) class ByteArrayChunk(MaxChunk): """A byte array of a .max chunk.""" def __init__(self, superid, types, level, number, size, data): MaxChunk.__init__(self, superid, types, level, number, size, data) self.superid = superid self.children = [] def get_first(self, types): return None def set(self, data, fmt, start, end): try: self.data = struct.unpack(fmt, data[start:end]) except Exception as exc: self.data = data # print('\tStructError: %s' % exc) def set_string(self, data): try: self.data = data.decode('UTF-16LE') except: self.data = data.decode('UTF-8', 'replace') finally: self.data = data.decode('UTF-16LE', 'ignore') def set_meta_data(self, data): metadict = {} matkey = False try: mdatazip = list(zip(*[iter(data.split(b'\x00\x00\x00'))]*2)) metadata = list(filter(lambda tpl: b'' not in tpl, mdatazip)) for mdata in metadata: header = mdata[0] imgkey = header[-1] mtitle = b''.join(mdata[1].split(b'\x00')).decode('UTF-8', 'ignore') if (len(header) > ROOT_STORE): size = len(header[:-ROOT_STORE]) head = struct.unpack('<' + 'IH' * int(size / 6), header[:size]) meta = get_longs(header, size, len(header[size:]) // 4)[0] print(" metadata: %s '%s'...%s" % (hex(head[-1]), imgkey, mtitle)) metakey = meta[0] metadict[metakey] = [(head[0], head[1])] elif metadict and metakey: metadict[metakey].insert(-imgkey, (imgkey, mtitle)) print(" imgpath: %s -> '%s: %s'" % (hex(metakey), imgkey, mtitle)) except: self.data = data # print('\tStructError: %s' % exc) finally: self.data = metadict def set_data(self, data): if (self.types in TYP_NAME): self.set_string(data) elif (self.types in TYP_LINK): self.set(data, ' ROOT_STORE and level == 0): root, step = get_short(data, 0) long, step = get_long(data, step) print(" reading '%s'..." % self.name, len(data)) if (root == 0x8B1F): long, step = get_long(data, step) if (long in (0xB000000, 0xA040000, 0x8000001E)): data = zlib.decompress(data, zlib.MAX_WBITS | 32) elif (superid in (0xA, 0xB)): chunk = primReader(superid, root, level, 1, len(data), data) chunk.set_meta_data(data) return [chunk] while offset < len(data): old = offset offset, chunk = self.get_next_chunk(superid, data, offset, level, len(chunks), conReader, primReader) chunks.append(chunk) return chunks def get_next_chunk(self, superid, data, offset, level, number, conReader, primReader): header = 6 typ, siz = struct.unpack(" 0): pointlist.append(pt) return pointlist def get_point(floatval, default=0.0): uid = get_guid(floatval) if (uid == 0x2007): # Bezier-Float flv = floatval.get_first(0x7127) if (flv): try: return flv.get_first(0x2501).data[0] except: print("SyntaxError: %s - assuming 0.0!\n" % (floatval)) return default if (uid == FLOAT_POINT): # Float Wire flv = get_references(floatval)[0] return get_point(flv) else: return default def get_point_3d(chunk, default=0.0): floats = [] if (chunk): refs = get_references(chunk) for fl in refs: flt = get_point(fl, default) if (fl is not None): floats.append(flt) return floats def get_point_array(values): verts = [] if len(values) >= 4: count, offset = get_long(values, 0) while (count > 0): floats, offset = get_floats(values, offset, 3) verts.extend(floats) count -= 1 return verts def get_mesh_polys(data): count, offset = get_long(data, 0) polygons = [] while count > 0: poly, offset = get_longs(data, offset, 3) offset += 8 polygons.append(poly) count -= 1 return polygons def get_face_chunks(chunk): faceflags = get_long(chunk.data, 0) for cnk in chunk.children: if (cnk.types == 0x0110): size, step = get_long(cnk.data, 0) face, step = get_longs(cnk.data, step, size) return face def get_poly_data(chunk): offset = 0 polylist = [] data = chunk.data while (offset < len(data)): count, offset = get_long(data, offset) points, offset = get_longs(data, offset, count) polylist.append(points) return polylist def get_poly_loops(chunk): looplist = [] data = chunk.data counts, offset = get_long(data) while (offset < len(data)): count, offset = get_long(data, offset) point, offset = get_longs(data, offset, 2) (lp, sp), offset = get_longs(data, offset, 2) flags, offset = get_long(data, offset) loops = (*point, lp) if sp == FREESECT else point + (lp, sp) looplist.append(loops) return looplist def get_uvw_coords(chunk): offset = 0 uvindex = [] data = chunk.data while (offset < len(data)): idx, offset = get_long(data, offset) uvindex.append(idx) cnt = uvindex.pop(0) facelist = list(zip(*[iter(uvindex)]*3)) uvindex.clear() return facelist def get_tri_data(chunk): offset = 0 vindex = [] data = chunk.data head, offset = get_long(data) while (offset < head): idx, offset = get_long(data, offset) vindex.append(idx) triangles = list(zip(*[iter(vindex)]*3)) return triangles def get_property(properties, idx): for child in properties.children: if (child.types & 0x100E): if (get_short(child.data, 0)[0] == idx): return child return None def get_bitmap(chunk): if (chunk is not None): pathstring = matlib = None parameters = get_references(chunk) if (len(parameters) >= 2): custom = parameters[1].get_first(0x3) if (custom is not None): pathchunk = custom.get_first(0x1230) pathlink = custom.get_first(0x1260) if (pathchunk and pathchunk.data): pathstring = pathchunk.data elif (pathlink and pathlink.children): matlib = pathlink.children[0] if (matlib and matlib.data): idsize = len(matlib.data[:-4]) metaidx = get_longs(matlib.data, idsize, len(matlib.data[idsize:]) // 4)[0] pathstring = get_metadata(metaidx[0]) return pathstring return None def get_color(colors, idx): prop = get_property(colors, idx) if (prop is not None): siz = len(prop.data) - 12 col, offset = get_floats(prop.data, siz, 3) return (col[0], col[1], col[2]) return None def get_value(value, idx): prop = get_property(value, idx) if (prop is not None): siz = len(prop.data) - 4 val, offset = get_float(prop.data, siz) return val return None def get_parameter(values, fmt): if (fmt == 0x1): siz = len(values.data) - 12 para, offset = get_floats(values.data, siz, 3) else: siz = len(values.data) - 4 para, offset = get_float(values.data, siz) return para def get_standard_material(refs): material = None try: if (len(refs) > 2): texmap = refs[1] colors = refs[2] material = Material() parameter = get_references(colors)[0] bitmap = get_bitmap(get_reference(texmap).get(3)) shinmap = get_bitmap(get_reference(texmap).get(17)) transmap = get_bitmap(get_reference(texmap).get(13)) material.set('ambient', get_color(parameter, 0x00)) material.set('diffuse', get_color(parameter, 0x01)) material.set('specular', get_color(parameter, 0x02)) material.set('shinines', get_value(parameter, 0x0B)) material.set('emissive', get_color(parameter, 0x08)) parablock = refs[4] # ParameterBlock2 material.set('glossines', get_value(parablock, 0x02)) material.set('metallic', get_value(parablock, 0x05)) material.set('refraction', get_value(parablock, 0x06)) material.set('opacity', get_value(parablock, 0x01)) if (bitmap is not None): material.set('bitmap', Path(bitmap).name) if (shinmap is not None): material.set('shinmap', Path(shinmap).name) if (transmap is not None): material.set('transmap', Path(transmap).name) except Exception as exc: print("\t'StandardMtl' Error:", exc) return material def get_vray_material(vray): material = Material() try: parameter = vray.get(1) bitmap = get_bitmap(vray.get(7)) shinmap = get_bitmap(vray.get(8)) glossmap = get_bitmap(vray.get(11)) transmap = get_bitmap(vray.get(19)) normal = get_references(vray.get(10)) material.set('diffuse', get_color(parameter, 0x01)) material.set('specular', get_color(parameter, 0x02)) material.set('shinines', get_value(parameter, 0x03)) material.set('emissive', get_color(parameter, 0x33)) material.set('glossines', get_value(parameter, 0x06)) material.set('metallic', get_value(parameter, 0x19)) material.set('refraction', get_value(parameter, 0x09)) if (bitmap is not None): material.set('bitmap', Path(bitmap).name) if (shinmap is not None): material.set('shinmap', Path(shinmap).name) if (glossmap is not None): material.set('glossmap', Path(glossmap).name) if (transmap is not None): material.set('transmap', Path(transmap).name) if (normal and len(normal) > 0): material.set('strength', get_value(parameter, 0x06)) refs = get_references(normal[0]) normalmap = get_bitmap(refs[0]) if (normalmap is not None): material.set('normalmap', Path(normalmap).name) except Exception as exc: print("\t'VrayMtl' Error:", exc) return material def get_corona_material(mtl): material = Material() try: material = Material() corona = mtl[0].children parameter = get_reference(mtl[0]) bitmap = get_bitmap(parameter.get(0)) shinmap = get_bitmap(parameter.get(1)) glossmap = get_bitmap(parameter.get(2)) transmap = get_bitmap(parameter.get(5)) normal = get_references(parameter.get(6)) material.set('diffuse', get_parameter(corona[0x03], 1)) material.set('specular', get_parameter(corona[0x04], 1)) material.set('shinines', get_parameter(corona[0x3E], 2)) material.set('emissive', get_parameter(corona[0x08], 1)) material.set('glossines', get_parameter(corona[0x09], 2)) material.set('metallic', get_parameter(corona[0x0D], 2)) material.set('refraction', get_parameter(corona[0x40], 2)) material.set('opacity', 1.0 - get_parameter(corona[0x0B], 2)) if (bitmap is not None): material.set('bitmap', Path(bitmap).name) if (shinmap is not None): material.set('shinmap', Path(shinmap).name) if (glossmap is not None): material.set('glossmap', Path(glossmap).name) if (transmap is not None): material.set('transmap', Path(transmap).name) if (normal and len(normal) > 0): values = normal[0].children refs = get_references(normal[0]) normalmap = get_bitmap(refs[0]) material.set('strength', get_parameter(values[0x03], 2)) if (normalmap is not None): material.set('normalmap', Path(normalmap).name) except Exception as exc: print("\t'CoronaMtl' Error:", exc) return material def get_arch_material(ad): material = Material() try: material.set('diffuse', get_color(ad, 0x1A)) material.set('specular', get_color(ad, 0x05)) material.set('shinines', get_value(ad, 0x0B)) except: pass return material def adjust_material(filename, search, obj, mat): dirname = os.path.dirname(filename) material = None if (mat is not None): uid = get_guid(mat) if (uid == 0x0002): # Standard mtl_id = mat.get_first(0x4000) refs = get_references(mat) material = get_standard_material(refs) elif (uid == VRAY_MTL): # VRayMtl mtl_id = mat.get_first(0x5431) refs = get_reference(mat) material = get_vray_material(refs) elif (uid == CORO_MTL): # CoronaMtl mtl_id = mat.get_first(0x0FA0) refs = get_references(mat) material = get_corona_material(refs) elif (uid == ARCH_MTL): # Arch refs = get_references(mat) material = get_arch_material(refs[0]) elif (uid == 0x0200): # Multi/Sub-Object refs = get_references(mat) for ref in refs: if (ref is not None): material = adjust_material(filename, search, obj, ref) if (obj is not None) and (material is not None): matname = mtl_id.children[0].data if mtl_id else get_cls_name(mat) objMaterial = bpy.data.materials.get(matname) if objMaterial is None: objMaterial = bpy.data.materials.new(matname) obj.data.materials.append(objMaterial) shader = PrincipledBSDFWrapper(objMaterial, is_readonly=False, use_nodes=True) shader.base_color = objMaterial.diffuse_color[:3] = material.get('diffuse', (0.8, 0.8, 0.8)) shader.specular_tint = objMaterial.specular_color[:3] = material.get('specular', (1, 1, 1)) shader.specular = objMaterial.specular_intensity = material.get('glossines', 0.5) shader.roughness = objMaterial.roughness = 1.0 - material.get('shinines', 0.6) shader.alpha = objMaterial.diffuse_color[3] = material.get('opacity', 1.0) shader.metallic = objMaterial.metallic = material.get('metallic', 0.0) shader.emission_color = material.get('emissive', (0.0, 0.0, 0.0)) shader.ior = material.get('refraction', 1.45) texname = material.get('bitmap', None) shinmap = material.get('shinmap', None) glossmap = material.get('glossmap', None) transmap = material.get('transmap', None) normalmap = material.get('normalmap', None) if (texname is not None): image = load_image(str(texname), dirname, place_holder=False, recursive=search, check_existing=True) if (image is not None): shader.base_color_texture.image = image if (shinmap is not None): image = load_image(str(shinmap), dirname, place_holder=False, recursive=search, check_existing=True) if (image is not None): shader.roughness_texture.image = image if (glossmap is not None): image = load_image(str(glossmap), dirname, place_holder=False, recursive=search, check_existing=True) if (image is not None): shader.specular_texture.image = image if (normalmap is not None): shader.normalmap_strength = material.get('strength', 0.8) image = load_image(str(normalmap), dirname, place_holder=False, recursive=search, check_existing=True) if (image is not None): shader.normalmap_texture.image = image if (transmap is not None): if (transmap == texname and shader.node_principled_bsdf.inputs[0].is_linked): imgwrap = shader.base_color_texture.node_image imgwrap.image.alpha_mode = 'CHANNEL_PACKED' shader.material.node_tree.links.new(imgwrap.outputs[1], shader.node_principled_bsdf.inputs[4]) else: image = load_image(str(transmap), dirname, place_holder=False, recursive=search, check_existing=True) if (image is not None): shader.alpha_texture.image = image if (transmap or shader.node_principled_bsdf.inputs[4].default_value < 1.0): shader.material.blend_method = 'HASHED' def get_bezier_floats(pos): refs = get_references(pos) floats = get_point_3d(pos) if any(rf.get_first(0x2501) for rf in refs): floats.clear() for ref in refs: floats.append(ref.get_first(0x2501).data[0]) return floats def get_position(pos): position = mathutils.Vector() if (pos): uid = get_guid(pos) if (uid == MATRIX_POS): # Position XYZ position = mathutils.Vector(get_bezier_floats(pos)) elif (uid == 0x2008): # Bezier Position position = mathutils.Vector(pos.get_first(0x2503).data) elif (uid == 0x442312): # TCB Position position = mathutils.Vector(pos.get_first(0x2503).data) elif (uid == 0x4B4B1002): # Position List refs = get_references(pos) if (len(refs) >= 3): return get_position(refs[0]) return position def get_rotation(pos): rotation = mathutils.Quaternion() if (pos): uid = get_guid(pos) if (uid == 0x2012): # Euler XYZ rot = get_bezier_floats(pos) rotation = mathutils.Euler((rot[0], rot[1], rot[2])).to_quaternion() elif (uid == 0x442313): # TCB Rotation rot = pos.get_first(0x2504).data rotation = mathutils.Quaternion((rot[3], rot[2], rot[1], rot[0])) elif (uid == MATRIX_ROT): # Rotation Wire return get_rotation(get_references(pos)[0]) elif (uid == 0x4B4B1003): # Rotation List refs = get_references(pos) if (len(refs) > 3): return get_rotation(refs[0]) return rotation def get_scale(pos): scale = mathutils.Vector((1.0, 1.0, 1.0)) if (pos): uid = get_guid(pos) if (uid == MATRIX_SCL): # ScaleXYZ pos = get_bezier_floats(pos) elif (uid == 0x2010): # Bezier Scale scl = pos.get_first(0x2501) if (scl is None): scl = pos.get_first(0x2505) pos = scl.data elif (uid == 0x442315): # TCB Zoom scl = pos.get_first(0x2501) if (scl is None): scl = pos.get_first(0x2505) pos = scl.data elif (uid == 0x4B4B1002): # Scale List refs = get_references(pos) if (len(refs) >= 3): return get_scale(refs[0]) scale = mathutils.Vector(pos[:3]) return scale def create_matrix(prc): uid = get_guid(prc) mtx = mathutils.Matrix.Identity(4) if (uid == 0x2005): # Position/Rotation/Scale pos = get_position(get_references(prc)[0]) rot = get_rotation(get_references(prc)[1]) scl = get_scale(get_references(prc)[2]) mtx = mathutils.Matrix.LocRotScale(pos, rot, scl) elif (uid == 0x9154): # BipSlave Control biped = get_references(prc)[-1] if biped and (get_guid(biped) == BIPED_ANIM): ref = get_references(biped) scl = get_scale(get_references(ref[1])[0]) rot = get_rotation(get_references(ref[2])[0]) pos = get_position(get_references(ref[3])[0]) mtx = mathutils.Matrix.LocRotScale(pos, rot, scl) return mtx def get_matrix_mesh_material(node): refs = get_reference(node) if (refs): prs = refs.get(0, None) msh = refs.get(1, None) mat = refs.get(3, None) lyr = refs.get(6, None) else: refs = get_references(node) prs = refs[0] msh = refs[1] mat = refs[3] lyr = refs[6] if len(refs) > 6 else None return prs, msh, mat, lyr def adjust_matrix(obj, node): mtx = create_matrix(node) obj.matrix_world = mtx @ obj.matrix_world.copy() return mtx def draw_shape(name, mesh, faces): data = [] loopstart = [] looplines = loop = 0 nb_faces = len(faces) for fid in range(nb_faces): polyface = faces[fid] looplines += len(polyface) shape = bpy.data.meshes.new(name) shape.vertices.add(len(mesh.verts) // 3) shape.loops.add(looplines) shape.polygons.add(nb_faces) shape.vertices.foreach_set("co", mesh.verts) for vtx in faces: loopstart.append(loop) data.extend(vtx) loop += len(vtx) shape.polygons.foreach_set("loop_start", loopstart) shape.loops.foreach_set("vertex_index", data) return shape def draw_map(shape, uvcoords, uvwids): shape.uv_layers.new(do_init=False) coords = [co for i, co in enumerate(uvcoords) if i % 3 in (0, 1)] uvcord = list(zip(coords[0::2], coords[1::2])) uvloops = tuple(uv for uvws in uvwids for uvid in uvws for uv in uvcord[uvid]) try: shape.uv_layers.active.data.foreach_set("uv", uvloops) except Exception as exc: print('\tArrayLengthMismatchError: %s' % exc) return shape def create_shape(context, settings, node, mesh, mat): filename, obtypes, search = settings name = node.get_first(0x0962) if name is not None: name = name.data meshobject = draw_shape(name, mesh, mesh.faces) if ('UV' in obtypes and mesh.maps): for idx, uvm in enumerate(mesh.maps): select = idx if len(mesh.cords[idx]) <= len(mesh.verts) else 0 meshobject = draw_map(meshobject, mesh.cords[select], mesh.uvids[select]) meshobject.validate() meshobject.update() obj = bpy.data.objects.new(name, meshobject) context.view_layer.active_layer_collection.collection.objects.link(obj) if ('MATERIAL' in obtypes): adjust_material(filename, search, obj, mat) if (len(mesh.mats) > 0): obj.data.polygons.foreach_set("material_index", mesh.mats) object_list.append(obj) return object_list def create_dummy_object(context, node, uid): dummy = bpy.data.objects.new(get_node_name(node), None) dummy.empty_display_type = 'SINGLE_ARROW' if uid == BIPED_OBJ else 'PLAIN_AXES' context.view_layer.active_layer_collection.collection.objects.link(dummy) return dummy def create_editable_poly(context, settings, node, msh, mat): polychunk = msh.get_first(0x08FE) created = [] if (polychunk): mesh = Mesh3d() for child in polychunk.children: if isinstance(child.data, tuple): created = create_shape(context, settings, node, mesh, mat) elif (child.types == 0x0100): mesh.verts = calc_point(child.data) elif (child.types == 0x0108): mesh.polys = get_poly_loops(child) elif (child.types == 0x010A): mesh.tris = calc_point_float(child.data) elif (child.types == 0x0118): mesh.faces.append(get_face_chunks(child)) elif (child.types == 0x011A): mesh.points = calc_point_3d(child) elif (child.types == 0x0124): mesh.maps.append(get_long(child.data, 0)[0]) elif (child.types == 0x0128): mesh.cords.append(calc_point_float(child.data)) elif (child.types == 0x012B): mesh.uvids.append(get_poly_data(child)) elif (child.types == 0x0310): mesh.polys = get_poly_data(child) if (mesh.points is not None): mesh.set(mesh.points) created += create_shape(context, settings, node, mesh, mat) elif (mesh.faces is not None): created += create_shape(context, settings, node, mesh, mat) elif (mesh.tris is not None) and 'UV' not in obtypes: created += create_shape(context, settings, node, mesh, mat) return created def create_editable_mesh(context, settings, node, msh, mat): meshchunk = msh.get_first(0x08FE) created = [] if (meshchunk): editmesh = Mesh3d() vertex_chunk = meshchunk.get_first(0x0914) faceid_chunk = meshchunk.get_first(0x0912) if (vertex_chunk and faceid_chunk): editmesh.verts = get_point_array(vertex_chunk.data) editmesh.faces = get_mesh_polys(faceid_chunk.data) for chunk in meshchunk.children: if (chunk.types == 0x0959): editmesh.maps.append(get_long(chunk.data, 0)[0]) elif (chunk.types == 0x2394): editmesh.cords.append(get_point_array(chunk.data)) elif (chunk.types == 0x2396): editmesh.uvids.append(get_uvw_coords(chunk)) created += create_shape(context, settings, node, editmesh, mat) return created def create_shell(context, settings, node, shell, mat, mtx): refs = get_references(shell) created = [] if refs: msh = refs[-1] created, uid = create_mesh(context, settings, node, msh, mat, mtx) return created def create_plane(context, node, plane, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Plane '%s' ..." % name) parablock = get_references(plane)[0] try: length = get_float(parablock.children[1].data, 15)[0] width = get_float(parablock.children[2].data, 15)[0] except: length = UNPACK_BOX_DATA(parablock.children[1].data)[6] width = UNPACK_BOX_DATA(parablock.children[2].data)[6] bpy.ops.mesh.primitive_plane_add(size=1.0, scale=(width, length, 0.0)) obj = context.selected_objects[0] if name is not None: obj.name = str(name) adjust_matrix(obj, mtx) plane.geometry = obj created.append(obj) return created def create_box(context, node, box, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Box '%s' ..." % name) parablock = get_references(box)[0] try: length = get_float(parablock.children[1].data, 15)[0] width = get_float(parablock.children[2].data, 15)[0] depth = get_float(parablock.children[3].data, 15)[0] except: length = UNPACK_BOX_DATA(parablock.children[1].data)[6] width = UNPACK_BOX_DATA(parablock.children[2].data)[6] depth = UNPACK_BOX_DATA(parablock.children[3].data)[6] height = -depth if (depth < 0) else depth bpy.ops.mesh.primitive_cube_add(size=1.0, scale=(width, length, height)) obj = context.selected_objects[0] if name is not None: obj.name = name adjust_matrix(obj, mtx) box.geometry = obj created.append(obj) return created def create_sphere(context, node, sphere, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Sphere '%s' ..." % name) parablock = get_references(sphere)[0] try: rd = get_float(parablock.children[1].data, 15)[0] except: rd = UNPACK_BOX_DATA(parablock.children[1].data)[6] bpy.ops.mesh.primitive_uv_sphere_add(radius=rd) obj = context.selected_objects[0] if name is not None: obj.name = name adjust_matrix(obj, mtx) sphere.geometry = obj created.append(obj) return created def create_torus(context, node, torus, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Torus '%s' ..." % name) parablock = get_references(torus)[0] try: rd1 = get_float(parablock.children[1].data, 15)[0] rd2 = get_float(parablock.children[2].data, 15)[0] except: rd1 = UNPACK_BOX_DATA(parablock.children[1].data)[6] rd2 = UNPACK_BOX_DATA(parablock.children[2].data)[6] bpy.ops.mesh.primitive_torus_add(major_radius=rd1, minor_radius=rd2) obj = context.selected_objects[0] if name is not None: obj.name = str(name) adjust_matrix(obj, mtx) torus.geometry = obj created.append(obj) return created def create_cylinder(context, node, cylinder, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Cylinder '%s' ..." % name) parablock = get_references(cylinder)[0] try: rd = get_float(parablock.children[1].data, 15)[0] hg = get_float(parablock.children[2].data, 15)[0] except: rd = UNPACK_BOX_DATA(parablock.children[1].data)[6] hg = UNPACK_BOX_DATA(parablock.children[2].data)[6] rad = -rd if (rd < 0) else rd height = -hg if (hg < 0) else hg bpy.ops.mesh.primitive_cylinder_add(radius=rad, depth=height) obj = context.selected_objects[0] if name is not None: obj.name = name adjust_matrix(obj, mtx) cylinder.geometry = obj created.append(obj) return created def create_cone(context, node, cone, mat, mtx): created = [] name = node.get_first(0x0962) if name is not None: name = name.data print("\tbuilding Cone '%s' ..." % name) parablock = get_references(cone)[0] try: rd1 = get_float(parablock.children[1].data, 15)[0] rd2 = get_float(parablock.children[2].data, 15)[0] hgt = get_float(parablock.children[3].data, 15)[0] except: rd1 = UNPACK_BOX_DATA(parablock.children[1].data)[6] rd2 = UNPACK_BOX_DATA(parablock.children[2].data)[6] hgt = UNPACK_BOX_DATA(parablock.children[3].data)[6] height = -hgt if (hgt < 0) else hgt bpy.ops.mesh.primitive_cone_add(radius1=rd1, radius2=rd2, depth=height) obj = context.selected_objects[0] if name is not None: obj.name = str(name) adjust_matrix(obj, mtx) cone.geometry = obj created.append(obj) return created def create_skipable(context, node, skip): name = node.get_first(0x0962) if name is not None: name = name.data print(" skipping %s '%s'... " % (skip, name)) return [] def create_mesh(context, settings, node, msh, mat, mtx): created = [] object_list.clear() uid = get_guid(msh) if (uid == EDIT_MESH): created = create_editable_mesh(context, settings, node, msh, mat) elif (uid in {EDIT_POLY, POLY_MESH}): created = create_editable_poly(context, settings, node, msh, mat) elif (uid == 0x010): created = create_box(context, node, msh, mat, mtx) elif (uid == 0x011): created = create_sphere(context, node, msh, mat, mtx) elif (uid == 0x012): created = create_cylinder(context, node, msh, mat, mtx) elif (uid == 0x020): created = create_torus(context, node, msh, mat, mtx) elif (uid == CONE): created = create_cone(context, node, msh, mat, mtx) elif (uid == PLANE): created = create_plane(context, node, msh, mat, mtx) elif (uid in {0x2032, 0x2033}): created = create_shell(context, settings, node, msh, mat, mtx) elif (uid == DUMMY and 'EMPTY' in settings[1]): created = [create_dummy_object(context, node, uid)] elif (uid == BIPED_OBJ and 'ARMATURE' in settings[1]): created = [create_dummy_object(context, node, uid)] else: skip = SKIPPABLE.get(uid) if (skip is not None): created = create_skipable(context, node, skip) return created, uid def create_object(context, settings, node, transform): parent = get_node_parent(node) nodename = get_node_name(node) parentname = get_node_name(parent) prs, msh, mat, lyr = get_matrix_mesh_material(node) created, uid = create_mesh(context, settings, node, msh, mat, prs) created = [idx for ob, idx in enumerate(created) if idx not in created[:ob]] for obj in created: if obj.name != nodename: parent_dict[obj.name] = parentname if (transform and obj.type == 'MESH'): nodeloca = node.get_first(0x96A) noderota = node.get_first(0x96B) nodesize = node.get_first(0x96C) quats = noderota.data if noderota else (0.0, 0.0, 0.0, 1.0) pivot = mathutils.Vector(nodeloca.data if nodeloca else (0.0, 0.0, 0.0)) angle = mathutils.Quaternion((quats[3], quats[2], quats[1], quats[0])) scale = mathutils.Vector(nodesize.data[:3] if nodesize else (1.0, 1.0, 1.0)) p_mtx = mathutils.Matrix.LocRotScale(pivot, angle, scale) obj.data.transform(p_mtx) matrix_dict[nodename] = create_matrix(prs) parent_dict[nodename] = parentname return nodename, created def make_scene(context, settings, mscale, transform, parent): imported = [] for chunk in parent.children: if isinstance(chunk, SceneChunk) and get_guid(chunk) == 0x1 and get_super_id(chunk) == 0x1: try: imported.append(create_object(context, settings, chunk, transform)) except Exception as exc: print("\tImportError: %s %s" % (exc, chunk), get_node_name(chunk)) # Apply matrix and assign parents to objects objects = dict(imported) for idx, objs in objects.items(): pt_name = parent_dict.get(idx) parents = objects.get(pt_name) obj_mtx = matrix_dict.get(idx, mathutils.Matrix()) prt_mtx = matrix_dict.get(pt_name, mathutils.Matrix()) for obj in objs: if parents: try: obj.parent = parents[0] except TypeError as te: print("\tTypeError: %s '%s'" % (te, pt_name)) if obj_mtx: if obj.parent and obj.parent.empty_display_type != 'SINGLE_ARROW': trans_mtx = obj.parent.matrix_world @ obj_mtx else: trans_mtx = prt_mtx @ obj_mtx if transform: obj.matrix_world = trans_mtx obj.matrix_world = mscale @ obj.matrix_world def read_scene(context, maxfile, settings, mscale, transform): global SCENE_LIST, META_DATA metasid = max(entry.sid for entry in maxfile.direntries if entry is not None) SCENE_LIST = read_chunks(maxfile, 'Scene', SceneChunk) META_DATA = read_chunks(maxfile, maxfile.direntries[metasid].name, superId=metasid) if metasid >= 0xA else [] make_scene(context, settings, mscale, transform, SCENE_LIST[0]) # print('Directory', maxfile.direntries[0].kids_dict.keys()) def read(context, filename, mscale, obtypes, search, transform): if (is_maxfile(filename)): settings = filename, obtypes, search maxfile = ImportMaxFile(filename) read_class_data(maxfile, filename) read_config(maxfile, filename) read_directory(maxfile, filename) read_class_directory(maxfile, filename) read_video_postqueue(maxfile, filename) read_scene(context, maxfile, settings, mscale, transform) else: print("File seems to be no 3D Studio Max file!") def load(operator, context, files=[], directory="", filepath="", scale_objects=1.0, use_collection=False, use_image_search=True, object_filter=None, use_apply_matrix=True, global_matrix=None): object_dict.clear() parent_dict.clear() matrix_dict.clear() context.window.cursor_set('WAIT') mscale = mathutils.Matrix.Scale(scale_objects, 4) if global_matrix is not None: mscale = global_matrix @ mscale if not len(files): files = [Path(filepath)] directory = Path(filepath).parent if not object_filter: object_filter = {'MATERIAL', 'UV', 'EMPTY'} default_layer = context.view_layer.active_layer_collection.collection for fl in files: if use_collection: collection = bpy.data.collections.new(Path(fl.name).stem) context.scene.collection.children.link(collection) context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[collection.name] read(context, os.path.join(directory, fl.name), mscale, obtypes=object_filter, search=use_image_search, transform=use_apply_matrix) object_dict.clear() parent_dict.clear() matrix_dict.clear() active = context.view_layer.layer_collection.children.get(default_layer.name) if active is not None: context.view_layer.active_layer_collection = active context.window.cursor_set('DEFAULT') return {'FINISHED'}