Skip to content

Models (.modelbin)

ForzaTechGeometryApplies toFH6

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

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

Modl blobvariableFH6100% known

Little-endian. Fields after the LOD flags appear from the blob version shown.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x02u16mesh countProven
0x22u16buffer countProven
0x42u16vertex layout countProven
0x62u16material countProven
0x81u8has LODProven
0x91i8min LODProven
0xA1i8max LODProven
0xB1u8paddingProven
0xC2u16LOD flagsObservedbit 1 is LOD0, bit i+1 is LOD i
0xE2u8[2]decompress flagsObservedv1.2 and later: u8 flags, u8 padding
0x101u8retain CPU buffer copyObservedv1.3 and later
0x111u8visibility flagsObservedv1.4 and later
0x124u32variant countProvenv1.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

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

IndB, VerB and MBuf blobs share a header:

Buffer blob header16 bytesFH6100% known

From blob version 1: 16 bytes. Before: 12 bytes with two padding bytes in place of the sub-element count and format.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x04i32lengthObserved
0x44i32sizeObservedbytes of payload; or length × stride
0x82u16strideObserved
0xA1u8sub-element countObserved
0xB1u8paddingObserved
0xC4u32DXGI formatObserved

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

Vertex layout element20 bytesFH6100% known
ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x02i16semantic name indexObserved
0x22i16semantic indexObserved
0x42i16input slotObserved
0x62i16input slot classObserved
0x84i32DXGI formatObserved
0xC4i32aligned byte offsetObserved-1 means append: accumulate the element sizes of the slot in declaration order
0x104i32instance data step rateObserved

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

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 recordvariableFH6100% known

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.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x04i32material group countProvenv1.13
0x48 × group counti16[4][]material idsProvenv1.9 and later: spring, summer, autumn, winter per group; -1 means the group's summer id. Before: one i16
after groups2i16rigid bone indexProven
+22u16LOD flagsProven
+42u8[2]LOD level 1 and 2Proven
+62u16bucket flagsProvensee below
+81u8bucket orderProven
+91u8skinning elements countProvenv1.2 and later
+104u32morph target countProvenv1.10 and later; a u8 before
+141u8is morph damageProvenv1.3 and later
+151u8is 32-bit indicesProven
+162u16topologyProven4 = triangle list on all 407,461 meshes
+184i32index buffer indexProven
+224i32index buffer offsetProven
+264i32index buffer draw offsetProvenstart index, in indices
+304i32indexed vertex offsetProvenadded to every vertex id
+344i32index countProven
+384i32primitive countProvenprimitive count × 3 = index count on every mesh
+428f32, u32ACMR and referenced vertex countProvenv1.6 and later
+504 + 4nu32[]referenced vertex indicesProvenv1.11 and later
next4i32vertex layout indexProven
+44 + 20 × buffersbuffer ref[]vertex buffersProvencount, then per buffer i32 buffer id, u32 input slot, u32 stride, u32 offset and, v1.12 and later, a u32 reserved
next8i32[2]morph and skinning buffer indicesProvenv1.4 and later
next4 + 4ni32[]constant buffersProven
next4u32source mesh indexProvenv1.1 and later
next80f32[4][5]texture coordinate transformsProvenv1.5 and later: five (uOffset, uScale, vOffset, vScale)
next32f32[4][2]position scale and translateProvenv1.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

  1. Proven Each mesh has index buffer index 0, which is also that blob’s id; the mesh draws index count indices from index 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

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

  1. Read index count indices from the index buffer at index buffer offset + draw offset × stride. Observed
  2. The vertex range is min index + indexed vertex offset to max index + indexed vertex offset. Observed
  3. 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
  4. 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

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

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

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

Skel blobvariableFH6100% known

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.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x02u16bone countObserved
0x24u32name lengthObservedper bone, followed by the name
per bone6i16[3]parent, first child, next siblingObserved-1 = none
per bone64f32[16]local rest transformObservedrow-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

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

What is the unknown dword of each blob record?

Offset 20 of the 24-byte blob record is not used by any reader found.