A pixel names a color
Total Annihilation uses a shared 256-entry palette. GAF images, TNT terrain, minimaps, font colors, and model face colors ultimately resolve through it. An image byte stores a slot number; the palette supplies its RGB color. Change the table and the same image bytes can produce a different picture.
Same pixels. Different table.
Day palette
Night palette
640 identical pixel bytes → two different RGB images
For the beacon light, a pixel byte of 06 selects entry 6, at file offset 6 × 4 = 24 (0x18). In the day palette those four bytes are FA B9 48 00, giving opaque RGB (250, 185, 72). The pixel contains neither those channels nor an alpha value.
Four files, four table shapes
The logical filename and its consumer establish the role. Size alone cannot distinguish LHT from SHD. None has a magic value, version, row count, or header.
Scroll table horizontally to see all fields →
| File | Shape | Total bytes | Lookup | Output |
|---|---|---|---|---|
PALETTE.PAL | 256 × 4-byte entries | 1,024 | 4 × index | RGB + reserved byte |
PALETTE.ALP | 256 rows × 256 columns | 65,536 | a × 256 + b | Blend result index |
PALETTE.LHT | 32 rows × 256 columns | 8,192 | level × 256 + index | Lighting result index |
PALETTE.SHD | 32 rows × 256 columns | 8,192 | row × 256 + index | Shading result index |
The renderer also holds a generated 256-entry gray table and a 256-entry blue table. They are not additional shipped palettes/ files. The gray table supports palette color searches. Session initialization builds the blue table for submerged hull tint from (r >> 1, g >> 1, (b >> 1) + 50), finding a nearest palette color through the gray table’s search [03 §4.3.3; R-WATER-01 §2].
Byte layouts
PAL entry
Offsets below are relative to entry index × 4.
Scroll table horizontally to see all fields →
| Offset | Bytes | Type | Field | Meaning |
|---|---|---|---|---|
+0x00 | 1 | u8 | Red | Full channel range 0–255. |
+0x01 | 1 | u8 | Green | Full channel range 0–255. |
+0x02 | 1 | u8 | Blue | Full channel range 0–255. |
+0x03 | 1 | u8 | Reserved | Observed zero in inspected retail palettes; preserved as source data. |
These are full 8-bit channels, not 6-bit VGA channel values. The reference palette’s entry 255 is (255, 255, 255, 0). During display installation the outgoing fourth byte is generated as zero; derived color searches use RGB only.
Lookup-table cells
Each cell is one unsigned byte, storing a palette index. Row-major storage means that each next row starts 256 bytes later.
Scroll table horizontally to see all fields →
| File | Offset | Bytes | Type | Meaning |
|---|---|---|---|---|
| ALP | a × 256 + b | 1 | u8 | Result for operands in the consumer’s order. |
| LHT | level × 256 + source | 1 | u8 | Remap source through level 0–31. |
| SHD | row × 256 + source | 1 | u8 | Remap source through row 0–31. |
For example, LHT[31][1] is byte 31 × 256 + 1 = 7,937 (0x1F01). A value of 249 means look up PAL slot 249; it does not mean a channel intensity of 249.
Blend, brighten, shade
ALP: two indexes become one
ALP[a × 256 + b] approximates a blend in palette space. It is a precomputed lookup, not exact channel arithmetic. It need not be symmetric: swapping a and b is not generally safe.
The inspected reference table maps every ALP[i][i] to i; the recovery builder explicitly preserves this diagonal, even for duplicate RGB entries [03 §4.3.4]. Structure anti-alias downscaling depends on it: four background pixels must remain the same background index after three ALP lookups. Other consumers include the minimap supersample composite and translucency effects.
LHT: a brightening remap
Lighting performs one byte lookup after the logical-to-physical source mapping. The reference LHT’s mean brightness increases with row; individual cells can still darken because the result is quantized to an available palette entry.
| LHT row | Mean luminance change | Entries mapping to themselves |
|---|---|---|
| 0 | +0.00 | 242 / 256 |
| 1 | +0.24 | 230 / 256 |
| 3 | +6.83 | 125 / 256 |
| 5 | +12.60 | 52 / 256 |
| 15 | +31.37 | 8 / 256 |
| 31 | +51.51 | 8 / 256 |
These are bounded measurements from totala1.hpi, using 0.299R + 0.587G + 0.114B; they are not validation rules for authored replacements. As a counterexample to per-cell monotonicity, LHT[3][161] = 66 lowers luminance by 10.764 in those tables.
The calculated explosion flash uses LHT[(discByte − 0x4F) × 256 + destination]: visible disc bytes 0x4F…0x6E select rows 0–31; transparent pixels preserve the destination. The pool submits this secondary flash after projectile drawing and before its primary art, after the unit passes. Its shrinking frames and tick countdown supply the whole envelope; there is no additional age-based fade in this path [03 §4.3.1; R-FX-01 §4; 06 R-WFX-01 §2].
SHD: darkness through brightness
SHD’s reference ramp crosses identity around row 15, then continues into brightening. Row 15 preserves 232 of 256 indexes; row 0 is near-black, while row 31 has a mean luminance increase of about 54. It does not simply approach identity from darkness.
| Rendering case | SHD behavior |
|---|---|
BMcode=0 structure, Shading enabled | Interpolated rows remap textured and flat-colored faces. |
Same structure, dont-shade piece | Select row 15 directly. |
| Unshaded structure renderer | Bypass SHD. |
BMcode=1 mobile renderer | No face SHD step; dont-shade is inert. |
| Model shadow | Uses ALP tint, not a special SHD row. |
canmove does not choose this renderer: factories can author canmove=1, MaxVelocity=0, and BMcode=0. The row comes from the face-averaged smooth normal: trunc(dot(N, L) × 5.0) mod 32, with shipped default L = (−0.8, 1.0, 0.25). The renderer interpolates rows over faces. These contracts belong to [03 §4.3.2; R-REN-03A §5; R-RND-02A] and the 3DO research.
LHT and the upper half of SHD overlap in mean brightening range, but they are distinct files. Do not synthesize one from the other.
Palette roles and recovery
PALETTE.PAL supplies the display colors. GUIPAL.PAL supplies semantic GUI color fields that retail maps into active PALETTE.PAL indexes before drawing primitives or glyphs. GAF, PCX, and TNT image bytes already contain active indexes and bypass that GUI map. An OTA does not select a terrain palette.
Model team textures select source-authored player frames from LOGOS.GAF, with SHD applied only by the shaded renderer. They do not use a global palette substitution. Beam colors and sidebar energy/metal colors reference palette slots through their TDF fields.
| Situation | Retail behavior | Nanolathe behavior |
|---|---|---|
| Nonempty PAL exists | Read whole, without size/content validation. | Preserve source entries; resolved colors are opaque. |
| PAL missing or empty | Decode same-name PCX; pack its trailer into 1,024 PAL bytes. | Recover in memory. |
| Successful PCX recovery | Attempt host PAL write; delete host ALP/LHT/SHD files to regenerate. | Do not write or delete original assets. |
| Derived table missing or empty | Recovery behavior belongs to [03 §4.3.4]. | Build each missing/empty table individually; nonempty authored tables remain authoritative. |
The retail fallback is fatal, with the path as its message, if PCX decoding also fails [02 R-MALF-01 §9].
Sources and example files
This page adapts the owning PAL research at the pinned revision shown in the sidebar; its evidence IDs above retain their original scope. A complete source snapshot includes the upstream bibliography and publication omissions. No omitted retail-derived image was recreated here.
The day palette, night palette, and index buffer are original reproducible fixtures generated by scripts/pal-example.py. The two beacon SVGs are resolved directly from those bytes.