joost

personal web page

Elite on the MSX1: how a ship becomes pixels

← back to Elite on the MSX1

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).

What a blueprint holds

Each blueprint starts with a small pointer table, then three geometry blocks and the name:

bytesmeaning
+0/+1pointer -> vertex data
+2/+3pointer -> edge data
+4/+5pointer -> face data (normals, for hidden-line render)
+6/+7pointer -> name string (also inline at +0x1a)
+8face 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.

Vertices: six bytes each

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 0x4a100x4a2f 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.)

Edges

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.

Faces

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.

Every ship’s geometry

shipvefshipvef
Sidewinder9157Anaconda132512
Viper13207Asp MkII193014
Cobra Mk1111810Boa Cruiser132413
Cobra MkIII273815Krait17216
Gecko10179Fer-de-Lance182710
Moray13199Mamba23285
Adder182913Thargoid192610
Python102613Wolf MkII284318
Transporter374614Cougar19267
Orbit Shuttle183013Constrictor152410

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 MSX draws Elite orthographically

The vertex-to-screen path in CODE.COM is three steps, and decisively there is no divide-by-z anywhere in it:

  1. Rotate (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.
  2. Project (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.
  3. Offset (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).

addrrole
blueprint +0/+2/+4vertex / edge / face data pointers
0x4a100x4a2fper-vertex: LDI × 6 record -> 0xe7ea/ec/ee (three 16-bit coords)
0x4464rotate vertex by 3×3 orientation matrix (0xe6cc…, via signed mult 0x2160)
0x4873project one vertex: rotate, then scale each axis via 0x4847
0x4847uniform scale (× 0xe7e8, no z-divide), orthographic
0x44d2add screen centre (0xe6d8/0xf7d0)
0x43030x433cvertex 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.

The ship you fly is never a model

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.

Extraction recipe

  1. 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).
  2. 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.
  3. Feed 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.