Roblox FileMesh Format Specification

( Latest Revision: 3/30/2025 )


When a mesh is uploaded to Roblox, it is converted into an in-house format called FileMesh, that the game engine can read and efficiently work with. Roblox’s FileMesh format isn’t a format you can normally export to or import into Roblox Studio, but you can download meshes by their assetId using the assetdelivery API.

This resource aims to be an unofficial spec for interpreting this format, so you might be able to write code externally that can read FileMesh files. It is assumed the reader is familiar with interpreting C-like structure declarations and how that maps to raw binary data.

Version Header

Every FileMesh 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 10 known versions that exist:

  • version 1.00
  • version 1.01
  • version 2.00
  • version 3.00
  • version 3.01
  • version 4.00
  • version 4.01
  • version 5.00
  • version 6.00
  • version 7.00

Each version has it’s own specific rules and quirks that may need to be addressed while reading the file. Increments to the decimal part of the version usually indicate miscellaneous changes that don’t change the actual structure of the format.

version 1.00 & 1.01

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]

Format

For every face defined in the mesh, there are 3 vertex points that make it up. 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.

They are 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.

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 (and all versions following) are stored in binary, and files may differ in structure depending on factors that aren’t based on the version number. This format is still actively authored for simple static meshes that don’t have any skinning or LODs.

Data Specification

Once you have read past the version 2.00\n text at the beginning of the file, the binary data begins!

A version 2.00 mesh is structured as follows:

struct FileMeshV2
{
    FileMeshHeaderV2 Header;
    FileMeshVertex Verts[Header.numVerts];
    FileMeshFace Faces[Header.numFaces];
}

FileMeshHeaderV2

The first chunk of data is the FileMeshHeaderV2, represented by the following struct:

struct FileMeshHeaderV2
{
    ushort sizeof_FileMeshHeaderV2;
    byte   sizeof_FileMeshVertex;
    byte   sizeof_FileMeshFace;

    uint   numVerts;
    uint   numFaces;
}

The numVerts and numFaces are used for reading the FileMeshVertex and FileMeshFace arrays that follow. Depending on when the mesh was created, sizeof_FileMeshVertex may either be 36 or 40.

FileMeshVertex

Once you have read the header, you should expect to read numVerts entries of the following struct:

struct FileMeshVertex
{
   float px, py, pz;     // Position
   float nx, ny, nz;     // Normal
   float tu, tv;         // UV

   sbyte tx, ty, tz, ts; // Tangent Vector & Bi-Normal Direction
   byte? r, g, b, a;     // RGBA Color Tinting (May not be present!)
}

This array represents all of the vertices in the mesh, which can be linked together into faces!

Important: RGBA Data is Optional!

Not all mesh files have RGBA data in version 2.00! Its important to double check against FileMeshHeaderV2.sizeof_FileMeshVertex. If its value is 36 instead of 40, then you should substitute the RGBA values with 255. If this isn’t accounted for, the mesh data will become misaligned and you won’t be able to read it correctly!

Interpreting & Error Checking the Tangent and Binormal vectors.

While the Position, Normal, and UV vectors are stored using floats, the Tangent uses 4 bytes that are mapped from a signed-byte value of [-127, 127] to a decimal value of [-1, 1].

The first 3 bytes are the XYZ values, while ts is the sign value of the tangent (either -127 or 127, which is mapped to -1 or 1)

The bytes which represent the tangent vector were previously occupied by an unused zero’d out tw value, when the UV coordinates were stored as a Vector3 instead of a Vector2. As a result it’s possible to get an invalid tangent vector with a value of:

{ X: -1, Y: -1, Z: -1, Sign: -1 } // HEX: 00000000 (Invalid!)

If you read a tangent vector that isn’t normalized, the mesh was likely authored prior to Roblox’s introduction of PBR materials, and thus won’t be able to render them correctly.

If you absolutely need them, you can either assign it a valid value and hope that it looks correct, or attempt to compute your own tangent and bi-tangent vectors.
(Ideally using something like MikkTSpace if necessary)

{ X:  0, Y:  0, Z: -1, Sign: 1 } // HEX: 7F7F00FE (Valid!)

