Mesh Format
When a mesh is uploaded to Roblox, it is converted into an in-house format that the game engine can read. Roblox’s mesh format isn’t a format you can export to, but you can download meshes off of their servers, which you can use to view files like these. This article will cover the file specification for this mesh format, so that you might be able to write code externally that can read Roblox mesh files.
Version Header
Every mesh in Roblox starts with a header that is 12 characters in length, followed by a newline ( \n
) character. The header is represented in ASCII, and it is used to indicate what version of the mesh format is being used.
Currently, there are 5 known versions that exist:
version 1.00
version 1.01
version 2.00
version 3.00
version 4.00
Each version has its own specific rules and quirks that may need to be addressed while reading the file.
version 1.00
This is the original version of Roblox’s mesh format, which is stored purely in ASCII and can be read by humans. These files are stored as 3 lines of text:
version 1.00 num_faces (data...)
The num_faces line represents the number of polygons to expect in the data line.
The (data…) line represents a series of concatenated Vector3
pairs, stored in-between brackets with the XYZ coordinates separated by commas as such: [1.00,1.00,1.00]
Data Specification
For every polygon defined in the mesh, there are 3 vertex points that make up the face. Each vertex point contains 3 Vector3 pairs, representing the Position , Normal, and UV respectively.
Thus, you should expect to read num_faces * 9
concatenated Vector3 pairs in the (data…) line!
Each vertex is tokenized like so:
[pos_X,pos_Y,pos_Z][norm_X,norm_Y,norm_Z][tex_U,tex_V,tex_W]
Position
The 1st Vector3, [pos_X,pos_Y,pos_Z]
is the position of the vertex point.
version 1.01
, then you can safely assume the mesh is using the correct scale.
Normal
The 2nd Vector3, [norm_X,norm_Y,norm_Z]
is the normal vector of the vertex point, which is used to smooth out the shading of the mesh.
This Vector3 is expected to be a unit vector, so its Magnitude should be exactly 1
. The mesh might have unexpected behavior if this isn’t the case!
UV
The 3rd Vector3, [tex_U,tex_V,tex_W]
is the UV texture coordinate of the vertex point, which is used to determine how the mesh’s texture is applied to the mesh. The tex_W coordinate is unused, so you can expect its value to be 0
.
version 2.00
The version 2.00 format is stored in a binary format, and files may differ in structure depending on factors that aren’t based on the version number.
Data Specification
Once you have read past the version 2.00\n
text at the beginning of the file, the binary data begins!
There will be three struct types used to read the file:
- MeshHeader
- Vertex
- Face
The variables in each of these structs are defined in a specific order, and the type of each variable specifies how many bytes should be sequentially read and copied into the variable.
MeshHeader
The first chunk of data is the MeshHeader, represented by the following struct definition:
struct MeshHeader { short sizeof_MeshHeader; byte sizeof_Vertex; byte sizeof_Face; uint numVerts; uint numFaces; }
If you read MeshHeader.sizeof_MeshHeader
and it does not share the same sizeof(MeshHeader)
as the one in your code, then it is unlikely that you’ll be able to read it correctly! This could be due to a revision to the format though, as will be discussed below.
Vertex
Once you have read the MeshHeader, you should expect to read an array, Vertex[numVerts] vertices;
using the following struct:
struct Vertex { float px, py, pz; // XYZ position of the vertex's position float nx, ny, nz; // XYZ unit vector of the vertex's normal vector. float tu, tv, tw; // UV coordinate of the vertex (tw is reserved) byte r, g, b, a; // RGBA color of the vertex }
This array represents all of the vertices in the mesh, which can be linked together into faces.
Face
Finally, you should expect to read an array, Face[numFaces] faces;
using the following struct:
struct Face { uint a; // 1st Vertex Index uint b; // 2nd Vertex Index uint c; // 3rd Vertex Index }
This array represents indexes in the Vertex array that was noted earlier. The three Vertex
structs that are indexed using the Face
are used to form a polygon in the mesh!
version 3.00
Version 3 of the mesh format is a minor revision which introduces support for LOD meshes.
Firstly, here are the changes to the MeshHeader
:
struct MeshHeader { short sizeof_MeshHeader; byte sizeof_Vertex; byte sizeof_Face; [+] short sizeof_LOD; [+] short numLODs; uint numVerts; uint numFaces; }
After reading the faces of the mesh file, there will be (numLODs * 4
) bytes at the end of the file, representing an array of numLODs
ints, or just:
int mesh_LODs[numLODs];
The array uses integers because sizeof_MeshLOD
should always have a value of 4 to be considered valid.
The mesh_LODs
array represents a series of face ranges, the faces of which form meshes that can be used at various distances by Roblox’s mesh rendering system.
For example, you might have an array that looks like this:
{ 0, 1820, 2672, 3045 }
This values in this array are interpreted as follows:
- The Main mesh is formed using faces
[0 - 1819]
- The 1st LOD mesh is formed using faces
[1820 - 2671]
- The 2nd LOD mesh is formed using faces
[2672 - 3044]
All of these faces should be stored in whatever array of Faces you have defined.
version 4.00 [WIP]
Version 4 of the mesh format is a major revision that introduces skeletal data and mesh deformation. Data such as vertices, faces, and LODs remain relatively unchanged, but everything else is shifted around and new.
A full V4 mesh file can be represented using the following pseudo-header:
struct Mesh { MeshHeader header; Vertex[numVerts] verts; Envelope[numVerts]? envelopes; Face[numFaces] faces; int[numLODs] lods; Bone[numBones] bones; byte[nameTableSize] nameTable; SkinData[numSkinData] skinData; }
Read Procedure
First up we have the MeshHeader, which has changed significantly:
struct MeshHeader { short sizeof_MeshHeader; short numMeshes; int numVerts; int numFaces; short numLODs; short numBones; int nameTableSize; short unknown; short numSkinData; }
As you can see, there is no longer any size information for anything besides the header itself. The sizeof_MeshHeader
field is expected to equal 24
.
Once you’ve read the new MeshHeader, you will read the same Vertex[numVerts] vertices;
array as you did in version 2.00
:
struct Vertex { float px, py, pz; float nx, ny, nz; float tu, tv, tw; byte r, g, b, a; }
The difference this time is that Vertex
will always have RGBA data. Each Vertex
is 40 bytes in size.
If the mesh has any bones in its data, then you’ll then read Envelope[numVerts] envelopes;
using the following struct:
struct Envelope { byte bones[4]; // [citation needed] byte weights[4]; // [citation needed] }
This appears to be per-vertex envelope data, which controls how much each influence each bone has over the vertex. Each byte in the bones
table represents an index to the bones[numBones]
table, while each byte in the weights
table is a weight value weight[i]
(between 0-255) for how much bones[i]
can deform verts[i]
.
As mentioned before, this data will not be present if MeshHeader.numBones == 0
. There are meshes on production that do this, so watch out for them!
From there you’ll read Face[numFaces] faces
and int[numLODs] lods
in the same way as version 3.00
:
struct Face { uint a, b, c; }
Face[numFaces] faces; int[numLODs] lods;
Nothing has changed regarding how the faces are read and how the LOD range indices are interpreted.
Following the faces and LODs, you’ll read Bone[numBones] bones
, using the following struct:
struct Bone { int nameTableIndex; short id; // [citation needed] short parent; float unknown; // [stub] float r00, r01, r02; float r10, r11, r12; float r20, r21, r22; float x, y, z; }
Each bone holds a position and rotation matrix with the same behavior as Roblox’s CFrames. It also holds an index to the name table, which will be explained in the next step.
There is a float preceding the rotation matrix which I haven’t figured out the purpose of yet. The id/parent values are always identical for some reason, so I’m not 100% sure I’ve figured out what id
is.
After reading the bone data, you’ll read:
byte[nameTableSize] nameTable;
The name table is a null-terminated array of UTF-8 strings stored in a raw buffer, and it holds the names of each bone. The nameTableIndex
field of the Bone
struct is the starting index of that bone’s name in the name table. To get the name of a bone, you’ll need to read from that index until you encounter a null character in the sequence.
Finally, you’ll read SkinData[numSkinData] skinData;
using the following struct:
struct SkinData { int facesBegin; int facesLength; int vertsBegin; int vertsLength; int numBoneIndices; short boneIndices[23]; }
I haven’t fully deciphered the purpose of the SkinData as of yet, but my closest guess is that it specifies which bones are used by a range of vertices and faces in the mesh.
Each SkinData uses a fixed size of 72 bytes. Any extra values in the boneIndices
table are just set to -1. The order of the bone indices isn’t entirely clear either, it appears to be random. This needs further investigation before I’m confident that I understand how its supposed to work, but it can at least be read.