Flight Curves & Void Wreckage — Design¶
- Date: 2026-06-05
- Status: Parts 1–4 implemented + verified (web+native): Rift serpentine, Terrain
sweeping course, Void true-heading change, Void wreckage-field variant. Branch
feat/flight-curves-wreckage. - Part 5 (City true 90° corner) deferred — it is a city-geometry rework (the city is a single-axis corridor; a believable corner needs a real built cross-street + heading + scroll-axis swap + dust/recycle remap) that needs a dedicated build→look→tune loop with on-device iteration. Not rushed blind at the tail of a long session. Next focused step.
- Deviations: Rift/Void use a slow phase-drift / eased-heading curve (not a one-shot turn); wreckage uses wireframe box debris (flatShading is a no-op on the unlit MeshBasicMaterial — the wireframe is already hard-faceted), differentiated by erratic tumble + denser fog; web/native debris template sets differ slightly (parity is by concept, not geometry).
- Author: Johannes + Claude (pair)
- Scope: Scene-flight enrichment (the roadmap's "D-next" strand). One cohesive feature, built + verified incrementally, merged once. Builds on the FlightDirector/film engine.
1. Why this exists¶
The flight is always straight-ahead. Johannes wants the world to turn: gentle curves in the Rift, a sweeping direction-change over Terrain, a real heading change in the Void, a believable 90° corner in the City, and a wreckage field as a Void variant the film sometimes flies through. This adds variety + a sense of navigating a place.
Grounding (from the scene-flight analysis): Terrain/City/Rift use the scroll-world model (geometry streams toward a near-stationary camera) — a banked sweeping curve reads convincingly there, but a true heading change needs new geometry. Void is the only scene that truly translates the camera through world-static rocks — a real heading change works best there.
2. Scope (all of it, one brick)¶
- Rift — gentle curves (easy): seam-safe periodic wall offset + synced bank → serpentine.
- Terrain — sweeping direction-change (easy): long banked lateral course + held yaw.
- Void — true heading change (medium): vectorise the forward step + reframe spawn/ recycle/no-spawn-tube into the heading frame.
- Void — wreckage field (medium): a NEW scene id (
wreckage) reusing Void's flythrough engine with debris templates (panels/struts/station fragments); the FilmDirector picks it. - City — true 90° corner (hard): an intersection chunk (perpendicular street + flanking buildings) + heading state + scroll-axis swap (z↔x).
Non-goals: combat actors (still gated off), radio (Brick E), per-scene settings UI (F). No new HUD. calm mode: curves still happen but gentler (no hard banks); the 90° corner still turns (it's geometry-driven, not motion-heavy) but with reduced over-roll.
3. Shared concepts¶
- Heading is scene-owned state (a closure var in
build()), eased toward a target with a sin²/1-exp(-dt/τ)curve, fired on a seeded scheduler (mirror CameraFly's event model). CameraFly'sf.yawis cosmetic (weave-coupled) — the course/heading is added ON TOP:cam.rotation.set(f.pitch, f.yaw + headingYaw, f.roll + bank). Never accumulate ontocam.rotationacross frames (the engine restores a baseline each frame) — compute from absolute progress and SET. - Bank sells the turn. Every curve adds a roll proportional to the heading rate (bank into the turn), damped (~τ for inertia). calm mode → smaller bank.
- Determinism: turn timing/direction from the scene's seeded
ctx.rng()— noMath.random. - Parity: web-first, native fast-follow; same constants/curves mirrored.
4. Per-scene design¶
4.1 Rift — gentle curves (rift.ts)¶
Define a periodic horizontal curve sharing the chunk wavenumber so recycled chunks stay
seam-free: curveX(z) = CURVE_AMP * sin(z * K_BASE) (K_BASE = 2π/CHUNK_LEN already exists).
Each frame, after the scroll/recycle block: cA.position.x = cB.position.x = curveX(camZ-ish)
applied to the chunk groups (not the per-vertex walls). Offset the camera by the same
curveX at the camera's z so it stays centred between the bending walls (clearance is only
±13). Add bank = -curveSlope * gain (damped) to rotation.z (ADD to the sway+barrel-roll,
don't overwrite) + a small rotation.y into the turn. CURVE_AMP ≈ 4, gentle serpentine.
4.2 Terrain — sweeping direction-change (terrain.ts)¶
Scene-local course layered on the weave. A seeded scheduler fires a long one-direction
arc: courseProgress ramps 0→1→0 over ~12 s (sin²). After fly.sample:
cam.position.x = f.x + dir*COURSE_AMP*cs (COURSE_AMP ≈ 38), cam.rotation.set(f.pitch,
f.yaw + dir*0.22*cs, f.roll + dir*0.18*cs). Terrain-following clamp still samples
cam.position.x so NOE clearance holds through the arc. Keep COURSE_AMP+weave < ~130
(WIDTH 280). Optional: extra dust lateral whip during the arc for g-load.
4.3 Void — true heading change (void.ts)¶
Replace the scalar forward step with a vector along a yawed heading:
let heading = 0, headTarget = 0, nextTurnT = 18 + rng()*22;
// scheduler: at t>nextTurnT, headTarget += (rng()<0.5?-1:1) * (rng()<0.4 ? π/2 : 0.4..0.9); reschedule
heading += (headTarget - heading) * (1 - Math.exp(-dts/τ)); // τ≈4s → sweeping arc
const fwd = new THREE.Vector3(Math.sin(heading), 0, -Math.cos(heading));
camPath.addScaledVector(fwd, spd*14*dts); // smoothed path point
right=(cos h,0,sin h)); body yaw =
heading + f.yaw; bank from heading rate. Reframe the spawner (the load-bearing fix):
ahead = (rock.pos - camPath)·fwd; recycle when ahead < BEHIND; respawn at
camPath + fwd*(FAR..) + right*(±FIELD_W) + up*beltY; the no-spawn tube tests against the
camPath line, not the world origin. (heading=0 reduces to today's behaviour — safe superset.)
4.4 Void — wreckage field (new scene wreckage)¶
Extract Void's flythrough core (spawn/recycle/heading/weave) into a shared builder
parameterised by a template set + scene id. void uses the rock templates; wreckage
uses debris:
- panels: thin BoxGeometry(w,h,0.06), random aspect, a corner sheared.
- struts/girders: long thin BoxGeometry(0.12,0.12,L) (or crossed boxes).
- station fragments: 2–4 boxes merged at hard angles (mergeGeometries) → L/cruciform.
flatShading for hard facets; erratic non-uniform tumble; non-uniform mesh.scale. Keep the
infectable ~30% subset (CORP fragments tint to enemy colour). Register wreckage as a
SceneId (union, SCENES, SCENE_LABELS, DICT.MODE_LABELS/BOOT_HEADERS, native
SceneRegistry/SceneMeta) and add it to the FilmDirector PHASE_CANDIDATES (PANIC/SILENCE
tier — debris aftermath). Foreshadow/arrival lines for wreckage.
4.5 City — true 90° corner (city.ts)¶
The honest version (no fake). Add heading state. Author an intersection chunk variant
in buildChunk(): at one slot, open the turn-side wall + lay a perpendicular street with
flanking buildings (reuse the building loop with x/z swapped). Heading-aware scroll:
advance chunks along the current heading vector (not always +z). Gate a turn event on the
intersection chunk reaching the camera (cA.position.z ≈ 0), then ease heading 0→±π/2 over
~3.5 s (sin²) with a banked over-roll, and swap the scroll axis z→x so the perpendicular
street becomes the new corridor; the dust-stream + chunk-recycle follow the active axis. Fold
heading into cam.rotation.set(f.pitch, f.yaw + heading, f.roll). Re-map fly.x to the new
lateral axis after the swap. Budget the cross-street buildings to appear within fog range.
5. Build order (within the one brick)¶
Rift → Terrain → Void heading → Void wreckage scene → City corner. Verify each visually (web screenshots) + commit per part; native mirror per part; one merge at the end.
6. Testing & verification¶
- Native logic tests: heading easing is C1/monotone toward target; curve offset is
periodic (seam-safe,
curveX(z)==curveX(z+CHUNK_LEN)); FilmDirector includeswreckagein its candidate set; void respawn stays within±FIELD_Wof the heading path. - Web
typecheck+verify-crt: capture each scene mid-curve (rift serpentine, terrain arc, void heading sweep, wreckage field, city corner) — confirm no wall-clipping, no seam pop, the camera stays in-corridor (rift/city), the void path stays in the field. - Host-boundary clean; app compile;
?scene=wreckage+?scene=cityreachable for tuning.
7. Risks & invariants¶
- Seam tiling (rift/city/terrain): curve offsets must be periodic or chunk-recycle pops.
- Camera-in-corridor (rift/city): offset the camera with the curve so it never clips a
wall (rift ±13, city corridor); these scenes are
cameraLocked(scene owns the camera). - Void spawner reframe is mandatory for a sustained/90° void turn or rocks stop recycling (recycle is z-only today) and spawn off-path (tube is origin-hardcoded).
- City scroll-axis swap must carry the recycle + dust + lateral-weave remap, or geometry pops / dust flies sideways / weave reads as forward-surge.
- New scene id
wreckagetouches the web SceneId union + DICT maps + native SceneRegistry - SceneMeta (runtime crash if a map entry is missing — add all).
- Determinism + parity + calm as in every feature. No
Math.randomin turn logic.
8. Out of scope → later¶
City beyond a single corner (multi-turn navigation), combat actors redo (D), radio (E), per-scene settings UI (F).