IRON LITANY

Iron Litany — physical slit sight

Current v0.3.0 contract — shared outdoor cones overhead

The creator's latest instruction asks for more slits to contribute almost all the time. Current overhead visibility therefore uses eight short horizontal cones of the actual shared outdoor world, independent of priest facing. Each starts at its physical outer port face, ends after 4.9 m, widens by 0.22 m per metre and fades over the final 0.5 m. Openings narrow the cones; grime attenuates them. Black gaps and the finite outer range remain. The ground and actors are masked directly: no eye-image cards, independent window scenery or offscreen captures are instantiated.

This is a deliberate cutaway convention that relaxes the overhead eye-height test to show actual ground tops and partial passing silhouettes. First person retains the strict real-eye/facing test through both faces of each small armour tunnel, including interior obstruction, shutters and grime. Both modes share the same moving objects, deck and port locations. Each port's centre height follows its local surveyed deck plus 1.48 m.

createExteriorVisionMask().update({mode:'overhead', ...}) selects the convention. Omitting mode returns to the strict first-person contract, so an earlier overhead update cannot leak its exception into later use. The pure oracle is traceOverheadContext; constants are in OVERHEAD_CONE. World diagnostics report shared-world-cones, zero capture targets and zero offscreen captures. The old overhead-glimpses.js capture helper and planners remain an unused, bounded experiment with historical tests.

Actual normal-size evidence: shared-world cones, final bearing work and exterior, and physical first-person slit. Current scope and acceptance limits are in INTERIOR_AND_INCURSIONS_V030.md and PLAYTHROUGH_V7.md. Automated tests distinguish the two contracts; the native GPU probe also requires overhead ground, facing invariance, closed shutters and black beyond-range output.

Historical v0.2.x design and retired overhead experiments

The remainder records the previous eye-aligned overhead convention and its attempted image unfolding. Its shared-world/first-person geometry remains useful, but its claims that overhead must use facing and offscreen eye captures do not describe the current runtime.

The outdoors is black except the portions the custodian can see through the tank's eight small armoured slits. The cutaway camera is an aid to interior work; it does not grant an external scouting camera. Both modes admit rays from the same physical eye, at the current body position, through the same apertures into one shared outdoor scene. A new candidate overhead presentation unfolds the eye's actual slit image beside the opening, so a valid glimpse need not disappear when viewed from above.

Geometry and information limits

dist/game/src/vision.js supplies a pure geometric oracle and a bounded Three material helper. Tank-local coordinates use y for height and negative z for the bow. There are four slits on each side, at x ±4.1 and z −4.5, −1.5, 1.5 and 4.5. Their centre height is 1.48m, their opening is 0.45m wide by 0.18m high, and each tunnel crosses 0.45m of armour. The passage must line up at both the inner face (|x| 3.875) and outer face (|x| 4.325). The outer hull rectangle ends at |z| 6.94. A ray that crosses the bow or stern cannot borrow a side slit.

A distant slit subtends a narrow cone. Moving the custodian changes that cone; walking alongside the side wall changes which slits are useful. The armour tunnel caps the view near a slit. Even from the centre aisle an excessively oblique line to the farthest slit can fail, which is intentional geometry rather than a missing outdoor actor. A horizontal facing gate defaults to 120° total. Turning toward an aperture matters in overhead as well as first person. The aperture itself limits the vertical view; the active camera then determines what part of that physically visible world appears on screen.

The renderer rejects rays blocked by six coarse interior solid masses: reactor, breech, coolant tanks, vox, reliquary backing and ready rack. Low service tables and pipes are excluded from these solid volumes. They are bounds of the current procedural foundation, not authored collision geometry. Future station art must update the boxes, especially the rotating gun cradle.

Each aperture has independent opening and grime values between zero and one. A shutter descends from the upper edge, reducing the actual gap rather than merely dimming it. Grime reduces transmitted light by up to 85%; it never increases the visible cone. These inputs can react to a shot, heat, ash and station upkeep. They do not themselves change simulation rules or spend resources.

Three integration

Install before TankWorld.optimizeScene() so masked external material clones can still batch. The ground and all shared outdoor actors need the mask. The tank, treads, gun barrel, crew, work lamps and interior panels must keep their original materials. All of them may currently share this.mats, so modifying the originals would hide the interior.

import {createExteriorVisionMask,VISION_PORTS} from './vision.js';

// Set when the scene is created. Darkness outside the physical glimpses is intentional.
this.scene.background = new T.Color(0x000000);
this.scene.fog = new T.FogExp2(0x000000, .018);

