# Apache's model format

The reference `3DDATA/3DOBJS.BIN` contains 1,314 model records. These are drawing programs rather than a flat triangle mesh. They combine polygons, points, lines, textures, surface ordering, state switches and calls to attached models.

## Archive and header

The archive begins with a little-endian 32-bit count, followed by that many 32-bit offsets from the beginning of the file. Each nonzero offset identifies a model header, 188 bytes long.

| Offset | Type | Meaning |
| --- | --- | --- |
| `00` | int32 | Coordinate encoding: 0 = signed 16-bit, 1 = grid |
| `04` | int32 | Distance threshold for the next level of detail |
| `08` | int32 | Next model record, or -1 |
| `0c` | six int32 | Minimum and maximum X, Y and Z |
| `24` | int32 | Horizontal bounding radius |
| `28` | int32 | Height |
| `2c` | uint32 | Attached-model definitions |
| `30` | uint32 | Eight bounding-box vertices |
| `34` | uint32 | Vertex list |
| `38` | uint32 | Normal list |
| `3c` | uint32 | Face drawing program |
| `40` | uint32 | Shadow data |
| `44` | uint32 | Additional grid-lighting data |
| `48` | char[16] | Short authoring name |
| `58` | char[64] | Description |
| `98` | uint32 | Properties: bit 0 casts a shadow, bit 1 receives shadows, bit 2 enables mobile collision |
| `9c` | int32 | Target filtering code: 10,000 / 30,000 / 50,000 select levels 1 / 2 / 3 |
| `a0` | int32 | Further model metadata |
| `a4` | int32 | Further model metadata, patched for ship type |
| `a8` | int32 | Accumulated-damage threshold for destruction |
| `ac` | int32 | Explosion size and damage-radius input |
| `b0` | int32 | Replacement or auxiliary model: scenery wreckage, projectile exhaust or muzzle flash, depending on its caller |
| `b4` | int32 | Radar symbol/type selector |
| `b8` | uint32 | Gameplay flags; bit 0 selects target-class scoring |

The seven internal offsets are relative to the beginning of their own model. Zero means absent. `initialise_3d_objects_` adds the loaded model address to each nonzero offset. The independent loader matches every byte of the original's relocated model and texture archives.

## Coordinates and attached models

A vertex list starts with a 16-bit count. Each vertex occupies six bytes. For ordinary models, these are three signed 16-bit coordinates, multiplied by 16 when transformed. There are 848 records with this encoding.

The other 466 records use a compact grid representation. The first three bytes select coarse X/Y/Z grid entries; the next three select fine entries. The local coordinate on each axis is `(coarse - 32) × 4096 + (fine - 32) × 64`. The renderer builds rotated lookup tables before scanning these vertices. Its fixed-point arithmetic introduces small rounding differences that an interactive inspection view does not need to preserve.

The attached-model block contains a 32-bit count followed by 52-byte records. Each record has a model ID, three signed 32-bit translations and nine Q31 rotation coefficients. A zero model ID is a slot filled by gameplay, such as a weapon attachment. It is not a reference to the archive's null model. `construct_3d_object_` recursively allocates these instances and copies their transforms.

There are 250 records with attachment definitions. The original constructor and the independent decoder agree on all 2,094 instances produced when each archive record is constructed with its variable attachments empty, including 780 attached transforms.

## Drawing language

The byte at each instruction selects one of eight operations:

| Opcode | Action |
| --- | --- |
| 0 | Draw a primary face and update its winding result |
| 1 | Draw a secondary face if the preceding winding result was nonnegative |
| 2 | Evaluate a polygon's winding and draw two branches in the resulting order |
| 3 | Execute a branch when an animation bit mask is set |
| 4 | Choose between two branches using an animation bit mask |
| 5 | Select an authored branch using a shifted and masked animation value |
| 6 | Draw an attached model by slot number |
| 7 | Return from this branch |

Branch offsets are signed 16-bit displacements relative to the byte immediately after the displacement. A decision plane or state switch also supplies the continuation address after its branches return. The winding routine uses the first three screen-space vertices and the signed high word of their cross product. It does not require a polygon normal.

A select instruction stores an explicit case count as well as its mask. The mask can express values beyond the authored table. The browser restricts selection to the authored entries; out-of-bounds original execution is not emulated.

## Primitive packets

After the drawing opcode, a primary packet starts with a one-byte primitive type and vertex count. A secondary packet adds a preceding byte giving the complete packet length, allowing the renderer to skip it.

| Types | Material / primitive | Header bytes, including type and count | Bytes per vertex |
| --- | --- | --- | --- |
| 0, 1, 2 | Points, polyline, flat polygon | 3 | 1 |
| 3 | Alternating two-colour polygon | 4 | 1 |
| 4, 5 | Fine or coarse mesh | 3 | 1 |
| 6, 21 | Glass or heat-haze remapping | 2 | 1 |
| 7, 8 | Texture, optionally masked | 5 | 3 |
| 9, 10, 11 | Shaded points, polyline, flat polygon | 4 | 1 |
| 12 | Shaded two-colour polygon | 5 | 1 |
| 13, 14 | Shaded fine or coarse mesh | 4 | 1 |
| 15, 16 | Shaded texture, optionally masked | 6 | 3 |
| 17 | Gouraud-shaded polygon | 4 | 2 |
| 18 | Two-colour Gouraud polygon | 5 | 2 |
| 19, 20 | Gouraud texture, optionally masked | 6 | 4 |

