""" Python Library for reading and writing Roblox mesh files. Written by something.else on 21/9/2023 Supported meshes up to version 5. Mesh Format documentation by MaximumADHD: https://devforum.roblox.com/t/roblox-mesh-format/326114 """ import struct from dataclasses import dataclass import sys def debug_print( message : str ) -> None: if __name__ == "__main__": print(message) """ struct FileMeshVertexNormalTexture3d { float vx,vy,vz; float nx,ny,nz; float tu,tv; signed char tx, ty, tz, ts; unsigned char r, g, b, a; }; """ @dataclass class FileMeshVertexNormalTexture3d: vx: float vy: float vz: float nx: float ny: float nz: float tu: float tv: float tx: int ty: int tz: int ts: int r: int g: int b: int a: int def read_data( self, data : str ): if len(data) < 40: raise Exception(f"FileMeshVertexNormalTexture3d.read_data: data is too short ({len(data)} bytes)") self.vx = struct.unpack(" bytearray: return bytearray( struct.pack(" bytearray: return bytearray( struct.pack(" bytearray: return bytearray( self.a.to_bytes(4, "little") + self.b.to_bytes(4, "little") + self.c.to_bytes(4, "little") ) """ struct FileMeshHeader { unsigned short cbSize; unsigned char cbVerticesStride; unsigned char cbFaceStride; unsigned int num_vertices; unsigned int num_faces; }; """ @dataclass class FileMeshHeader: cbSize: int cbVerticesStride: int cbFaceStride: int num_vertices: int num_faces: int def read_data( self, data : str ): if len(data) < 12: raise Exception(f"FileMeshHeader.read_data: data is too short ({len(data)} bytes)") self.cbSize = int.from_bytes(data[0:2], "little") if self.cbSize != 12: raise Exception(f"FileMeshHeader.read_data: invalid cbSize ({self.cbSize})") self.cbVerticesStride = int.from_bytes(data[2:3], "little") self.cbFaceStride = int.from_bytes(data[3:4], "little") self.num_vertices = int.from_bytes(data[4:8], "little") self.num_faces = int.from_bytes(data[8:12], "little") def export_data(self) -> bytearray: return bytearray( self.cbSize.to_bytes(2, "little") + self.cbVerticesStride.to_bytes(1, "little") + self.cbFaceStride.to_bytes(1, "little") + self.num_vertices.to_bytes(4, "little") + self.num_faces.to_bytes(4, "little") ) def __str__(self) -> str: return f"FileMeshHeader(cbSize={self.cbSize}, cbVerticesStride={self.cbVerticesStride}, cbFaceStride={self.cbFaceStride}, num_vertices={self.num_vertices}, num_faces={self.num_faces})" def __repr__(self) -> str: return str(self) """ struct FileMeshHeaderV3 { unsigned short cbSize; unsigned char cbVerticesStride; unsigned char cbFaceStride; unsigned short sizeof_LOD; unsigned short numLODs; unsigned int num_vertices; unsigned int num_faces; }; """ @dataclass class FileMeshHeaderV3: cbSize: int cbVerticesStride: int cbFaceStride: int sizeof_LOD: int numLODs: int num_vertices: int num_faces: int def read_data( self, data : str ): if len(data) < 16: raise Exception(f"FileMeshHeaderV3.read_data: data is too short ({len(data)} bytes)") self.cbSize = int.from_bytes(data[0:2], "little") if self.cbSize != 16: raise Exception(f"FileMeshHeaderV3.read_data: invalid cbSize ({self.cbSize})") self.cbVerticesStride = int.from_bytes(data[2:3], "little") self.cbFaceStride = int.from_bytes(data[3:4], "little") self.sizeof_LOD = int.from_bytes(data[4:6], "little") self.numLODs = int.from_bytes(data[6:8], "little") self.num_vertices = int.from_bytes(data[8:12], "little") self.num_faces = int.from_bytes(data[12:16], "little") def export_data(self) -> bytearray: return bytearray( self.cbSize.to_bytes(2, "little") + self.cbVerticesStride.to_bytes(1, "little") + self.cbFaceStride.to_bytes(1, "little") + self.sizeof_LOD.to_bytes(2, "little") + self.numLODs.to_bytes(2, "little") + self.num_vertices.to_bytes(4, "little") + self.num_faces.to_bytes(4, "little") ) """ struct FileMeshHeaderV4 { unsigned short sizeof_MeshHeader; unsigned short lodType; unsigned int numVerts; unsigned int numFaces; unsigned short numLODs; unsigned short numBones; unsigned int sizeof_boneNamesBuffer; unsigned short numSubsets; unsigned char numHighQualityLODs; unsigned char unused; }; """ @dataclass class FileMeshHeaderV4: sizeof_MeshHeader: int lodType: int numVerts: int numFaces: int numLODs: int numBones: int sizeof_boneNamesBuffer: int numSubsets: int numHighQualityLODs: int unused: int def read_data( self, data : str ): if len(data) < 24: raise Exception(f"FileMeshHeaderV4.read_data: data is too short ({len(data)} bytes)") self.sizeof_MeshHeader = int.from_bytes(data[0:2], "little") if self.sizeof_MeshHeader != 24: raise Exception(f"FileMeshHeaderV4.read_data: invalid sizeof_MeshHeader ({self.sizeof_MeshHeader})") self.lodType = int.from_bytes(data[2:4], "little") self.numVerts = int.from_bytes(data[4:8], "little") self.numFaces = int.from_bytes(data[8:12], "little") self.numLODs = int.from_bytes(data[12:14], "little") self.numBones = int.from_bytes(data[14:16], "little") self.sizeof_boneNamesBuffer = int.from_bytes(data[16:20], "little") self.numSubsets = int.from_bytes(data[20:22], "little") self.numHighQualityLODs = int.from_bytes(data[22:23], "little") self.unused = int.from_bytes(data[23:24], "little") def export_data(self) -> bytearray: return bytearray( self.sizeof_MeshHeader.to_bytes(2, "little") + self.lodType.to_bytes(2, "little") + self.numVerts.to_bytes(4, "little") + self.numFaces.to_bytes(4, "little") + self.numLODs.to_bytes(2, "little") + self.numBones.to_bytes(2, "little") + self.sizeof_boneNamesBuffer.to_bytes(4, "little") + self.numSubsets.to_bytes(2, "little") + self.numHighQualityLODs.to_bytes(1, "little") + self.unused.to_bytes(1, "little") ) """ struct Envelope { unsigned char bones[4]; unsigned char weights[4]; }; """ @dataclass class Envelope: bones: list[int] weights: list[int] def read_data( self, data : str ): if len(data) < 8: raise Exception(f"Envelope.read_data: data is too short ({len(data)} bytes)") for i in range(0, 4): self.bones.append(int.from_bytes(data[i:i+1], "little")) self.weights.append(int.from_bytes(data[i+4:i+5], "little")) def export_data(self) -> bytearray: return bytearray( self.bones[0].to_bytes(1, "little") + self.bones[1].to_bytes(1, "little") + self.bones[2].to_bytes(1, "little") + self.bones[3].to_bytes(1, "little") + self.weights[0].to_bytes(1, "little") + self.weights[1].to_bytes(1, "little") + self.weights[2].to_bytes(1, "little") + self.weights[3].to_bytes(1, "little") ) """ struct Bone { unsigned int boneNameIndex; unsigned short parentIndex; unsigned short lodParentIndex; float culling; float r00, r01, r02; float r10, r11, r12; float r20, r21, r22; float x, y, z; }; """ @dataclass class Bone: boneNameIndex: int parentIndex: int lodParentIndex: int culling: float r00: float r01: float r02: float r10: float r11: float r12: float r20: float r21: float r22: float x: float y: float z: float def read_data( self, data : str ): if len(data) < 60: raise Exception(f"Bone.read_data: data is too short ({len(data)} bytes)") self.boneNameIndex = int.from_bytes(data[0:4], "little") self.parentIndex = int.from_bytes(data[4:6], "little") self.lodParentIndex = int.from_bytes(data[6:8], "little") self.culling = struct.unpack(" bytearray: return bytearray( self.boneNameIndex.to_bytes(4, "little") + self.parentIndex.to_bytes(2, "little") + self.lodParentIndex.to_bytes(2, "little") + struct.pack(" bytearray: subsetData : bytearray = bytearray( self.facesBegin.to_bytes(4, "little") + self.facesLength.to_bytes(4, "little") + self.vertsBegin.to_bytes(4, "little") + self.vertsLength.to_bytes(4, "little") + self.numBoneIndicies.to_bytes(4, "little") ) for i in range(0, 26): subsetData += self.boneIndicies[i].to_bytes(1, "little") return subsetData """ struct FileMeshHeaderV5 { unsigned short sizeof_MeshHeader; unsigned short lodType; unsigned int numVerts; unsigned int numFaces; unsigned short numLODs; unsigned short numBones; unsigned int sizeof_boneNamesBuffer; unsigned short numSubsets; unsigned char numHighQualityLODs; unsigned char unusedPadding; unsigned int facsDataFormat; unsigned int facsDataSize; }; """ @dataclass class FileMeshHeaderV5: sizeof_MeshHeader: int lodType: int numVerts: int numFaces: int numLODs: int numBones: int sizeof_boneNamesBuffer: int numSubsets: int numHighQualityLODs: int unusedPadding: int facsDataFormat: int facsDataSize: int def read_data( self, data : str ): if len(data) < 32: raise Exception(f"FileMeshHeaderV5.read_data: data is too short ({len(data)} bytes)") self.sizeof_MeshHeader = int.from_bytes(data[0:2], "little") if self.sizeof_MeshHeader != 32: raise Exception(f"FileMeshHeaderV5.read_data: invalid sizeof_MeshHeader ({self.sizeof_MeshHeader})") self.lodType = int.from_bytes(data[2:4], "little") self.numVerts = int.from_bytes(data[4:8], "little") self.numFaces = int.from_bytes(data[8:12], "little") self.numLODs = int.from_bytes(data[12:14], "little") self.numBones = int.from_bytes(data[14:16], "little") self.sizeof_boneNamesBuffer = int.from_bytes(data[16:20], "little") self.numSubsets = int.from_bytes(data[20:22], "little") self.numHighQualityLODs = int.from_bytes(data[22:23], "little") self.unusedPadding = int.from_bytes(data[23:24], "little") self.facsDataFormat = int.from_bytes(data[24:28], "little") self.facsDataSize = int.from_bytes(data[28:32], "little") def export_data(self) -> bytearray: return bytearray( self.sizeof_MeshHeader.to_bytes(2, "little") + self.lodType.to_bytes(2, "little") + self.numVerts.to_bytes(4, "little") + self.numFaces.to_bytes(4, "little") + self.numLODs.to_bytes(2, "little") + self.numBones.to_bytes(2, "little") + self.sizeof_boneNamesBuffer.to_bytes(4, "little") + self.numSubsets.to_bytes(2, "little") + self.numHighQualityLODs.to_bytes(1, "little") + self.unusedPadding.to_bytes(1, "little") + self.facsDataFormat.to_bytes(4, "little") + self.facsDataSize.to_bytes(4, "little") ) @dataclass class FileMeshData: vnts: list[FileMeshVertexNormalTexture3d] faces: list[FileMeshFace] header : FileMeshHeader | FileMeshHeaderV3 | FileMeshHeaderV4 | FileMeshHeaderV5 LODs : list[int] bones : list[Bone] boneNames : str meshSubsets : list[MeshSubset] full_faces : list[FileMeshFace] envelopes : list[Envelope] def read_data( data : str, offset : int, size : int ) -> str: """Reads a string of data from a given offset and size.""" if len(data) < offset + size: raise Exception(f"read_data: offset is out of bounds (offset={offset}, size={size})") return data[offset:offset+size] def get_mesh_version( data : bytearray ) -> float: """Gets the version of the mesh file. Throws an exception if the version is not supported.""" if len(data) < 12: raise Exception(f"get_mesh_version: data is too short ({len(data)} bytes)") if not data[0:8] == b"version ": raise Exception(f"get_mesh_version: invalid mesh header ({data[0:8]})") if data[0:12] == b"version 1.00": return 1.0 elif data[0:12] == b"version 1.01": return 1.1 elif data[0:12] == b"version 2.00": return 2.0 elif data[0:12] == b"version 3.00": return 3.0 elif data[0:12] == b"version 3.01": return 3.1 elif data[0:12] == b"version 4.00": return 4.0 elif data[0:12] == b"version 4.01": return 4.1 elif data[0:12] == b"version 5.00": return 5.0 elif data[0:12] == b"version 5.01": return 5.1 else: raise Exception(f"get_mesh_version: unsupported mesh version ({data[0:12]})") def read_mesh_v1( data : bytearray, offset : int, scale : int = 0.5, invertUV : bool = True) -> FileMeshData: data = data.decode("ASCII") meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], []) numFaces : int = int(data.split("\n")[1]) debug_print(f"read_mesh_v1: numFaces={numFaces}") # [0.551563,-0.0944613,0.0862401] we need to find every vector3 in the file # Each vert has 3 vector3 values so we need to find 3 vector3 values for each vert startingIndex = data.find("[") allVectors : list[str] = data[startingIndex:].split("]") for i in range(0, len(allVectors)): vector : str = allVectors[i].strip() vector = vector.replace("[", "").replace("]", "") if vector == "": del allVectors[i] continue vector_floats : list[float] = [float(x) for x in vector.split(",")] if len(vector_floats) != 3: raise Exception(f"read_mesh_v1: invalid vector3 ({vector})") #debug_print(f"read_mesh_v1: allVectors[{i}]={vector_floats}") allVectors[i] = vector_floats if len(allVectors) != numFaces * 9: raise Exception(f"read_mesh_v1: invalid number of verticies ({len(allVectors)}), expected {numFaces * 9}") for i in range(0, len(allVectors), 3): vertPos : list[float] = allVectors[ i ] vertNorm : list[float] = allVectors[ i + 1 ] vertUV : list[float] = allVectors[ i + 2 ] meshData.vnts.append(FileMeshVertexNormalTexture3d( vertPos[0] * scale, vertPos[1] * scale, vertPos[2] * scale, vertNorm[0], vertNorm[1], vertNorm[2], # Version 1.0 has the UVs inverted, it was only fixed in 1.1 vertUV[0], float(( 1 - vertUV[1] ) if invertUV else vertUV[1]), int(vertUV[2]), 0, 0, 0, 0, 0, 0, 0 )) debug_print(f"read_mesh_v1: vnts[{i // 3}]={meshData.vnts[i // 3]}") for i in range(0, numFaces): meshData.faces.append(FileMeshFace(i * 3, i * 3 + 1, i * 3 + 2)) debug_print(f"read_mesh_v1: faces[{i}]={meshData.faces[i]}") debug_print(f"read_mesh_v1: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully") return meshData def read_mesh_v2( data : bytearray, offset : int ) -> FileMeshData: meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], []) meshHeader : FileMeshHeader = FileMeshHeader(0, 0, 0, 0, 0) meshHeader.read_data(read_data(data, offset, 12)) offset += 12 debug_print(f"read_mesh_v2: meshHeader={meshHeader}") if meshHeader.num_vertices == 0 or meshHeader.num_faces == 0: raise Exception(f"read_mesh_v2: empty mesh") meshData.header = meshHeader isRGBAMissing = meshHeader.cbVerticesStride == 36 if isRGBAMissing: for i in range(0, meshHeader.num_vertices): meshData.vnts.append(FileMeshVertexNormalTexture3dNoRGBA(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) meshData.vnts[i].read_data(read_data(data, offset + i * 36, 36)) debug_print(f"read_mesh_v2: vnts[{i}]={meshData.vnts[i]}") else: for i in range(0, meshHeader.num_vertices): meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40)) debug_print(f"read_mesh_v2: vnts[{i}]={meshData.vnts[i]}") offset += meshHeader.num_vertices * meshHeader.cbVerticesStride for i in range(0, meshHeader.num_faces): meshData.faces.append(FileMeshFace(0, 0, 0)) meshData.faces[i].read_data(read_data(data, offset + i * 12, 12)) debug_print(f"read_mesh_v2: faces[{i}]={meshData.faces[i]}") offset += meshHeader.num_faces * meshHeader.cbFaceStride if offset != len(data): raise Exception(f"read_mesh_v2: unexpected data at end of file ({len(data) - offset} bytes)") debug_print(f"read_mesh_v2: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully") return meshData def read_mesh_v3( data : bytearray, offset : int ) -> FileMeshData: meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], []) meshHeader : FileMeshHeaderV3 = FileMeshHeaderV3(0, 0, 0, 0, 0, 0, 0) meshHeader.read_data(read_data(data, offset, 16)) offset += 16 debug_print(f"read_mesh_v3: meshHeader={meshHeader}") if meshHeader.num_vertices == 0 or meshHeader.num_faces == 0: raise Exception(f"read_mesh_v3: empty mesh") meshData.header = meshHeader for i in range(0, meshHeader.num_vertices): meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40)) debug_print(f"read_mesh_v3: vnts[{i}]={meshData.vnts[i]}") offset += meshHeader.num_vertices * meshHeader.cbVerticesStride for i in range(0, meshHeader.num_faces): meshData.faces.append(FileMeshFace(0, 0, 0)) meshData.faces[i].read_data(read_data(data, offset + i * 12, 12)) debug_print(f"read_mesh_v3: faces[{i}]={meshData.faces[i]}") offset += meshHeader.num_faces * meshHeader.cbFaceStride # LODs ( sizeof_LOD [ unsigned int ] * numLODs ) meshLODs : list[int] = [] for i in range(0, meshHeader.numLODs): meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little")) debug_print(f"read_mesh_v3: meshLODs[{i}]={meshLODs[i]}") offset += meshHeader.numLODs * 4 # We only keep the first LOD in the mesh data if len(meshLODs) > 1: meshData.full_faces = meshData.faces meshData.faces = meshData.faces[0:meshLODs[1]] debug_print(f"read_mesh_v3: only keeping {meshLODs[1]}/{meshHeader.num_faces} faces") meshData.LODs = meshLODs if offset != len(data): raise Exception(f"read_mesh_v3: unexpected data at end of file ({len(data) - offset} bytes)") debug_print(f"read_mesh_v3: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully") return meshData def read_mesh_v4( data : bytearray, offset : int ) -> FileMeshData: meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], []) meshHeader : FileMeshHeaderV4 = FileMeshHeaderV4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) meshHeader.read_data(read_data(data, offset, 24)) offset += 24 debug_print(f"read_mesh_v4: meshHeader={meshHeader}") if meshHeader.numVerts == 0 or meshHeader.numFaces == 0: raise Exception(f"read_mesh_v4: empty mesh") meshData.header = meshHeader for i in range(0, meshHeader.numVerts): meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 127, 127, 127, 127, 127)) meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40)) debug_print(f"read_mesh_v4: vnts[{i}]={meshData.vnts[i]}") offset += meshHeader.numVerts * 40 if meshHeader.numBones > 0: for i in range(0, meshHeader.numVerts): meshData.envelopes.append(Envelope([], [])) meshData.envelopes[i].read_data(read_data(data, offset + i * 8, 8)) debug_print(f"read_mesh_v4: envelopes[{i}]={meshData.envelopes[i]}") offset += meshHeader.numVerts * 8 for i in range(0, meshHeader.numFaces): meshData.faces.append(FileMeshFace(0, 0, 0)) meshData.faces[i].read_data(read_data(data, offset + i * 12, 12)) debug_print(f"read_mesh_v4: faces[{i}]={meshData.faces[i]}") offset += meshHeader.numFaces * 12 # LODs ( sizeof_LOD [ unsigned int ] * numLODs ) meshLODs : list[int] = [] for i in range(0, meshHeader.numLODs): meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little")) debug_print(f"read_mesh_v4: meshLODs[{i}]={meshLODs[i]}") offset += meshHeader.numLODs * 4 if len(meshLODs) > 1: meshData.full_faces = meshData.faces meshData.faces = meshData.faces[0:meshLODs[1]] debug_print(f"read_mesh_v4: only keeping {meshLODs[1]}/{meshHeader.numFaces} faces") meshData.LODs = meshLODs if meshHeader.numBones > 0: for i in range(0, meshHeader.numBones): meshData.bones.append(Bone(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -127, -127, -127, 0, 0)) meshData.bones[i].read_data(read_data(data, offset + i * 60, 60)) debug_print(f"read_mesh_v4: bones[{i}]={meshData.bones[i]}") offset += meshHeader.numBones * 60 boneNames : str = read_data(data, offset, meshHeader.sizeof_boneNamesBuffer).decode("utf-8") offset += meshHeader.sizeof_boneNamesBuffer debug_print(f"read_mesh_v4: boneNames={boneNames}") meshSubsets : list[MeshSubset] = [] for i in range(0, meshHeader.numSubsets): meshSubsets.append(MeshSubset(0, 0, 0, 0, 0, [])) meshSubsets[i].read_data(read_data(data, offset + i * 72, 72)) debug_print(f"read_mesh_v4: meshSubsets[{i}]={meshSubsets[i]}") meshData.meshSubsets = meshSubsets offset += meshHeader.numSubsets * 72 offset += meshHeader.unused if offset != len(data): raise Exception(f"read_mesh_v4: unexpected data at end of file ({len(data) - offset} bytes)") debug_print(f"read_mesh_v4: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully") return meshData def read_mesh_v5( data : bytearray, offset : int ) -> FileMeshData: meshData : FileMeshData = FileMeshData([], [], FileMeshHeader(0, 0, 0, 0, 0), [], [], "", [], [], []) meshHeader : FileMeshHeaderV5 = FileMeshHeaderV5(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) meshHeader.read_data(read_data(data, offset, 32)) offset += 32 debug_print(f"read_mesh_v5: meshHeader={meshHeader}") if meshHeader.numVerts == 0 or meshHeader.numFaces == 0: raise Exception(f"read_mesh_v5: empty mesh") meshData.header = meshHeader for i in range(0, meshHeader.numVerts): meshData.vnts.append(FileMeshVertexNormalTexture3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 127, 127, 127, 127, 127)) meshData.vnts[i].read_data(read_data(data, offset + i * 40, 40)) debug_print(f"read_mesh_v5: vnts[{i}]={meshData.vnts[i]}") offset += meshHeader.numVerts * 40 if meshHeader.numBones > 0: for i in range(0, meshHeader.numVerts): temp = Envelope([], []) temp.read_data(read_data(data, offset + i * 8, 8)) offset += meshHeader.numVerts * 8 for i in range(0, meshHeader.numFaces): meshData.faces.append(FileMeshFace(0, 0, 0)) meshData.faces[i].read_data(read_data(data, offset + i * 12, 12)) debug_print(f"read_mesh_v5: faces[{i}]={meshData.faces[i]}") offset += meshHeader.numFaces * 12 # LODs ( sizeof_LOD [ unsigned int ] * numLODs ) meshLODs : list[int] = [] for i in range(0, meshHeader.numLODs): meshLODs.append(int.from_bytes(read_data(data, offset + i * 4, 4), "little")) debug_print(f"read_mesh_v5: meshLODs[{i}]={meshLODs[i]}") offset += meshHeader.numLODs * 4 if len(meshLODs) > 1: meshData.full_faces = meshData.faces meshData.faces = meshData.faces[0:meshLODs[1]] debug_print(f"read_mesh_v5: only keeping {meshLODs[1]}/{meshHeader.numFaces} faces") meshData.LODs = meshLODs if meshHeader.numBones > 0: for i in range(0, meshHeader.numBones): meshData.bones.append(Bone(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -127, -127, -127, 0, 0)) meshData.bones[i].read_data(read_data(data, offset + i * 60, 60)) debug_print(f"read_mesh_v5: bones[{i}]={meshData.bones[i]}") offset += meshHeader.numBones * 60 boneNames : str = read_data(data, offset, meshHeader.sizeof_boneNamesBuffer).decode("utf-8") offset += meshHeader.sizeof_boneNamesBuffer debug_print(f"read_mesh_v5: boneNames={boneNames}") meshSubsets : list[MeshSubset] = [] for i in range(0, meshHeader.numSubsets): meshSubsets.append(MeshSubset(0, 0, 0, 0, 0, [])) meshSubsets[i].read_data(read_data(data, offset + i * 72, 72)) debug_print(f"read_mesh_v5: meshSubsets[{i}]={meshSubsets[i]}") meshData.meshSubsets = meshSubsets offset += meshHeader.numSubsets * 72 offset += meshHeader.unusedPadding offset += meshHeader.facsDataSize # No way I am doing allat if offset != len(data): raise Exception(f"read_mesh_v5: unexpected data at end of file ({len(data) - offset} bytes)") debug_print(f"read_mesh_v5: read {len(meshData.vnts)} vertices and {len(meshData.faces)} faces successfully") return meshData def export_mesh_v2( meshData : FileMeshData ) -> bytearray: if meshData is None: raise Exception(f"export_mesh_v2: meshData is None") if len(meshData.vnts) == 0 or len(meshData.faces) == 0: raise Exception(f"export_mesh_v2: meshData is empty") finalmesh : bytearray = b"" finalmesh += b"version 2.00\n" finalmesh += FileMeshHeader( 12, 36 if len(meshData.vnts) > 0 and isinstance(meshData.vnts[0], FileMeshVertexNormalTexture3dNoRGBA) else 40, 12, len(meshData.vnts), len(meshData.faces) ).export_data() for i in range(0, len(meshData.vnts)): finalmesh += meshData.vnts[i].export_data() for i in range(0, len(meshData.faces)): finalmesh += meshData.faces[i].export_data() return finalmesh def export_mesh_v3( meshData : FileMeshData ) -> bytearray: if meshData is None: raise Exception(f"export_mesh_v3: meshData is None") if len(meshData.vnts) == 0 or len(meshData.faces) == 0: raise Exception(f"export_mesh_v3: meshData is empty") finalmesh : bytearray = b"" finalmesh += b"version 3.00\n" finalmesh += FileMeshHeaderV3( 16, 40, 12, 4, len(meshData.LODs), len(meshData.vnts), len(meshData.full_faces) if len(meshData.LODs) > 1 and len(meshData.full_faces) > len(meshData.faces) else len(meshData.faces), ).export_data() for i in range(0, len(meshData.vnts)): finalmesh += meshData.vnts[i].export_data() if len(meshData.LODs) > 1 and len(meshData.full_faces) > len(meshData.faces): for i in range(0, len(meshData.full_faces)): finalmesh += meshData.full_faces[i].export_data() else: for i in range(0, len(meshData.faces)): finalmesh += meshData.faces[i].export_data() for i in range(0, len(meshData.LODs)): finalmesh += meshData.LODs[i].to_bytes(4, "little") return finalmesh def read_mesh_data( data : bytearray ) -> FileMeshData: meshVersion = get_mesh_version(data) startingOffset = data.find(b"\n") debug_print(f"meshVersion={meshVersion}, startingOffset={startingOffset}") if meshVersion == 1.0: meshData : FileMeshData = read_mesh_v1(data, startingOffset + 1, 0.5) elif meshVersion == 1.1: meshData : FileMeshData = read_mesh_v1(data, startingOffset + 1, 1.0, False) elif meshVersion == 2.0: meshData : FileMeshData = read_mesh_v2(data, startingOffset + 1) elif meshVersion == 3.0 or meshVersion == 3.1: meshData : FileMeshData = read_mesh_v3(data, startingOffset + 1) elif meshVersion == 4.0 or meshVersion == 4.1: meshData : FileMeshData = read_mesh_v4(data, startingOffset + 1) elif meshVersion == 5.0 or meshVersion == 5.1: meshData : FileMeshData = read_mesh_v5(data, startingOffset + 1) else: raise Exception(f"read_mesh_data: unsupported mesh version ({meshVersion})") return meshData if __name__ == "__main__": arguments = sys.argv[1:] if len(arguments) < 1: debug_print("Usage: RBXMesh.py ") exit(1) meshFile = open(arguments[0], "rb") meshData = meshFile.read() meshFile.close() meshData = read_mesh_data(meshData) if len(arguments) > 1: if arguments[1] == "2.0": with open(f"{arguments[0]}.v2", "wb") as f: f.write(export_mesh_v2(meshData)) elif arguments[1] == "3.0": with open(f"{arguments[0]}.v3", "wb") as f: f.write(export_mesh_v3(meshData))