FLIGHTSIM-ENGINE — AAA planetary flight-sim engine roadmap (design + state)
Branch
agent/map3d-flightsim-engine. The plan a Gemini consult laid out (Rickard endorsed) to pushfacett-map3dtoward an AAA planetary FLIGHT-SIM renderer, and exactly what landed vs spiked vs roadmapped in this pass.Two-tier architecture (ratified):
facett-map3dstays lightweight + embeddable: flat ground + extruded house markers, rotatable, CPU-fallback (works headless / wasm / in a deck pane). No f64 floating-origin overhead, no DEM/CDLOD, no GPU-required paths. The only change it took this pass is the robust CPU earcut correctness proof (a pure win, zero perf/embeddability cost).facett-map3d-flight(NEW crate) carries every heavy, GPU-required, full-screen "bells & whistles" feature. It reuses (does not fork)facett-map3d'sMesh3, the wgpu pipeline (HDR offscreen, bloom, ACES, FXAA, SSAO, CSM, Gerstner water, analytic sky), and theSunSkylighting seam.
STATE — landed / spiked / roadmapped
| # | Feature | State | Where |
| 1 | Floating origin (f64 world + camera-relative f32 rebase) | ✅ LANDED (CPU foundation, tested) | facett-map3d-flight/src/origin.rs |
| 2 | Precomputed atmospheric scattering (transmittance LUT + sky-view + aerial perspective) | ✅ LANDED (CPU model + SunSky seam, tested as DATA) |
facett-map3d-flight/src/atmos.rs, lib.rs::atmospheric_sunsky |
| — | Camera rig (6DOF free-fly / orbit / flight-stick) | ✅ LANDED (CPU state machine, tested) | facett-map3d-flight/src/camera_fly.rs |
| 3a | Robust CPU earcut for concave footprints | ✅ LANDED (already robust; added watertight area-conservation proof) | facett-map3d/src/extrude.rs::ear_clip + test |
| 3b | GPU-compute extrusion (BuildingMeta marshaller + indirect draw) | ✅ LANDED (marshaller + extrude.wgsl compute pass, GPU output proven == CPU extrude_prism_mesh oracle; RED-when-broken GPU-less, dispatch parity self-skips; indirect-draw render wiring still roadmap) |
facett-map3d-flight/src/extrude_gpu.rs + extrude.wgsl; docs extrude-gpu-*.md |
| 4 | HLOD + impostors (quadtree merge / billboards / semantic filter) | 🟡 ROADMAP | this doc §4, wires facett-map3d/src/lod.rs |
| 5 | Out-of-core GeoParquet streaming (Hilbert sort + async range reads) | 🟡 ROADMAP | this doc §5, facett-osm |
| 6 | CDLOD terrain quadtree (distance-LOD selection + crack-free morph, draped on the landed DEM terrain) | ✅ LANDED (CPU selection math + drape, RED-when-broken GPU-less; GPU height-morph parity behind wgpu; render-pass wiring still roadmap) |
facett-map3d-flight/src/cdlod.rs + cdlod_morph.wgsl; docs cdlod-terrain-*.md |
| 7 | GPU scattering LUT precompute (compute pass → Rgba32Float texture, CPU-oracle-validated) | ✅ LANDED (behind wgpu); sky.wgsl SAMPLING now WIRED into facett-map3d's live sky pass (on-screen, adapter-proven) |
facett-map3d-flight/src/scatter_gpu.rs + scatter_lut.wgsl; wiring in facett-map3d/src/{sky.wgsl,gpu.rs,lib.rs}; docs scatter-lut-gpu-*.md |
| 8 | Time-of-day sun driver + weather (animate SunSky: east→season-noon→west, golden-hour reddening from the reused transmittance LUT, twilight/night dimming, overcast variant) |
✅ LANDED (CPU model, headless-tested — the single biggest realism win) | facett-map3d-flight/src/daycycle.rs; demo facett-demo/src/demo_photoreal.rs; bench benches/heavy_daycycle.rs; docs daycycle-{design,guide,for-idiots}.md |
The CPU foundation (#1, #2, rig) is fully build + test green, headless, deterministic (FC-7). The GPU layers (3b/6/7) are the next implementation pass; the CPU models landed here are their reference oracles.
1. FLOATING ORIGIN — LANDED
Problem. WGS-84 → Web-Mercator metres (EPSG:3857) puts a tile near the antimeridian / high latitude ~2×10⁷ m from the projection origin. f32 mantissa is 24 bits → at 2×10⁷ m the grid step is ~2 m: every vertex snaps to a 2 m lattice, so edges shimmer (jitter) and Z-fight at altitude, and a sub-metre DEM is impossible.
Fix (landed). Keep the whole sim in f64 metres (WorldPos). Each frame, anchor a camera-centred origin (FloatingOrigin, snapped to a coarse grid so it only jumps in discrete steps — no per-frame re-translate crawl) and upload geometry as f32 relative to that anchor. The subtraction is done in f64 before the cast, so the f32 only carries the small camera-relative offset and keeps sub-mm precision anywhere on Earth.
WorldPos::from_lonlat— the spherical Web-Mercatorf64world transform; unit-tested to agree withfacett_map::mercator(same globe → OSM tiles line up).FloatingOrigin::{at, snapped, rebase, rebase_all}— the per-frame rebase.- Tests: a tile 2×10⁷ m out with the camera 50 m away rebases to <1e-3 m error while the naive cast-then-subtract path loses >0.5 m (the jitter); precision holds across a global sweep; snapping is stable within a grid cell + idempotent.
Roadmap (GPU wiring). Feed origin.rebase_all(tile_world_positions) into the wgpu vertex buffers and pass the camera-relative eye (FlightCamera::eye_relative) into the existing view_proj. The matrices stay f32; only the per-tile rebase is new.
2. ATMOSPHERIC SCATTERING — LANDED (CPU model + seam); GPU pass roadmapped
Model (landed). A compact, deterministic, curved-planet Bruneton/Hillaire-style atmosphere (atmos.rs, all maths in km):
- Transmittance LUT (
TransmittanceLut) — a precomputed 2D tableT(altitude, μ)of Rayleigh + Mie + ozone extinction to the top of the atmosphere, bilinear-sampled. The CPU analogue of Bruneton's transmittance texture; it is the oracle the GPU LUT will be validated against. - Single-scatter sky-view (
Atmosphere::sky_radiance) — ray-marches the curved atmosphere, integrating Rayleigh + Mie in-scatter with the LUT for sun-transmittance, plus a cheap isotropic multi-scatter fill so the zenith never crushes to black. - Aerial perspective (
Atmosphere::aerial_perspective) —(transmittance, inscatter)over a bounded distance, so terrain/buildings shade assurface·T + inscatter(real haze build-up that replaces the harsh distance-fog clamp). - Derived sky tints (
Atmosphere::sky_colors) +lib::atmospheric_sunsky— the seam: returns afacett_map3d::SunSkywhosesky_coloris the physical zenith tint at the current altitude + sun, ready to drive the existing analyticsky.wgsluniforms / the CPU fog base, with the horizon tint alongside.
Tests (render-proof as DATA). Transmittance is physical + reddens at grazing angles; the daytime zenith is blue; the horizon reddens as the sun sets; the zenith darkens with altitude (12 km, 40 km); aerial perspective builds haze with distance + reddens distant surfaces; the derived tints feed the uniforms + track altitude; evaluation is deterministic.
GPU pass — LANDED (transmittance LUT), sky-sampling roadmap. The transmittance LUT precompute now runs on the GPU as a compute pass (scatter_gpu.rs + cs_transmittance in scatter_lut.wgsl, behind the wgpu feature): one invocation per (altitude, μ) texel ray-marches the exact same Rayleigh+Mie+ozone extinction as atmos::optical_depth_to_top, reads back to GpuLut::texels, and uploads a sampled Rgba32Float texture. It is validated texel-for-texel against the CPU TransmittanceLut oracle (GpuLut::parity_vs): above-horizon (μ≥0) parity ~2e-5, the only divergences the below-horizon sqrt-cancellation knife-edge (≤ h+2 texels, all in the occluded band). The CPU model stays the fallback + oracle (L2).
Sky-pass sampling — NOW WIRED (on-screen). The LUT is sampled in facett-map3d's live sky pass: sky.wgsl gained a texture_2d<f32> + non-filtering sampler (bindings 1/2) and, when the LUT-on flag is set, multiplies the sun disc/glow by the sampled transmittance-to-sun (indexed by the sun's elevation cosine μ and the viewer altitude), so the sun physically reddens at low elevation. The seam is device-neutral raw texels — facett_map3d::SkyTransmittanceLut::from_rgb(mu, h, &gpu_lut.texels, alt_norm) hands the flight crate's GpuLut readback to Map3D::set_sky_transmittance_lut (a wgpu::Texture can't cross devices, so the renderer re-uploads the texels on the render device in gpu::Map3dRenderer::ensure_sky_lut). Additive/no-regression (L2): a 1×1 all-ones default LUT is bound until a real one arrives, so the analytic sky is byte-identical when no LUT is set (proven: the unit-LUT frame changes 0 px). Proven on-screen by tests/sky_lut_proof.rs (adapter-gated, self-skips) — a reddening LUT shifts ~60% of the frame's sun-facing sky blue-ward-down (mean Δblue ≈ −40, Δred ≈ 0); GPU-less RED-when-broken half in gpu.rs::sky_lut_wiring_registered_and_uniform_sized. Still roadmap: the mesh aerial-perspective, and the multi-scatter + sky-view textures. See scatter-lut-gpu-design.md / -guide.md / -for-idiots.md.
CAMERA RIG — LANDED (free-fly primary / orbit / flight-stick)
camera_fly.rs — a deterministic state machine over the f64 world frame, the rig that honestly exercises the floating origin:
- Free-fly (primary) — 6DOF noclip: WASD/up-down translate along the camera basis, mouse-look drives yaw + pitch (clamped shy of the poles).
- Cinematic orbit — swing a world target at a fixed radius (showcase).
- Flight-stick — pitch/yaw/roll rates + throttle along the banked forward axis.
Injected dt, no clock → a scripted robot-UI flythrough is bit-reproducible (FC-7). Tests: orthonormal basis at any pose; free-fly moves where the keys say + clamps pitch; the stick rolls + throttles; orbit holds its radius; the rig's rebased eye stays within a grid cell of the snapped origin even 2×10⁷ m out.
Roadmap. Wire egui input → FlyInput; mode hotkeys; mouse-capture for mouse-look; collision/terrain-clamp against the DEM (§6). The existing OrbitCamera in facett-map3d stays the lightweight embeddable camera; this rig is the flight crate's.
3a. ROBUST CPU EARCUT — LANDED (in facett-map3d)
The hand-rolled centroid-fan was already replaced by a standard O(n²) ear-clip (extrude.rs::ear_clip) that triangulates concave footprints correctly (the "starburst" regression test guards the no-centroid-fan property). This pass added a stronger correctness proof: earclip_is_watertight_and_area_conserving_on_concave_shapes sweeps L / U / plus / zig-zag footprints and asserts each triangulation emits exactly n−2 non-degenerate triangles whose areas sum to the polygon area (watertight, no gaps/overlaps/fabricated area).
Not handled (roadmap): holes (courtyards / donuts / OSM multipolygon inner rings). The Way model carries a single ring; hole support needs (a) inner-ring geometry on Way, and (b) a hole-aware earcut (bridge inner rings to the outer, or adopt a constrained-Delaunay / earcutr-style indexer). See §3b for where the cap indexer lands in the GPU path.
3b. GPU-COMPUTE EXTRUSION — LANDED (marshaller + compute pass; oracle-proven)
Refactor extrude.rs from a geometry generator into a data marshaller: the CPU only packs raw OSM into tight Arrow/storage buffers; the GPU builds the mesh. Landed in facett-map3d-flight/src/extrude_gpu.rs + extrude.wgsl (docs extrude-gpu-{design,guide, for-idiots}.md): BuildingMarshal::from_footprints packs the two storage buffers + BuildingMeta; cs_extrude (one invocation per building) emits walls + caps; the GPU output is proven vertex-for-vertex equal to the landed CPU extrude_prism_mesh oracle. The facett-map3d CPU extrude_prism is untouched and stays the fallback + oracle (L2); the robust §3a ear-clip is reused via the new facett_map3d::cap_triangulation / open_cap_ring / extrude_prism_mesh seam (L5). Still roadmap: the min_height OSM wiring into the OSM Way model, hole/inner-ring caps, and the indirect draw_indexed_indirect render-pass wiring into facett-map3d's pipeline (needs a GPU box to robot-verify on-screen). The original spike plan (retained):
- Two storage buffers: (1) a flat 2D contour vertex buffer; (2) a metadata buffer of
struct BuildingMeta { vertex_start: u32, vertex_count: u32, height: f32, min_height: f32 /* OSM building:min_height / overpass float */, building_type: u32 /* styling */ }. extrude.wgslcompute, parallel per building: generate walls (each sequential contour edge → 2 tris frommin_height→height) and emit the caps.- Hybrid cap tessellation (the key nuance): do not attempt full robust concave tessellation in the compute shader from scratch. A lightweight CPU/parallel earcut indexer (the §3a ear-clip, extended for holes) pre-computes the cap triangle indices (handles concave + holed footprints: H-shapes, courtyards, donuts); the GPU extrudes and the draw uses an indirect buffer (
RenderPass::draw_indexed_indirect). This fixes concave-footprint breakage and offloads the CPU. - Status: the
Mesh3contract + the robust CPU earcut (§3a) are the landed pieces the marshaller reuses; the storage-buffer layout +extrude.wgsl+ indirect draw are the spike to build behind the flight crate'swgpufeature.
4. HLOD + IMPOSTORS — ROADMAP
A hierarchical quadtree over OSM features: high altitude → merged low-poly block clusters / 2.5D billboard impostors with parallax; semantic filtering by importance (query only highway/skyscraper at altitude via the Arrow column). Wire into the existing altitude ladder in facett-map3d/src/lod.rs (LodTier already buckets ground/low/mid/high) — extend it with per-tier feature budgets + an impostor atlas.
5. OUT-OF-CORE GeoParquet STREAMING — ROADMAP
Hilbert-curve-sorted GeoParquet + async parquet range-reads (footer-first → frustum- driven byte ranges) on a background worker dispatching Msg::TileLoaded(SpatialKey, RecordBatch) to the render loop (non-blocking, FC-7). Lives in facett-osm; the floating origin (§1) is the prerequisite that makes streamed planetary tiles render without jitter, and WorldPos::ground_distance is the frustum/priority metric seam.
6. CDLOD TERRAIN QUADTREE — LANDED (CPU selection + drape; GPU morph parity; render wiring roadmap)
The real DEM terrain (terrain.rs, §6-DEM) already broke the flat y=0 quad. This pass adds the continuous-distance LOD on top: a CPU CDLOD quadtree (cdlod.rs) that selects which terrain patches to draw at which resolution by camera distance, with a crack-free morph factor, feeding the existing build_terrain_mesh drape (reused, not forked). See cdlod-terrain-design.md / -guide.md / -for-idiots.md / -reasoning.md.
Cdlod::select(camera)— Strugar's recursion over the four lon/lat quadrants: draw a node whole at its level unless the finer level's range reaches it, else subdivide; children the finer range misses are drawn at the parent level → adjacent nodes differ by ≤ 1 level and the selected regions exactly tile the extent (crack-free, proven watertight). Pure function of the camera (FC-7 deterministic).morph_factor/morph_height— the per-node0→1blend across the outer third of each range, and the per-vertex height lerp toward the parent grid it drives (the crack-closer, no popping).morph_heightis the oracle the GPU pass matches.Cdlod::build_meshes— the drape: oneTerrainMeshper node over its region, near = small patch (fine topography), far = large patch (coarse), at a constant per-node grid → bounded triangle budget.- GPU (
cdlod_morph.wgsl·cs_morph, behindwgpu) — a compute twin ofmorph_height, read back and validated texel-for-texel vs the CPU oracle (CdlodMorphGpu), self-skipping with a recorded sentinel when no adapter is present.
Tests (GPU-less RED-when-broken): subdivision-follows-distance, watertight tiling, ≤ 1-level neighbour invariant, morph-ramp endpoints + monotonicity, morph-height blend, bounded triangle budget, deterministic selection, drape-reuses-terrain. Emitters: cdlod_subdivision, cdlod_morph_factor, cdlod_drape, cdlod_morph_gpu_parity.
Still roadmap (needs a GPU box to robot-verify): wire the selected node instances + an in-shader morphing terrain vertex pass into facett-map3d's renderer (the on-screen draw), frustum-cull the selection, and join CDLOD depth to the §4 HLOD feature budget.
The original §6 plan (retained for context)
- A global heightmap texture the terrain vertex shader displaces by
y = textureSample(elev, uv)·scale. - DEM source: Lantmäteriet GSD-Höjddata 2 m as the primary for Sweden; SRTM / AWS Terrain Tiles as the global fallback. Wire the loader behind a feature/seam; a synthetic noise heightmap fixture is fine for the headless test.
- The drape problem (once terrain ≠ flat, footprints/roads float or bury): sample the elevation heightmap at each footprint centroid and offset
min_height+heightby that base elevation before extrude (ties into the §3b GPU marshaller). Roads: prefer conforming tessellation (project/tessellate the road vector into the terrain eval pass) over screen-space decals (decals smear at grazing flight angles).
Build / test notes
- The flight crate's CPU foundation builds + tests without a GPU adapter (
cargo test -p facett-map3d-flight). The GPU layers (3b/6/7) will sit behind thewgpufeature and self-skip without an adapter (the kittest pattern the existingfacett-map3drender-proofs use). - Worktree caveat: sibling cross-repo path deps (
../../znippy-zoomies, etc.) hard- reference the canonical../facett/*checkout, so the full workspace only resolves from/home/rickard/git/facett; a git worktree at another path hits a lockfile collision. Validate flight-crate changes from a standalone harness pointing at the canonicalfacett-*crates (or from the canonical checkout).facett-map3dchanges were validated transiently from the canonical checkout. No benches; branch-only.