FileMeshFace

Finally, you should expect to read numFaces entries of the following struct:

struct FileMeshFace
{
    uint a; // 1st Vertex Index
    uint b; // 2nd Vertex Index
    uint c; // 3rd Vertex Index
}

This array represents the faces of the mesh. The 3 indices map against the FileMeshVertex list, representing a polygon formed by those three verts.

version 3.00 & 3.01

Version 3 of the mesh format is a minor revision which introduces support for LOD meshes. It makes a change to the header and appends some data at the end of the file, but is otherwise very similar to version 2:

struct FileMeshV3
{
    FileMeshHeaderV3 Header;
    FileMeshVertex verts[numVerts];
    FileMeshFace faces[numFaces];
    uint lodOffsets[numLodOffsets];
}

FileMeshHeaderV3

Firstly, here are the changes from FileMeshHeaderV2 to FileMeshHeaderV3:

struct FileMeshHeaderV3
{
	ushort sizeof_FileMeshHeaderV3;
	byte   sizeof_FileMeshVertex; 
	byte   sizeof_FileMeshFace;

[+]	ushort sizeof_LodOffset; // unused, always 4
[+]	ushort numLodOffsets;

	uint   numVerts; 
	uint   numFaces; 
}

Read the faces, but don’t create them yet!

There are faces defined not just for the main mesh, but for the LOD meshes too! If you try to create every face at once, you’ll get an odd result.

Reading the LOD Offsets

After reading the verts and faces of the mesh file, there will be (numLodOffsets * 4) bytes at the end of the file.

uint lodOffsets[numLodOffsets];

It 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 array is 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 FileMeshFace objects you have defined.


version 4.00 & 4.01

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.

FileMeshV4

A full v4 mesh file can be represented with the following pseudo-header:

struct FileMeshV4
{
	FileMeshHeaderV4 header;
	
	FileMeshVertex verts[numVerts];
	FileMeshSkinning? skinning[numVerts];
	
	FileMeshFace faces[numFaces];
	uint lodOffsets[numLodOffsets];

	FileMeshBone bones[numBones];
	byte boneNames[sizeof_boneNames];

	FileMeshSubset subsets[numSubsets];
}

FileMeshHeaderV4

The following struct describes the new mesh header revision:

struct FileMeshHeaderV4:
{
	ushort sizeof_FileMeshHeaderV4;
	ushort lodType;

	uint   numVerts;
	uint   numFaces;
	
	ushort numLodOffsets;
	ushort numBones;
	
	uint   sizeof_boneNames;
	ushort numSubsets;

	byte   numHighQualityLODs;
	byte   unused;
}

There is no longer any size information for anything besides the header itself. sizeof_FileMeshHeaderV4 is expected to be 24 in v4 meshes.

The lodType is metadata from Roblox describing what technique was used to auto-generate LODs for this mesh. It maps to the following enum, but otherwise is unimportant:

enum FileMeshLodType
{
	None = 0,
	Unknown = 1,
	RbxSimplifier = 2,
	ZeuxMeshOptimizer = 3,
}

FileMeshVertex

Once you’ve read the new MeshHeader, you will read the same FileMeshVertex[numVerts] vertices; array as you did in version 2.00.

Skinning

If FileMeshHeaderV4.numBones > 0, then you’ll then read numVerts entries of the following struct:

struct FileMeshSkinning
{
	byte bones[4];
	byte weights[4];
}

FileMeshSkinning define the deformation influences of up to 4 bones for each vertex. Each byte in the bones table represents an index to the mesh subset associated with vertex[i], which in-turn indexes the bones[numBones] table. More information about mesh subsets will follow near the bottom of the v4 spec.

Each byte in the weights table is a weight value weight[i] (between 0-255) for how much the bone at index bones[meshSubset[i]] can deform the vertex at 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!

Faces

Next, you’ll read:

FileMeshFace faces[numFaces];
uint lodOffsets[numLodOffsets];

Nothing has changed regarding how the faces are read and how the lod offsets are interpreted.

Bones

