MeshSets and vertex declarations
A MeshSet resource (0x49b156d4) describes LODs and their material sections. Each LOD names a geometry chunk
holding vertex buffers followed by the index buffer. Stored pointers are relative to byte 16 of the resource.
A census reads all 11,619 MeshSets found across the base-game TOCs without parse failures. Proven · all 11,619 MeshSets
The header holds u64 LOD pointers starting at 0x30 and a u16 LOD count at 0xb4. LODs are located through
those pointers; their allocation stride is not needed to locate the records. Proven · MeshSet census
A MeshSet is one relocatable block: fixed-size records whose pointers name a pool after them, then a relocation table, then trailing data that only one development mesh has. Its order is the header, the LOD records, every LOD’s section records, then the pool. Every one of the 47,758 MeshSets of the base data, and its bundle resource metadata, is written back byte for byte from this description. Proven · all 47,758 MeshSets
0xd0 bytes, or 0xe0 when the mesh carries bone bounds. Pointers are relative to byte 16.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | u32 | header bytes minus 16 | Proven | |
| 0x4 | 4 | u32 | LOD record bytes | Proven | 0xb0, 0xb8 with bone masks, 0xbc with a bone map |
| 0x8 | 4 | u32 = 384 | section record bytes | Proven | |
| 0xC | 4 | bytes | unknown | Unknown | |
| 0x10 | 16 | f32[4] | bounds minimum | Observed | Over every section's bounds. |
| 0x20 | 16 | f32[4] | bounds maximum | Observed | |
| 0x30 | 56 | u64[7] | LOD record pointers | Proven | At most seven LODs. |
| 0x68 | 8 | u64 | name pointer | Proven | |
| 0x70 | 8 | u64 | pointer to the name's last part | Proven | |
| 0x78 | 4 | u32 | name hash | Observed | djb2-XOR of the MeshSet name; equals the mesh asset's NameHash. |
| 0x7C | 1 | u8 | mesh type | Observed | 0 rigid (46,977 MeshSets), 1 skinned (296), 2 composite (485); the names are Frostbite's mesh types |
| 0x7D | 1 | u8 | unknown | Unknown | 3 on 47,666 MeshSets, 5 on 92 |
| 0xA8 | 4 | u32 | layout flags | Observed | bit 0: geometry in chunks (clear only on the MeshSet with inline geometry); bit 2: some LOD sets its bit 4; bits 1, 4, 7, 8 to 23 and 29 to 31 take other values that are not named here |
| 0xB4 | 2 | u16 | LOD count | Proven | |
| 0xC4 | 2 | u16 | bone count | Proven | 0xe0 header only. |
| 0xC6 | 2 | u16 | bounded bone count | Proven | 0xe0 header only. |
| 0xC8 | 8 | u64 | bounded bone ids pointer | Proven | 0xe0 header only; u16 each |
| 0xD0 | 8 | u64 | bone bounds pointer | Proven | 0xe0 header only; f32x4 minimum and maximum per bounded bone, else per bone |
The pool holds, in order: the bone lists (then 16-aligned); the strings, per LOD its section names then its
Mesh:<name>_lodN name, then the MeshSet’s name (16-aligned); each LOD’s five section-index lists (16-aligned);
each LOD’s bone masks (16-aligned) or bone map (4-aligned); the bone ids and bone bounds (each 16-aligned).
The relocation table lists every pointer field as a u32 of its offset minus 16, in record order. The bundle’s
resource metadata is {table offset, table bytes, trailing bytes, 0}. Proven · all 47,758 MeshSets
Exactly one stock MeshSet has trailing data (2,001,568 bytes, a development character mesh), and its LOD 0 is the only one of the 126,014 LODs whose geometry chunk GUID is all zero. That LOD’s vertex buffer (1,733,616 bytes) and index buffer (267,948 bytes) add up to 2,001,564 bytes, four less than the trailing data, so the trailing data is that LOD’s geometry stored inside the resource instead of in a chunk. Proven · all 47,758 MeshSets for the counts The trailing data is the vertex buffer, then the index buffer, then four zero bytes: read as the chunk is read, all 133,974 16-bit indices name one of the section’s 24,078 vertices (the largest is 24,077, and 24,078 × 72 bytes is the vertex buffer), and the last four bytes are zero. Observed · the one MeshSet with trailing data
Little-endian; offsets are relative to the pointed-to LOD record.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x8 | 4 | u32 | section count | Proven | |
| 0xC | 8 | u64 | section array pointer | Proven | |
| 0x14 | 60 | {u32, u64}[5] | section-index lists | Proven | Count and pointer of five byte lists, at 12-byte steps |
| 0x50 | 4 | u32 | layout flags | Observed | bit 6: the geometry is a chunk (clear only on the LOD with inline geometry); bit 4 only on LODs of MeshSets that set header bit 2; bits 0, 9, 17 and 18 take other values that are not named here |
| 0x54 | 4 | u32 | index format | Proven | 0x21 is 16-bit; 0x2e is 32-bit |
| 0x58 | 4 | u32 | index buffer byte count | Proven | |
| 0x5C | 4 | u32 | vertex buffer byte count | Proven | Multiple of 16 in every stock MeshSet; the game also loads community MeshSets that are not (65,400) |
| 0x74 | 16 | GUID | geometry chunk | Proven | |
| 0x8C | 8 | u64 | name pointer | Proven | |
| 0x94 | 8 | u64 | pointer to a part of the name | Proven | |
| 0x9C | 8 | u64 | pointer to the name's last part | Proven | |
| 0xA4 | 4 | u32 | path-part name hash | Observed | djb2-XOR of <name>_lod0 |
| 0xB0 | variable | bytes | bone masks or bone map | Proven | 0xb8 records: pointer to a 24-byte bone mask per section; 0xbc records: u32 count, then pointer to u32 bone map |
LOD records are 16-aligned, so a 0xb8- or 0xbc-byte record occupies 192 bytes. Proven · all 47,758 MeshSets
In the geometry chunk the index data starts at the vertex buffer byte count; indices are relative to their section. Proven · MeshSet census
The mesh type decides the form of the LOD records and the header: a rigid MeshSet has 0xb0-byte LOD records and the 0xd0-byte header, a skinned one 0xbc-byte records (a bone map) and the 0xe0-byte header with bone bounds, a composite one 0xb8-byte records (a bone mask per section) and the 0xe0-byte header; every section of the 781 non-rigid MeshSets declares blend indices and no section of a rigid one does. Proven · all 47,758 MeshSets Header bit 0 and LOD bit 6 are clear exactly on the one MeshSet, and its one LOD, whose geometry is stored inside the resource. Header bit 2 is set on exactly the 321 MeshSets with a LOD that sets LOD bit 4. Proven[all 47,758 MeshSets and 126,014 LODs] The other bits of both words take several values (a block of four bits at each of header bits 8 to 11, 12 to 15, 16 to 19 and 20 to 23, whose counts rise within the first block: 31,976, 32,149, 33,300, 37,009 MeshSets) and their meaning is not known; LOD bit 0 is set on 50,130 LODs and is not the first LOD of its MeshSet. Observed[all 47,758 MeshSets]
The five lists name the sections each pass draws: list 0 the main pass, list 2 decals, lists 3 and 4 the depth and shadow passes. Inferred This is the order of the subset categories Frostbite meshes use in other games (opaque, transparent, transparent decal, depth-only, shadow). Inferred
The lists are used unevenly. Of the 126,014 LODs of the 47,758 stock MeshSets, list 0 is non-empty in 123,529, list 1 in 228, list 2 in 9,054, list 3 in 123,137 and list 4 in 123,285. The 300,875 sections fall into 23 combinations of list membership and section flags; the largest are below. Proven · all 47,758 MeshSets
| Lists holding the section | Section flags | Sections |
|---|---|---|
| 0 | 0x0a |
156,048 |
| 3 and 4 | 0x04 |
113,763 |
| 0, 3 and 4 | 0x0e |
10,929 |
| 2 | 0x0a |
9,943 |
| 0 and 3 | 0x0a |
4,242 |
| 4 | 0x04 |
3,134 |
| 3 | 0x00 |
1,268 |
| 3 and 4 | 0x0c |
532 |
The flags byte takes 15 values (0x00, 0x01, 0x04, 0x05, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0e, 0x0f,
0x1a, 0x1b, 0x1e, 0x1f). 0x0a marks both the sections that only list 0 holds (156,048) and the decal sections
that only list 2 holds (9,943), so the byte does not separate an opaque section from a decal. Proven · all 47,758 MeshSets
Where a LOD carries shadow-proxy twins of its opaque sections, its primitive counts
summed over all sections are twice the triangles it draws in the main pass. Observed
Little-endian; undescribed ranges are not interpreted here.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x8 | 8 | u64 | material name pointer | Proven | |
| 0x10 | 8 | u64 | bone list pointer | Proven | |
| 0x18 | 2 | u16 | bone count | Proven | |
| 0x1B | 1 | u8 | flags | Observed | 15 values below 0x20; 0x0a (170,233 sections), 0x04 (116,897) and 0x0e (10,935) are the commonest; the byte does not say which section list holds the section (0x0a is on sections of list 0 alone and of list 2 alone) |
| 0x1C | 1 | u8 | material slot index | Observed | Index in the mesh asset's Materials. |
| 0x1E | 1 | u8 | total vertex stride | Proven | |
| 0x1F | 1 | u8 | primitive type | Observed | 3 (triangle list) in all 300,875 sections |
| 0x20 | 4 | u32 | primitive count | Proven | |
| 0x24 | 4 | u32 | start index | Proven | |
| 0x28 | 4 | u32 | vertex byte offset | Proven | |
| 0x2C | 4 | u32 | vertex count | Proven | |
| 0x54 | 4 | u32 | repeated vertex byte offset | Proven | |
| 0x58 | 4 | f32 | UV0 texel density | Observed | Median over the triangles of sqrt(UV area / area); 1.0 when no triangle has area |
| 0x5C | 4 | f32 | UV1 texel density | Observed | |
| 0x70 | 100 | declaration | stored vertex declaration | Proven | |
| 0xD4 | 100 | declaration | draw vertex declaration | Proven | |
| 0x138 | 8 | u64 | material key | Proven | |
| 0x140 | 4 | u32 | repeated start index | Proven | |
| 0x144 | 4 | u32 | material name hash | Observed | djb2-XOR of the lowercased material name. |
| 0x14C | 4 | f32 | surface area | Observed | Square metres. |
| 0x150 | 16 | f32[4] | bounds minimum | Observed | w is 0 |
| 0x160 | 16 | f32[4] | bounds maximum | Observed |
The surface area matches the area of the decoded triangles on the 1,222 rigid Quick Drop sections with UV0
(1st–99th percentile of stored/measured 0.996–1.003), and the UV0 density matches the median per-triangle
sqrt(UV area / area) on the same sections (25th–75th percentile 0.996–1.001). The density does not fit sections
whose UVs fold many triangles to nothing. Observed
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 64 | Element[16] | element slots | Proven | Each element is four bytes in usage/format/offset/stream order. |
| 0x40 | 32 | u8[16][2] | stream descriptors | Proven | First byte of each pair is stride throughout the MeshSet census; second byte's meaning is not established here. |
| 0x60 | 1 | u8 | element count | Proven | |
| 0x61 | 1 | u8 | stream count | Proven | |
| 0x62 | 2 | bytes | unknown | Unknown |
Stored streams occupy consecutive stride * vertex_count ranges from a section’s vertex offset. The draw
declaration can reorder elements, supply an unbound element on stream 255, alias a stored stream or append
runtime streams; 830 measured skinned sections use the latter. Treating the two declarations as interchangeable
loses this distinction. Proven · MeshSet census
| Usage | Meaning | Common format | Confidence |
|---|---|---|---|
1 |
Position | Float3 0x03 or Half4 0x08 |
Observed |
2 |
Blend indices | UShort4 0x17 |
Observed |
4 |
Blend weights | UByte4N 0x0d |
Observed |
30, 31 |
Color0, Color1 | UByte4N | Observed |
33, 34 |
UV0, UV1 | Half2 0x06 |
Observed |
51 |
Submaterial selector | UByte4 0x0c |
Observed |
52 |
Packed tangent space | UByte4N | Observed |
Packed tangent space
Section titled “Packed tangent space”Usage 52 stores a vertex’s tangent frame in four bytes (UByte4N). Observed
| Bits | Meaning | Confidence |
|---|---|---|
| Byte 3, bits 0–2 | Largest normal axis and sign: code 0–5 is +x, −x, +y, −y, +z, −z | Observed |
| Byte 3, bit 3 | Bitangent sign, set for −1 | Observed |
| Byte 3, bits 4–5 | Ninth bits of the two other normal components | Observed |
| Byte 3, bits 6–7 | Quadrant of the tangent angle | Observed |
| Bytes 0, 1 | Low eight bits of the two other normal components (axes z,y for codes 0–1; x,z for 2–3; x,y for 4–5); a 9-bit value q decodes as (q-256)/(255*sqrt 2) |
Observed |
| Byte 2 | Angle within the quadrant, 0–255 for 0–90° | Observed |
The largest normal component is the remainder of unit length. The tangent is a reference axis per code
(+z, −z, +x, −x, −x, +x) made perpendicular to the normal and turned about it by the angle,
t0 cos a + (n × t0) sin a; the bitangent is sign * (n × t). On the LOD-0 sections of 120 stock Quick Drop
meshes, the decoded normal lies along the face normal (median dot 0.998), the tangent along increasing U (median
1.000) and the bitangent along increasing V (median 0.998). The zero of the 9-bit components is the value 256: over 8,818,960 tangent frames in the 8,629 main-pass sections of the first
6,000 MeshSets read, the 9-bit minor component has the value 256 in 4,188,116 of 17,637,920 components, against 255 in
947,800 and 257 in 179,223, and the most common frame has both minor components 256 (1,055,763 frames, normals along
an axis). Observed Re-encoding gives back 22,085 of 23,704 stored frames
of 60 one-LOD meshes byte for byte; the rest are the same frame under its second encoding, at a quadrant edge or
between two axes. Observed
LOD groups
Section titled “LOD groups”The LOD switch distances are not stored in the MeshSet: the sampled community level Numbani has zero LOD-distance float fields. Distance policy comes from the mesh’s LodGroup. Observed
San Van’s level root uses 230 stock LodGroups across 9,579 MeshSets, which have one to six LODs (762 / 3,527 / 4,009 / 1,145 / 99 / 37). Lod1 is mostly 180, 45, 90 or 112 m, the last switch mostly 180 or 90 m and the terminal distance 1,000 m. CullScreenArea is 0.005–0.02; about 2,000 meshes set a CullDistance enum value 1–4 (detail, short, medium, long cull), 420 set a ShadowDistance of 5–80 m, and the level has 3,021 DingoDetailFadeComponentData. Observed
The stock WorldObject_MediumVista_2Lod group is Lod1 90 m, Lod2 1,000 m, CullScreenArea 0.005 and WpsDistance 4;
DingoVistaLodGroup is Lod1 10,000 m with CullScreenArea 0. Observed
| WpsDistance | Meaning | Confidence |
|---|---|---|
| 0 | Persistent | Inferred |
| 1 | ExtraLongCull | Inferred |
| 2 | LongVista | Observed |
| 3 | LongCull | Inferred |
| 4 | MediumVista | Observed |
| 5 | MediumCull | Observed |
| 6 | ShortCull | Observed |
| 7 | DetailCull | Observed |