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 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),
);
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 = ();
type PostEffects = ();
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| {
let at = ((YARD / 2 * YARD + across) * 4) as usize;
[pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
};
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 = ();
type PostEffects = ();
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)?))
}
#[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,
blocker: Option<Transform>,
}
impl Game for Cascades {
type Meshes = GroundSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
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(LONG)))
.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 over(camera: Camera, light: Light, blocker: Option<f32>) -> Option<[u8; 4]> {
let config = raw("headless cascades").with_shadow_resolution(512);
let scene = Cascades {
camera,
light,
blocker: blocker.map(standing),
};
let pixels = sized(config, UVec2::splat(FIELD), scene)?;
let at = ((FIELD / 2 * FIELD + FIELD / 2) * 4) as usize;
Some([pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]])
}
#[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}"
);
}