DARKER Research archive

Animated geometry and linked model states

2026-09-18. Model atlas · City explorer · Native geometry, motion coefficients and state graph.

Results

The previously unsupported Delphi gate types 17/19/21/23 and fountain types 120–123 now decode. All 5,454 placed Delphi cells and all placed cells in the other nine maps have decoded geometry. This is drawing-command coverage, not completion of mission state, visibility, collision or original rasterization.

Following both city-model header links discovers 27 definitions without their own city-directory entries: 22 in bank 30, one in bank 31, four in bank 32. The complete city graph has 154, 60 and 132 distinct definitions respectively. All parse successfully. Deliberately empty definitions remain empty.

The atlas includes a Linked alternate / damage definitions category and a Model state selector. State choices with the same target are collapsed. Gate extension and Fountain clock inspect recovered parameters; the fountain plays at 500 source-clock units per second: a 4.096-second cycle, calibrated to the video observation of slightly over four but under five seconds. This replaces the initial 50-unit inspection speed. The original timer rate has not yet been independently established; playback advances by elapsed time rather than by rendered frame count. OBJ downloads contain the default pose; JSON preserves animation. The city controls apply chosen high state bits globally for inspection, not as an assertion about an actual mission's destruction or gate state. Camera, selection and isolation survive pose changes.

Gate commands: saved anchor and interpolation

The ordinary FCxx drawing interpreter dispatches:

Opcode Handler Recovered operation
32 index 31B2 Load parameter through 319E, shift right once, save anchor
35 31BB Replace anchor, retaining interpolation parameter
38 3215 Emit an additional interpolated vertex; retain coordinate registers
3B index 334A Replace the current C-axis contribution with the parameter value

Parameter lookup is table[((cell_state & 31) + index) & 255], with the word table at SS:7935. The city-state high bits select model definitions separately.

For a world-space inspection mesh, gate vertex interpolation is:

factor = (uint16(parameter) >> 1) / 32768
vertex = anchor + (current_point - anchor) * factor

38 emits a new vertex: it does not move or replace the preceding one. That cursor distinction is essential for subsequent polygon indices. 35 establishes a second anchor for another group of vertices. The native implementation works on depth and projected-coordinate accumulators with fixed-point intermediate rounding; the viewer uses world-space interpolation with modern projection.

The landing routine writes parameter zero, increasing it by step << 6 and capping it at E800 in one path (C656–C66E). Thus the ordinary game range stops at an interpolation factor of 0.90625, not 1. Other paths derive the parameter from landing/takeoff state and approach distance. The inspection control is labelled extension rather than assuming a universal open/closed state mapping.

Fountain command: height replacement, not general scaling

The earlier description of 3B as a scaling transform was incomplete. 334A multiplies the selected signed parameter by each C-axis projection coefficient and replaces that axis's accumulated contributions. In native model coordinates this sets C; it is not a multiplier applied to the existing vertex height. Since the viewers use Y = -C, negative C is above the model origin.

Types 120–123 use parameters 9–14. DB1A–DB69 computes these six values from clock word 0BF8 and the stored sine table at 944A:

  1. Initialize all six parameters to -384 (FE80).
  2. Compute phase = ((clock << 5) & FFFF) * 6.
  3. The high word selects a band, 0–5. The low word shifted right seven indexes the original 1,024-entry signed sine table.
  4. Add floor(amplitude[band] * sine / 65536) to that band's height.
  5. Subtract floor(2 * amplitude[band+1] * sine / 65536) from the next band's height, wrapping the destination from band five to band zero.

The stored amplitude bytes are 118, 154, 136, 112, 85, 60, 118. The seventh byte supplies the wrapped band's amplitude. The complete phase cycle is 2,048 source clock units. This produces a travelling height ripple across the mesh; it is separate from the sphere-like screen discs in pool types 101–104.

Alternate and damaged definitions

Only city-model headers are traversed as link records. Special-object headers share the drawing-body offset but their first words are not city damage links; blindly treating them as such yields invalid pointers.

Both city links are signed offsets relative to the current header: +0 is the next damage definition, +2 the alternate definition. Selection follows the alternate link first if state bit 80 is set, then follows the damage link (state & mask) >> 5 times. The mask is 20 for bank 30 and 60 for banks 31/32. The original selection routine and installed bank masks verify this order. Selected header heights also feed city placement; variants retain their own height instead of inheriting the base definition's height.

