Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map3d-flight/.nornir/flightsim-engine-roadmap.md

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 push facett-map3d toward an AAA planetary FLIGHT-SIM renderer, and exactly what landed vs spiked vs roadmapped in this pass.

Two-tier architecture (ratified):

  • facett-map3d stays 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's Mesh3, the wgpu pipeline (HDR offscreen, bloom, ACES, FXAA, SSAO, CSM, Gerstner water, analytic sky), and the SunSky lighting 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.

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):

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:

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):

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.

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)


Build / test notes