// After makeTank() and makeWorld(), before optimizeScene().
this.exteriorVision = createExteriorVisionMask(T);
this.exteriorVision.maskObject(this.ground);
this.exteriorVision.maskObject(this.roadBanks);
for (const piece of this.outside) this.exteriorVision.maskObject(piece.root);
this.portOpenings = Array(8).fill(1);
this.portGrime = Array(8).fill(0);

maskObject() caches one clone per source material. It composes a source onBeforeCompile, adds a program cache key, and shares uniforms across the clones. It includes instancing and batching transforms. Standard, Basic, Lambert and Phong materials have the supported chunks; unsupported custom shaders throw a readable integration error. It rejects masked fragments early. The black scene fills those fragments, and invisible outdoor geometry cannot write opaque black depth over the cutaway interior. Grime attenuation runs after fog and colour output so fog cannot illuminate hidden regions.

Update immediately before rendering. The eye is the character's physical eye, never the orthographic camera's position. Apply the same opening array to the actual shutter meshes.

const forward = this.mode === 'first'
  ? {x:-Math.sin(this.yaw), z:-Math.cos(this.yaw)}
  : {x:Math.sin(player.facing), z:Math.cos(player.facing)};
this.exteriorVision.update({
  eye:{x:player.x, y:1.48, z:player.z},
  forward,
  openings:this.portOpenings,
  grime:this.portGrime,
});

First-person facing is independent of movement in the current foundation. When returning to overhead, synchronize player.facing = this.yaw + Math.PI so the character remains facing the direction just inspected. Do not reveal different outdoor pieces on camera change. A small head bob is presentation motion; holding the physical eye at 1.48m keeps that bob from causing a distracting slit flicker. Crouching or leaning should later change the real eye height/position as an explicit mechanic.

The fragment helper bounds work to eight aperture checks, and six box intersections only after a candidate slit passes. Uniform objects persist across updates; no per-frame shader compile or separate outside simulation is needed. The existing one shared actor pool continues to provide consistent passing pieces through adjacent slits. The overhead presentation below adds bounded eye-camera captures of that same geometry.

Candidate readable overhead slit images

The previous mask correctly answered whether an exterior surface could be seen by the physical eye, but the overhead camera then had to see that same surface. Most nearby ground tops lie below the slit cone. Useful distant terrain faces lie outside the overhead frame; nearer eye-facing sides can be hidden from the overhead camera or occupy only a few pixels. A positive shader probe and one passing green fragment did not establish readability during ordinary overhead play. The creator's latest feedback reopens that acceptance gate.

slitEyeViewPlan() intersects the inner and outer opening rectangles as seen from the actual eye. It returns an off-axis perspective frustum whose bounds cannot exceed either face of the 0.45 m-thick tunnel. A nine-ray admission sample additionally rejects facing-away, fully obstructed, closed and invalid-body views. The existing fragment shader still checks every captured pixel against the real facing, six interior masses, shutters and grime. No physical cone or opening has been enlarged.

dist/game/src/overhead-glimpses.js renders the closest one or two useful slit views from the physical eye. Its cameras use an off-axis projection towards the side armour; they do not stand outside the tank. The render targets sample the already-moving ground, road banks and actor pool, using their actual geometry, material and lighting. No independent window scenery, clips, random enemy pictures or painted RGB placeholder view is introduced.

Each image occupies a skinny trapezoid anchored just outside its real outer slit. Its centre axis runs from the eye through the admitted opening. The display extends at most 3.5 m outward and is at most 0.74 m across its far edge; the near edge is the admitted aperture width. The longer display clears the tank's 5.38 m outer track edge and puts most image columns in surrounding black instead of resembling another moving tread. Its narrow aspect also fits the 128 × 24 eye image much more closely. Typical normal overhead footprints are about 71–78 × 31–38 framebuffer pixels. This is a deliberately magnified peripheral presentation of the same admitted image, not a newly revealed top-down terrain patch. The physical eye-camera frustum, aperture and material lighting remain unchanged. The rest of the exterior remains black. Truly black capture pixels are discarded so an empty image cannot leave a black card covering the physical bronze throat or moving tracks. First person hides every foldout immediately.

The captured image's horizontal dimension unfolds along the ribbon's long axis; its short vertical band crosses the narrow width. The first quad mapping instead crushed 128 horizontal columns across the throat while stretching 24 vertical rows along its whole length, distorting passing silhouettes and placing black upper rows at the outer tip. Correcting those UVs retains every sampled ray and the original material brightness. The ribbon is an intentionally unfolded eye image, not a geographic map of the ground beneath its display surface. Its overhead presentation must be judged in the actual full frame, not by an enlarged diagnostic crop alone.

