Models (.modelbin)
Every model of the game is a .modelbin, a Grub bundle with a header blob
(Modl), one record per mesh (Mesh), vertex layouts (VLay), one index buffer (IndB), vertex buffers (VerB) and
material instances (MatI). A model is one level of detail of a template: a building has one file per LOD, and files
whose names end _clusterNNN.i.modelbin are those levels or pieces of a smashable (LODs and clusters).
Observed
The FH6 mesh record (version 1.13) is where FH6 diverges from FH4 and FH5 and where the published readers stop working. Sourced · ForzaTools
This page describes Steam build 25588841. A strict reader (it refuses an unexpected value, a reserved byte that is not
zero, a blob that does not end where its fields end, a count that does not match the blobs and a reference to nothing)
read all 113,868 modelbins of the Tokyo track Brio’s GeoChunk0 (407,461 meshes) and all 82,752 under media in zips
(cars and characters; 1,339,215 meshes) with no refusals; each statement marked Proven below held on all of them. The
remaining counts come from a recorded census of GeoChunk0 taken before. Observed
A model holds these blobs. The versions and counts are those of the two sets above; the Tokyo track has no morph or instance-layout blobs.
| Tag | Versions (Tokyo and zips) | Blobs in Tokyo; in zips | What |
|---|---|---|---|
Modl |
1.4, 1.5 | 113,868 (110,578 and 3,290); 82,752 (82,374 and 378) | the header |
Skel |
1.0 | 113,868; 82,752 | the skeleton: one in every model |
MatI |
1.0 | 1,134,500; 396,324 | a material slot |
Mesh |
1.12, 1.13 | 407,461 (400,931 and 6,530); 1,339,215 (1,338,642 and 573) | one draw |
VLay |
1.1 | 167,984; 175,041 | a vertex layout |
ILay |
1.1 | none; 144 | an instance layout, stored as a VLay is |
IndB |
1.0 | 113,868; 82,059 | the index buffer |
VerB |
1.1 | 275,919; 233,356 | a vertex buffer |
MBuf |
1.1 | none; 49,600 | a morph buffer |
Skin |
1.1 | 20; 12,434 | a skinning buffer |
Mrph |
0.0 | none; 31,831 | the morph target names |
No other tag, version or metadata record occurs. Proven In the 75,108 Tokyo models that a second reader also read, the
blobs come in the order Skel, MatI, Mesh, IndB, VLay, VerB, Modl, each kind in a run. Observed
Material slots
Section titled “Material slots”A MatI blob is either empty, a slot with no material instance, or a Grub bundle holding the material instance
(Grub bundles). 5,621 of the Tokyo track’s 1,134,500 slots are empty, and 918
of the zips’ 396,324. Proven A mesh’s material id names the Id of one of these slots. Proven
Little-endian. Fields after the LOD flags appear from the blob version shown.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 2 | u16 | mesh count | Proven | |
| 0x2 | 2 | u16 | buffer count | Proven | |
| 0x4 | 2 | u16 | vertex layout count | Proven | |
| 0x6 | 2 | u16 | material count | Proven | |
| 0x8 | 1 | u8 | has LOD | Proven | |
| 0x9 | 1 | i8 | min LOD | Proven | |
| 0xA | 1 | i8 | max LOD | Proven | |
| 0xB | 1 | u8 | padding | Proven | |
| 0xC | 2 | u16 | LOD flags | Observed | bit 1 is LOD0, bit i+1 is LOD i |
| 0xE | 2 | u8[2] | decompress flags | Observed | v1.2 and later: u8 flags, u8 padding |
| 0x10 | 1 | u8 | retain CPU buffer copy | Observed | v1.3 and later |
| 0x11 | 1 | u8 | visibility flags | Observed | v1.4 and later |
| 0x12 | 4 | u32 | variant count | Proven | v1.5 and later, followed by variant count × mesh count bytes: the material variant table |
In .i.modelbin files minLOD and maxLOD are equal (4,992 of 4,992 checked). In animated .a.modelbin files all LODs
share one file and the pair is stored as (coarsest, finest), for example min=4 max=0 with LOD flags 0x3E. Observed
Material variants
Section titled “Material variants”Row v of the variant table gives, for every mesh in file order, the index of the material group that mesh uses in
variant v; row 0 is the base look. A placement picks its row with a byte of its own record (placements,
hand-placed instances). This is how one facade model shows different signs, colour
schemes or window types. Observed The material named …_VARIANT_k is not reliably the one row k selects; in about
28% of models it is not. Observed
The table length is exactly 22 + variant count × mesh count on all 3,290 v1.5 blobs of the Tokyo track and the 378 of the
zips, and every entry names a material group its mesh has. Proven Row 0 is all zeros in the 1,808 v1.5 blobs of the earlier
census. Observed A sample of 90,000 .pgeo records has 42,000 of 42,000 indices
below the model’s variant count and 47,500 of 47,500 zeros on models without a table. Observed
Buffers
Section titled “Buffers”IndB, VerB and MBuf blobs share a header:
From blob version 1: 16 bytes. Before: 12 bytes with two padding bytes in place of the sub-element count and format.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | i32 | length | Observed | |
| 0x4 | 4 | i32 | size | Observed | bytes of payload; or length × stride |
| 0x8 | 2 | u16 | stride | Observed | |
| 0xA | 1 | u8 | sub-element count | Observed | |
| 0xB | 1 | u8 | padding | Observed | |
| 0xC | 4 | u32 | DXGI format | Observed |
Vertex layout (VLay)
Section titled “Vertex layout (VLay)”A VLay blob lists semantic names, then elements of 20 bytes each, then one packed format word per element and, from
v1.1, a flags word. Observed
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 2 | i16 | semantic name index | Observed | |
| 0x2 | 2 | i16 | semantic index | Observed | |
| 0x4 | 2 | i16 | input slot | Observed | |
| 0x6 | 2 | i16 | input slot class | Observed | |
| 0x8 | 4 | i32 | DXGI format | Observed | |
| 0xC | 4 | i32 | aligned byte offset | Observed | -1 means append: accumulate the element sizes of the slot in declaration order |
| 0x10 | 4 | i32 | instance data step rate | Observed |
Slot 0 is normally the position stream; slot 1 packs normals, UVs, colour and tangents. Observed
The elements of every mesh of the Tokyo track and of the zips are only the ones below. Proven Over the 113,868 Tokyo models every element uses the append offset. Observed
| Semantic | DXGI format | Decoding | Confidence |
|---|---|---|---|
POSITION0 (slot 0) |
13 R16G16B16A16_SNORM | s16 / 32767 ×3 (−32768 clamps to −1), then raw × positionScale + positionTranslate; w feeds the normal |
Observed |
NORMAL0 |
37 R16G16_SNORM | (position.w, s16/32767, s16/32767), normalised |
Observed |
TEXCOORD0 to TEXCOORD4 |
35 R16G16_UNORM | u16 / 65535, then the mesh’s per-set transform |
Observed |
COLOR0 |
28 R8G8B8A8_UNORM | four bytes / 255 | Observed |
TANGENT0 to TANGENT4 |
24 R10G10B10A2_UNORM | one tangent per UV set, xyz × 2 − 1 normalised; the high bit of the 2-bit field is the bitangent sign |
Observed |
The float variants (formats 6, 10, 16, 2) of these semantics are read by the reference decoder but no FH6 model uses them. Observed 449 vertices in 79 meshes have a stored normal of (0, 0, 0). Observed
Tangent frame and texture coordinates
Section titled “Tangent frame and texture coordinates”B = (a ≥ 2 ? +1 : −1) × cross(N, T) points along +v of UV set k, with D3D’s v pointing down the image. Against the UV
derivatives of every mesh, T agrees with dP/du for 74.4 million sampled triangles and opposes it for 0.24 million; the
signed bitangent agrees with dP/dv for 73.6 million and opposes it for 52,000. Observed The low bit of the 2-bit field is
not decoded. Unknown
Texture coordinates are normalised: the mesh carries five transforms (uOffset, uScale, vOffset, vScale) and the vertex
shaders compute TEXCOORDn = in × scale + offset. Non-identity transforms are common (texture-atlas sub-rectangles). The
base colour samples TEXCOORD0 in the environment shaders and the signage mask samples TEXCOORD3. Observed
On COLOR0, the environment shaders read .xy: red switches the swatch tint off and green is the palette column (Environment shaders). Observed
Mesh record (version 1.13)
Section titled “Mesh record (version 1.13)”Little-endian. Fields marked with a version appear from that blob version on; offsets are given for the 1.13 layout in reading order, with variable parts called out.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | i32 | material group count | Proven | v1.13 |
| 0x4 | 8 × group count | i16[4][] | material ids | Proven | v1.9 and later: spring, summer, autumn, winter per group; -1 means the group's summer id. Before: one i16 |
| after groups | 2 | i16 | rigid bone index | Proven | |
| +2 | 2 | u16 | LOD flags | Proven | |
| +4 | 2 | u8[2] | LOD level 1 and 2 | Proven | |
| +6 | 2 | u16 | bucket flags | Proven | see below |
| +8 | 1 | u8 | bucket order | Proven | |
| +9 | 1 | u8 | skinning elements count | Proven | v1.2 and later |
| +10 | 4 | u32 | morph target count | Proven | v1.10 and later; a u8 before |
| +14 | 1 | u8 | is morph damage | Proven | v1.3 and later |
| +15 | 1 | u8 | is 32-bit indices | Proven | |
| +16 | 2 | u16 | topology | Proven | 4 = triangle list on all 407,461 meshes |
| +18 | 4 | i32 | index buffer index | Proven | |
| +22 | 4 | i32 | index buffer offset | Proven | |
| +26 | 4 | i32 | index buffer draw offset | Proven | start index, in indices |
| +30 | 4 | i32 | indexed vertex offset | Proven | added to every vertex id |
| +34 | 4 | i32 | index count | Proven | |
| +38 | 4 | i32 | primitive count | Proven | primitive count × 3 = index count on every mesh |
| +42 | 8 | f32, u32 | ACMR and referenced vertex count | Proven | v1.6 and later |
| +50 | 4 + 4n | u32[] | referenced vertex indices | Proven | v1.11 and later |
| next | 4 | i32 | vertex layout index | Proven | |
| +4 | 4 + 20 × buffers | buffer ref[] | vertex buffers | Proven | count, then per buffer i32 buffer id, u32 input slot, u32 stride, u32 offset and, v1.12 and later, a u32 reserved |
| next | 8 | i32[2] | morph and skinning buffer indices | Proven | v1.4 and later |
| next | 4 + 4n | i32[] | constant buffers | Proven | |
| next | 4 | u32 | source mesh index | Proven | v1.1 and later |
| next | 80 | f32[4][5] | texture coordinate transforms | Proven | v1.5 and later: five (uOffset, uScale, vOffset, vScale) |
| next | 32 | f32[4][2] | position scale and translate | Proven | v1.8 and later |
A model with a version 1.13 mesh can have several material groups: 3,209 Tokyo models have a mesh whose group count is not
- Proven Each mesh has index buffer index 0, which is also that blob’s id; the mesh draws
index countindices fromindex buffer offset + draw offset × index stride, with 4-byte indices when the flag is set. Observed No mesh has a (0, 0, 0) position scale. Observed
Draw buckets
Section titled “Draw buckets”bucket flags bits: 0x01 opaque, 0x02 decal, 0x04 transparent, 0x08 shadow, 0x10 not shadow, 0x20
alpha-to-coverage, 0x40 occluder. Values seen in Brio are 0x18, 0x19, 0x1C, 0x38 (foliage, impostors), 0x3C and 0x58
(occluders). Observed
Two kinds of mesh are never drawn in the main view: those with bit 0x40 (every OCC_* mesh has it and no other does:
5,938 of 5,938 in GeoChunk0; they carry POSITION only and no NORMAL), and those with bucket & 0x27 == 0 (no colour pass:
flattenable-bush proxies, area bounds, development meshes). Observed Shadow-only meshes (0x08 set, 0x10 clear) do not occur
in Brio. A test of bucket & 7 alone is wrong because foliage is 0x38. Observed Terrain decals carry bucket 0x38, not
the decal bit. Observed
Decoding a mesh
Section titled “Decoding a mesh”- Read
index countindices from the index buffer atindex buffer offset + draw offset × stride. Observed - The vertex range is
min index + indexed vertex offsettomax index + indexed vertex offset. Observed - For each layout element, take the vertex buffer whose input slot matches and read
offset + (index + indexed vertex offset) × stride + element offset. Every vertex of every mesh lies inside its stream. Observed - Triangles wind (A, C, B) for a left-handed frame; the engine’s frame is Y-up left-handed in metres. Observed
Decoding every mesh of the 113,868 GeoChunk0 models and the 82,752 models in zips strictly (a topology other than a
triangle list, an index count other than three times the primitive count, a semantic or format that is not decoded, an
index or vertex outside its buffer, an input slot no vertex buffer is bound to, a variant row selecting a missing group, a
material id without a MatI, a variant table that does not end at the blob end all count as failures) gave no failures.
Proven Together they hold 792,016,881 indices on 243,811,731 vertices in the Tokyo track and 1,932,566,781 indices on
962,738,195 vertices in the zips. Proven 5,938 Tokyo files contain only occluder meshes. Observed
A mesh can hold the same triangle more than once. Over the 3,297,221 triangles of every LOD of the models placed in the
Shibuya box, 3,565 (168.7 m²) repeat an earlier triangle of the same mesh corner for corner, bit for bit in every vertex
stream and with the same winding; 3,062 of them are environment triangles, 157 environment_rcsm, 95 TERR_Mega_Distant
and 30 deferred_decal_standard. A further 6,843 repeat only the positions, in any corner order (10,408 in all,
5,597 m²); the largest area of those is in environment_glass (172 triangles, 1,453 m², none of them a full repeat).
Observed On an opaque surface a full repeat adds nothing to the picture (a blended one would draw twice), and a position-only repeat in glass is most likely the reversed
back face of a pane, as in the terrain meshes. Inferred
Normals at the instance
Section titled “Normals at the instance”The instanced vertex shaders (environment, uber_rock, uber_rock_array) place a normal as
WORLDNORMAL = n.x × row0 + n.y × row1 + n.z × cross(row0, row1), with the record’s unit rows and no scale, then the pixel
shader normalises it. Positions use the rows times the scale. So on a stretched placement (40% of the Shibuya rows have a
non-uniform scale) the game’s normal is the rotated stored normal, not the geometric normal of the stretched
surface. Observed Whether hand-placed instances go through the instanced path (the non-instanced shader reads a separate
normal matrix filled by the CPU) was not traced. Unknown
What the published readers do with it
Section titled “What the published readers do with it”Run over the 113,868 Tokyo models, the bundle classes of ForzaTools read 75,108 and throw on 38,760: 5,617 on a MatI blob
that is empty, and 33,143 in the mesh record, whose class is written for Forza Horizon 5. Observed That class reads the
morph target count as one byte where version 1.10 and later store a u32, which would move every later field.
Sourced · ForzaTools The importer of ForzaTech-extraction-tools reads 110,591 and raises on 3,208: 3,140 models that have a
mesh with several material groups, and 68 that have a vertex whose stored normal is (0, 0, 0), which it normalises by dividing
by the length. Observed It reads the first group’s four ids of a version 1.13 mesh and carries on from there.
Sourced · ForzaTech-extraction-tools Where both read a model, the header, skeleton, meshes, layouts, buffers and material
slots agree with the strict reader field by field (75,108 and 110,591 models); 69 further models with several material
groups were not compared with the importer. Observed
Animated models
Section titled “Animated models”Five .a.modelbin files exist in Brio, all in GeoChunk0: the Ferris wheel and Equinox ride, the wind turbine
inf_lmt_wind_01_a and the two trains. Their clips are in the track’s animation.zip. Observed They use the same
bundle. Every model, animated or not, has one Skel blob; skinned meshes (93 in the Tokyo track, 55,074 in the zips) add a
Skin blob. Proven
Little-endian. The walk ends 4 bytes before the blob end on every Skel of the Tokyo track and the zips, and those 4 bytes are a u32.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 2 | u16 | bone count | Observed | |
| 0x2 | 4 | u32 | name length | Observed | per bone, followed by the name |
| per bone | 6 | i16[3] | parent, first child, next sibling | Observed | -1 = none |
| per bone | 64 | f32[16] | local rest transform | Observed | row-vector 4×4, translation in the last row |
The Skin blob is one per skinned vertex range, with the buffer header above, DXGI format 34 (R16G16_FLOAT) and stride
4 × influences, each influence an (f16 weight, f16 bone index). The mesh’s skinning element count is the influence count
(1 on the turbine and Equinox, 4 on the wheel). Observed
The stored vertices are the bind pose in model space, not bone-local: grouped by dominant bone, each group is centred on
that bone’s accumulated rest translation (wind turbine Rotor_Blades is at (0, 84.0, −7.19) against a vertex centroid of
(−0.02, 84.16, −7.13)), and every Skel rotation is the identity. Applying the bone transforms again would double the
offsets. Observed Only 8 of 13,310 wheel vertices have more than one non-zero weight. Observed The trains have no LOD0
(min=3 max=1). Observed The ribbon places nine of them in the world; the other five instances are the trains, at the
origin with identity transforms, which the game positions at run time along their clip. Observed
Morph targets
Section titled “Morph targets”Mesh.morph target count is n and the morph data buffer index names an MBuf blob (buffer header format 10,
R16G16B16A16_FLOAT, sub-element count 2n), one per slot-1 vertex buffer, indexed like it. Per vertex there are 2n half4
values: n position deltas then n normal deltas; each vector’s w is the index of its weight slot in the morph weight
constant buffer, and the Mrph blob names the slots as weight[0], weight[1], … Observed The vertex shader computes
p += w[slot] × dp and n = normalize(n + w[slot] × dn) per target, in model space before skinning. Observed
The Tokyo track has no morph buffers; the zips hold 49,600 MBuf blobs, 31,831 Mrph blobs (a u16 count, then each name as
a u32 length and that many bytes) and 732,030 meshes with morph deltas. Proven
Wheels and tyres use morphs to take their size; see Wheels and tyres. Body panels carry one target on slot 0 (damage). Observed
Offset 20 of the 24-byte blob record is not used by any reader found.