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/src/lib.rs

//! **facett-map3d-flight** — the AAA planetary **FLIGHT-SIM** engine layer over the
//! lightweight, embeddable [`facett_map3d`] map. This crate carries the heavy,
//! GPU-required, full-screen "bells and whistles"; `facett-map3d` stays lean
//! (flat ground + extruded house markers, CPU-fallback, deck-pane-embeddable).
//!
//! ## What has LANDED here (CPU foundation — build + test green, headless)
//! - [`origin`] — **floating origin**: an `f64` Web-Mercator world frame +
//!   per-frame camera-centred rebase to `f32`, killing far-from-origin vertex
//!   jitter / Z-fighting at planetary scale (FLIGHT-SIM #1).
//! - [`atmos`] — **precomputed atmospheric scattering**: a Bruneton/Hillaire-style
//!   transmittance LUT + single-scatter sky-view + aerial perspective, feeding both
//!   the sky AND the terrain/building haze, varying with altitude + sun angle
//!   (FLIGHT-SIM #2). Derived sky tints are ready to drive `facett_map3d`'s existing
//!   analytic `sky.wgsl` uniforms.
//! - [`camera_fly`] — the **camera rig**: a deterministic 6DOF state machine
//!   (free-fly primary / cinematic orbit / flight-stick) over the `f64` world frame,
//!   exercising the floating origin.
//! - [`terrain`] — **real terrain elevation (DEM)**: load a Digital Elevation Model
//!   (SRTM `.hgt`, zero extra deps) or the deterministic synthetic fallback, sample
//!   `elevation_at(lon, lat)` (bilinear), and build the 3D terrain mesh from actual
//!   heights (with normals, rebased onto the floating origin) — breaking the flat
//!   `y = 0` plane so flying over Sweden has real topography (FLIGHT-SIM #6).
//!
//! ## What is LANDED behind the `wgpu` feature
//! - [`scatter_gpu`] — the **GPU scattering-LUT precompute pass**: a compute shader
//!   (`scatter_lut.wgsl`) ray-marches the transmittance LUT into a storage buffer and
//!   a sampled `Rgba32Float` texture, validated texel-for-texel against the [`atmos`]
//!   CPU oracle. The CPU model stays the fallback + reference (L2, no regression).
//! - [`extrude_gpu`] — the **GPU-compute building extrusion** (FLIGHT-SIM #3b): a
//!   `BuildingMeta` marshaller (always available, headless) packs footprints into
//!   storage buffers + the shared §3a ear-clip cap indices, and `extrude.wgsl`
//!   (`cs_extrude`, behind `wgpu`) builds the prism geometry on the GPU, proven
//!   vertex-for-vertex equal to `facett_map3d::extrude_prism_mesh` (the untouched CPU
//!   path stays the fallback + oracle, L2). The RED-when-broken oracle gate runs
//!   GPU-less; the dispatch parity self-skips without an adapter.
//!
//! ## What is ROADMAPPED / spiked (see `.nornir/flightsim-engine-roadmap.md`)
//! Sampling the GPU LUT texture in `sky.wgsl` (+ the mesh aerial-perspective + the
//! multi-scatter/sky-view textures), the GPU-compute-extrusion indirect
//! `draw_indexed_indirect` render wiring (the [`extrude_gpu`] marshaller + compute pass
//! have landed), CDLOD quadtree LOD selection + the building/road drape
//! onto the now-real terrain, and the out-of-core Hilbert-sorted GeoParquet streaming.
//! The DEM heightfield + terrain mesh (the base of §6) has **landed** in [`terrain`];
//! the CDLOD subdivision + drape ride on top of it. The shared `Mesh3` + wgpu
//! pipeline + sky/water shaders are **reused** from `facett_map3d` (re-exported
//! below), not forked.
//!
//! Everything that lands here is pure CPU math (no GPU, no egui at the foundation
//! layer), so the precision + scattering + rig invariants are directly
//! inject-assert-testable and deterministic (FC-7).

pub mod atmos;
pub mod camera_fly;
pub mod cdlod;
/// **Time-of-day sun driver + weather** — the day/night cycle that animates the
/// shared [`SunSky`] (sun rises east → season-dependent noon → sets west; golden-hour
/// reddening derived from the reused [`atmos`] transmittance LUT; overcast variant).
/// The single biggest realism win over a fixed midday sun. See [`daycycle`].
pub mod daycycle;
pub mod cinematic;
pub mod control;
/// **GPU-compute building extrusion** (FLIGHT-SIM #3b) — the `BuildingMeta` marshaller
/// (always available, headless) + the `extrude.wgsl` compute pass (behind `wgpu`),
/// with `facett_map3d::extrude_prism_mesh` as the reference oracle. See [`extrude_gpu`].
/// **The FLIGHT viewer's `Facet`** — the host-facing pane over this engine. Closes
/// `.nornir/osm-3d-gaps.md` C:69 ("6DOF flight camera — engine exists, no `Facet`
/// yet") and C:72 (first-person pedestrian walk, a FLIGHT-owned feature). Only
/// compiled with the `egui` feature, since a `Facet` is by definition an egui pane;
/// the engine core stays egui-free and headless.
#[cfg(feature = "egui")]
pub mod facet;
pub mod extrude_gpu;
pub mod origin;
pub mod sweden;
pub mod terrain;
pub mod tiles;

