Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3D Model File Format

Binary, little-endian static (single-frame) model format.

.3D files are static (single-frame) models. For the animated multi-frame variant, see 3DC.md.

Versions

Four versions exist: v2.6, v2.7, v4.0, v5.0. See GOG vs Original CD Differences for which directories contain which versions.

  • v2.6 / v2.7 — Different header semantics and face data encoding from v4.0/v5.0.
  • v5.0 — Adds Section4 (SubObject BVH).

Header (64 bytes)

OffsetSizeTypeNameDescription
0x004[u8; 4]versionASCII version string, e.g. v4.0
0x044u32num_verticesVertex count
0x084u32num_facesFace count
0x0C4u32radiusCollision sphere radius
0x104u32num_framesAnimation frame count. Always 1 for .3D files.
0x144u32offset_frame_dataOffset to frame data array
0x184u32total_face_verticesSum of all face vertex counts. Also the entry count for the vertex-normal indirection table.
0x1C4u32offset_section4Offset to Section4 (SubObject BVH). 0 when absent. Engine header copier zeroes this field at load time.
0x204u32section4_countNumber of Section4 entries.
0x244u32unused_24Unused — the engine bulk-copies the 64-byte header but never reads this field. Always 0 in v4.0/v5.0. Non-zero in 5 v2.6 files in /3dart (values 4490, 6434), likely leftovers from an older build pipeline. Parsers should ignore.
0x284u32offset_normal_indicesOffset to vertex-normal indirection table. Each entry is a u32 file offset into the vertex-normal data section. Count = total_face_vertices.
0x2C4u32offset_vertex_normalsOffset to per-vertex normal data (f32 × 3 per vertex).
0x304u32offset_vertex_coordsOffset to vertex coordinate data.
0x344u32offset_face_normalsOffset to face normal data.
0x384u32total_face_vertices_dupDuplicate of field at 0x18 in v4.0/v5.0 (engine header copier copies this from file). Different value in v2.6/v2.7 but engine rejects those versions.
0x3C4u32offset_face_dataOffset to face data. Always 64 (0x40) in v4.0/v5.0. Variable (112–3736) in v2.6/v2.7.

v2.6/v2.7 Header Differences

The header is the same 64 bytes, but many fields have different semantics:

Fieldv4.0/v5.0 meaningv2.6/v2.7 meaning
0x18total_face_vertices (count)Offset to vertex-normal data. Parsers should remap this into the 0x2C slot (vertex-normal offset) and zero this field. The engine also zeroes it during frame construction. Not a vertex count despite sharing the offset with v4.0’s total_face_vertices.
0x1Coffset_section4 (0 in .3DC)Often non-zero but zeroed by the engine header copier regardless of file value. Not Section4 (SubObject BVH does not exist in v2.6/v2.7). Discarded at load time.
0x24Always 0Non-zero in 5 files (RICHA001=4490, GARDA001-004=6434)
0x2Coffset_vertex_normalsUnrelated to vertex normals in v2.x files. Acts as a variant marker: 0 for v2.7 .3D, 1 for .3DC (both v2.6 and v2.7). Not an offset — treat as a flag.
0x38Duplicate of 0x18Does NOT equal 0x18 in v2.7. Copied by the engine header copier. However, the engine rejects v2.6/v2.7 files at load time (version byte < '4'), so this field is only used in practice for v4.0+ where it duplicates 0x18.
0x3CAlways 64Variable (112–3736) — face data does NOT start after header

v2.6/v2.7 Frame Data Differences

The 16-byte frame data record has different field semantics:

Fieldv4.0/v5.0v2.6/v2.7
offset 0x08 (reserved)Always 0Vertex offset adjustment (values up to 63,662)
offset 0x0C (frame_type)0, 2, 4, or 8Values like 8209 (0x2011), 16402 (0x4012) — different encoding

v2.6/v2.7 Face Data Differences

Aspectv4.0/v5.0v2.6/v2.7
Face texture header5 bytes (u16 + u16 + u8)3 bytes (u8 + u16)
tex_hi values61–91, 2550, 6, 15, 18, 20, 30, 36, 44, 50, 60, 63
Solid-color faces~5% of faces (tex_hi=0xFF)Rare — texture_id < 2 indicates solid color
Texture ID decodingBCD-like: (raw >> 8) - 4000000Simple: texture_id = raw >> 7
Vertex indexDirect indexByte offset (divide by 12)

Parsers must remap header fields before use. See External References for the remap logic.

Engine version gate: The shipped engine rejects files where the version byte (offset 0x01) is less than ASCII '4' (0x34). This means v2.6/v2.7 .3DC files on the game disc are not loaded at runtime — they are development-era leftovers. The engine only loads v4.0+ files. External parsers (DaveHumphrey, RGUnity, this project) support both versions for preservation completeness.

Section Layout

Sections appear in this order (offsets from header):

  1. Face Data — at 0x40, num_faces variable-size records
  2. Vertex Coordinatesnum_vertices × 12 bytes
  3. Face Normalsnum_faces × 12 bytes
  4. Frame Datanum_frames × 16-byte records
  5. Section4 — SubObject BVH entries (when present)
  6. Vertex-Normal Indirection Tabletotal_face_vertices × u32
  7. Vertex Normalsnum_vertices × 12 bytes

Frame Data

Located at offset_frame_data. Array of num_frames × 16-byte records.

For .3D files this is always a single entry pointing to the base geometry.

