use super::*;
const STANDING: Vec3 = Vec3::new(-2.0, 2.0, 0.0);
const SOLID: Material = Material::lit(Color::WHITE);
const FIELD: u32 = 128;
const OVERHEAD: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 10.0, 0.0), Vec3::ZERO).with_up(Vec3::NEG_Z),
Projection::orthographic(ACROSS),
);
const OVER_THE_SHOULDER: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 4.0, 5.0), Vec3::new(0.0, 1.0, 0.0)),
Projection::perspective(45.0),
);
const LONG: f32 = 100.0;
const LONGEST: f32 = 600.0;
const CLOSE: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 1.2, 2.5), Vec3::ZERO),
Projection::perspective(60.0),
);
const BACK: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 12.0, 30.0), Vec3::ZERO),
Projection::perspective(60.0),
);
const HIGH: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 100.0, 0.0), Vec3::ZERO).with_up(Vec3::NEG_Z),
Projection::perspective(60.0),
);
const ALONG: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 100.0, 100.0), Vec3::ZERO),
Projection::perspective(60.0),
);
const LEVEL: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 5.0, 30.0), Vec3::new(0.0, 5.0, 0.0)),
Projection::perspective(60.0),
);
const TALL: u32 = 512;
const DOWN: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 10.0, 0.0), Vec3::ZERO).with_up(Vec3::NEG_Z),
Projection::orthographic(ACROSS),
);
const LAMP: Vec3 = Vec3::new(0.0, 24.0, 18.0);
const LAMP_RANGE: f32 = 40.0;
const CONE: f32 = 0.6;
const ALONGSIDE: View = View::look_at(Vec3::new(0.0, 2.0, 12.0), Vec3::new(0.0, 2.0, 0.0));
const LOW: Vec3 = Vec3::new(-2.0, -1.0, 0.0);
const EDGE_DEPTH: f32 = 15.0;
const CLIPPED_AT: [f32; 2] = [0.1, 6.0];
const REACHING: f32 = 400.0;
const NEARER: f32 = 150.0;
const FURTHER: f32 = 220.0;
const NEARER_HAND: f32 = 9.0;
const FURTHER_HAND: f32 = 20.0;
const BESIDE: u32 = 20;
const STEP: u8 = 32;
meshes! { enum RelievedSet { Relieved } }
struct Cast {
light: Light,
blocker: Option<(Vec3, Material)>,
faced: bool,
fade: f32,
}
impl Game for Cast {
type Meshes = GroundSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(Camera::new(
View::look_at(Vec3::Y * 10.0, Vec3::ZERO).with_up(Vec3::NEG_Z),
Projection::orthographic(ACROSS),
));
ctx.light(self.light);
ctx.draw(
Ground::Floor
.at(Transform::from_scale(Vec3::splat(ACROSS)))
.material(Material::lit(Color::WHITE)),
);
if let Some((standing, material)) = self.blocker {
let blocker = Ground::Blocker
.at(standing)
.material(material)
.faded(self.fade);
ctx.draw(if self.faced {
blocker.billboard()
} else {
blocker
});
}
}
}
fn cast(light: Light, blocker: Option<(Vec3, Material)>) -> Option<([u8; 4], [u8; 4])> {
shone(Cast {
light,
blocker,
faced: false,
fade: 1.0,
})
}
fn shone(scene: Cast) -> Option<([u8; 4], [u8; 4])> {
let config = raw("headless shadows").with_shadow_resolution(512);
let pixels = sized(config, UVec2::splat(YARD), scene)?;
let ground = |across: u32| pixel(&pixels, across, YARD / 2, YARD);
Some((ground(YARD / 2), ground(YARD / 2 + 24)))
}
#[test]
fn a_translucent_blocker_takes_the_fraction_of_the_light_its_alpha_covers() {
let sun = Light::directional(Vec3::new(1.0, -1.0, 0.0), Color::WHITE).shadow();
let glass = |alpha| Material::lit(Color::rgba(1.0, 1.0, 1.0, alpha));
let Some((behind_glass, _)) = cast(sun, Some((STANDING, glass(0.5)))) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some((behind_a_pane_of_air, _)) = cast(sun, Some((STANDING, glass(0.0)))) else {
return;
};
let Some((clear, _)) = cast(sun, None) else {
return;
};
assert!(
behind_glass[0] > 0 && behind_glass[0] < clear[0],
"half of the light passes: {behind_glass:?} against {clear:?}"
);
assert_eq!(
behind_a_pane_of_air, clear,
"and a draw covering nothing blocks nothing"
);
}
#[test]
fn an_additive_blocker_casts_nothing() {
let sun = Light::directional(Vec3::new(1.0, -1.0, 0.0), Color::WHITE).shadow();
let Some((behind, _)) = cast(sun, Some((STANDING, SOLID.additive()))) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some((clear, _)) = cast(sun, None) else {
return;
};
assert_eq!(behind, clear, "a draw that is light blocks none of it");
}
struct Standing {
sprite: Relieved,
seen: Camera,
casts: bool,
}
impl Game for Standing {
type Meshes = RelievedSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(self.seen);
let sun = Light::directional(Vec3::new(0.0, -1.0, 1.0), Color::WHITE);
ctx.light(if self.casts { sun.shadow() } else { sun });
ctx.draw(
Relieved::Ground
.at(Transform::from_scale(Vec3::splat(ACROSS)))
.material(Material::lit(Color::WHITE)),
);
ctx.draw(
self.sprite
.at(Transform::from_scale_rotation_translation(
Vec3::new(2.0, 2.0, 1.0),
Quat::IDENTITY,
Vec3::Y,
))
.material(Material::lit(Color::WHITE).cutout())
.billboard(),
);
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Relieved {
Ground,
Flat,
Mapped,
Domed,
}
impl Catalog for Relieved {
fn catalog() -> Vec<Self> {
vec![Self::Ground, Self::Flat, Self::Mapped, Self::Domed]
}
}
impl Mesh for Relieved {
fn build(&self, assets: &Assets) -> MeshData {
let white = TextureData::rgba8(UVec2::ONE, vec![u8::MAX; 4]);
match self {
Self::Ground => Plane.build(assets),
Self::Flat => Quad.build(assets).with_texture(white),
Self::Mapped => Quad
.build(assets)
.with_texture(white)
.with_relief(ReliefData::rgba8(
UVec2::ONE,
vec![128, 128, u8::MAX, u8::MAX],
)),
Self::Domed => Quad
.build(assets)
.with_texture(white)
.with_relief(ReliefData::rgba8(UVec2::ONE, vec![128, 218, 218, u8::MAX])),
}
}
}
fn stood(sprite: Relieved, seen: Camera, casts: bool) -> Option<[u8; 4]> {
let config = raw("headless relief").with_shadow_resolution(512);
let scene = Standing {
sprite,
seen,
casts,
};
Some(middle(&sized(config, UVec2::splat(SIDE), scene)?, SIDE))
}
#[test]
fn a_faced_draw_never_stands_in_its_own_shadow() {
for seen in [OVERHEAD, OVER_THE_SHOULDER] {
let sprite = |sprite, casts| stood(sprite, seen, casts);
let Some(domed) = sprite(Relieved::Domed, true) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(domed_clear), Some(mapped), Some(mapped_clear), Some(flat), Some(flat_clear)) = (
sprite(Relieved::Domed, false),
sprite(Relieved::Mapped, true),
sprite(Relieved::Mapped, false),
sprite(Relieved::Flat, true),
sprite(Relieved::Flat, false),
) else {
return;
};
assert_eq!(
domed, domed_clear,
"a relief turned off the sprite clears its own record"
);
assert_eq!(
mapped, mapped_clear,
"one straight out of it clears its own"
);
assert_eq!(flat, flat_clear, "and a flat sprite clears its own");
assert_ne!(mapped, flat, "where the three are lit by different normals");
assert_ne!(domed, mapped, "the turned one taking its own share");
}
}
struct Cascades {
camera: Camera,
light: Light,
ground: f32,
blocker: Option<Transform>,
}
impl Game for Cascades {
type Meshes = GroundSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(self.camera);
ctx.light(self.light);
ctx.draw(
Ground::Floor
.at(Transform::from_scale(Vec3::splat(self.ground)))
.material(SOLID),
);
if let Some(standing) = self.blocker {
ctx.draw(Ground::Blocker.at(standing).material(SOLID));
}
}
}
fn standing(side: f32) -> Transform {
let lift = side * 0.75;
Transform::from_scale_rotation_translation(
Vec3::splat(side),
Quat::IDENTITY,
Vec3::new(0.0, lift, -lift),
)
}
fn lofted() -> Transform {
Transform::from_scale_rotation_translation(
Vec3::splat(8.0),
Quat::IDENTITY,
Vec3::new(-30.0, 30.0, 0.0),
)
}
fn slab() -> Transform {
Transform::from_scale_rotation_translation(
Vec3::new(300.0, 1.0, 305.0),
Quat::IDENTITY,
Vec3::new(0.0, 150.0, -132.5),
)
}
fn wall() -> Transform {
Transform::from_scale_rotation_translation(
Vec3::new(1.0, 10.0, 120.0),
Quat::IDENTITY,
Vec3::new(10.5, 5.0, -20.0),
)
}
fn floating() -> Transform {
Transform::from_scale_rotation_translation(
Vec3::new(4.0, 0.5, 120.0),
Quat::IDENTITY,
Vec3::new(4.0, 4.0, -20.0),
)
}
fn ground_depth(camera: Camera, across: u32, row: u32) -> Option<f32> {
let at = Vec2::new(across as f32 + 0.5, row as f32 + 0.5);
let ray = camera.ray_through(at, UVec2::splat(TALL));
let reached = ray.hit_plane(crate::ray::Plane {
point: Vec3::ZERO,
normal: Vec3::Y,
})?;
let view = camera.view();
Some(view.direction().dot(ray.at(reached) - view.eye()))
}
fn rows_between(camera: Camera, nearer: f32, further: f32) -> Vec<u32> {
(0..TALL)
.rev()
.filter(|&row| {
ground_depth(camera, TALL / 2, row)
.is_some_and(|depth| (nearer..further).contains(&depth))
})
.collect()
}
fn shadow_across(pixels: &[u8], row: u32) -> Option<f32> {
let line: Vec<u32> = (0..TALL)
.map(|across| u32::from(pixel(pixels, across, row, TALL)[0]))
.collect();
let darkest = *line.iter().min()?;
let lightest = *line.iter().max()?;
if darkest * 2 >= lightest {
return None;
}
let shadowed = |reading: &u32| reading * 2 < darkest + lightest;
let first = line.iter().position(shadowed)?;
let last = line.iter().rposition(shadowed)?;
Some((first + last) as f32 / 2.0)
}
fn field(side: u32, scene: Cascades) -> Option<Vec<u8>> {
let config = raw("headless cascades").with_shadow_resolution(512);
sized(config, UVec2::splat(side), scene)
}
fn darkest_ground(camera: Camera, light: Light) -> Option<u8> {
let scene = Cascades {
camera,
light,
ground: LONG,
blocker: None,
};
let pixels = field(FIELD, scene)?;
(3 * FIELD / 4..FIELD)
.flat_map(|row| (0..FIELD).map(move |across| (across, row)))
.map(|(across, row)| pixel(&pixels, across, row, FIELD)[0])
.min()
}
fn over(camera: Camera, light: Light, blocker: Option<f32>) -> Option<[u8; 4]> {
let scene = Cascades {
camera,
light,
ground: LONG,
blocker: blocker.map(standing),
};
Some(middle(&field(FIELD, scene)?, FIELD))
}
#[test]
fn a_suns_cascades_darken_the_ground_near_the_camera_and_far_from_it() {
let sun = Light::directional(Vec3::new(0.0, -1.0, 1.0), Color::WHITE).shadow();
let Some([near, ..]) = over(CLOSE, sun, Some(1.0)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some([near_clear, ..]), Some([far, ..]), Some([far_clear, ..])) = (
over(CLOSE, sun, None),
over(BACK, sun, Some(3.0)),
over(BACK, sun, None),
) else {
return;
};
assert!(
near < near_clear,
"the nearest cascade darkens the ground behind a blocker, \
which reads {near} against {near_clear}"
);
assert!(
far < far_clear,
"and the widest carries what is past its split, \
which reads {far} against {far_clear}"
);
}
#[test]
fn a_suns_shadow_lands_a_hundred_meters_from_the_camera() {
let sun = Light::directional(Vec3::new(1.0, -1.0, 0.0), Color::WHITE).shadow();
let ground = |blocker| {
let scene = Cascades {
camera: HIGH,
light: sun,
ground: LONGEST,
blocker,
};
Some(middle(&field(FIELD, scene)?, FIELD)[0])
};
let Some(shadowed) = ground(Some(lofted())) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(clear) = ground(None) else {
return;
};
assert!(
shadowed < clear / 2,
"a sun darkens ground a hundred meters out, which reads \
{shadowed} against {clear}"
);
}
#[test]
fn a_shadow_crossing_the_end_of_a_suns_reach_fades_out_rather_than_ending_on_a_line() {
let scene = Cascades {
camera: ALONG,
light: Light::directional(Vec3::NEG_Y, Color::WHITE).shadow(),
ground: LONGEST,
blocker: Some(slab()),
};
let Some(pixels) = field(TALL, scene) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let line: Vec<u8> = rows_between(ALONG, NEARER, FURTHER)
.into_iter()
.map(|row| pixel(&pixels, TALL / 2, row, TALL)[0])
.collect();
let (Some(&shadowed), Some(&lit)) = (line.first(), line.last()) else {
panic!("the camera reads ground between {NEARER} and {FURTHER} meters out");
};
assert!(
line.len() > 64,
"the end of the reach is read over {} pixels of ground",
line.len()
);
assert!(
u32::from(shadowed) * 2 < u32::from(lit),
"which run out of the shadow into the light, from {shadowed} to {lit}"
);
for (at, (behind, ahead)) in line.iter().zip(&line[1..]).enumerate() {
assert!(
ahead.abs_diff(*behind) <= STEP,
"and read {behind} against {ahead} between pixels {at} and {}, \
where the whole line reads {line:?}",
at + 1
);
}
}
#[test]
fn the_edge_of_a_shadow_crossing_the_first_hand_over_reads_without_a_step() {
let scene = Cascades {
camera: LEVEL,
light: Light::directional(Vec3::new(-1.0, -1.0, 0.0), Color::WHITE).shadow(),
ground: LONGEST,
blocker: Some(wall()),
};
let Some(pixels) = field(TALL, scene) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let rows = rows_between(LEVEL, NEARER_HAND, FURTHER_HAND);
let beside = |across: u32| -> Vec<u8> {
rows.iter()
.map(|&row| pixel(&pixels, across, row, TALL)[0])
.collect()
};
let columns: Vec<Vec<u8>> = (1..=BESIDE)
.flat_map(|off| [beside(TALL / 2 - off), beside(TALL / 2 + off)])
.collect();
let readings = || columns.iter().flatten().copied();
let (Some(darkest), Some(lightest)) = (readings().min(), readings().max()) else {
panic!("the camera reads ground between {NEARER_HAND} and {FURTHER_HAND} meters out");
};
assert!(
rows.len() > 64,
"the first hand-over is read over {} rows of ground",
rows.len()
);
assert!(
u32::from(darkest) * 2 < u32::from(lightest),
"which hold the shadow's own edge, reading {darkest} against {lightest}"
);
for (at, line) in columns.iter().enumerate() {
for (row, (behind, ahead)) in line.iter().zip(&line[1..]).enumerate() {
assert!(
ahead.abs_diff(*behind) <= STEP,
"and column {at} reads {behind} against {ahead} between its rows \
{row} and {}, where the whole column reads {line:?}",
row + 1
);
}
}
}
#[test]
fn a_shadow_lands_in_one_place_whichever_cascade_reads_the_ground_under_it() {
let seen = |near: f32| {
Camera::new(
ALONGSIDE,
Projection::perspective(60.0).clip(near..REACHING),
)
};
let shadow = |near: f32, row: u32| {
let scene = Cascades {
camera: seen(near),
light: Light::directional(LOW, Color::WHITE).shadow(),
ground: LONGEST,
blocker: Some(floating()),
};
Some(shadow_across(&field(TALL, scene)?, row))
};
let rows = rows_between(seen(CLIPPED_AT[0]), EDGE_DEPTH - 0.5, EDGE_DEPTH + 0.5);
let Some(&row) = rows.first() else {
panic!("the camera reads ground {EDGE_DEPTH} meters along its view");
};
let Some(inside) = shadow(CLIPPED_AT[0], row) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(past) = shadow(CLIPPED_AT[1], row) else {
return;
};
let (Some(inside), Some(past)) = (inside, past) else {
panic!("the row the two are read along holds a band of shadow: {inside:?}, {past:?}");
};
assert!(
(inside - past).abs() <= 1.0,
"one caster's shadow lands where it lands, reading the column between \
its edges at {inside} from the slice holding the ground and at {past} \
from the slice past it"
);
}
#[test]
fn a_lit_plane_at_a_slant_to_the_sun_stands_in_no_shadow_of_its_own() {
let sun = Light::directional(Vec3::new(0.0, -1.0, 1.0), Color::WHITE);
let Some(near) = darkest_ground(CLOSE, sun.shadow()) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(near_clear), Some(far), Some(far_clear)) = (
darkest_ground(CLOSE, sun),
darkest_ground(BACK, sun.shadow()),
darkest_ground(BACK, sun),
) else {
return;
};
assert_eq!(
near, near_clear,
"ground the nearest cascade reads is as lit under a sun that casts"
);
assert_eq!(
far, far_clear,
"and so is ground the widest reads, whose texels are wider"
);
}
#[test]
fn a_lit_plane_at_a_slant_to_a_lamp_stands_in_no_shadow_of_its_own() {
let lamp = Light::point(LAMP, Color::WHITE, LAMP_RANGE);
let Some(casting) = darkest_ground(DOWN, lamp.shadow()) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(clear) = darkest_ground(DOWN, lamp) else {
return;
};
assert_eq!(
casting, clear,
"ground near the end of a lamp's range is as lit under one that casts"
);
}
#[test]
fn a_lit_plane_at_a_slant_to_a_cone_stands_in_no_shadow_of_its_own() {
let cone = Light::spot(Spot {
position: LAMP,
direction: -LAMP,
color: Color::WHITE,
range: LAMP_RANGE,
angle: CONE,
});
let Some(casting) = darkest_ground(DOWN, cone.shadow()) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(clear) = darkest_ground(DOWN, cone) else {
return;
};
assert_eq!(casting, clear, "and so is ground near the end of a cone's");
}