The first per-vertex byte is always a vertex index. Textured records add unsigned U and V coordinates. Gouraud records add a normal index. The reference archive uses 19 of the 22 implemented types.

Normals have two 16-bit counts followed by signed three-byte vectors. The first count includes all normal entries; the second is the subset required when Gouraud shading is disabled. The original transforms the light direction into model space, takes its dot product with each normal, clamps negative values to zero and uses that result to index the palette intensity table.

## Textures and colours

`OBJTEXT.BIN` has the same counted offset table as the model archive. It contains 480 slots, of which 43 are populated. Every populated reference texture has a 12-byte header followed by 4,096 indexed pixels arranged as 64 × 64. The loader rewrites the header's final word to point to those pixels. Header word 0 is the texture type, selecting the row stride: 0 uses 16 pixels, 1 uses 32, and 2 uses 64. Every populated texture in this archive uses type 2. The same descriptors are used for the effect bitmaps.

`3DPAL.RGB` supplies 224 RGB colours. `initialise_3d_palette_` builds the shading ramps, loads sky and fog colours and adds the chosen warzone's terrain colours. The browser uses the resulting Fort palette and intensity tables captured from that original routine. Its glass and heat-haze preview uses transparency; the game instead remaps pixels already in the framebuffer.

## Coplanar details in the browser

Buildings such as `citblkd0` (records 16 and 17) put wall trim and windows exactly on the backing wall plane. The original drawing program paints these surfaces in order. Ordinary triangle depth interpolation in WebGL can disagree slightly between the wall and its details, causing speckling as the view rotates.

The shared model and terrain viewer uses WebGL 2 and a canonical plane equation for each triangle. Matching planes receive identical coefficients before the camera transform, passed without interpolation. The fragment shader intersects the viewing ray with that plane to calculate depth; equal-depth fragments then follow the model's drawing order. Separate surfaces retain depth testing, and terrain, surrounding features and roads retain their layer offsets.

`tools/check_model_depth.js` compares 36 coplanar views with painter-order rendering, including reversed vertex order, different zooms and large map coordinates. It also checks foreground occlusion, road layers, transparent texture holes, representative models and terrain loading.

## Verification

- `native-loader-check.json`: both loaded archives match byte for byte after pointer relocation.
- `native-program-check.json`: 70,364 primary/secondary packet checks; 15,768 control-flow cases; 899,317 matching face and attached-model draw events.
- `native-geometry-check.json`: complete attachment construction and grid coordinate scales.
- `work/checks/workbench.json`: browser loading, representative models and artwork, with screenshots.

Tools: `tools/models.py`, `tools/check_model_loader.py`, `tools/check_models_native.py`, `tools/check_model_geometry_native.py`, `tools/export_native_palette.py`. The native checks execute the January 1998 Windows software engine. They replace file I/O or raster output where stated in their reports; the routines being compared run unchanged.

## Invalid lighting references in shipped models

A complete-model comparison found surfaces whose normal index exceeds the stored normal list. These include line details on A10, AIM9L and several trees. In the standard isolated view, 71 of the 1,314 models reach at least one such lookup. The instructions in `draw_completely_visible_primary_face_` use these indices directly, reading uninitialized stack lighting for ordinary models or adjacent record bytes for grid models.

The reconstruction keeps the authored colour at full intensity when a normal is missing. It does not reproduce the stray-memory result. Frame comparisons identify these cases separately; they remain covered by bounded rendering and sanitizer checks.

Texture coordinates also include `u = 64` for 64-pixel images. The original uses a contiguous image address, so a right-edge endpoint samples the next row when that address is still within the texture. This is distinct from reading beyond the image allocation.

The `CasStrch` model also has an authored texture coordinate with `v = 80` on a 64-pixel image. A varied-camera test reaches a sample beyond that texture allocation. The reconstruction omits that sample; the original adjacent-memory colour is excluded from frame equivalence.

## Moving objects and shadows

Mobile objects occupy a 64 × 64 sector map. New arrivals go to the front of their sector list. Moving within the same sector preserves order; crossing its boundary removes and reinserts the object. Each scene scans the camera's sector and its eight neighbours. Clouds and near/far depth limits filter this list before distance sorting.

The shadow renderer uses the model's face program and attachments. It projects each vertex along the sunlight direction onto a horizontal plane at the sampled terrain height, or onto the cloud layer when above it. Most faces become a fine black mesh; glass becomes a medium mesh. Points, lines and heat-haze faces do not draw shadow pixels. The Apache models `0x4ba` and `0x4bb` use model `0x40a` for their shadow geometry.

Model property bit 1 allows a mobile object to receive a shadow at sea level. The original tests the projected position against the receiver's rotated bounds, then raises the shadow plane to its authored height. In this archive only `TARAWA` (model 130) has that bit. The feature lets aircraft shadows fall on the carrier deck.

The reconstruction matches 3,392 complete shadow frames across all 848 true-coordinate models, plus 384 mobile drawing queues and 400 deck-receiver queries. The assembled scene test also matches 96 frames with aircraft, carrier models, shadows, smoke, explosions and terrain. Reports: [shadows](../verification/cpp/shadows/report.json), [mobile state](../verification/cpp/mobiles/report.json), [combined frames](../verification/cpp/scene-mobiles/report.json).