Next you’ll read numBones entries of the following struct:

struct FileMeshBone
{
	uint boneNameIndex;

	ushort parentIndex;
	ushort lodParentIndex;

	float culling;
	
	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 (Y-up, -Z forward). It also holds an index into the bone names buffer, which will be explained in the next stage. Note that the CFrame value is stored in world-space instead of object-space.

The parentIndex and lodParentIndex values index a parent bone in the bones table. When the value is 0xFFFF, you can assume the bone has no parent (i.e. it’ll be parented to the MeshPart). The intended use of lodParentIndex is to collapse the skeleton at lower quality LOD levels.

The culling value stores the distance between the bone and the furthest vertex that can be influenced by the bone. It is used to help compute a bounding box around the MeshPart for frustum culling.

Bone Names Buffer

After reading the bones, you’ll read:

byte boneNames[sizeof_boneNames];

The boneNames is a set of null-terminated UTF-8 strings stored in a raw buffer, and it holds the names of each bone.

The boneNameIndex field of the FileMeshBone 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 byte in the sequence.

Subsets

Finally, you’ll read numSubsets entries of the following struct:

struct FileMeshSubset
{
	uint facesBegin;
	uint facesLength;
	
	uint vertsBegin;
	uint vertsLength;
	
	uint numBoneIndices;
	ushort boneIndices[26];
}

Each FileMeshSubset defines a range of vertices and faces that are influenced by no more than 26 distinct bones. This specific number is used because some older GPUs can only support 26 bones per mesh, so Roblox splits the mesh up into subsets to work around the limitation.

The weights data read above indexes into these subsets. You can mark each vertex’s associated subset by iterating vertsLength vertices, starting from vertsBegin.

The boneIndices table will always have 26 values, while numBoneIndices should be a value between [1-26]. Any extra values in the boneIndices table will be set to 0xFFFF.


version 5.00

Version 5 of the mesh format is a major, but isolated revision to version 4 of the mesh format. It adds a blob of additional data at the end of the file to assist with driving facial animation through the Facial Action Coding System standard.

FileMeshHeaderV5

In this revision, FileMeshHeaderV4 is modified into FileMeshHeaderV5 through the following changes:

struct FileMeshHeaderV5
{
	ushort sizeof_MeshHeader;
	
	ushort numMeshes;
	uint   numVerts;
	uint   numFaces;
	
	ushort numLodOffsets;
	ushort numBones;
	
	uint   sizeof_boneNameBuffer;
	ushort numSubsets;

	byte   numHighQualityLODs;
	byte   unusedPadding;

[+]	uint   facsDataFormat;
[+]	uint   facsDataSize;
}

The facsDataFormat field should always have a 32-bit value of 1. If the value is 0, you can assume there is no valid FACS data in the mesh.

The facsDataSize field represents how many bytes have been appended at the end of the file for the FACS data. It is a separate chunk of data that can be ignored if you don’t intend to support facial animation yourself. If present, this data is appended directly after the mesh subset data described in version 4.

FileMeshV5

The pseudo-header for FileMeshV5 is as follows:

struct FileMeshV5
{
	FileMeshHeaderV5 header;
	
	FileMeshVertex verts[numVerts];
	FileMeshWeight weights[numVerts]?;
	
	Face faces[numFaces];
	uint lodOffsets[numLodOffsets];
	
	FileMeshBone bones[numBones];
	byte nameTable[nameTableSize];
	
	FileMeshSubset subsets[numSubsets];
[+]	FileMeshFacsData? facsData; // sizeof(FileMeshFacsData) == header.facsDataSize
}

If you have no intention of supporting facial animation, these new blocks of data can be freely ignored and you’ll still be able to read it as normal.

FileMeshFacsData

The FileMeshFacsData of a mesh stores both the FACS Poses and the Combination Poses that were authored by the mesh artist. The facsData is represented by the following struct:

struct FileMeshFacsData
{
	
    uint sizeof_faceBoneNamesBlob;
    uint sizeof_faceControlNamesBlob;
    ulong sizeof_quantizedTransforms;
	
