codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! Fitting the shadow map's light box to the scene: a bad fit shows up as a
//! shadow that is missing or coarse.
use codecraft::ecs::Application as EcsApp;
use codecraft::glam::Vec3;
use codecraft::render3d::{
    MeshId, MeshInstance, ModelDrawList, Render3dPlugin, Transform, fit_light, scene_bounds,
};
use codecraft::ui::Color;

const MAP: f32 = 2048.0;
const LIGHT: Vec3 = Vec3::new(0.4, 1.0, 0.6);

fn cube() -> (Vec3, Vec3) {
    (Vec3::new(-0.5, 0.0, -0.5), Vec3::new(0.5, 1.0, 0.5))
}

fn draws(placements: &[(MeshId, Transform)]) -> Vec<(MeshId, codecraft::render3d::ModelInstance)> {
    let mut app = EcsApp::new();
    app.add_plugin(Render3dPlugin);
    for (mesh, transform) in placements {
        app.world.spawn((
            *transform,
            MeshInstance {
                mesh: *mesh,
                color: Color::WHITE,
                material: codecraft::render3d::MaterialId(0),
            },
        ));
    }
    app.update();
    app.world.resource::<ModelDrawList>().opaque.clone()
}

#[test]
fn bounds_cover_every_instance_where_it_stands() {
    let list = draws(&[
        (MeshId(0), Transform::at(-2.0, 0.0, 0.0)),
        (MeshId(0), Transform::at(3.0, 0.0, 1.0)),
    ]);

    let (min, max) = scene_bounds(&list, |_| Some(cube())).expect("two instances");
    assert!((min.x - -2.5).abs() < 1e-5, "left edge of the left cube");
    assert!((max.x - 3.5).abs() < 1e-5, "right edge of the right cube");
    assert!((min.y - 0.0).abs() < 1e-5, "both stand on y = 0");
    assert!((max.y - 1.0).abs() < 1e-5);
}

#[test]
fn nothing_to_draw_means_nothing_to_fit() {
    assert!(scene_bounds(&[], |_| Some(cube())).is_none());
    let list = draws(&[(MeshId(7), Transform::default())]);
    assert!(scene_bounds(&list, |_| None).is_none());
}

#[test]
fn the_light_box_holds_the_whole_scene() {
    let (min, max) = (Vec3::new(-0.16, 0.0, -0.16), Vec3::new(0.16, 0.08, 0.16));
    let fit = fit_light(min, max, LIGHT, 1.5, MAP);

    for i in 0..8 {
        let corner = Vec3::new(
            if i & 1 == 0 { min.x } else { max.x },
            if i & 2 == 0 { min.y } else { max.y },
            if i & 4 == 0 { min.z } else { max.z },
        );
        let clip = fit.light_view_proj * corner.extend(1.0);
        let ndc = clip.truncate() / clip.w;
        assert!(
            ndc.x.abs() <= 1.0 && ndc.y.abs() <= 1.0,
            "corner {corner:?} falls outside the map at {ndc:?}",
        );
        assert!(
            (0.0..=1.0).contains(&ndc.z),
            "corner {corner:?} falls outside the depth range at {}",
            ndc.z,
        );
    }
}

#[test]
fn a_wider_light_opens_a_wider_penumbra() {
    let (min, max) = (Vec3::splat(-1.0), Vec3::splat(1.0));
    let hard = fit_light(min, max, LIGHT, 0.0, MAP);
    let narrow = fit_light(min, max, LIGHT, 1.0, MAP);
    let wide = fit_light(min, max, LIGHT, 10.0, MAP);

    assert_eq!(hard.spread, 0.0, "a point light has no penumbra");
    assert!(wide.spread > narrow.spread);
    // The scene's scale cancels out of the penumbra: same light, same fraction of the map.
    let bigger = fit_light(min * 10.0, max * 10.0, LIGHT, 1.0, MAP);
    assert!((bigger.spread - narrow.spread).abs() < 1e-6);
}

#[test]
fn the_bias_scale_follows_the_maps_resolution() {
    let (min, max) = (Vec3::splat(-1.0), Vec3::splat(1.0));
    let coarse = fit_light(min, max, LIGHT, 1.5, 512.0);
    let fine = fit_light(min, max, LIGHT, 1.5, MAP);

    assert!(
        coarse.texel_world > fine.texel_world,
        "a coarser map needs a bigger normal offset",
    );
    assert!((coarse.texel_world / fine.texel_world - 4.0).abs() < 1e-4);
}

#[test]
fn the_fit_does_not_swing_as_the_light_moves() {
    let (min, max) = (Vec3::new(-2.0, 0.0, -1.0), Vec3::new(2.0, 1.0, 1.0));
    // Sized to the bounding sphere, or texel density would change as the light turns.
    let a = fit_light(min, max, Vec3::new(0.0, 1.0, 0.0), 1.5, MAP);
    let b = fit_light(min, max, Vec3::new(1.0, 1.0, -1.0), 1.5, MAP);
    assert!((a.texel_world - b.texel_world).abs() < 1e-9);
}

#[test]
fn the_default_light_has_some_size() {
    assert!(codecraft::Light::default().angle > 0.0);
}