Every ship in the game is a wireframe model packed into a compact blueprint. This page covers the blueprint format byte by byte, how the numbers turn into a picture, and the render path that proves the MSX draws Elite orthographically.
Source: SHAPES.DAT, which relocates to 0xa857 at load (its first bytes are the
blueprint pointer table). Geometry read from the live image elite_live_memory_45s.bin
and confirmed against the renderer in CODE.COM (base 0x0100). Vertices and edges are
fully decoded and verified; the face-normal encoding is only partly cracked (below).
Each blueprint starts with a small pointer table, then three geometry blocks and the name:
| bytes | meaning |
|---|---|
+0/+1 | pointer -> vertex data |
+2/+3 | pointer -> edge data |
+4/+5 | pointer -> face data (normals, for hidden-line render) |
+6/+7 | pointer -> name string (also inline at +0x1a) |
+8 | face count · +9 edge count |
The combat stats share the same structure: hull at +0x0c, max speed at +0x0d,
behaviour at +0x12, missiles at +0x13, and the kill-value at +0x18/+0x19 (see the
ranks page for how the AI and scorer read those). Vertex count is implicit:
the vertex region is edgeCount-index addressable and ends at a terminator record.
Each vertex is six bytes, three 16-bit little-endian coordinates:
x = (b0, b1) y = (b2, b3) z = (b4, b5) # low byte first
value = (hi & 0x3f) * 256 + lo, negated if (hi & 0x40) # 14-bit magnitude, sign at bit 14
The sign is bit 14 (bit 6 of the high byte), which is why mirrored left/right vertex pairs
differ by exactly 0x4000. Bit 15 (bit 7 of the high byte) is a face/visibility flag,
ignored for position. A word of 0xffff is a zero marker; a record with b0..b3 = 0xff
terminates the list.
Worked example, the Sidewinder is a textbook flat arrowhead: nose (∓5120, 0, +5294),
wingtips (±6144, 0, −4923), dorsal and ventral fins (0, ±4096, −4923), all in one
plane.
How it was verified: in the draw path, the transform at 0x4464 reads the vertex to
rotate from 0xe7ea / 0xe7ec / 0xe7ee as three 16-bit words, and 0x4a10–0x4a2f
fills those by LDI-copying six bytes straight from the vertex table (stride 6, indexed
base + idx*6). So the six-byte record is three 16-bit coordinates, fed verbatim into the
rotate/project math. (An earlier dead end read each axis as a single sign-magnitude byte;
it renders the Sidewinder but flattens ships like the Cobra MkIII whose low z byte is
near-constant, because the depth lives in the high byte.)
Each edge is two bytes, [vertexIndexA, vertexIndexB]; the edge region is exactly
edgeCount × 2 bytes for every ship. This list alone is the wireframe. Skip any pair whose
index is at or above the vertex count: a few ships (Asp MkII, Cougar) carry trailing
0xfe/0xfb/0xff marker bytes. Keep edges in blueprint order, because the face block
references them by index.
Each face is [6-byte normal][attr][edgeCount][edgeCount edge-indices]; the count lists
edges (indices into the edge list), not vertices. Confirmed on the Cobra MkIII (15
faces, all 38 edges referenced) and parses cleanly for all 20 ships. The game does
hidden-line removal by drawing a face’s edges only when that face points at the camera,
which is what makes decorative edges (cockpit lines, the Adder’s laser slot: faces with
attr 0x16/0xff) appear and vanish correctly.
The stored 6-byte normal’s encoding is not fully cracked (mirror faces do not come out
mirrored under the vertex encoding), so tools/extract_faces.py computes the outward
normal geometrically from each face’s own vertices instead, which is enough for the
visibility sign.
| ship | v | e | f | ship | v | e | f | |
|---|---|---|---|---|---|---|---|---|
| Sidewinder | 9 | 15 | 7 | Anaconda | 13 | 25 | 12 | |
| Viper | 13 | 20 | 7 | Asp MkII | 19 | 30 | 14 | |
| Cobra Mk1 | 11 | 18 | 10 | Boa Cruiser | 13 | 24 | 13 | |
| Cobra MkIII | 27 | 38 | 15 | Krait | 17 | 21 | 6 | |
| Gecko | 10 | 17 | 9 | Fer-de-Lance | 18 | 27 | 10 | |
| Moray | 13 | 19 | 9 | Mamba | 23 | 28 | 5 | |
| Adder | 18 | 29 | 13 | Thargoid | 19 | 26 | 10 | |
| Python | 10 | 26 | 13 | Wolf MkII | 28 | 43 | 18 | |
| Transporter | 37 | 46 | 14 | Cougar | 19 | 26 | 7 | |
| Orbit Shuttle | 18 | 30 | 13 | Constrictor | 15 | 24 | 10 |
v is vertices, e is edges, f is faces.
Here is the whole fleet, rendered live from that decoded geometry. Drag any ship to turn them all; they are drawn orthographically, the same way the game does it (see below).
The vertex-to-screen path in CODE.COM is three steps, and decisively there is no
divide-by-z anywhere in it:
0x4464): a plain 3×3 orientation-matrix × vertex, x' = x·M00 + y·M01 + z·M02 and so on, nine signed multiplies via 0x2160. The matrix (0xe6cc…,
0xeb68…) is pure orientation and magnitude-preserving.0x4873): calls the scaler 0x4847 once per axis on x’, y’ and z’.
0x4847 multiplies its input by a single scale factor (0xe7e8/0xe7e9) and returns
the high byte. The same factor is applied to all three axes, the tell-tale of a uniform
scale, not a per-vertex 1/z.0x44d2): add the screen centre (0xe6d8 / 0xf7d0).So the screen position is (cx + s·x', cy + s·y') with one scale s per object. z’ is
computed but used only for depth and visibility, never to foreshorten x/y. That is
orthographic projection. Ships shrink with distance because s shrinks per object, but a
single ship is drawn with no internal perspective. The only in-ship foreshortening you see
is from the rotation itself (a flat delta turned edge-on genuinely collapses to a sliver).
| addr | role |
|---|---|
blueprint +0/+2/+4 | vertex / edge / face data pointers |
0x4a10–0x4a2f | per-vertex: LDI × 6 record -> 0xe7ea/ec/ee (three 16-bit coords) |
0x4464 | rotate vertex by 3×3 orientation matrix (0xe6cc…, via signed mult 0x2160) |
0x4873 | project one vertex: rotate, then scale each axis via 0x4847 |
0x4847 | uniform scale (× 0xe7e8, no z-divide), orthographic |
0x44d2 | add screen centre (0xe6d8/0xf7d0) |
0x4303–0x433c | vertex loop (ptr 0xf7f9, count 0xf7fd, LOD flag 0xf804) |
This is why the interactive gallery above uses a Three.js OrthographicCamera and a single
uniform scale per ship, with no perspective divide: adding a fake x/(d+z) warps the model
as it rotates (it pulses and stretches), which the game never does.
Proportions are correct, there is no z-stretch. Measuring the decoded bounding boxes gives real Elite proportions: the Cobra MkIII is 1.18 to 1 wider than it is deep, the Sidewinder 1.20 to 1, the long freighters correctly deeper than wide (Python 0.90, Anaconda 0.88), and every ship’s height is its smallest dimension. A z-scaling error would break the Cobra’s 1.18 to 1 ratio; it does not.
One aside that surprises people: the player’s own Cobra MkIII is never rendered. Classic
Elite draws the cockpit view looking outward, with no external chase-cam and no
launch/dock cutscene of your hull, so during play the player “ship” is not 3D geometry at
all. It is a bundle of flight constants (max speed, roll and pitch rates, laser and shield
banks, cargo, fuel) hardcoded in CODE.COM, entirely separate from the SHAPES.DAT
geometry. The Cobra in SHAPES.DAT only matters to how other ships would see you, which
in single-player is never. The NPC blueprint table (pointer table at 0xa857) is fully
mapped; the player flight-model constants have not yet been pinned down in the disassembly.
tools/ship_export.py -> ships.json (20 ships). Each ship has verts (decoded
x,y,z), edges (index pairs), and verts_raw (untouched 6-byte records).tools/add_hulls.py adds a convex-hull hull field for a solid hidden-surface preview;
tools/extract_faces.py computes the geometric face normals for the game’s own
hidden-line rule.verts + edges into a Three.js BufferGeometry -> THREE.LineSegments.
Coordinates are roughly ±16000; scale to taste, they render at any uniform scale.
tools/ship_viewer.html is a self-contained, dependency-free viewer for eyeballing them.The whole toolset is packaged for download: elite-msx-tools.zip
(23 KB) bundles the Python scripts above (ship_export.py, extract_faces.py,
add_hulls.py, the zdis.py disassembler, and the fat12_* disk-image extractors), the
decoded ships.json, and the standalone ship_viewer.html, with a README describing the
pipeline. No game binaries or RAM dumps are included; bring your own copy of the game.