    uint sizeof_twoPoseCorrectives;
    uint sizeof_threePoseCorrectives;

    byte faceBoneNamesBlob[sizeof_faceBoneNamesBlob];
    byte faceControlNamesBlob[sizeof_faceControlNamesBlob];
    QuantizedTransforms quantizedTransforms;

    TwoPoseCorrective twoPoseCorrectivess[sizeof_twoPoseCorrectives / 4];	
    ThreePoseCorrective threePoseCorrectives[sizeof_threePoseCorrectives / 6];
}

Face Bone/Control Names

The faceBoneNames and faceControlNames correlate to variable-width arrays of UTF-8 strings, similar to the boneNames in v4 meshes.

This time however, there’s nothing explicitly mapping where each string starts, nor any indication of how many strings are in both buffers. You instead just have to manually split each buffer into a list of strings, where \0 is the terminator for each string.

The bone names represent which bones are targeted for influence by the FaceControls object, while the control names are abbreviations for properties of the FaceControls object.

The abbreviations map to the following FaceControls properties:

Abbreviation FaceControls Property
c_COR Corrugator
c_CR ChinRaiser
c_CRUL ChinRaiserUpperLip
c_ELD EyesLookDown
c_ELL EyesLookLeft
c_ELR EyesLookRight
c_ELU EyesLookUp
c_FN Funneler
c_FP FlatPucker
c_JD JawDrop
c_JL JawLeft
c_JR JawRight
c_LLS LowerLipSuck
c_LP LipPresser
c_LPT LipsTogether
c_ML MouthLeft
c_MR MouthRight
c_PK Pucker
c_TD TongueDown
c_TO TongueOut
c_TU TongueUp
c_ULS UpperLipSuck
l_BL LeftBrowLowerer
l_CHP LeftCheekPuff
l_CHR LeftCheekRaiser
l_DM LeftDimpler
l_EC LeftEyeClosed
l_EULR LeftEyeUpperLidRaiser
l_IBR LeftInnerBrowRaiser
l_LCD LeftLipCornerDown
l_LCP LeftLipCornerPuller
l_LLD LeftLowerLipDepressor
l_LS LeftLipStretcher
l_NW LeftNoseWrinkler
l_OBR LeftOuterBrowRaiser
l_ULR LeftUpperLipRaiser
r_BL RightBrowLowerer
r_CHP RightCheekPuff
r_CHR RightCheekRaiser
r_DM RightDimpler
r_EC RightEyeClosed
r_EULR RightEyeUpperLidRaiser
r_IBR RightInnerBrowRaiser
r_LCD RightLipCornerDown
r_LCP RightLipCornerPuller
r_LLD RightLowerLipDepressor
r_LS RightLipStretcher
r_NW RightNoseWrinkler
r_OBR RightOuterBrowRaiser
r_ULR RightUpperLipRaiser

You’ll want to map both of these buffers into two arrays, hereby labeled faceControlNames and faceBoneNames.

Reading The Transform Data

The quantizedTransforms is represented by the following struct:

struct QuantizedTransforms
{
	QuantizedMatrix px;
	QuantizedMatrix py;
	QuantizedMatrix pz;

	QuantizedMatrix rx;
	QuantizedMatrix ry;
	QuantizedMatrix rz;
}

QuantizedMatrix (which will be spec’d below) is a 2D array of compressed floating point channels, where the 2D array is indexed via:

  1. The index of the specified FACS pose in the faceControlNames table.
  2. The index of the specified Bone in the faceBoneNames table.

The 6 matrices stored sequentially in QuantizedTransform correlate to the positions and rotations mapped out for each facial animation pose.

QuantizedMatrix

The QuantizedMatrix object itself is defined with the following struct:

struct QuantizedMatrix
{
	ushort version;
	uint rows, cols;

	// if version == 1 ...
	float v1_matrix[rows][cols];

