( 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:
- The index of the specified FACS pose in the
faceControlNames
table. - 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 labeledi
) …- For each bone in
faceJointNames
(hereby labeledj
)…- Read the value
matrix[i][j]
from each channel of theQuantizedTransforms
data blob to get the XYZ Vector3Position
andRotation
of this bone. - Store these vectors for later use.
- Read the value
- For each bone in
To apply the entire state of a FaceControls
object:
- Loop through the joints in
faceJointNames
- Create two empty “total” vectors for
Position
andRotation
respectively.- Loop through each name in
faceControlNames
- Fetch the stored
Position
andRotation
of the bone for thefaceControl
.- 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.
- Fetch the
- Otherwise…
- The weight value is the defined
0-1
weight value of the faceControl property.
- The weight value is the defined
- If the
- Scale the
Position
andRotation
vectors by this0-1
weight. - Add the scaled vectors to their respective “total” vectors.
- Fetch the stored
- Loop through each name in
- Convert the
Rotation
into a CFrame by…- Multiplying the vector by
pi / 180
- Passing the multiplied XYZ values into
CFrame.fromEulerAnglesXYZ
- Multiplying the vector by
-
Add the
Position
value to the calculatedCFrame
value. - The
CFrame
can now be applied to the joint’sTransform
.
- Create two empty “total” vectors for
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!