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/flightsim_phase1.rs

//! **Phase 1 flight-sim lighting — PNG blob oracle.** The Phase-1 quality jump is
//! sun + PBR + sky + height-fog in the GPU `map3d.wgsl` shader. The render is
//! invisible from a return value, so this test:
//!   1. renders the SAME extruded block town through the GPU PBR path under two
//!      different **sun angles** (noon vs golden hour), reads back both frames, and
//!   2. asserts the frames DIFFER (the sun angle actually re-shades the faces) and
//!      that each frame has a real lit gradient (directional sun: bright sun-facing
//!      faces + darker faces, not one flat colour), then
//!   3. saves both PNGs the agent looks at.
//!
//! Run: `cargo test -p facett-map3d --features wgpu,testmatrix --test flightsim_phase1 -- --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};

/// A small grid of block buildings (the HARD CONSTRAINT: buildings are blocks; the
/// quality is light, not geometry). ~1.5 km town so the calibration is realistic.
fn town() -> Vec<Way> {
    let (lon0, lat0) = (9.50_f64, 47.10_f64);
    let step = 0.0025;
    let bsz = 0.0011;
    let mut ways = Vec::new();
    let mut id = 1i64;
    for r in 0..4 {
        for c in 0..4 {
            let bx = lon0 + c as f64 * step;
            let by = lat0 + r as f64 * step;
            let ring = [(bx, by), (bx + bsz, by), (bx + bsz, by + bsz), (bx, by + bsz), (bx, by)];
            let tags = vec![
                ("building".to_string(), "yes".to_string()),
                ("height".to_string(), format!("{}", 12 + (r * 4 + c) % 5 * 8)),
            ];
            ways.push(Way::from_lonlat(id, &ring, &tags, &facett_core::Theme::default()).unwrap());
            id += 1;
        }
    }
    ways
}

fn scene(sun: SunSky) -> Map3D {
    // This oracle isolates the PBR **sun + sky-ambient + height-fog** shading ON THE
    // GEOMETRY (its mean-luma metric averages the lit faces). The T3.2 skybox is a
    // separate concern (its own proof: `sky_proof.rs`); leaving it on would fill the
    // formerly near-black background with a bright analytic sky that dominates the
    // average and masks the sun-angle luma delta. Disable it so this stays a clean
    // geometry-shading oracle.
    let mut m =
        Map3D::from_ways("phase1", &town(), 0.3).with_gpu(true).with_sun(sun).with_sky(false);
    m.spin = false;
    m.set_clock(0.0);
    m.view_preset(ViewPreset::Oblique);
    m.cam.goal_distance = 2.6;
    m.cam.goal_target = facett_map3d::V3::new(0.0, 0.05, 0.0);
    m.cam.settle();
    m
}

fn render(sun: SunSky) -> image::RgbaImage {
    let rs = create_render_state(default_wgpu_setup(), Default::default());
    facett_map3d::gpu::install_renderer(&rs);
    let mut view = scene(sun);
    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")
}

/// Count distinct non-background shade buckets + mean luminance.
fn shades_and_luma(img: &image::RgbaImage) -> (usize, f64) {
    let mut set = std::collections::HashSet::<u32>::new();
    let mut sum = 0.0;
    let mut n = 0u64;
    for p in img.pixels() {
        let (r, g, b) = (p.0[0], p.0[1], p.0[2]);
        // Skip near-black background.
        if (r as u32 + g as u32 + b as u32) < 24 {
            continue;
        }
        set.insert(((r as u32 >> 3) << 10) | ((g as u32 >> 3) << 5) | (b as u32 >> 3));
        sum += 0.2126 * r as f64 + 0.7152 * g as f64 + 0.0722 * b as f64;
        n += 1;
    }
    (set.len(), if n > 0 { sum / n as f64 } else { 0.0 })
}

/// Fraction of pixels that differ between two frames (any channel > thresh).
fn frame_diff_fraction(a: &image::RgbaImage, b: &image::RgbaImage) -> f64 {
    let mut diff = 0usize;
    let total = (a.width() * a.height()) as usize;
    for (pa, pb) in a.pixels().zip(b.pixels()) {
        let d = (pa.0[0] as i32 - pb.0[0] as i32).abs()
            + (pa.0[1] as i32 - pb.0[1] as i32).abs()
            + (pa.0[2] as i32 - pb.0[2] as i32).abs();
        if d > 18 {
            diff += 1;
        }
    }
    diff as f64 / total as f64
}

#[test]
fn phase1_sun_pbr_sky_fog_shades_and_reacts_to_sun_angle() {
    let noon = render(SunSky::noon());
    let gold = render(SunSky::golden_hour());

    image::DynamicImage::ImageRgba8(noon.clone()).save("/tmp/map3d_phase1_noon.png").ok();
    image::DynamicImage::ImageRgba8(gold.clone()).save("/tmp/map3d_phase1_golden.png").ok();

    let (noon_shades, noon_luma) = shades_and_luma(&noon);
    let (gold_shades, gold_luma) = shades_and_luma(&gold);
    let diff = frame_diff_fraction(&noon, &gold);

    eprintln!(
        "EMIT-PHASE1 noon_shades={noon_shades} noon_luma={noon_luma:.1} gold_shades={gold_shades} gold_luma={gold_luma:.1} sun_diff={diff:.4}"
    );

    // (1) Directional sun + PBR: a real lit GRADIENT — many distinct shades, not one
    // flat-shaded colour. The old single-pass `abs(dot)` gave few; PBR + sky ambient
    // + height fog spreads luminance across faces.
    assert!(noon_shades >= 12, "PBR/sun yields a lit gradient (got {noon_shades} shades)");
    assert!(noon_luma > 5.0, "the scene is actually lit (mean luma {noon_luma:.1})");

    // (2) The sun ANGLE matters: noon vs golden-hour re-shade the same geometry, so a
    // healthy fraction of pixels differ. A scene that ignored the sun would be ~identical.
    assert!(diff > 0.02, "changing the sun angle re-shades the city (diff {diff:.4})");
    // Golden hour (warm, low) is overall warmer/different luma than noon.
    assert!((noon_luma - gold_luma).abs() > 1.0, "the two suns produce different overall light");

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map3d",
        "phase1_sun_pbr_sky_fog",
        noon_shades >= 12 && noon_luma > 5.0 && diff > 0.02,
        &format!(
            "blob=/tmp/map3d_phase1_noon.png noon_shades={noon_shades} noon_luma={noon_luma:.1} sun_diff={diff:.4}"
        ),
    );
}