	// elseif version == 2 ...
	float v2_min, v2_max;	
	ushort v2_matrix[rows][cols];
}

The version field indicates whether the floating point values are stored raw (1) or quantized (2).

When quantized, the values in v2_matrix map in the range [0-65535] to the specified [v2_min-v2_max] floats of the struct. Each value then gets converted into a float through the following formula:

value = v2_min + (quantizedValue * precision)
where precision = (v2_max - v2_min) / 65535f

The rows of the matrix map to bones defined in the Face Bone Names table declared earlier, and the cols map to any number of the 50 standard FACS poses, plus any corrective poses declared.

You can expect the value of cols to be:
numFaceControlNames + (sizeof_twoPoseCorrectives / 4) + (sizeof_threePoseCorrectives / 6)

Corrective Poses

In addition to the usual 50 standard FACS poses, Roblox also allows the definition of corrective poses, which are poses that represent the combination of 2-3 FACS poses.

The 2-3 combinations for these poses are serialized using twoPoseCorrectivesBuffer and threePoseCorrectivesBuffer respectively. Each buffer represents an array of 16-bit integers, whose number of values are divisible by 2 and 3. We can map them out as such:

var twoPoseCorrectives = new TwoPoseCorrective[sizeof_twoPoseCorrectivesBuffer / 4]
var threePoseCorrectives = new ThreePoseCorrective[sizeof_threePoseCorrectivesBuffer / 6]

The TwoPoseCorrective and ThreePoseCorrective types match the following declarations:

struct TwoPoseCorrective
{
	ushort controlIndex0;
	ushort controlIndex1;
}

struct ThreePoseCorrective
{
	ushort controlIndex0;
	ushort controlIndex1;
	ushort controlIndex2;
}

The indices of each corrective declaration reference previously defined controls. You’ll want to fetch the names of these controls and add them to your list of face controls with some special marker indicating they are a corrective pose.

For two pose correctives, the name could be be something like:

local controlA = faceControlNames[corrective.controlIndex0]
local controlB = faceControlNames[corrective.controlIndex1]
table.insert(faceControlNames, `{controlA}_{controlB}`)

And for three pose correctives:

local controlA = faceControlNames[corrective.controlIndex0]
local controlB = faceControlNames[corrective.controlIndex1]
local controlC = faceControlNames[corrective.controlIndex2]
table.insert(faceControlNames, `{controlA}_{controlB}_{controlC}`)

Pre-requisite to Composing the Data…

There are a couple important notes to make before putting all of the matrix data together:

  • Keep the Position and Rotation data as a Vector3 until you’ve factored in all possible transforms and corrections. — This is important for parity with how Roblox applies the facial animations. There may be multiple sources of positioning and rotation coming into each bone, and they should not be multiplied together as CFrames or else you’ll get object space pivoting and things will look incorrect.
  • Position is separate from Rotation — Translations to the bone are always relative to the bone’s original frame of reference. Rotations are applied afterwards.

The FACS joint controls compose additively. The position and rotation mapped to each FaceControls property is scaled by the 0-1 weight value taken on by each said property.

Composing the Data into CFrames

To load the poses for the FaceControls object, use the following steps:

  • For each control name in faceControlNames (hereby labeled i) …
    • For each bone in faceJointNames (hereby labeled j)…
      • Read the value matrix[i][j] from each channel of the QuantizedTransforms data blob to get the XYZ Vector3 Position and Rotation of this bone.
      • Store these vectors for later use.

To apply the entire state of a FaceControls object:

