use super::*;
const AROUND: [[u8; 4]; 4] = [
[0, 0, u8::MAX, u8::MAX],
[0, u8::MAX, 0, u8::MAX],
[u8::MAX, 0, 0, u8::MAX],
[u8::MAX, u8::MAX, 0, u8::MAX],
];
const BEHIND: usize = 0;
const LEFT: usize = 1;
const AHEAD: usize = 2;
const RIGHT: usize = 3;
const SKY_SIZE: UVec2 = UVec2::new(64, 32);
const GREY: u8 = 89;
const STEP: u32 = 2;
meshes! { enum SphereSet { Sphere } }
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Painted {
Quarters,
Above,
Unlit,
Blinding,
White,
AboveQuarterLit,
AboveUnlit,
WhiteUnlit,
WhiteOverRed,
}
impl Catalog for Painted {
fn catalog() -> Vec<Self> {
vec![
Self::Quarters,
Self::Above,
Self::Unlit,
Self::Blinding,
Self::White,
Self::AboveQuarterLit,
Self::AboveUnlit,
Self::WhiteUnlit,
Self::WhiteOverRed,
]
}
}
impl Skyboxes for Painted {
fn build(&self, _assets: &Assets) -> SkyboxData {
match self {
Self::Quarters => SkyboxData::equirect(quarters()),
Self::Above => SkyboxData::gradient(Color::WHITE, Color::BLACK, Color::BLACK),
Self::Unlit => SkyboxData::gradient(Color::BLACK, Color::BLACK, Color::BLACK),
Self::Blinding => {
SkyboxData::gradient(Color::rgb(40.0, 40.0, 40.0), Color::BLACK, Color::BLACK)
}
Self::White => SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE),
Self::AboveQuarterLit => Self::Above.build(_assets).lit_by(0.25),
Self::AboveUnlit => Self::Above.build(_assets).lit_by(0.0),
Self::WhiteUnlit => Self::White.build(_assets).lit_by(0.0),
Self::WhiteOverRed => Self::White
.build(_assets)
.with_ground(Color::rgb(1.0, 0.0, 0.0)),
}
}
}
fn quarters() -> TextureData {
let texels: Vec<u8> = (0..SKY_SIZE.y)
.flat_map(|_| {
(0..SKY_SIZE.x).flat_map(|across| {
let longitude = (across as f32 + 0.5) / SKY_SIZE.x as f32;
AROUND[((longitude + 0.125) * 4.0) as usize % AROUND.len()]
})
})
.collect();
TextureData::rgba8(SKY_SIZE, texels)
}
struct Empty {
sky: Option<Painted>,
camera: Camera,
}
impl Empty {
fn looking(sky: Option<Painted>, direction: Vec3) -> Self {
Self {
sky,
camera: Camera::new(
View::look_at(Vec3::ZERO, direction),
Projection::perspective(60.0),
),
}
}
fn along_y(sky: Painted, up: bool) -> Self {
let side = match up {
true => 1.0,
false => -1.0,
};
Self {
sky: Some(sky),
camera: Camera::new(
View::look_at(Vec3::ZERO, Vec3::Y * side).with_up(Vec3::Z),
Projection::perspective(60.0),
),
}
}
}
impl Game for Empty {
type Meshes = NoMeshes;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
if let Some(sky) = self.sky {
ctx.set_skybox(sky);
}
ctx.set_camera(self.camera);
}
}
struct Square(Material);
impl Game for Square {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_skybox(Painted::Quarters);
ctx.set_camera(Camera::new(
View::look_at(Vec3::Z * 2.0, Vec3::ZERO),
Projection::orthographic(2.0),
));
ctx.draw(
Quad.at(Transform::from_scale(Vec3::splat(0.5)))
.material(self.0),
);
}
}
struct Facing {
sky: Painted,
up: bool,
material: Material,
lit: bool,
}
impl Game for Facing {
type Meshes = GroundSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
let side = match self.up {
true => 1.0,
false => -1.0,
};
ctx.set_skybox(self.sky);
ctx.set_camera(Camera::new(
View::look_at(Vec3::Y * 2.0 * side, Vec3::ZERO).with_up(Vec3::Z),
Projection::orthographic(0.5),
));
let color = match self.lit {
true => Color::rgb(0.5, 0.5, 0.5),
false => Color::BLACK,
};
ctx.light(Light::directional(Vec3::NEG_Y * side, color));
let turn = match self.up {
true => Quat::IDENTITY,
false => Quat::from_rotation_z(core::f32::consts::PI),
};
ctx.draw(
Ground::Floor
.at(Transform::from_rotation(turn))
.material(self.material),
);
}
}
struct Ball {
sky: Painted,
material: Material,
from: Vec3,
}
impl Ball {
fn ahead(material: Material) -> Self {
Self {
sky: Painted::Quarters,
material,
from: Vec3::Z * 2.0,
}
}
}
impl Game for Ball {
type Meshes = SphereSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
let up = match self.from.x == 0.0 && self.from.z == 0.0 {
true => Vec3::Z,
false => Vec3::Y,
};
ctx.set_skybox(self.sky);
ctx.set_camera(Camera::new(
View::look_at(self.from, Vec3::ZERO).with_up(up),
Projection::orthographic(1.0),
));
ctx.light(Light::directional(Vec3::NEG_Z, Color::BLACK));
ctx.draw(
Sphere { subdivisions: 4 }
.at(Transform::from_scale(Vec3::splat(0.8)))
.material(self.material),
);
}
}
fn near(read: [u8; 4], expected: [u8; 4]) -> bool {
read.iter()
.zip(expected)
.all(|(read, expected)| u32::from(*read).abs_diff(u32::from(expected)) <= STEP)
}
#[test]
fn the_sky_draws_the_texel_the_camera_looks_along() {
let read = |direction| center(Empty::looking(Some(Painted::Quarters), direction));
let Some(ahead) = read(Vec3::NEG_Z) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(right) = read(Vec3::X) else {
return;
};
let Some(left) = read(Vec3::NEG_X) else {
return;
};
assert!(
near(ahead, AROUND[AHEAD]),
"a camera looking along `-Z` reads the quarter of the sky around it, \
got {ahead:?} against {:?}",
AROUND[AHEAD]
);
assert!(
near(right, AROUND[RIGHT]),
"and one turned a right angle reads the quarter that is around there, \
got {right:?} against {:?}",
AROUND[RIGHT]
);
assert!(
near(left, AROUND[LEFT]),
"which is the other quarter from the one turned the other way, got \
{left:?} against {:?}",
AROUND[LEFT]
);
}
#[test]
fn the_sky_draws_the_zenith_over_a_camera_looking_up_and_the_nadir_under_one_looking_down() {
let read = |up| center(Empty::along_y(Painted::Above, up));
let Some(zenith) = read(true) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(nadir) = read(false) else {
return;
};
assert!(
zenith[0] > 200,
"a camera looking straight up reads the sky's own zenith, got {zenith:?}"
);
assert!(
nadir[0] < 16,
"and one looking straight down reads its nadir, got {nadir:?}"
);
}
#[test]
fn a_frame_that_sets_no_sky_draws_the_default_grey() {
let Some(read) = center(Empty::looking(None, Vec3::NEG_Z)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert!(
near(read, [GREY, GREY, GREY, u8::MAX]),
"the default sky is one neutral grey the whole way around, got {read:?}"
);
}
#[test]
fn a_gradient_the_vocabulary_names_draws_its_own_colors() {
let read = |sky| center(Empty::along_y(sky, true));
let Some(bright) = read(Painted::Above) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(dark) = read(Painted::Unlit) else {
return;
};
assert!(
bright[0] > 200,
"a gradient draws where nothing else was drawn, got {bright:?}"
);
assert!(
dark[0] < 16,
"and its own colors are what it draws, got {dark:?}"
);
}
#[test]
fn a_frame_draws_the_sky_it_set_and_the_default_grey_where_it_set_none() {
let Some(mut session) = started(raw("headless skybox frames"), UVec2::splat(SIDE), |_ctx| {
Ok(Empty::along_y(Painted::Above, true))
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let mut read = |sky| {
session.game_mut().sky = sky;
session.step();
middle(&session.pixels().expect("the target reads back"), SIDE)
};
let bright = read(Some(Painted::Above));
let dark = read(Some(Painted::Unlit));
let set_again = read(Some(Painted::Unlit));
let none = read(None);
assert!(bright[0] > 200 && dark[0] < 16, "{bright:?} {dark:?}");
assert_eq!(dark, set_again, "the same sky again draws the same");
assert!(
near(none, [GREY, GREY, GREY, u8::MAX]),
"and a frame that sets none is drawn by the default grey, whatever \
an earlier frame set, got {none:?}"
);
}
#[test]
fn a_camera_with_no_foreshortening_reads_one_texel_over_the_whole_frame() {
let flat = Empty {
sky: Some(Painted::Quarters),
camera: Camera::new(
View::look_at(Vec3::ZERO, Vec3::NEG_Z),
Projection::orthographic(2.0),
),
};
let Some(pixels) = rendered(raw("headless skybox flat"), flat) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let corner = pixel(&pixels, 0, 0, SIDE);
assert!(
near(corner, AROUND[AHEAD]),
"parallel rays all read the one texel the view looks along, got \
{corner:?} against {:?}",
AROUND[AHEAD]
);
assert_eq!(
corner,
middle(&pixels, SIDE),
"so the frame holds that one texel and nothing else"
);
}
#[test]
fn the_sky_lies_behind_an_opaque_draw_and_under_a_translucent_one() {
let read = |material| {
let pixels = rendered(raw("headless skybox over"), Square(material))?;
Some((middle(&pixels, SIDE), pixel(&pixels, 0, 0, SIDE)))
};
let Some((opaque, corner)) = read(Material::color(Color::BLACK)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some((blended, _)) = read(Material::color(Color::rgba(0.0, 0.0, 0.0, 0.5))) else {
return;
};
assert!(
near(corner, AROUND[AHEAD]),
"the sky draws where the square does not, got {corner:?}"
);
assert_eq!(
opaque,
[0, 0, 0, u8::MAX],
"an opaque draw leaves nothing of the sky behind it"
);
assert!(
blended[0] < corner[0] && blended[0] > opaque[0],
"and a translucent one blends over the sky rather than over what the \
frame was cleared to, got {blended:?} between {opaque:?} and {corner:?}"
);
}
#[test]
fn a_lit_surface_takes_the_light_of_the_sky_it_faces() {
let read = |up, material| {
center(Facing {
sky: Painted::Above,
up,
material,
lit: false,
})
};
let Some(facing_up) = read(true, Material::lit(Color::WHITE)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(facing_down) = read(false, Material::lit(Color::WHITE)) else {
return;
};
let Some(unlit_up) = read(true, Material::color(Color::WHITE)) else {
return;
};
let Some(unlit_down) = read(false, Material::color(Color::WHITE)) else {
return;
};
assert!(
facing_up[0] > 2 * facing_down[0],
"a surface facing the bright zenith takes far more light than one \
facing the dark nadir, got {facing_up:?} against {facing_down:?}"
);
assert_eq!(
unlit_up, unlit_down,
"and an unlit surface reads its own tint whichever way it faces"
);
assert!(
unlit_up[0] > facing_up[0],
"which is more than the lit one takes of that sky, got {unlit_up:?} \
against {facing_up:?}"
);
}
#[test]
fn the_sky_lands_no_light_under_zero_on_a_surface_facing_away_from_it() {
let read = |sky| {
center(Facing {
sky,
up: false,
material: Material::lit(Color::WHITE).roughness(0.0),
lit: true,
})
};
let Some(blinding) = read(Painted::Blinding) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(none_at_all) = read(Painted::Unlit) else {
return;
};
assert!(
none_at_all[0] > 64,
"the light of the frame's own lands on the surface, so light taken \
away would read, got {none_at_all:?}"
);
assert_eq!(
blinding, none_at_all,
"and a zenith the nine coefficients cannot hold lands none of itself \
on a surface facing the nadir, never less than none"
);
}
#[test]
fn a_metal_reflects_the_sky_and_a_rough_one_blends_it() {
let metal = |roughness| {
center(Ball::ahead(
Material::lit(Color::WHITE)
.metallic(1.0)
.roughness(roughness),
))
};
let Some(mirror) = metal(0.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(rough) = metal(1.0) else {
return;
};
assert!(
near(mirror, AROUND[BEHIND]),
"a mirror facing the camera reflects the quarter of the sky behind \
it, got {mirror:?} against {:?}",
AROUND[BEHIND]
);
for channel in 0..3 {
let read = i32::from(rough[channel]);
let lowest = AROUND.iter().map(|color| i32::from(color[channel])).min();
let largest = AROUND.iter().map(|color| i32::from(color[channel])).max();
assert!(
Some(read) > lowest && Some(read) < largest,
"and a fully rough one reads a blend of every quarter, got \
{rough:?} in channel {channel}"
);
}
}
#[test]
fn a_mirror_reflects_the_quarter_of_the_sky_behind_the_camera_from_either_side() {
let mirror = |from| {
center(Ball {
sky: Painted::Quarters,
material: Material::lit(Color::WHITE).metallic(1.0).roughness(0.0),
from,
})
};
let Some(from_right) = mirror(Vec3::X * 2.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(from_left) = mirror(Vec3::NEG_X * 2.0) else {
return;
};
assert!(
near(from_right, AROUND[RIGHT]),
"a camera at `+X` reads the quarter around `+X` in the mirror, got \
{from_right:?} against {:?}",
AROUND[RIGHT]
);
assert!(
near(from_left, AROUND[LEFT]),
"and one at `-X` the quarter around `-X`, got {from_left:?} against \
{:?}",
AROUND[LEFT]
);
}
#[test]
fn a_mirror_reflects_the_zenith_from_above_and_the_nadir_from_below() {
let mirror = |from| {
center(Ball {
sky: Painted::Above,
material: Material::lit(Color::WHITE).metallic(1.0).roughness(0.0),
from,
})
};
let Some(from_above) = mirror(Vec3::Y * 2.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(from_below) = mirror(Vec3::NEG_Y * 2.0) else {
return;
};
assert!(
from_above[0] > 200,
"a camera above reads the bright zenith in the mirror, got {from_above:?}"
);
assert!(
from_below[0] < 16,
"and one below reads the dark nadir, got {from_below:?}"
);
}
#[test]
fn a_mirror_reflects_the_ground_a_sky_is_over_from_below_and_the_sky_from_above() {
let mirror = |from| {
center(Ball {
sky: Painted::WhiteOverRed,
material: Material::lit(Color::WHITE).metallic(1.0).roughness(0.0),
from,
})
};
let Some(from_above) = mirror(Vec3::Y * 2.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(from_below) = mirror(Vec3::NEG_Y * 2.0) else {
return;
};
assert!(
from_above[0] > 200 && from_above[1] > 200 && from_above[2] > 200,
"a camera above reads the white sky in the mirror, got {from_above:?}"
);
assert!(
from_below[0] > 200 && from_below[1] < 16 && from_below[2] < 16,
"and one below reads the red ground, got {from_below:?}"
);
}
#[test]
fn a_surface_reflects_more_of_the_sky_at_a_grazing_angle_than_facing_it() {
let ball = Ball {
sky: Painted::White,
material: Material::lit(Color::BLACK).roughness(0.0),
from: Vec3::Z * 2.0,
};
let Some(pixels) = sized(raw("headless skybox grazing"), UVec2::splat(WIDE), ball) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let facing = wide_pixel(&pixels, WIDE / 2, WIDE / 2);
let grazing = wide_pixel(&pixels, WIDE / 2, 10);
assert!(
grazing[0] > facing[0] + 8,
"a surface reflects more of the sky the further the viewpoint is from \
facing it, got {grazing:?} against {facing:?}"
);
assert!(
facing[0] > 0,
"and reflects a share of it even facing the viewpoint straight on"
);
}
#[test]
fn a_fully_rough_surface_reflects_no_more_of_the_sky_at_a_grazing_angle_than_facing_it() {
let ball = |roughness| Ball {
sky: Painted::White,
material: Material::lit(Color::BLACK).roughness(roughness),
from: Vec3::Z * 2.0,
};
let Some(rough) = sized(raw("headless rough grazing"), UVec2::splat(WIDE), ball(1.0)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(mirror) = sized(
raw("headless mirror grazing"),
UVec2::splat(WIDE),
ball(0.0),
) else {
return;
};
let facing = |pixels: &[u8]| u32::from(wide_pixel(pixels, WIDE / 2, WIDE / 2)[0]);
let grazing = |pixels: &[u8]| u32::from(wide_pixel(pixels, WIDE / 2, 10)[0]);
assert!(
grazing(&rough) <= facing(&rough) + STEP,
"a fully rough surface reflects no more of the sky where the viewpoint \
meets it along the surface, got {} against {}",
grazing(&rough),
facing(&rough)
);
assert!(
grazing(&mirror) > facing(&mirror) + 8,
"where a mirror of the same color reflects far more of it there, got \
{} against {}",
grazing(&mirror),
facing(&mirror)
);
}
#[test]
fn a_fully_metallic_surface_reflects_the_sky_where_a_painted_one_takes_it_as_tint() {
let ball = |metallic| {
center(Ball::ahead(
Material::lit(Color::rgb(1.0, 0.0, 1.0))
.metallic(metallic)
.roughness(0.0),
))
};
let Some(painted) = ball(0.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(metal) = ball(1.0) else {
return;
};
assert!(
painted[0] > painted[1],
"a surface of no metallic at all takes the sky as its own tint, which \
holds no green at all, got {painted:?}"
);
assert!(
metal[2] > 0 && metal[0] == 0,
"and a fully metallic one reflects the sky behind the camera through \
that tint, which keeps none of a blue quarter's red, got {metal:?}"
);
}
#[test]
fn a_skybox_image_not_twice_as_wide_as_it_is_tall_stops_startup() {
let started = Session::new(
raw("headless skybox shape"),
UVec2::splat(SIDE),
|_ctx| -> Result<Misshapen, Error> { Ok(Misshapen) },
);
match started {
Ok(_) => panic!("a square image is no sky"),
Err(error) if error.to_string().starts_with(NO_ADAPTER) => {
eprintln!("skipped: this machine has no usable graphics adapter");
}
Err(error) => assert_eq!(
error.to_string(),
"the game's assets did not resolve: the skybox `Square` is 4x4; \
a skybox image is twice as wide as it is tall"
),
}
}
#[test]
fn a_skybox_name_no_source_holds_stops_startup() {
let started = Session::new(
raw("headless skybox missing"),
UVec2::splat(SIDE),
|_ctx| -> Result<Absent, Error> { Ok(Absent) },
);
match started {
Ok(_) => panic!("no source holds that name"),
Err(error) if error.to_string().starts_with(NO_ADAPTER) => {
eprintln!("skipped: this machine has no usable graphics adapter");
}
Err(error) => assert_eq!(
error.to_string(),
"the game's assets did not resolve: no asset is named `nowhere`"
),
}
}
struct Misshapen;
impl Game for Misshapen {
type Meshes = NoMeshes;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Wrong;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}
struct Absent;
impl Game for Absent {
type Meshes = NoMeshes;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Nowhere;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Wrong {
Square,
}
impl Catalog for Wrong {
fn catalog() -> Vec<Self> {
vec![Self::Square]
}
}
impl Skyboxes for Wrong {
fn build(&self, _assets: &Assets) -> SkyboxData {
SkyboxData::equirect(TextureData::rgba8(
UVec2::splat(4),
AROUND[AHEAD].repeat(4 * 4),
))
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Nowhere {
Sky,
}
impl Catalog for Nowhere {
fn catalog() -> Vec<Self> {
vec![Self::Sky]
}
}
impl Skyboxes for Nowhere {
fn build(&self, assets: &Assets) -> SkyboxData {
assets.skybox("nowhere")
}
}
struct Aside;
impl Aside {
const CENTER: Vec3 = Vec3::new(3.0, 0.0, 0.0);
const EYE: Vec3 = Vec3::new(0.0, 0.0, 2.0);
const RADIUS: f32 = 0.6;
fn camera() -> Camera {
Camera::new(
View::look_at(Self::EYE, Vec3::new(1.5, 0.0, 0.0)),
Projection::perspective(110.0),
)
}
fn facing() -> Vec3 {
Self::CENTER + (Self::EYE - Self::CENTER).normalize() * Self::RADIUS
}
}
impl Game for Aside {
type Meshes = SphereSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_skybox(Painted::Quarters);
ctx.set_camera(Self::camera());
ctx.light(Light::directional(Vec3::NEG_Z, Color::BLACK));
ctx.draw(
Sphere { subdivisions: 5 }
.at(Transform::from_scale_rotation_translation(
Vec3::splat(Self::RADIUS),
Quat::IDENTITY,
Self::CENTER,
))
.material(Material::lit(Color::WHITE).metallic(1.0).roughness(0.0)),
);
}
}
#[test]
fn a_mirror_off_to_one_side_of_a_foreshortened_view_reflects_the_quarter_behind_that_point() {
let size = UVec2::splat(WIDE);
let Some(pixels) = sized(raw("headless skybox aside"), size, Aside) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let at = Aside::camera()
.pixel_of(Aside::facing(), size)
.expect("the sphere is in view");
let read = wide_pixel(&pixels, at.x as u32, at.y as u32);
let back = (Aside::EYE - Aside::facing()).normalize();
assert!(back.x < -0.7, "the reflection looks back along {back}");
assert!(
near(read, AROUND[LEFT]),
"the point facing the camera reflects the quarter that holds the \
direction from that point back to the camera, got {read:?} at {at} \
against {:?}",
AROUND[LEFT]
);
}
#[test]
fn a_mirror_reflects_the_zenith_where_its_surface_is_turned_halfway_up() {
let ball = Ball {
sky: Painted::Above,
material: Material::lit(Color::WHITE).metallic(1.0).roughness(0.0),
from: Vec3::Z * 2.0,
};
let Some(pixels) = sized(raw("headless skybox halfway"), UVec2::splat(WIDE), ball) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let halfway = wide_pixel(&pixels, WIDE / 2, WIDE / 2 - 18);
let facing = wide_pixel(&pixels, WIDE / 2, WIDE / 2);
assert!(
halfway[0] > 200,
"the point turned halfway up reflects the bright zenith, got {halfway:?}"
);
assert!(
facing[0] < 32,
"and the point facing the camera reflects the dark horizon behind it, \
got {facing:?}"
);
}
#[test]
fn lit_by_scales_the_light_a_sky_lands_and_leaves_the_sky_it_draws_whole() {
let lit = |sky| {
center(Facing {
sky,
up: true,
material: Material::lit(Color::WHITE),
lit: false,
})
};
let Some(whole) = lit(Painted::Above) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(dim) = lit(Painted::AboveQuarterLit) else {
return;
};
let Some(dark) = lit(Painted::AboveUnlit) else {
return;
};
let Some(drawn) = center(Empty::along_y(Painted::AboveQuarterLit, true)) else {
return;
};
let linear = |byte: u8| {
let encoded = f32::from(byte) / 255.0;
match encoded <= 0.04045 {
true => encoded / 12.92,
false => ((encoded + 0.055) / 1.055).powf(2.4),
}
};
let share = linear(dim[0]) / linear(whole[0]);
assert!(
(0.2..0.3).contains(&share),
"a sky lit by a quarter lands a quarter of its light, got {dim:?} \
against {whole:?}, a share of {share}"
);
assert!(
dark[0] < 4,
"and one lit by nothing lands none, got {dark:?}"
);
assert!(
drawn[0] > 200,
"while the sky it draws is unchanged, got {drawn:?}"
);
}
#[test]
fn a_mirror_reflects_none_of_a_sky_that_lands_none_of_its_light() {
let Some(read) = center(Ball {
sky: Painted::WhiteUnlit,
material: Material::lit(Color::WHITE).metallic(1.0).roughness(0.0),
from: Vec3::Z * 2.0,
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert!(
read[0] < 4,
"a sky that lands none of its light leaves nothing in a mirror, got {read:?}"
);
}
#[derive(Catalog, Hash, PartialEq, Eq, Clone)]
struct Occluded;
impl Mesh for Occluded {
fn build(&self, assets: &Assets) -> MeshData {
Quad.build(assets).with_shading(ShadingData::rgba8(
UVec2::ONE,
vec![96, u8::MAX, u8::MAX, u8::MAX],
))
}
}
meshes! { enum OccludedSet { Quad, Occluded } }
struct Shade(bool);
impl Game for Shade {
type Meshes = OccludedSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = Painted;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_skybox(Painted::White);
ctx.set_camera(Camera::new(
View::look_at(Vec3::Z * 2.0, Vec3::ZERO),
Projection::orthographic(1.0),
));
ctx.light(Light::directional(Vec3::NEG_Z, Color::BLACK));
let material = Material::lit(Color::WHITE).roughness(0.5).metallic(0.5);
let at = Transform::from_scale(Vec3::splat(0.5));
match self.0 {
true => ctx.draw(Occluded.at(at).material(material)),
false => ctx.draw(Quad.at(at).material(material)),
}
}
}
#[test]
fn a_shading_maps_occlusion_darkens_the_skys_light_and_its_reflection_alike() {
let Some(open) = center(Shade(false)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(occluded) = center(Shade(true)) else {
return;
};
assert!(
occluded[0] > 0 && u32::from(occluded[0]) * 10 < u32::from(open[0]) * 7,
"occlusion darkens the sky's light and its reflection alike, got \
{occluded:?} against {open:?}"
);
}