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

nordisk/facett

facett-map3d/tests/water_altitude_blob.rs

//! **THE PICTURE, at the altitude korp actually flies.** A blob oracle for the water
//! fix — two renders of the SAME town, from the SAME region camera, differing only in
//! which rule decides whether the water surface is drawn.
//!
//! `flightsim_water.rs` renders the lake at 282 m, where the reflective pass runs. korp's
//! z12 pane sits at ~1.6 km (`metres_per_design_unit = 1875`, `camera.distance = 0.856`),
//! which is the altitude at which the lake used to disappear entirely — and no test in
//! the tree took a picture there. This one does, and writes both PNGs where a human can
//! open them.
//!
//! The number beside the look is `blue_px`: pixels whose blue channel clearly dominates
//! red. A shot with no water surface has the ground's own shading and little else; a
//! shot with the lake drawn has the lake.
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test water_altitude_blob -- --test-threads=1 --nocapture`

#![cfg(feature = "wgpu")]

use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use facett_core::Facet as _;
use facett_map::Way;
use facett_map3d::{Map3D, SunSky, ViewPreset};

/// `$HOME/scratch/mappolish/water`.
fn out_dir() -> String {
    format!("{}/scratch/mappolish/water", std::env::var("HOME").unwrap_or_default())
}

fn town_with_lake() -> Vec<Way> {
    let th = facett_core::Theme::default();
    let mut ways = Vec::new();
    let lake = Way::from_lonlat(
        1,
        &[
            (9.503, 47.103),
            (9.510, 47.103),
            (9.511, 47.107),
            (9.508, 47.110),
            (9.503, 47.109),
            (9.502, 47.106),
            (9.503, 47.103),
        ],
        &[("natural".to_string(), "water".to_string())],
        &th,
    )
    .unwrap();
    ways.push(lake);
    for (i, &(lon, lat)) in
        [(9.5005, 47.1005), (9.5125, 47.1015), (9.5135, 47.1085), (9.5015, 47.1095)].iter().enumerate()
    {
        let b = 0.0009;
        let ring = [(lon, lat), (lon + b, lat), (lon + b, lat + b), (lon, lat + b), (lon, lat)];
        let tags = vec![("building".to_string(), "yes".to_string()), ("height".to_string(), "60".to_string())];
        ways.push(Way::from_lonlat(10 + i as i64, &ring, &tags, &th).unwrap());
    }
    ways
}

/// `water: None` = let the product's own rule decide (what this change is about);
/// `Some(false)` = the explicit off, which is what the OLD rule produced up here.
fn scene(water: Option<bool>, dist: f32) -> Map3D {
    let mut m = Map3D::from_ways("water-altitude", &town_with_lake(), 0.3)
        .with_gpu(true)
        .with_sun(SunSky::from_angles(0.7, 0.45))
        .with_shadows(0.7)
        .with_water_override(water);
    m.spin = false;
    m.set_clock(0.6);
    m.view_preset(ViewPreset::Oblique);
    m.cam.goal_distance = dist;
    m.cam.goal_elevation = 0.55;
    m.cam.goal_target = facett_map3d::V3::new(0.0, 0.02, 0.0);
    m.cam.settle();
    m
}

fn render(view: Map3D) -> image::RgbaImage {
    let rs = create_render_state(default_wgpu_setup(), Default::default());
    facett_map3d::gpu::install_renderer(&rs);
    let mut v = view;
    let mut harness = Harness::builder()
        .with_size(egui::vec2(900.0, 640.0))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            v.ui(ui);
        });
    harness.run_steps(4);
    harness.render().expect("wgpu render")
}

/// Pixels whose blue channel clearly dominates red — the lake against the lit ground.
fn blue_px(img: &image::RgbaImage) -> usize {
    img.pixels().filter(|p| p.0[2] as i32 > p.0[0] as i32 + 40).count()
}

#[test]
fn the_lake_is_in_the_picture_from_a_region_camera() {
    // One process, two heavy wgpu renders: keep them apart (`kalm-5`/`hus3d-4` — several
    // adapter bring-ups racing in one process is what hangs and segfaults this crate's
    // GPU binaries).
    std::fs::create_dir_all(out_dir()).ok();

    // **The altitude korp's z12 pane actually flies**, not a convenient one. korp
    // publishes `metres_per_design_unit = 1875` with `camera.distance = 0.856`, i.e.
    // ~1.6 km up; this fixture's scale puts that at `dist = 4.4`. A first cut used
    // `dist = 18.0` (the 8 001 m camera `flightsim_water` uses for its far leg) and the
    // lake was small enough in frame that the difference was +642 px against a 2 000 px
    // threshold. Moving the THRESHOLD down to meet that number would have been fitting
    // the test to the result; moving the CAMERA to where the product is measures the
    // thing that was reported.
    let after = scene(None, 4.4);
    let alt = after.altitude_m();
    let tier = after.lod_tier().name().to_owned();
    let (active, reflect) = (after.water_active(), after.reflective_water_active());
    let img_after = render(after);

    let before = scene(Some(false), 4.4);
    let img_before = render(before);

    let (b_after, b_before) = (blue_px(&img_after), blue_px(&img_before));
    let pa = format!("{}/3d-water-AFTER.png", out_dir());
    let pb = format!("{}/3d-water-BEFORE.png", out_dir());
    image::DynamicImage::ImageRgba8(img_after).save(&pa).expect("write AFTER");
    image::DynamicImage::ImageRgba8(img_before).save(&pb).expect("write BEFORE");

    eprintln!(
        "[water-blob] altitude {alt:.0} m (tier {tier})  water_active={active} reflective={reflect}\n\
         [water-blob] blue px  BEFORE {b_before}  AFTER {b_after}  (+{})\n\
         [water-blob] {pb}\n[water-blob] {pa}",
        b_after as i64 - b_before as i64
    );

    assert!(alt > 300.0, "this proof is about the camera ABOVE the 300 m boundary, got {alt:.0} m");
    assert!(active, "the lake must be drawn at {alt:.0} m (tier {tier})");
    assert!(!reflect, "the planar reflection pass must stay off at {alt:.0} m (tier {tier})");
    // The picture, not the flag. A flag that flipped while the frame did not change is
    // exactly the green this file exists to refuse.
    assert!(
        b_after > b_before + 2_000,
        "the two frames are the same picture ({b_before} -> {b_after} blue px) — the flag \
         moved and the render did not"
    );
}