Skip to content

PP_F models (.c3d, .sky, .gro, .obj, .sht) ​

All 3D geometry is one format with the magic PP_F: car models (Game/Cars/*.c3d), the stage mesh (temp.c3d in every stage archive), the sky dome (temp.sky), the ground/backdrop (temp.gro), the scenery objects (temp.obj) and temp.sht. Files are wrapped in the BFL layers.

A PP_F file is a memory image: it is used where it was loaded, and the loader turns the offsets inside it into pointers by adding the base address of the array the field points into (C3d_Reloc 0x4b98f0: -1 becomes NULL, anything else gets the base added; the values are byte offsets from the start of that array). The port cannot keep 32-bit pointers inside a 64-bit-safe structure, so it keeps offsets and resolves them at use (or reads the records into native structs). Either way the record sizes below are fixed by the original layout.

C3d_Load 0x4b9380 checks the magic and calls C3d_Fixup 0x4b93c0. Its second argument is a parent object: when given, the model's root objects (+0x24 = 0xffff) are appended to that object's children (Obj_Attach 0x4ac200) instead of keeping their own parent field. The third goes to the texture loader.

Header (0x30 bytes) ​

OffsetTypeMeaning
0x00char[4]PP_F
0x04u32Kind/version: 6 for cars, 2 for temp.c3d, 5 for .sky/.gro, 4 for .sht, 0 for .obj. Passed on to the texture loader
0x08u32Offset of the texture-slot table (= end of the record arrays)
0x0cu32Offset of the texture path table
0x10u32Count of 0x14-byte records (equals the 0x4c count in cars)
0x14u32Vertex count: one 0x30-byte record and one 4-byte record each
0x18u16Object count (0x18c-byte records)
0x1au16Mesh count, first kind (0x120-byte records)
0x1cu16Mesh count, second kind (0x120-byte records plus one 0xa0-byte record each)
0x1eu16Count of 0x88-byte records (360 in a stage: track sections; 0 in cars)
0x20u32Count of 0x4c-byte records (polygons/strips; each has 10 texture indices, below)
0x24u16Texture count
0x26u16Count of 0x5c-byte records
0x28u16Count of 0x1c-byte records
0x2au16[3]unused so far

Body ​

The arrays follow the header in this order; the start of each is the end of the previous one:

0x30            objects        n18 x 0x18c
                meshes         (n1a + n1c) x 0x120     each starts with its part name, e.g. "01LFWhlLowL"
                meshes2 extra  n1c x 0xa0
                sections       n1e x 0x88
                (0x1c)         n28 x 0x1c
                polygons       n20 x 0x4c
                vertices       n14 x 0x30
                vertex extra   n14 x 4
                (0x14)         n10 x 0x14
                (0x5c)         n26 x 0x5c
[+0x08]         texture slots  n24 x 24     name[12] ("Texture 0"), u32 flags, u32, u16 index, u16
[+0x0c]         texture paths  n24 x 260    the artist's path, e.g. U:\Cars2\Focus\CRes03\Hier\UsePC\CFFDBoDF

The sum of the array sizes from 0x30 equals the value at +0x08 exactly in every file checked (cars of every set, stage meshes), which confirms the record sizes. The texture paths are MAX_PATH (260) slots; only the last path component matters: it is looked up in the mounted texture archive as <name>.dds, then <name>.tga (bfl.md).

Relocations done by C3d_Fixup (each into the named array):

RecordFieldInto
object+0x00 next sibling, +0x04 first child, +0x08 parent, +0x170objects
object+0x0cmeshes
mesh (per part, below)+0x0c vertices, +0x20 the 0x14 array, +0x24 polygons, +0x34 vertex extrasas named
mesh+0x10cthe 0x5c array
mesh2 extra (0xa0)+0x0cthe second-kind meshes; +0x98 the 0xa0 array
section (0x88)+0x10meshes
0x5c record+0x58the 0x5c array (a list)
polygon10 texture indices at +0x04..+0x2brebased by the running texture count (DAT_0065fa20), -1 stays

A mesh has parts of 0x108 bytes from +0x0c, their count in the byte at +0x113; the fixup walks them at that stride even past the 0x120-byte record, but the count is 1 in every file on the disc, so in practice the part is the mesh. Per part the original then copies the vertices into a shared vertex buffer (Mesh_AddToVB 0x4b1d00: buffers of 2,000 vertices, up to 100, "Failed to add shape to vertex buffer") and groups the triangles into runs of the same texture (Mesh_Batches 0x4b1ac0). The parts are registered in global tables (objects at 0x67f36c, meshes at 0x67b124 when the model has no sections, second-kind extras at 0x669364, 0x5c records at 0x66f124, sections at 0x71f608), which the renderer walks.

The port keeps the offsets and resolves them at use; C3d_Load (src/c3d.c) checks every field above against its array. All 1,211 PP_F files on the disc pass, and the resolved triangles hash the same in the C reader and tools/cmr2fmt.py.

Object (0x18c bytes) ​

OffsetTypeField
0x00, 0x04, 0x08offsetnext sibling, first child, parent (objects; -1 none)
0x0coffsetmesh (-1: a pure transform node, such as a car's root); for a light, a D3DLIGHT7
0x24i16the stage section the object is in, -1 none (roots on disc; -1 for all after loading)
0x26u16the object's slot in the global table, written at load
0x28i16[3]neighbouring sections (run time)
0x30u32type: low byte 0xfd a node whose children keep their local matrix as world; bit 12 (an id such as 0x1000, 0x1001, 0x64001002 seen)
0x58i32[16]rest matrix, 16.16 fixed point, row vectors (v' = v M, translation in the fourth row)
0x98i32[16]current local matrix (object to parent; equal to the rest one on disc)
0xd8i32[16]world matrix (identity on disc); its translation (+0x108) places the object in a section
0x118f32[16]world matrix in floats (run time)
0x170offsetnext object in the same section (run time)
0x174u32dirty (1 on disc)
0x178u32kind: 0 mesh, 1 light, 2, 3 node (a car's root is 3)
0x17cu8the drawing passes it takes part in, one bit each (0xff on disc)
0x180u32asks its section whether it is active (with a section and type bit 12 clear)
0x184u32takes its parent's world matrix as it is

How they are used: world.md.

Car wheels show the convention: the left wheels have -1.0 on the diagonal (mirrored) and their translation is the hub position. Vertices are in the object's space; chaining the first matrix up the parents places the parts (tools/cmr2fmt.py obj).

Mesh (0x120 bytes) ​

OffsetTypeField
0x00char[12]part name (01WhlLF01, 05BonCam, 06UnlitSim, 07SemiTrans; c3dShp<n> in stages)
0x0coffsetfirst vertex (and its 0x30-byte records follow)
0x10u32vertex count
0x14, 0x18u32vertex buffer number and first index in it (written at load)
0x20offsetinto the 0x14 array (-1 in stages)
0x24offsetfirst polygon
0x28u32polygon count
0x2cu321 seen
0x30u32flags (below; bit 18 cleared at load)
0x34offsetfirst vertex extra
0x38ptr[]texture batches (built at load, Mesh_Batches), count at 0x100
0x10coffsetinto the 0x5c array
0x112u8the part drawn (Mesh_Draw 0x49c940 takes part +0x112 of +0x113)
0x113u8part count (1)

Flags, as Mesh_Draw reads them: 0x1 two-sided (cull off), 0x8 alpha-blended (when 0x5207b4 is set), 0x20 keep the current lighting mode (else mode 2, or 3 with 0x40000), 0x200 reflective (a second pass with the reflection option), 0x2000 drawn polygon by polygon. Over all 468,590 meshes on the disc: 0x20 is set on all but 1,540, 0x40 on all, 0x800 on nearly all; 0x200 on 721, 0x2000 on 951; 0x40000 on none (it is set at run time). Every mesh has one part, at most 32 texture runs (the record has room for 50 batches) and at most 306 triangles in a run.

Polygon (0x4c bytes) ​

Every polygon is a triangle.

OffsetTypeField
0x00u32flags (0x8002, 0x8004 in cars); low 7 bits 0x70, 0x71, 0x73, 0x74 make one draw variant wrap stage 0 instead of clamping
0x04i32[10]texture indices: +0x04 stage 0, +0x08 stage 1 (the batching uses these two), -1 none
0x2cu32frame: which of the 10 texture indices is current (the polygon-by-polygon draw reads it; 0 on every polygon on the disc)
0x30u32unknown (1)
0x34u32[3]ARGB colours, one per corner (ff808080)
0x40u16[3]vertex indices, relative to the mesh's first vertex
0x46u16unknown
0x48u32unknown (1)

Texture slots and paths ​

C3d_Textures 0x4b9910 loads the model's textures from the path table: it upper-cases each path in place (Text_ToFontCase), keeps the part after the first backslash and formats a file name with a directory prefix and .tga ("%s%s.tga"); kind 6 (cars) take another route first. The texture member is then found by its base name as <name>.dds, then <name>.tga (bfl.md).

Vertex (0x30 bytes) ​

The vertex is already in the layout Direct3D 7 draws, flexible vertex format D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_SPECULAR | D3DFVF_TEX2:

OffsetTypeField
0x00f32[3]position
0x0cf32[3]normal
0x18u32diffuse, ARGB (ff808080 is typical)
0x1cu32specular, ARGB
0x20f32[2]texture coordinates, stage 0
0x28f32[2]texture coordinates, stage 1

The 4-byte per-vertex extra is a second colour (00808080 typical), probably the prelit/shadow colour.

Examples ​

FileKindObjectsMeshesSectionsPolygonsVerticesTextures
f99c3.c3d (Focus, set c3)698066988725
f99a1N.c3d (Focus, a1 normal)6151409351,16627
f99a1L.c3d654028834412
fra02 temp.c3d2014836012,76923,24454

Open ​

  • The remaining object fields (0x10-0x57, 0x118-0x16f, 0x174-0x18b), polygon flags, the second-kind meshes and their 0xa0 extras, the section record (0x88).
  • Meaning of the 0x14, 0x1c and 0x5c arrays.
  • How a car's kind-6 texture route differs.
  • How the simulation's 16.16 fixed-point world maps onto the float model space (scale and axes).

OpenCMR2 is released under the GPL-3.0. Colin McRae Rally 2.0 is © 2000 The Codemasters Software Company Limited. OpenCMR2 contains no original code or assets and is not affiliated with Codemasters: it runs the game from your own copy. It runs on gasm.