OffsetSizeTypeNameDescription
0x004u32vertex_offsetFile offset to vertex position data
0x044u32normal_offsetFile offset to face normal data
0x084u32reservedAlways 0 in v4.0/v5.0. Used as vertex offset adjustment in v2.6/v2.7.
0x0C4u32frame_type0 for static .3D models in v4.0. See 3DC.md for animated frame types. Different encoding in v2.6/v2.7.

vertex_offset and normal_offset point to the base vertex coordinate and face normal sections (same values as header fields 0x30 and 0x34).

Face Data

Located at offset_face_data (always 0x40). Array of num_faces variable-size records.

v4.0 / v5.0 Face Record

OffsetSizeTypeNameDescription
0x001u8vertex_countNumber of vertices in this face (3–10)
0x011u8tex_hiPer-face flags/high byte field (often 0xFF for solid-color faces)
0x024u32texture_data_rawPacked texture encoding value
0x064u32unused_04Unused — always 0x00000000. Parsers should skip.
0x0A8×NFaceVertex[N]verticesN = vertex_count

Texture Decoding (v4.0 / v5.0)

Solid color (if texture_data_raw >> 20 == 0x0FFF):

  • color_index = (texture_data_raw >> 8) & 0xFF
  • tex_hi is always 0xFF for solid-color faces.

Textured (otherwise):

  • Texture file ID (BCD-like encoding):

    tmp = (texture_data_raw >> 8) - 4000000
    ones     = (tmp / 250) % 40
    tens     = ((tmp - ones*250) / 1000) % 100
    hundreds = (tmp - ones*250 - tens*1000) / 4000
    texture_id = ones + tens + hundreds
    

    This yields the TEXBSI.### file number.

  • Image sub-ID within that TEXBSI file:

    ones = (texture_data_raw & 0xFF) % 10
    tens = ((texture_data_raw & 0xFF) / 40) * 10
    image_id = ones + tens
    

v2.6 / v2.7 Face Record

OffsetSizeTypeNameDescription
0x001u8vertex_countNumber of vertices
0x011u8u1Separate byte field (not part of texture encoding)
0x022u16texture_dataPacked texture encoding
0x044u32unused_04Unused — always 0x00000000. Parsers should skip.
0x088×NFaceVertex[N]verticesN = vertex_count

Texture decode from texture_data (the u16 at offset 0x02):

  • Solid color if (texture_data >> 7) < 2: color_index = texture_data & 0xFF
  • Textured: texture_id = texture_data >> 7, image_id = texture_data & 0x7F

FaceVertex

OffsetSizeTypeNameDescription
0x004u32vertex_indexIndex into vertex coordinate array. For v2.6/v2.7: byte offset, divide by 12. For v4.0+: direct index.
0x042i16u_deltaU texture coordinate delta
0x062i16v_deltaV texture coordinate delta

UV coordinates are cumulative deltas in 4-bit fixed-point format: each vertex’s absolute U/V = previous vertex’s U/V + this delta. First vertex in the face starts from 0.

The raw i16 values are in 1/16th pixel precision. To convert to pixel-space texture coordinates, multiply by 1/16 (or divide by 16). To normalize to 0–1 UV range, divide by 16 × texture_width (U) and 16 × texture_height (V). This fixed-point scale applies identically to first-vertex values and subsequent deltas.

Face Vertex Count Distribution

Vertices/FaceFrequency
3 (triangle)~93%
4 (quad)~6.5%
5–10<0.5%

Vertex Coordinates

Located at offset_vertex_coords. Array of num_vertices × 12 bytes.

Each vertex is 3 × i32 (signed 32-bit integers), scaled by 1/256.0 to get world-space coordinates.

Face Normals

Located at offset_face_normals. Array of num_faces × 12 bytes.

Each normal is 3 × i32, scaled by 1/256.0. Per-face normals (one per face, not per vertex).

Vertex Normals

Located at offset_vertex_normals (header offset 0x2C). Array of num_vertices × 12 bytes.

Each entry is 3 × f32 (IEEE 754 single-precision). Per-vertex normals (unit vectors). Some entries may be NaN (bit pattern 0xFFC00000 for all three components), indicating no normal was computed for that vertex.

Vertex-Normal Indirection Table

Located at offset_normal_indices (header offset 0x28). Array of total_face_vertices × u32.

Each entry is a file offset pointing into the vertex-normal data section. Maps each face-vertex to its vertex normal, allowing faces that share a vertex to use different normals (for hard edges).

Shading Model Selection

Per-vertex shading is determined by the indirection table and vertex normal validity:

ConditionShadingNormal used
Indirection table present, referenced vertex normal is validSmooth (Gouraud)Vertex normal from indirection lookup
Indirection table absent, vertex_normals[vertex_index] is validSmooth (Gouraud)Vertex normal by direct index
Vertex normal is NaN (0xFFC00000) or unavailableFlatFace normal

.3D files typically have the indirection table (per-face-vertex normal control). .3DC files always omit it (offset_normal_indices = 0) and fall back to direct vertex-index lookup.

Section4: SubObject BVH

Located at offset_section4 (header offset 0x1C). Contains section4_count variable-size entries. Present when offset and count are non-zero.

Each entry is a bounding-volume node with face references:

SizeTypeDescription
4i32Center X
4i32Center Y
4i32Center Z
4u32Radius
2u16Face reference count
4f32Extent X
4f32Extent Y
4f32Extent Z
6×NFace references (u32 offset + u16 index, divide index by 4)

External References