Create after optimizeScene(), because the capture helper caches the final exterior batches. It identifies only meshes whose materials have userData.exteriorVision, plus existing lights. Layer 31 is reserved for the capture transaction. Source and light layers are temporarily enabled there, then their exact previous masks are restored; interior meshes and foldouts stay out of that layer. Do not put unrelated interior content on layer 31. If the exterior is rebuilt or rebatches later, dispose and recreate the helper.

import {createOverheadGlimpses} from './overhead-glimpses.js';

// After all external masking and optimizeScene().
this.overheadGlimpses = createOverheadGlimpses(T, {
  renderer:this.renderer, scene:this.scene, vision:this.vision,
  maxActive:2, resolution:[128,24], hz:10,
});

// Reuse the same sight object passed to this.vision.update(sight).
// After moving the shared actors/banks and updating working lights,
// immediately before the normal scene render:
this.overheadGlimpses.update({...sight, mode:this.mode});
this.overheadGlimpses.render({time:state.time});
this.renderer.render(this.scene, this.camera);

// Read-only diagnostics and shutdown:
const evidence = this.overheadGlimpses.stats();
this.overheadGlimpses.dispose();

The normal stationary cadence is 10 Hz at no more than two 128 × 24 targets. A newly admitted port, substantial eye/facing change, shrinking shutter or return from first person forces a fresh capture. Minor motion can retain the last observed image for up to a normal refresh interval; it cannot discover a new unobserved outside location. Losing admission hides the display immediately. Refresh uses simulation time, so pause freezes passing scenery. The two display meshes add four triangles. Half-float targets preserve the shared lighting before the main pass applies its existing tone mapping; an unsigned-byte fallback is available when the renderer lacks float colour targets.

QA may pass diagnostics:true to the helper to read each physical port's tiny target once. stats().readbacks reports black/readable pixel counts, linear mean RGB, peak, coverage and the capture eye; it does not expose raw image data or alter the view. There are at most eight readbacks per explicit diagnostic pass, none during normal play. requestReadback() rearms an intentional later comparison. The first actual readback found genuine colored terrain in 63% of a port image; its initial overhead screenshot was covered by the restore toast, and later clear frames still made the short image resemble the track. A 1.65 m ribbon and its UV correction did not pass actual full-frame legibility. An active-port diagnostic alone does not meet visual acceptance.

The 3.5 m presentation passes baseline actual visibility at the normal 1280 × 720 player viewport. screenshots/playthrough-v6-overhead-slit-view.png shows the port-2 strip extending left into surrounding black; playthrough-v6-overhead-slit-moving.png changes its captured terrain with a green/red passing fragment. After turning and allowing the refresh to settle, playthrough-v6-overhead-facing-away.png removes the old left strip and admits a different thin starboard-tail view. playthrough-v6-first-person-window-final.png shows the same green bank through the physical slit at that body position. The evidence establishes a working visible peripheral view, not final art acceptance: its terrain still reads as a small, narrow rod or ribbon, and silhouette recognition, material treatment and broader port-position QA remain in review. Keep the current capture and geometry budgets fixed while refining that presentation.

Every capture uses try/finally to restore the previous target/cube face/mipmap, viewport, scissor and test, clear colour/alpha, automatic clear state, shadow update flags and all source/light layer masks. It never changes the main camera or the shared eye uniforms. Shadow maps are reused during tiny captures rather than recalculated per port. stats() reports active port IDs, resolution, target count, captures, last capture time, cached source meshes and lights; it does not mutate simulation state.

Focused checks prove the actual Three off-axis camera's sampled rays still pass the same slit; partial shutters are limited at both faces; closed, facing-away, obstructed and outside-body negatives; eye-relative display anchoring; a small but nonvanishing footprint at the real 720 × 285 overhead camera; shared exterior/light layer isolation; bounded stationary refresh; first-person suppression; and complete restoration even when a render throws. These checks do not establish final visual quality. Root integration still needs normal actual overhead play, moving and turning, shutter/obstruction negatives, a first-person comparison at the same body position, and a passing shared actor observed at successive apertures.

Physical armour and shutters

The implementation replaces the former broad windows with the following small gaps in fullWalls, for first person. The low side sill remains a readable overhead cutaway boundary. Side armour thickness is 0.45m at x ±4.1.

Piecey rangez range
Existing low sill0 to 0.85−6.65 to 6.65
Lower solid wall0.85 to 1.39−6.65 to 6.65
Upper solid wall1.57 to 3.0−6.65 to 6.65
Middle solid strip1.39 to 1.57−6.65 to −4.725
Middle solid strip1.39 to 1.57−4.275 to −1.725
Middle solid strip1.39 to 1.57−1.275 to 1.275
Middle solid strip1.39 to 1.571.725 to 4.275
Middle solid strip1.39 to 1.574.725 to 6.65