model-states.json records every reachable city's links, directory aliases, state destinations, native faces/lines/discs, transform commands and animation coefficients. linked-manifest.json lists the 27 additional atlas entries. Per-definition OBJ files use bankN_linked_OFFSET.obj. Existing type and special OBJ names remain available. Original resources remain unchanged.

Verification and limits

The modern viewers still flatten visibility branches and use their existing inspection renderers. Near-plane/clipped FE-path rounding, DOS painter ordering, distance shades, pixel-exact rasterization, mission-specific parameter scheduling and actual game playback speed are not reproduced by these controls. State inspection does not implement collision or destruction gameplay.

Reproduction

python3 tools/extract_models.py
python3 tools/extract_model_states.py
python3 tools/inspect_model_components.py
python3 tools/model_viewer.py
python3 tools/city_scene.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_animation_parameters.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_model_animation.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_model_vertices.py
NODE_PATH=/tmp/darker-browser/node_modules node tools/verify_model_animation_browser.js
NODE_PATH=/tmp/darker-browser/node_modules node tools/verify_city_browser.js

In-game identifications and default beacon display

Gameplay and model inspection confirmed the following:

The city viewer defaults to Beacons lit, selecting state 80 for Delphi type 1 only and leaving other cells in state zero. All base states (beacons off) restores the former view. Lit beacon discs use palette index 27, RGB (254,255,255), the bright end of their dynamic shade range (equivalent to dynamic input 31 in our recovered colour lookup). This matches the normal in-game appearance; it is an inspection setting, not a claim to reproduce runtime intensity, faults, flicker or charging state. Other dynamic discs retain their existing shade.

Warehouse and home hangar: type 77

Visual inspection identifies bank-30 type 77's base model as a generic warehouse, and its alternate as the populated player starting/return hangar. The atlas now labels these “Warehouse exterior” and “Player hangar / populated interior”. This is a gameplay identification; mission-specific activation is not newly claimed from code in this pass.

The stored Delphi grid independently supports the approach arrangement. Using zero-based (column, row) coordinates, the central line is:

Types 77 also occupy (48,114) and (50,114), flanking that central cell. There are nine type-77 cells in the map in total, so the type alone does not identify the player's home at every occurrence. Its base header is at bank-pool 6524h; state bit 80 selects 66E3h. Damage-state bits do not select additional models for this type in bank 30.

Timer evidence following the cycle measurement

We measured a complete fountain cycle at close to four seconds, slightly longer but below five. This prompted inspection of the timer setup. The fragment at 0321 loads DX from 1774 (0952h = 2,386) and calls 1838. Executing that fragment in Unicorn produces port writes (43h,36h), (40h,52h), (40h,09h): PIT channel 0, mode 3, divisor 2,386. Using the 1,193,180 Hz PIT frequency also encoded in the music driver's timing constant gives approximately 500.075 interrupts/second, and a 2,048-unit fountain period of 4.09538 seconds. This supports the 500-unit viewer calibration independently of the visual estimate.

The interrupt routine at 0BEA increments the word stored at 0BF0 via the self-modifying ADD AX,1 at 0BF4, with an enable byte and maximum elapsed-step handling. Frame update B0CE–B0D8 takes the difference from 0BF8 and advances that snapshot; DB1A's fountain calculation reads 0BF8. These are timer units, not rendered frames. Pause/enable and step limiting matter, and the whole game's possible timer reconfiguration paths have not been exhaustively traced.

The executed setup result is recorded in timer-setup-verification.json. The pre-existing shield-display startup deadline of 06FFh (1,791 units) would be approximately 3.58 seconds at this rate; beacon fade intervals of 256 units would be approximately 0.512 seconds. These provide possible independent in-game observations, subject to identifying the correct visible transitions.

1995 demo fountain parameter slots

The 1995 demo uses slots 5–10, rather than retail's 9–14, for fountain types 120–123. Its update routine at 4696–46E5 reads clock word 178C and writes six signed heights at SS:6292. Its sine table at 7906 and seven amplitude bytes at 46E6 are identical to retail's; the baseline is likewise -384. tools/verify_demo_animation.py executes the demo routine for every one of the 2,048 clock positions and verifies all 12,288 height words against the recovered calculation. Results are in analysis/demos/animation-verification.json.

The initial demo viewer mistakenly initialized/animated retail slots 9–14. This left demo slots 5–8 at zero, putting those vertices at the model's base, and drove slots 9–10 with the wrong phase bands. The demo exports, model browser and city explorer now use the demo's parameter range, including both initial geometry and incremental animation updates. The shared preview speed remains the retail-calibrated speed; this check verifies the demo's values per clock tick, not a separately measured demo wall-clock period.