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