The holes match the mask, with small bronze lower lips. Each inner armour face has a 0.18m-high, 0.45m-wide sliding shutter. slitShutterCentreY(port, opening) returns port.y + port.height*opening. Its lower edge is therefore exactly 1.39 + .18*opening, the same upper boundary used by CPU and GPU visibility. At full opening it rises above the gap; at zero it covers the gap. Port/shutter ordering is the four port-side centres first, then the four starboard centres. The earlier centre formula 1.57 + .18*opening misplaced the lower edge by 9cm and has been removed.

For overhead, the low sill may carry a restrained physical slit-frame indicator at each z centre. It must not paint an independently illuminated view or expose a giant opening. Tiny inspection labels or edge highlights may help the player identify where to walk without broadening what is visible outside.

Shared terrain and passing silhouettes

The custodian's eye is 1.48m above the tank floor; the road is at −1m. Looking horizontally through a slit often sees above nearby road debris and human heads. This is why a populated world may still provide an empty black aperture. Outdoor tank silhouettes now have grounded 1.5m tracks and an elevated turret, while ruin columns use their actual half-height as the vertical centre instead of extending tall columns partly underground.

roadsideDebrisLayout() supplies one deterministic shared road-cut rubble family, located at x ±32m. Its three copies repeat a 132m pattern while the whole group translates by worldDistance % 132. This makes the visible terrain coherent with the passing actors and freezes it when the simulation pauses. The copies contain 120 boxes, merged into two masked material batches and 1,440 triangles. Rough, nearest-filtered dirt maps with irregular fracture lines provide the existing low-resolution grain; this is real geometry with the same fragment mask, not scenery painted into individual windows. Its distant faces provide a terrain reference in first person, while nearer moving armour and ruin fragments can appear in the overhead frame. Black sky and solid armour remain black.

Evidence and remaining review

Eight focused automated cases verify the geometric contract: all eight aligned tunnels; eye movement and facing; eye height; the two armour faces; bow/armour rejection; adjacent glimpses in one moving world; shutter and grime state; blocking masses; exact physical shutter alignment; and actual bounded roadside geometry intersecting the real starboard-2 ray throughout its movement cycle. Material and batching checks verify source material isolation, stable shared uniforms, mask retention and the terrain render bound, using the actual Three library. Run node --test tests/vision.test.mjs.

Actual first-person gameplay now shows the rough terrain face through starboard-2 at eye x 3.2, y 1.48, z −1.605, facing +X, while black sky and solid armour remain black. Evidence: normal gameplay screenshot. The initial rough face was only RGB (21,14,6) in the native framebuffer, visually indistinguishable from an empty aperture during play. Brighter contrasting soil/stone fixes the readability, with no extra visibility. Uploaded GPU eye, hull, aperture and facing uniforms were separately inspected and were correct; this was not an all-zero uniform or broken shader diagnosis.

[Native GPU evidence](VISION_GPU_QA_V5.json) records the actual production mask rendered into a 64×64 target: an aligned aperture colours 640 of 4,096 pixels; all pixels remain black with closed shutters, reversed facing, a moved eye or an interior obstruction. The smoke fails if the aligned fragment is absent or exceeds 30% of the frame. This probe is embedded in native desktop verification for both platforms. It supplements actual play rather than claiming art approval.

Actual overhead gameplay at the same eye position and facing shows a green armoured fragment outside starboard-2, surrounded by black, at road cycle 42.6. Evidence: normal overhead screenshot. This was captured through ordinary camera, pause and movement controls, with no diagnostic material, state edits or widened aperture. Nearer passing armour is within the overhead frame; the distant road-cut bank is outside it. Both views use the same world and eye-relative mask.

Continue checking real frames while moving and turning, at a side slit and behind the reactor. Nearly all outdoors must remain black, armour must remain thick, shutters must match their input, and masked outdoor geometry must not hide the cutaway interior. An enemy traversing successive slits must keep identity, direction, position and timing. Cannon shock and pause must not leave shutters permanently closed or drive terrain with wall-clock time.

Current limitations: occlusion uses six conservative boxes, not every pipe or crew body; there is no leaning or crouching; grime attenuates rather than blurs; and the rigid tank-local origin assumes the hull is not translated or rotated as a whole. A future world-moving hull must transform eyes, aperture and occluder bounds together. None of these limits justify a broad external view in overhead.