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