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:
- Initialize all six parameters to -384 (FE80).
- Compute
phase = ((clock << 5) & FFFF) * 6. - The high word selects a band, 0–5. The low word shifted right seven indexes the original 1,024-entry signed sine table.
- Add
floor(amplitude[band] * sine / 65536)to that band's height. - 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
- Parameter/state checks: all 12,288 fountain height words across a full cycle match original x86 execution exactly; all 3,128 high-bit city-state cases also match exactly.
- Animated projection checks: ten animated definitions (including two linked gates), six parameter/clock combinations and two identity-axis projections. All emitted positions agree within one screen pixel of the original ordinary FCxx interpreter.
- Vertex regression checks: 496 polygon-bearing directory/special/extra entries, 35,112 vertex events; static comparisons remain exact, animated comparisons allow one pixel.
- Browser animation checks: visible pose changes, animation playback, 27 linked-model selections, state OBJ links, 24 map/state combinations and camera/selection preservation; no JS errors.
- Existing city checks still pass for all ten maps, GPU picking, orientation, isolation, movement and responsive rendering.
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:
- Types 17/19/21/23 are directional hangar entrances, paired with landing-light models 18/20/22/24. In this bank only the placed orientations 17/18 and 21/22 have distinct alternate definitions. The gate alternates expose interiors; the light alternates indicate activation/permission to enter, not interiors.
- Type 26 is a tunnel entrance; its alternate definition exposes the tunnel mouth's interior.
- Type 1 is the energy beacon. Its base model is off; its alternate has the lit top surface. The off disc uses fixed colour 0D, while the alternate disc uses dynamic token 1C. The model browser now labels these states by their observed meanings, without assigning those meanings universally to bit 80.
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:
- (49,112): type 18 landing approach lights.
- (49,113): type 17 entrance/gate.
- (49,114): type 77 warehouse/hangar.
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.