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/terrain-reasoning.md

Terrain DEM — reasoning (why it's built this way)

Design-only notes live in the module docs (src/terrain.rs) and the roadmap (.nornir/flightsim-engine-roadmap.md §6). This file is the rationale — the choices behind the real-elevation terrain (roadmap §6, the "flat ground today" depth item), and what is deliberately left for the next pass.

Why SRTM .hgt as the "real DEM source"

The task asked for a real DEM format (GeoTIFF / heightmap tile / open Swedish dataset) plus a synthetic fallback. We chose SRTM .hgt because:

Lantmäteriet GSD-Höjddata (the §6 "primary for Sweden") is a licensed/bulk GeoTIFF product; wiring its loader is a follow-up (a new Dem::from_geotiff behind an optional feature) — the grid/sampler/mesh contract here is exactly what it feeds.

Why bilinear (not nearest / bicubic)

Bilinear is the standard heightmap filter: continuous (no stair-steps that would read as terraces when flying low), cheap, and exact for a linear field — which gives a hard, non-fuzzy unit-test oracle (bilinear_is_exact_on_a_linear_ramp). Bicubic would smooth ridgelines slightly better but costs 16 taps + overshoot handling; not worth it before the GPU CDLOD pass exists.

Frame choice — Y-up render vertices (the subtle bit)

The f64 world frame (origin::WorldPos) is (x=easting, y=northing, z=altitude). But the render/camera frame is Y-up: camera_fly::FlightCamera::up() is +Y and roadmap §6 literally says the terrain shader displaces along y. So the mesh builder rebases in the world frame and then maps to render axes [easting, elevation, northing] — elevation is the Y axis. This keeps the terrain consistent with the existing camera basis and the shared facett_map3d::Mesh3 (Y-up), so a TerrainMesh::to_mesh3() drops straight into the reused wgpu pipeline. (The raw origin.rebase output is [easting, northing, elev]; the terrain swaps the last two. Buildings/roads, when they are draped onto terrain in a later pass, must use the same swap — noted for the §3b/§6 drape work.)

Normals from central differences, forced up

Per-vertex normals come from the cross product of the north/east neighbour tangents (central differences on the already-rebased local metres, so the differences are small and precise). We force normal.y ≥ 0 rather than relying on winding, so a lighting bug can't come from an accidental flip; the flat-DEM test pins that a zero- relief field yields straight-up normals (the old flat-plane behaviour as the zero special-case).

Vertical exaggeration knob

build_terrain_mesh(..., vertical_exaggeration) scales elevation. Real-world relief is tiny next to the Mercator horizontal span at Sweden's latitude, so a showcase fly often wants 1.5–3×; the default is 1.0 (true scale). It is a pure multiplier on the sampled height, so it never touches the sampler/DEM correctness.

Determinism / CI (FC-7)

No clock, no RNG, no network. The synthetic ProceduralTerrain is analytic and bounded (base ± amplitude), so CI with no DEM tiles on disk is green and bit-reproducible; TerrainField::load_hgt_or_synthetic is the seam that picks the real tile when present and the synthetic field otherwise (returning bool so a host can log "using synthetic terrain").

What is NOT done here (next pass — stays roadmap §6)

Bench note

benches/heavy_terrain.rs is HEAVY (full cores, large tile) — parts + rollup: .hgt parse (3601² = 1-arc-second), bilinear + procedural sampling throughput (1 M points), and the full mesh build (default 1024² grid ≈ 1.05 M verts / 2.1 M tris). Grid side overridable via MAP3D_FLIGHT_BENCH_GRID. Numbers + machine (cores/mem) recorded in the crate benchmarks doc / the agent report.