use super::*;
const RAMP: [f32; 6] = [0.0, 0.25, 0.5, 1.0, 4.0, 64.0];
struct Glow {
light: Color,
scale: f32,
exposure: f32,
bloom: f32,
}
impl Glow {
fn lit(light: Color) -> Self {
Self {
light,
scale: 1.0,
exposure: 1.0,
bloom: 0.0,
}
}
}
impl Game for Glow {
type Meshes = QuadSet;
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::Z, Vec3::ZERO),
Projection::orthographic(1.0),
));
ctx.set_exposure(self.exposure);
ctx.set_bloom(self.bloom);
ctx.draw(
Quad.at(Transform::from_scale(Vec3::splat(self.scale)))
.material(Material::color(Color::BLACK).emissive(self.light)),
);
}
}
fn ramp(curve: Tonemap) -> Option<Vec<u8>> {
RAMP.iter()
.map(|&light| {
let config = Config::new("headless tone map").with_tonemap(curve);
let pixels = rendered(config, Glow::lit(Color::rgb(light, light, light)))?;
Some(middle(&pixels)[0])
})
.collect()
}
#[test]
fn every_curve_rises_with_the_light_it_maps_and_holds_at_white() {
let Some(off) = ramp(Tonemap::Off) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(neutral) = ramp(Tonemap::Neutral) else {
return;
};
let Some(aces) = ramp(Tonemap::Aces) else {
return;
};
for mapped in [&off, &neutral, &aces] {
assert_eq!(mapped[0], 0, "no light leaves no light, got {mapped:?}");
assert!(
mapped.windows(2).all(|pair| pair[0] <= pair[1]),
"{mapped:?} never falls as its light rises"
);
assert_eq!(
mapped.last(),
Some(&u8::MAX),
"{mapped:?} reaches white and stays a number"
);
}
assert_eq!(off[3], u8::MAX, "off clamps at the screen's range");
assert!(
neutral[3] < u8::MAX && aces[3] < u8::MAX,
"a curve keeps room past what the screen shows, got {neutral:?} {aces:?}"
);
}
#[test]
fn exposure_scales_the_frame_before_the_curve() {
let quarter = Color::rgb(0.25, 0.25, 0.25);
let at = |exposure| {
let square = Glow {
exposure,
..Glow::lit(quarter)
};
Some(middle(&rendered(raw("headless exposure"), square)?)[0])
};
let Some(plain) = at(1.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(doubled) = at(2.0) else {
return;
};
let Some(none) = at(0.0) else {
return;
};
assert!(doubled > plain, "{doubled} is more light than {plain}");
assert_eq!(none, 0, "no exposure leaves no light");
}
#[test]
fn bloom_spreads_light_past_what_drew_it_and_none_skips_the_chain() {
let read = |bloom| {
let square = Glow {
scale: 0.25,
bloom,
..Glow::lit(Color::WHITE)
};
let pixels = sized(raw("headless bloom"), UVec2::splat(WIDE), square)?;
let beside = ((WIDE / 4 * WIDE + WIDE / 4) * 4) as usize;
let center = ((WIDE / 2 * WIDE + WIDE / 2) * 4) as usize;
Some((pixels[beside], pixels[center], pixels[0]))
};
let Some((dark, full, background)) = read(0.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some((scattered, spent, _)) = read(0.5) else {
return;
};
assert_eq!(
dark, background,
"nothing reaches beside the square with the chain skipped"
);
assert_eq!(full, u8::MAX, "the square itself fills the screen's range");
assert!(
scattered > background,
"the chain spreads light in beside it, got {scattered} against {background}"
);
assert!(
spent < full,
"and takes it from the square, which keeps the frame's light whole"
);
}
struct Tilted;
impl Game for Tilted {
type Meshes = QuadSet;
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::Z, Vec3::ZERO),
Projection::orthographic(2.0),
));
ctx.draw(
Quad.at(Transform::from_rotation(Quat::from_rotation_z(0.4)))
.material(Material::color(Color::WHITE)),
);
}
}
#[test]
fn antialiasing_leaves_edge_pixels_between_the_two_sides_of_an_edge() {
let between = |antialiasing| {
let config = raw("headless edges").with_antialiasing(antialiasing);
let pixels = sized(config, UVec2::splat(WIDE), Tilted)?;
let background = i32::from(wide_pixel(&pixels, 0, 0)[0]);
Some(
pixels
.chunks(4)
.filter(|pixel| {
let red = i32::from(pixel[0]);
red > background + 16 && red < 240
})
.count(),
)
};
let Some(smooth) = between(true) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(stepped) = between(false) else {
return;
};
assert_eq!(stepped, 0, "one sample per pixel takes one side or other");
assert!(smooth > 0, "four of them take both where an edge crosses");
}