  • Loop through the joints in faceJointNames
    • Create two empty “total” vectors for Position and Rotation respectively.
      • Loop through each name in faceControlNames
        • Fetch the stored Position and Rotation of the bone for the faceControl.
          • If the faceControl is a corrective pose…
            • Fetch the 0-1 weight values of the 2-3 faceControl properties being corrected.
            • Multiply the weights together, this will be the weight value you apply.
          • Otherwise…
            • The weight value is the defined 0-1 weight value of the faceControl property.
        • Scale the Position and Rotation vectors by this 0-1 weight.
        • Add the scaled vectors to their respective “total” vectors.
    • Convert the Rotation into a CFrame by…
      • Multiplying the vector by pi / 180
      • Passing the multiplied XYZ values into CFrame.fromEulerAnglesXYZ
    • Add the Position value to the calculated CFrame value.
    • The CFrame can now be applied to the joint’s Transform.

Handling Corrective Poses

As mentioned above, corrective poses have derivative weights and cannot have their 0-1 weight value directly modified. Additionally, activating a 3-pose corrective will also include potential 2-pose correctives.

See this table for a list of which corrective poses you can hypothetically expect to be activated:

Active Controls Correctives Applied Total Active Controls
a + b a_b 3
a + b + c a_b + b_c + a_c + a_b_c 7

version 6.00

version 6.00 introduces a chunk layout scheme to help future revisions deal with new additions to the mesh schema, and a (currently unused) chunk of metadata to flag whether certain faces should be ignored by Roblox’s Hidden Surface Removal system.

Chunk Format

Fixed file headers are gone in this version. Going forward, chunks immediately follow the version 6.00\n header, and are described with the following struct:

struct MeshChunk
{
    byte chunkType[8];
    uint chunkVersion;

    uint size;
    byte data[size];
}

There is no chunk count. You are expected to keep reading chunks until reaching the end of the file, which should be aligned with exactly how many bytes were need to be read to the last chunk.

The data in these chunks derives heavily from the types defined above, so look to them if you aren’t sure what something is down here.

Chunk Types

"COREMESH"

The COREMESH chunk holds all of the vertex/face information about the mesh. In version 6.00 it uses chunkVersion 1.

The following struct describes the data part of a v1 COREMESH chunk:

struct FileMesh_COREMESH_v1
{
    uint numVerts;
    FileMeshVertex verts[numVerts];

    uint numFaces;
    FileMeshFace faces[numFaces];
}

"LODS"

The LODS chunk holds all of the LOD metadata and offset indices for a mesh that has LODs. In version 6.00 it uses chunkVersion 1.

The following struct describes the data part of a v1 LODS chunk:

struct FileMesh_LODS_v1
{
    ushort lodType;
    byte numHighQualityLODs;
    
    uint numLodOffsets;
    uint lodOffsets[numLodOffsets];
}

"SKINNING"

The SKINNING chunk holds all of the skinning, bones (+ their names), and subsets needed to add mesh deformation support. In version 6.00 it uses chunkVersion 1.

The following struct describes the data part of a v1 SKINNING chunk:

struct FileMesh_SKINNING_v1
{
    uint numSkinnings; // Needs to match numVerts

    uint numBones;
    FileMeshBone bones[numBones];

    uint nameTableSize;
    byte nameTable[nameTableSize];

