Mesh Format

From RoAPI

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.

If the mesh is specified to be using 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.