DARKER Research archive

Model colours, Gouraud shading and destruction links

Browse all recovered 256-entry palettes, including source RGB values and entries retained from earlier palette loads.

2026-09-18. Open the coloured atlas. The viewer offers stored game colours with vertex shading, the flat-colour fallback, and neutral geometry inspection. Hover the colour swatches for the face byte and base palette index. OBJ downloads now have companion bank MTL files; keep both in the same directory. MTL exports use flat fallback colours.

Collision-volume inspection

The model viewer has a Collision volumes checkbox, off by default, for retail and demo city types. It overlays translucent boxes and outlines, visible through the model so recessed vulnerable areas remain inspectable. Cyan denotes category 0 (ordinary ground-target surface), red 1 (protected), yellow 2 (general breakable), and purple 3 (Diffuser surface). These are response categories, not a claim that every coloured surface is currently destructible; weapon eligibility still applies.

Volumes follow the selected base, alternate or damaged model. They are the unexpanded native boxes; player/projectile collision radii enlarge them at runtime. Gate/fountain animation sliders do not deform these static boxes. The Collision OBJ link beside OBJ + MTL exports each box as a named object with its category in the name, in the same coordinate system as the model OBJ. OBJ geometry has no transparency material; the translucency is a viewer feature.

tools/model_collision.py decodes the selected header's collision pointer and world-data stream. Native 2E21 maps map offsets to model axes as A=-row*8, B=column*8; export is A,-C,B. Vertical bounds include the selected model header's height, with descriptor base elevation cancelling against model placement. Upper horizontal bounds include the native +1. Non-colliding descriptors expose no volumes. The 1993 and 1995 demo city banks use the same decoded collision-stream encoding; their overlays are built directly from the unpacked demo resources, including each selected linked state. Special-object collision systems remain separate and are not included in this overlay.

Tile placement and screen-space discs

The Placement checkbox (off by default) draws the map tile's square outline at ground level. It uses the type directory's column/row fractions and the selected model's header height and base elevation, rather than centring a square on the mesh bounding box. It is available for retail and demo city types; free-moving special objects have no fixed tile placement.

The model preview now draws stored screen-space discs as camera-facing circles, including the beacon light and fountain spheres. Their centres follow recovered vertex animation and share the preview's depth sorting with polygon faces. The original radius is numerator/projected_depth; this orthographic inspection view uses numerator/128 model units, scaled with the model, matching the existing city inspection convention. This is not a reproduction of DOS projection, clipping or pixel rounding. Dynamic colours use inspection palette values, with a bright value for the lit tower state. Discs contribute to preview framing, including models with no polygon faces. They remain excluded from polygon OBJ exports. See component definitions for native centres, colours and radius numerators.

View-dependent drawing rules

View-dependent rules is enabled by default in the model viewer. The drawing bytecode is executed again as the view rotates, including calls, branches, vertex-slot changes and gate/fountain parameters:

These primarily control hidden-surface/detail selection. They can avoid drawing unwanted overlapping geometry, but are not a conventional Z-buffer depth-fighting fix: the original renderer uses ordered software drawing.

Conditional face colours replaces colours on guarded polygons with the normal of the innermost facing test, mapped from [-1,1] to RGB [0,255]. It is not necessarily that polygon's own normal. Distance-only groups are amber; unconditional faces retain their ordinary colours. Disable view-dependent rules to inspect guarded faces that would otherwise be skipped. The status line reports tests reached and their rejection results at the selected view.

Facing is evaluated in the viewer's orthographic projection, without native integer rounding or near-plane clipping. Authored drawing order now preserves the executed face/disc order and rejects back-facing polygons; disable it to compare with the former inspection depth sort. In the assembled fountain, components are sorted by their origins, then each component retains its own command order. This is still an inspection approximation to whole-scene DOS ordering. OBJ files remain flattened inspection meshes; these dynamic view decisions do not rewrite exported assets.

tools/model_visibility.js handles retail and mapped demo opcodes from the unpacked geometry pools. Browser verification compares flattened output with existing geometry for 10,182 supported model/state combinations, and executes 30,546 rotated rule-enabled views without decoder failures. Those checks verify geometry preservation and branch execution, not pixel-exact DOS rendering. Run NODE_PATH=/tmp/riotfire-browser/node_modules node tools/verify_model_visibility_browser.js.

Face colour encoding

Every supported filled polygon has a colour byte in its command stream. The flat renderer's 3383–3398 splits it into:

range = colour & E0h        // one of eight 32-entry ranges
shade = colour & 1Fh
palette_index = range + shade_lookup[shade]

2CFC–2D04 selects the lookup according to distance. B73C–B77F generates the lookup tables; the first table is the identity mapping for shades 0–27. The viewer uses this base-distance case rather than guessing a camera distance. This preserves the model author's face shading instead of applying a new normal/light calculation.

Low-five-bit values 28–31 take a separate path (3399–33B2), using a runtime parameter at 339A. The viewer fixes that parameter to zero, which gives shade 16 with the identity table. Those special faces are not faithfully animated.

Correction: vertex shades were mistaken for texture coordinates

