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 0x00 (vertex_offset)File offset to vertex dataHeader + frame header size (always 88) — not a useful offset
offset 0x04 (normal_offset)File offset to face normal dataFace data byte count. The total size in bytes of all face records.
offset 0x08 (reserved)Always 0Gap byte count between face data end and vertex data (0 when no gap).
offset 0x0C (frame_type)0, 2, 4, or 8Values like 8209 (0x2011), 16402 (0x4012) — different encoding

v2.6/v2.7 Engine Header Handling

At load time, the software renderer copies the 64-byte header and then zeroes the offset fields 0x18, 0x1C, 0x30, 0x34, 0x38, and 0x3C. It reads the file sequentially using frame_data[0].normal_offset as the face data byte count and frame_data[0].reserved as the gap between face data and vertex data, rebuilding the section layout in memory. The raw offset fields in the file header are not used for data access.

v2.6/v2.7 Face Data Differences

Aspectv4.0/v5.0v2.6/v2.7
Face texture header5 bytes (u8 + u32)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 (0x40 in v4.0/v5.0; variable in v2.6/v2.7). 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.

v2.6/v2.7 animated .3DC files: The offset_vertex_coords header field does not point to valid vertex data. Instead, compute the vertex data offset from the first frame data entry:

face_data_end = offset_face_data + frame_data[0].normal_offset
vertex_offset = face_data_end + frame_data[0].reserved

frame_data[0].normal_offset is the face data byte count (verified across all 147 animated v2.6/v2.7 files). frame_data[0].reserved encodes the byte gap between face data and vertex data. In some files the gap is absent and vertex data begins directly at face_data_end; parsers should validate the primary offset and fall back (a raw coordinate magnitude threshold of 1 090 519 040 catches misplaced offsets while accepting all valid game coordinates). Static v2.6/v2.7 .3D files use offset_vertex_coords directly.

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