/// **GPU scattering-LUT precompute pass** (behind the `wgpu` feature) — dispatches
/// the transmittance LUT on the GPU into a sampled texture, validated texel-for-texel
/// against the [`atmos::TransmittanceLut`] CPU oracle. The CPU model stays the
/// fallback + reference (L2: additive, no regression).
#[cfg(feature = "wgpu")]
pub mod scatter_gpu;

pub use atmos::{Atmosphere, TransmittanceLut};
pub use camera_fly::{FlightCamera, FlyInput, FlyMode};
pub use daycycle::{DayCycle, SolarAngles, Weather, daylight_curve, golden_hour_factor, solar_angles, sun_dir};
pub use cdlod::{Cdlod, CdlodConfig, SelectedNode, morph_height};
pub use extrude_gpu::{BuildingMarshal, BuildingMeta, Footprint, GeomParity, geom_parity};
pub use cinematic::CinematicFlight;
pub use control::{ControlFrame, FlightController};
pub use origin::{FloatingOrigin, WorldPos};
#[cfg(feature = "egui")]
pub use facet::{FlightFacet, Locomotion, WALK_EYE_HEIGHT_M, WALK_PITCH_LIMIT, WALK_SPEED_MPS};
pub use sweden::{SpatialKey, SwedenExtent, tiles_in_view};
pub use terrain::{
    Dem, Elevation, GeoRegion, ProceduralTerrain, TerrainField, TerrainMesh, build_terrain_mesh, parse_hgt_name,
};
pub use tiles::{
    DirTileSource, FrameReport, InMemoryTileSource, Msg, TileLoader, TilePlan, TileSource, plan_tiles,
};

#[cfg(feature = "wgpu")]
pub use scatter_gpu::{GpuLut, GpuScatterLut};

#[cfg(feature = "wgpu")]
pub use cdlod::CdlodMorphGpu;

#[cfg(feature = "wgpu")]
pub use extrude_gpu::ExtrudeGpu;

/// Re-export the shared, reused pieces from the lightweight map so flight-sim
/// callers extend (not fork) them: the extruded mesh contract + the sun/sky model
/// (the seam the scattering plugs into) + the LOD ladder.
pub use facett_map3d::{LodTier, Mesh3, SunSky};

/// Bridge the [`atmos::Atmosphere`] scattering model onto `facett_map3d`'s
/// [`SunSky`] sky uniforms: returns a `SunSky` whose `sky_color` is the
/// atmosphere-derived **zenith** tint at the given altitude + sun, so the existing
/// analytic GPU sky pass (and the CPU painter fog) become altitude/sun-driven with
/// no new GPU pass. The horizon tint is returned alongside for the `sky.wgsl`
/// `horizon` uniform / the fog base colour.
///
/// This is the **seam** the roadmap's GPU LUT precompute slots behind: today the
/// tints are computed on the CPU from the deterministic LUT; tomorrow the same
/// numbers come from a GPU sky-view texture, with this function as the oracle.
#[must_use]
pub fn atmospheric_sunsky(base: SunSky, atmos: &Atmosphere, altitude_m: f32, exposure: f32) -> (SunSky, [f32; 3]) {
    let (zenith, horizon) = atmos.sky_colors(base.dir, altitude_m, exposure);
    let mut sky = base;
    sky.sky_color = zenith;
    (sky, horizon)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **INJECT-ASSERT (the scattering seam wires into the shared SunSky):** the
    /// atmosphere-derived `SunSky` keeps the base sun direction but replaces the sky
    /// tint with the physical zenith colour, and that tint CHANGES with altitude —
    /// proving the flight layer drives `facett_map3d`'s existing sky model as data.
    #[test]
    fn atmospheric_sunsky_drives_the_shared_sky_model() {
        let atmos = Atmosphere::earth();
        let base = SunSky::golden_hour();
        let (low, horizon_low) = atmospheric_sunsky(base, &atmos, 0.0, 12.0);
        // Sun direction is preserved (lighting unchanged); only the sky tint moves.
        assert_eq!(low.dir, base.dir, "the sun (lighting) is untouched");
        assert!(low.sky_color.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "valid tint");
        assert!(horizon_low.iter().all(|v| v.is_finite()), "horizon tint valid");
        // Climbing changes the sky tint (the atmosphere thins overhead).
        let (high, _) = atmospheric_sunsky(base, &atmos, 30_000.0, 12.0);
        let d: f32 = (0..3).map(|i| (high.sky_color[i] - low.sky_color[i]).abs()).sum();
        assert!(d > 1e-3, "the shared sky tint responds to altitude (Δ={d})");
    }

    /// **INJECT-ASSERT (the floating origin + rig + atmosphere compose):** a far-flung
    /// flight camera produces a small rebased eye AND a finite physical sky — the
    /// three landed pillars work together at planetary scale.
    #[test]
    fn pillars_compose_at_planetary_scale() {
        let cam = FlightCamera::free_fly(WorldPos::from_lonlat(151.2, -33.87, 8000.0)); // Sydney, 8 km up
        let origin = cam.make_origin(128.0);
        let eye = cam.eye_relative(origin);
        assert!(eye.iter().map(|v| v.abs()).fold(0.0, f32::max) <= 128.0 + 1e-3, "rebased eye is small");
        let atmos = Atmosphere::earth();
        let sky = atmos.sky_radiance([0.0, 1.0, 0.0], SunSky::noon().dir, 8000.0, 24);
        assert!(sky.iter().all(|v| v.is_finite() && *v >= 0.0), "physical sky at 8 km is finite");
    }
}