    uint numSubsets;
    FileMeshSubset subsets[numSubsets];
}

"FACS"

The FACS chunk (short for Facial Action Coding System) stores data for driving facial animation on meshes that support it. In version 6.00 it uses chunkVersion 1.

The following struct describes the data part of a v1 FACS chunk:

struct FileMesh_FACS_v1
{
     uint facsDataSize;
     FileMeshFacsData facsData; // facsDataSize == sizeof(FileMeshFacsData)
}

"HSRAVIS"

The HSRAVIS chunk (short for HSR Always Visible) is currently disabled and it’s unclear if it ever will be supported in the future. From what I’ve been able to gather, it’s just an array of booleans compressed down into bits. Each bit corresponds 1:1 with a face index in the mesh’s faces data. In version 6.00 it uses chunkVersion 1.

The following struct describes the data part of a v1 HSRAVIS chunk:

struct FileMesh_HSRAVIS_v1
{
    uint numAlwaysVisibleBitFlags;
    byte alwaysVisibleBitFlags[(numAlwaysVisibleBitFlags + 7) / 8];
}

version 7.00

version 7.00 increments the version of the COREMESH chunk to 2, and in doing so, introduces the Draco 3D Graphics Compression library from Google into it.

The entire data block of the v2 COREMESH chunk seems to be based around this spec:
https://google.github.io/draco/spec/

I am still in the process of figuring out what exactly comes out of the data once its decompressed and how you use it, but PRESUMABLY you’ll be able to get the verts and faces from it.

Depending on your implementation environment, you may either have to manually implement all of these edge cases, or you can just use the library directly to support it. Either way, you’ll 100% need the library to support encoding version 7.00.

Wrapping Up

Congratulations! You should now hopefully have a surface-level idea of how Roblox’s mesh format works! If you have any questions or insights, please feel free to post them below!

173 Likes

This is a highly needed subject! Good job you uploaded it as a load of people ask how this works.

4 Likes

This was previously a DevHub article that I wrote, but it was recently taken down for some reason. I guess they decided it was too niche to maintain, so I’ll take it from here.

10 Likes

DevHub articles are documentation. We do not document and do not promise anything about the mesh format, it’s purely internal and we can drop support for any format at any point once we convert all existing assets - so DevHub is not a good place for this information.

11 Likes

How do you actually get the meshes from the roblox server?

3 Likes

This is another one of those “things I’m not entirely sure I will need but I’ll learn it anyways”.

Great post! I’m surprised you actually took the time to decipher their system.

7 Likes

Edit from 2023: This edit is severely belated, but a little while after the audio privacy update, asset?id= stopped returning raw data and now just 404s. While there may be other ways to retrieve raw assets from the website, I don’t know anything about those methods. Please know that the following information is long outdated.


The asset?id=[number] link always provides the raw asset. If you copy the link for an asset and paste it into your browser, Roblox will simply provide you the asset as a data stream. For example, using this decal, you can use the asset link, which returns just the data for the image. Web browsers will cause a download prompt to appear. You can then apply the .png extension locally, and your OS will correctly register that the image is a PNG file.image
This is also applicable to everything else that Roblox serves in this manner, e.g. audio and meshes.

9 Likes

Hey everyone! I have extended this to include a WIP specification for version 4.00 of Roblox’s mesh format. I still haven’t fully deciphered the purpose of all the data it stores, but I’ve got it down enough to where it can at least be read fully.

Check out the version 4.00 [WIP] section to learn more!
Let me know if you guys have any insights to things I left stubbed out or wasn’t sure about.

11 Likes

Some time ago, I discovered some Python 2 code for a .mesh parser on GitHub and modified it to run standalone. Though these changes weren’t major, I’ve since lost it to hard-disk damage.

5 Likes

Um I have a question I gotta ask, Um how do you write the script mesh format 4.00 on the roblox studio?. Because I was trying to import a mesh the has like between 46k and 70k tris. And I was trying to figure out a way how to import them. But I don’t how to make a script for it to reduce it’s self. And I really need some help to learn how to do it.

Has anyone written anything that can read 1.00 mesh data and export it to a .obj?

1 Like

Really very interesting, I think I will use it for a project. Thanks for that! P.S.: How did you even get this information out?

I reverse engineered it, nothing too special lol.

3 Likes

Wow, I didn’t know. Anyway, it sounds very interesting, thanks for that!

This to me, seems to be incorrect, at least for 1.00 meshes. I wrote a parser for 1.x and 2/3 meshes, and whenever I invert the tex_V coord like mentioned here, textures look incorrect


This could just be a quirk with THREE.js or something with my code.

3 Likes

From checking some of the S15_Lola V4.00 mesh files I have the following comments that may be useful.

  1. The meshHeader field numSkinData is only 16 bits not 32 (and is followed by a 16 bit number set to 1 in mesh files used for S15_Lola)

  2. There is no meshHeader field to define the number of Envelopes so it is the same as numVertices except when the numBones is zero when there are no Envelope structs in the file.

  3. The SkinData struct appears to be an extension to the LOD data (the facesBegin and facesLength contain the same information).

  4. The SkinData struct would appear to define a different section of the shared vertex and face array and to allow the mapping of bone indices to the bone array.

2 Likes

I did notice a couple of these the other day when I was fixing some implementation issues with the mesh format in Rbx2Source

2 Likes

Hey everyone.

I’ve made a revision to the spec which accounts for the new tangent vector data, which is packed into the 4 bytes that were previously used for the UV’s dummied out tw Z-coordinate.

Let me know if you have any questions!

3 Likes

Made another set of revisions to finalize a majority of the version 4.00 mesh format specification!

4 Likes

ofc maybe we can of version format revisions that’s how mesh like question