facett-map3d/tests/sky_proof.rs
//! **Skybox / atmosphere render PROOF (T3.2, LAW #4).**
//!
//! The flat fog clear-colour background is replaced by an analytic gradient +
//! HDR sun-disc sky (`sky.wgsl`), rendered into the shared `Rgba16Float` HDR
//! offscreen so the existing bloom + ACES tonemap catch the sun. This test renders
//! the synthetic skyline through egui_kittest's wgpu backend at a **fixed clock +
//! fixed sun** (the camera aimed at a low sun) and asserts, as DATA (not pixel
//! eyeballing):
//!
//! 1. **non-blank** — a real, multi-shade frame drew;
//! 2. **a sun disc is present** — the HDR sun core blooms to a cluster of bright
//! near-white pixels; turning the sky OFF (the flat-clear control) removes it,
//! so the count is the FAIL-ON-BUG / PASS-ON-FIX discriminator;
//! 3. **the sky adds a gradient** — sky-ON shows strictly more distinct shades
//! than the flat-clear control;
//! 4. **deterministic** — two independent renders are byte-identical (FC-7).
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test sky_proof -- --nocapture`
#![cfg(feature = "wgpu")]
use egui_kittest::wgpu::{create_render_state, default_wgpu_setup, WgpuTestRenderer};
use egui_kittest::Harness;
use facett_core::Facet as _;
use facett_map3d::{Map3D, SunSky, V3};
/// Aim the synthetic skyline's camera at a **low sun** (so the disc sits in the
/// upper sky, in frame) with a fixed clock + no spin → a reproducible frame.
/// `sky_on` toggles the skybox pass (the negative control flat-clears instead).
fn scene(sky_on: bool) -> Map3D {
// Sun: azimuth 0 (toward +z), low elevation (~10°) so it rides just above the
// horizon. The camera looks toward +z (azimuth π) at a shallow downward tilt, so
// the sun lands in the upper-centre sky above the city.
let mut m = Map3D::synthetic("sky")
.with_gpu(true)
.with_sky(sky_on)
.with_sun(SunSky::from_angles(0.0, 0.18));
m.spin = false;
m.set_clock(0.0);
m.cam.azimuth = std::f32::consts::PI;
m.cam.goal_azimuth = std::f32::consts::PI;
m.cam.elevation = 0.06;
m.cam.goal_elevation = 0.06;
m.cam.distance = 2.6;
m.cam.goal_distance = 2.6;
m.cam.target = V3::new(0.0, 0.2, 0.0);
m.cam.goal_target = V3::new(0.0, 0.2, 0.0);
m.cam.settle();
m
}
/// Render a scene through kittest's wgpu backend with the depth renderer installed.
fn render(sky_on: bool) -> image::RgbaImage {
let rs = create_render_state(default_wgpu_setup(), Default::default());
facett_map3d::gpu::install_renderer(&rs);
let mut view = scene(sky_on);
let mut harness = Harness::builder()
.with_size(egui::vec2(640.0, 640.0))
.renderer(WgpuTestRenderer::from_render_state(rs))
.build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
view.ui(ui);
});
harness.run_steps(4);
harness.render().expect("wgpu render produced an image")
}
/// Count **near-white bright** pixels (the bloomed sun core) and the number of
/// distinct 5-bit-per-channel shade buckets (a flat frame has very few).
fn measure(img: &image::RgbaImage) -> (usize, usize) {
let mut white = 0usize;
let mut shades = std::collections::HashSet::<u32>::new();
for p in img.pixels() {
let (r, g, b) = (p.0[0], p.0[1], p.0[2]);
// The tonemapped sun core saturates to a warm near-white.
if r >= 235 && g >= 233 && b >= 215 {
white += 1;
}
shades.insert(
((u32::from(r) >> 3) << 10) | ((u32::from(g) >> 3) << 5) | (u32::from(b) >> 3),
);
}
(white, shades.len())
}
#[test]
fn skybox_renders_sun_disc_gradient_and_is_deterministic() {
let sky = render(true);
let flat = render(false);
image::DynamicImage::ImageRgba8(sky.clone()).save("/tmp/map3d_sky_on.png").ok();
image::DynamicImage::ImageRgba8(flat.clone()).save("/tmp/map3d_sky_off.png").ok();
let (sky_white, sky_shades) = measure(&sky);
let (flat_white, flat_shades) = measure(&flat);
eprintln!(
"EMIT-SKY sky_white={sky_white} flat_white={flat_white} sky_shades={sky_shades} flat_shades={flat_shades}"
);
// 1. Non-blank: a real, multi-shade frame drew.
assert!(sky_shades >= 8, "the skybox frame must be multi-shade (got {sky_shades})");
// 2. Sun disc: the HDR sun core blooms to a cluster of bright near-white pixels.
// The flat-clear control has (essentially) none, so this is the discriminator.
assert!(
sky_white > 150,
"the bloomed sun disc must paint a bright near-white cluster (got {sky_white})"
);
assert!(
sky_white > flat_white * 3 + 100,
"the sun disc is the SKY's doing: sky_white={sky_white} must dwarf flat_white={flat_white}"
);
// 3. The analytic sky adds a gradient → strictly more distinct shades than flat.
assert!(
sky_shades > flat_shades,
"the gradient sky shows more shades than the flat clear ({sky_shades} > {flat_shades})"
);
// 4. Determinism (FC-7): an independent re-render is byte-identical.
let sky2 = render(true);
assert_eq!(
sky.as_raw(),
sky2.as_raw(),
"the skybox render is deterministic (two renders byte-identical)"
);
#[cfg(feature = "testmatrix")]
{
let ok = sky_shades >= 8
&& sky_white > 150
&& sky_white > flat_white * 3 + 100
&& sky_shades > flat_shades
&& sky.as_raw() == sky2.as_raw();
facett_core::testmatrix::emit(
"facett-map3d",
"skybox_sun_disc_gradient_deterministic",
ok,
&format!(
"sky_white={sky_white} flat_white={flat_white} sky_shades={sky_shades} flat_shades={flat_shades}"
),
);
}
}