bMesh - themeldingwars/Documentation GitHub Wiki
bMesh files, which I assume stands for Binary Mesh are used for the majority of the meshes in Firefall, with R5A files holding some UI meshes and the terrain geo being in another.
The .oMesh extension refers to the same format. It is what shows up in pfx <Model> references and in the bMesh refMeshFileName field.
- Reader and Writer have been implemented in FauFau: BMesh.cs / BMesh32.cs
Version 32 is what the later builds ship and is the version documented below. Anything older is out of date and the engine will attempt to update it on load.
Older versions are not documented. An earlier, partly guessed description of a pre-32 layout used to live on this page. It did not match version 32 and has been removed because it was misleading, but the version 32 layout below is still the best starting point for an older file since the order of the sections has been fairly stable.
The whole file is little endian. Every array is preceded by an int32 count, and the sections
always appear in the order below. There are no offsets or chunk markers anywhere in the file,
so the only way to reach a later section is to walk every preceding one.
| Field | Type | Notes |
|---|---|---|
magic |
char[5] |
bMesh |
version |
uint16 |
32 |
meshUsage |
uint8 |
Not used any more, in practice 0
|
boundsMin |
float[3] |
Local space bounding box |
boundsMax |
float[3] |
Local space bounding box |
tangentType |
uint8 |
0 = Melody, 1 = Turtle |
tangentSplit |
uint8 |
0 = off, 1 = on |
Each row is an int32 count followed by that many elements, in this order:
| # | Section | Element size | Element layout |
|---|---|---|---|
| 1 | vertices |
12 | float x, y, z |
| 2 | vertexLodPositions |
? | Never seen populated, the count is 0 in every file we have |
| 3 | normals |
4 | Packed byte vector, see Packed normals and tangents |
| 4 | texcoord0 |
8 |
float u, v, the texture UVs |
| 5 | texcoord1 |
8 |
float u, v, the normal map UVs |
| 6 | tangents |
4 | Packed byte vector, the 4th byte is the bitangent flag |
| 7 | vertexColors |
4 | uint8 r, g, b, a |
| 8 | faceIndices |
4 |
uint32, used to look up the vertices referenced by a face |
| 9 | faces |
12 | uint32 v1, v2, v3 |
| 10 | bones |
200 | See Bone |
| 11 | links |
8 | See Link |
| 12 | boneWeights |
2 |
uint8 influence, uint8 boneIndex
|
| 13 | hardpoints |
388 | See Hardpoint |
Then the reference mesh block, which is not an array:
| Field | Type | Notes |
|---|---|---|
refMeshFileNameLen |
int32 |
0 when there is no reference mesh |
refMeshFileName |
char[n] |
Only present when the length is non-zero, points at an .oMesh
|
refMeshFileTime |
uint64 |
File time of the reference mesh |
meshType |
uint8 |
0 = none, 1 = base variant, 2 = conform mesh |
And then three more counted arrays:
| # | Section | Element size | Element layout |
|---|---|---|---|
| 14 | baseVariantDiffs |
6 | See Base variants and conform meshes |
| 15 | conformWeights |
12 | See Base variants and conform meshes |
| 16 | materialSections |
variable | See Material section |
| Field | Type | Notes |
|---|---|---|
name |
char[64] |
Null terminated, zero padded |
parent |
int32 |
Index into the bone array |
bindMatrix |
float[4][4] |
|
inverseBindMatrix |
float[4][4] |
|
hasSkinnedVertices |
uint8 |
1 if any vertex is skinned to this bone |
padding |
uint8[3] |
| Field | Type | Notes |
|---|---|---|
name |
char[64] |
Null terminated, zero padded |
parent |
int32 |
Index of the parent bone |
bindMatrix |
float[4][4] |
Absolute transform for the hard point |
inverseBindMatrix |
float[4][4] |
Used for skinning |
hpToBoneMatrix |
float[4][4] |
Hard point space to bone space, bindMatrix * bone.inverseBindMatrix
|
normalizedBindMatrix |
float[4][4] |
|
normalizedHpToBoneMatrix |
float[4][4] |
Two uint32 values per entry. The two known readers disagree on what they mean:
- FauFau reads them as
startWeightandweightCount, where the count is 1 to 4. That makes the array a per-vertex range intoboneWeights, which matches how the four influence slots of a skinned vertex are normally stored. - The 010 template reads them as
weightIndexandvertexIndex.
Both readings consume the same bytes, so either one parses the file. Treat the meaning as unconfirmed until it has been checked against a skinned mesh with known weights.
| Field | Type | Notes |
|---|---|---|
nameLen |
uint32 |
|
name |
char[n] |
Material name, not a file path |
faceStart |
uint32 |
First face covered by this material |
faceCount |
uint32 |
Number of faces covered by this material |
vertexMin |
uint32 |
Lowest vertex index used by those faces |
vertexMax |
uint32 |
Highest vertex index used by those faces |
Normals and tangents are not compressed, they are quantised. Each component is one unsigned
byte covering the range -1.0 to 1.0:
float Decode(byte b) => b * (2.0f / 255.0f) - 1.0f;
byte Encode(float f) => (byte)(((255 * (Clamp(f, -1, 1) + 1)) / 2) + 0.5f);The fourth byte differs between the two:
- For a normal it is unused padding.
- For a tangent it is a flag.
255means the bitangent is reversed/mirrored,0means it is not.
A "base variant" is for instance a head type: there is a base head and several variants of it.
baseVariantDiffs encodes the positional difference from this mesh to that base head, as a
uint16 face index followed by two half floats holding barycentric weights.
Conform meshes are meshes such as hair or beards. conformWeights holds three pairs of
uint16 geometry index plus half float weight, and is what lets a hair or beard mesh be
reshaped to fit any head.
refMeshFileName points at the mesh this one is a variant of or conforms to, and meshType
says which of the two roles this file plays.