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"
);
}