The previous report labelled opcodes 0E, 0F, 11 as textured polygons. That interpretation was wrong. They carry (vertex index, shade) pairs.

The path at 30EB preserves the face's upper colour-range bits. A99D–A9F7 looks up each vertex's shade and constructs a fixed-point palette value:

((range + shade_lookup[vertex_shade]) << 8)
    + shade_lookup[vertex_shade] + 80h

The clipping/raster code carries and interpolates this third attribute alongside screen X/Y. This is Gouraud shading in palette-index space, not a UV pair or a bitmap texture lookup. F9 changes this path to 312A, which skips the shade bytes and uses the face colour instead. The clipped path has the corresponding switch at 30A1/30BD.

Playtesting confirms the visible F9 mode toggle. The bytecode investigation refines what it does technically; no claim is made that every surface which looks textured to a player is backed by an image texture. No bitmap texture mapping has been established in these model commands.

Among the successfully parsed type streams (including aliases):

Bank Flat polygons Gouraud polygons
30 4,144 567
31 2,602 114
32 11,548 617

All observed extra shade bytes are in 0–27. These are counts over type records, not unique polygons across the whole bank. The city streams do contain shaded polygons, even though the buildings have no identified bitmap textures.

The viewer triangulates faces and interpolates palette indices using the stored vertex shades. It is a visual preview, not a reproduction of DOS scan conversion, rounding, clipping or painter ordering. Conditional visibility remains flattened.

Palette resources

The palette is loaded before the corresponding cockpit/image payload:

Bank Palette source
30, Delphi Resource 00/16
31, Halon Resource 00/17 (00/18 is the other Halon configuration, with an identical palette)
32, underground Resource 00/16, followed by overlay 00/19

BC69–BC77 selects the craft image; 4149 calls palette loader 4185. Underground mode additionally loads resource 00/19 at BC8E–BC91. The loader stores RGB triples at world_segment:A900:

The stored RGB values are 8-bit source values. Runtime colour ramps, fades, lighting options and VGA DAC conversion are separate (AFAD–B04E). The viewer shows source RGB, not a claim of screenshot-exact DAC output. Unspecified entries are retained as null in palettes.json; none of the currently exported face materials requires an unspecified entry after special-shade resolution.

Destruction: confirmed state write and replacement model

The observation of intact/destroyed pairs is supported directly by the model headers and collision code:

  1. CDB9 resolves the struck cell's current model.
  2. CDCD requires a nonzero first model-header word, then further collision and weapon tests determine whether the path reaches CE1A.
  3. CE1A jumps to 67C1; 67C7–67CA adds 20h to the state byte, preserving the map's type byte.
  4. The renderer follows the model header's relative link at +0 according to that stage. The link is relative to the current model, not a new type ID.

Crucially, BCB6–BCBC patches the selector mask using the bank table at BDEC:

Bank State mask Number of link traversals
30 20 (state & 20h) >> 5
31/32 60 (state & 60h) >> 5

Before these traversals, state bit 80 can select the separate relative link at header +2. That is a distinct branch; it should not be renamed the generic “destroyed bit.” Bit 0 also remains transient renderer state.

Examples in Halon bank 31 are 4 → 5, 6 → 7, 38 → 39, 41 → 42. The viewer links these exported pairs. Twenty-two Halon type records have a nonzero first damage link; aliases mean this is not 22 unique intact buildings. All those base chains reach a terminal model after one distinct replacement. The data also supports variants not listed as separate city type records.

The two underground type records 111 and 112 have nontrivial link chains, with up to three distinct models. Their exact gameplay role remains unidentified. All chains and any corresponding directory types are in damage-links.json.

Beacon outages and streetlights

Delphi streetlights go dark automatically with their regional energy beacon; there is no separate lamp on/off toggle involved. The renderer at 2D85 looks up the state byte at the nearest nine-cell lattice position, using 9*floor((coordinate+4)/9) independently for each map axis. This gives each beacon an effectively 9×9-cell lighting region rather than a circular radius. Unlike player charging, the rendering lookup does not check the sampled cell's type.

The sampled strength affects ordinary building shading and special luminous colours, including the streetlights. 2CE4–2D04 adds 16*(255-strength) to the model's shading depth and supplies F0h | (strength >> 4) for special colour codes 28–31. Intermediate strengths therefore dim the region during beacon fades, rather than simply switching every surface on or off. Halon's 2DAC uses constant strength 255 instead.

This regional lighting is separate from destroying a lamp: its own cell's damage state selects the broken model, and restoring beacon power does not repair it. Nor does the visual relationship prove that streetlights supply player energy; charging uses a separate calculation. Saved beacon codes restore off/full endpoints, while mission scripts can subsequently fade or restore the beacon strength.

Validation

tools/verify_model_colours.py executes original x86 code in Unicorn:

The browser viewer was checked in palette, flat and neutral modes; screenshots include colours-delphi.png, colours-halon.png and colours-destroyed.png. The previous vertex-interpreter validation remains applicable; geometry coordinate decoding has not changed.

python3 tools/extract_models.py
PYTHONPATH=/path/to/unicorn python3 tools/verify_model_colours.py
python3 tools/model_viewer.py