991 lines
35 KiB
Python
991 lines
35 KiB
Python
"""
|
|
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("<f", data[0:4])[0]
|
|
self.vy = struct.unpack("<f", data[4:8])[0]
|
|
self.vz = struct.unpack("<f", data[8:12])[0]
|
|
self.nx = struct.unpack("<f", data[12:16])[0]
|
|
self.ny = struct.unpack("<f", data[16:20])[0]
|
|
self.nz = struct.unpack("<f", data[20:24])[0]
|
|
self.tu = struct.unpack("<f", data[24:28])[0]
|
|
self.tv = struct.unpack("<f", data[28:32])[0]
|
|
self.tx = int.from_bytes(data[32:33], "little")
|
|
self.ty = int.from_bytes(data[33:34], "little")
|
|
self.tz = int.from_bytes(data[34:35], "little")
|
|
self.ts = int.from_bytes(data[35:36], "little")
|
|
self.r = int.from_bytes(data[36:37], "little")
|
|
self.g = int.from_bytes(data[37:38], "little")
|
|
self.b = int.from_bytes(data[38:39], "little")
|
|
self.a = int.from_bytes(data[39:40], "little")
|
|
|
|
def export_data( self ) -> bytearray:
|
|
return bytearray(
|
|
struct.pack("<f", self.vx) +
|
|
struct.pack("<f", self.vy) +
|
|
struct.pack("<f", self.vz) +
|
|
struct.pack("<f", self.nx) +
|
|
struct.pack("<f", self.ny) +
|
|
struct.pack("<f", self.nz) +
|
|
struct.pack("<f", self.tu) +
|
|
struct.pack("<f", self.tv) +
|
|
self.tx.to_bytes(1, "little") +
|
|
self.ty.to_bytes(1, "little") +
|
|
self.tz.to_bytes(1, "little") +
|
|
self.ts.to_bytes(1, "little") +
|
|
self.r.to_bytes(1, "little") +
|
|
self.g.to_bytes(1, "little") +
|
|
self.b.to_bytes(1, "little") +
|
|
self.a.to_bytes(1, "little")
|
|
)
|
|
|
|
"""
|
|
struct FileMeshVertexNormalTexture3dNoRGBA
|
|
{
|
|
float vx, vy, vz;
|
|
float nx, ny, nz;
|
|
float tu, tv;
|
|
|
|
signed char tx, ty, tz, ts;
|
|
};
|
|
"""
|
|
|
|
@dataclass
|
|
class FileMeshVertexNormalTexture3dNoRGBA:
|
|
vx: float
|
|
vy: float
|
|
vz: float
|
|
nx: float
|
|
ny: float
|
|
nz: float
|
|
tu: float
|
|
tv: float
|
|
tx: int
|
|
ty: int
|
|
tz: int
|
|
ts: int
|
|
|
|
def read_data( self, data : str ):
|
|
if len(data) < 36:
|
|
raise Exception(f"FileMeshVertexNormalTexture3dNoRGBA.read_data: data is too short ({len(data)} bytes)")
|
|
|
|
self.vx = struct.unpack("<f", data[0:4])[0]
|
|
self.vy = struct.unpack("<f", data[4:8])[0]
|
|
self.vz = struct.unpack("<f", data[8:12])[0]
|
|
self.nx = struct.unpack("<f", data[12:16])[0]
|
|
self.ny = struct.unpack("<f", data[16:20])[0]
|
|
self.nz = struct.unpack("<f", data[20:24])[0]
|
|
self.tu = struct.unpack("<f", data[24:28])[0]
|
|
self.tv = struct.unpack("<f", data[28:32])[0]
|
|
self.tx = int.from_bytes(data[32:33], "little")
|
|
self.ty = int.from_bytes(data[33:34], "little")
|
|
self.tz = int.from_bytes(data[34:35], "little")
|
|
self.ts = int.from_bytes(data[35:36], "little")
|
|
|
|
def export_data( self ) -> bytearray:
|
|
return bytearray(
|
|
struct.pack("<f", self.vx) +
|
|
struct.pack("<f", self.vy) +
|
|
struct.pack("<f", self.vz) +
|
|
struct.pack("<f", self.nx) +
|
|
struct.pack("<f", self.ny) +
|
|
struct.pack("<f", self.nz) +
|
|
struct.pack("<f", self.tu) +
|
|
struct.pack("<f", self.tv) +
|
|
self.tx.to_bytes(1, "little") +
|
|
self.ty.to_bytes(1, "little") +
|
|
self.tz.to_bytes(1, "little") +
|
|
self.ts.to_bytes(1, "little")
|
|
)
|
|
|
|
"""
|
|
struct FileMeshFace
|
|
{
|
|
unsigned int a;
|
|
unsigned int b;
|
|
unsigned int c;
|
|
};
|
|
"""
|
|
|
|
@dataclass
|
|
class FileMeshFace:
|
|
a: int
|
|
b: int
|
|
c: int
|
|
|
|
def read_data( self, data : str ):
|
|
if len(data) < 12:
|
|
raise Exception(f"FileMeshFace.read_data: data is too short ({len(data)} bytes)")
|
|
|
|
self.a = int.from_bytes(data[0:4], "little")
|
|
self.b = int.from_bytes(data[4:8], "little")
|
|
self.c = int.from_bytes(data[8:12], "little")
|
|
|
|
def export_data( self ) -> 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("<f", data[8:12])[0]
|
|
self.r00 = struct.unpack("<f", data[12:16])[0]
|
|
self.r01 = struct.unpack("<f", data[16:20])[0]
|
|
self.r02 = struct.unpack("<f", data[20:24])[0]
|
|
self.r10 = struct.unpack("<f", data[24:28])[0]
|
|
self.r11 = struct.unpack("<f", data[28:32])[0]
|
|
self.r12 = struct.unpack("<f", data[32:36])[0]
|
|
self.r20 = struct.unpack("<f", data[36:40])[0]
|
|
self.r21 = struct.unpack("<f", data[40:44])[0]
|
|
self.r22 = struct.unpack("<f", data[44:48])[0]
|
|
self.x = struct.unpack("<f", data[48:52])[0]
|
|
self.y = struct.unpack("<f", data[52:56])[0]
|
|
self.z = struct.unpack("<f", data[56:60])[0]
|
|
|
|
def export_data(self) -> bytearray:
|
|
return bytearray(
|
|
self.boneNameIndex.to_bytes(4, "little") +
|
|
self.parentIndex.to_bytes(2, "little") +
|
|
self.lodParentIndex.to_bytes(2, "little") +
|
|
struct.pack("<f", self.culling) +
|
|
struct.pack("<f", self.r00) +
|
|
struct.pack("<f", self.r01) +
|
|
struct.pack("<f", self.r02) +
|
|
struct.pack("<f", self.r10) +
|
|
struct.pack("<f", self.r11) +
|
|
struct.pack("<f", self.r12) +
|
|
struct.pack("<f", self.r20) +
|
|
struct.pack("<f", self.r21) +
|
|
struct.pack("<f", self.r22) +
|
|
struct.pack("<f", self.x) +
|
|
struct.pack("<f", self.y) +
|
|
struct.pack("<f", self.z)
|
|
)
|
|
|
|
"""
|
|
struct MeshSubset
|
|
{
|
|
unsigned int facesBegin;
|
|
unsigned int facesLength;
|
|
|
|
unsigned int vertsBegin;
|
|
unsigned int vertsLength;
|
|
|
|
unsigned int numBoneIndicies;
|
|
unsigned short boneIndicies[26];
|
|
};
|
|
"""
|
|
|
|
@dataclass
|
|
class MeshSubset:
|
|
facesBegin: int
|
|
facesLength: int
|
|
vertsBegin: int
|
|
vertsLength: int
|
|
numBoneIndicies: int
|
|
boneIndicies: list[int]
|
|
|
|
def read_data( self, data : str ):
|
|
if len(data) < 72:
|
|
raise Exception(f"MeshSubset.read_data: data is too short ({len(data)} bytes)")
|
|
|
|
self.facesBegin = int.from_bytes(data[0:4], "little")
|
|
self.facesLength = int.from_bytes(data[4:8], "little")
|
|
self.vertsBegin = int.from_bytes(data[8:12], "little")
|
|
self.vertsLength = int.from_bytes(data[12:16], "little")
|
|
self.numBoneIndicies = int.from_bytes(data[16:20], "little")
|
|
for i in range(0, 26):
|
|
self.boneIndicies.append(int.from_bytes(data[20+i:21+i], "little"))
|
|
|
|
def export_data(self) -> 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 <mesh file location>")
|
|
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)) |