use super::styles::refused;
use super::*;
const VIGNETTE: f32 = 0.5;
const LIFTED: f32 = 0.1;
const REACH: f32 = 100.0;
const GLOW: Color = Color::rgb(4.0, 0.0, 0.0);
#[derive(ShaderValues)]
struct Unchanged;
impl PostEffect for Unchanged {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> { return pixel.color; }";
}
#[derive(ShaderValues)]
struct Corners {
depth: f32,
}
impl PostEffect for Corners {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
let middle = vec2<f32>(0.5);
let out = distance(pixel.uv, middle) / length(middle);
return vec4<f32>(pixel.color.rgb * (1.0 - effect.depth * out), pixel.color.a);
}";
}
#[derive(ShaderValues)]
struct Lift {
by: f32,
}
impl PostEffect for Lift {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
return vec4<f32>(pixel.color.rgb + vec3<f32>(effect.by), pixel.color.a);
}";
}
#[derive(ShaderValues)]
struct Halve;
impl PostEffect for Halve {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
return vec4<f32>(pixel.color.rgb * 0.5, pixel.color.a);
}";
}
#[derive(ShaderValues)]
struct Past;
impl PostEffect for Past {
const STAGE: EffectStage = EffectStage::Lit;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
return vec4<f32>(max(pixel.color.rgb - vec3<f32>(1.0), vec3<f32>(0.0)), 1.0);
}";
}
#[derive(ShaderValues)]
struct Reach {
over: f32,
}
impl PostEffect for Reach {
const STAGE: EffectStage = EffectStage::Lit;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
return vec4<f32>(vec3<f32>(depth_at(pixel.uv) / effect.over), 1.0);
}";
}
#[derive(ShaderValues)]
struct Glass;
impl PostEffect for Glass {
const STAGE: EffectStage = EffectStage::OverUi;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> {
return vec4<f32>(pixel.color.rgb * 0.5, pixel.color.a);
}";
}
#[derive(ShaderValues)]
struct Shattered;
impl PostEffect for Shattered {
const STAGE: EffectStage = EffectStage::ToneMapped;
const SHADER: &'static str = "fn draw(pixel: Pixel) -> vec4<f32> { return nowhere; }";
}
post_effects! { enum Plain { Unchanged } }
post_effects! { enum Vignetting { Corners } }
post_effects! { enum Ordered { Lift, Halve } }
post_effects! { enum Emitting { Past } }
post_effects! { enum Reaching { Reach } }
post_effects! { enum Glassing { Glass } }
post_effects! { enum Halving { Halve } }
post_effects! { enum Shattering { Shattered } }
fn court<G: Game>(ctx: &mut FrameContext<'_, G>)
where
G::Meshes: Holds<Quad> + From<Quad>,
{
ctx.set_camera(FLAT);
ctx.draw(
Quad.at(Vec3::ZERO)
.material(Material::color(Color::rgb(0.6, 0.3, 0.15))),
);
}
struct Bare;
impl Game for Bare {
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>) {
court(ctx);
}
}
struct PostEffected {
running: bool,
}
impl Game for PostEffected {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Plain;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
court(ctx);
if self.running {
ctx.set_post_effect(Unchanged);
}
}
}
struct Vignetted;
impl Game for Vignetted {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Vignetting;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
court(ctx);
ctx.set_post_effect(Corners { depth: VIGNETTE });
}
}
struct Sequenced;
impl Game for Sequenced {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Ordered;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
court(ctx);
ctx.set_post_effect(Halve);
ctx.set_post_effect(Lift { by: LIFTED });
}
}
struct Emissive {
bloom: f32,
screened: bool,
}
impl Game for Emissive {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Emitting;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(FLAT);
ctx.set_bloom(self.bloom);
ctx.draw(
Quad.at(Transform::from_scale(Vec3::splat(0.25)))
.material(Material::color(Color::BLACK).emissive(GLOW)),
);
if self.screened {
ctx.set_post_effect(Past);
}
}
}
struct Depths;
impl Game for Depths {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Reaching;
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 * 4.0, Vec3::ZERO),
Projection::perspective(45.0).clip(0.1..REACH),
));
ctx.draw(Cube.at(Vec3::ZERO).material(Material::color(Color::WHITE)));
ctx.set_post_effect(Reach { over: REACH });
}
}
struct Cracking;
impl Game for Cracking {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Shattering;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}
#[test]
fn an_effect_no_frame_runs_leaves_the_frame_as_a_game_with_none_draws_it() {
let drawn = |game| rendered(raw("headless effects"), game);
let Some(none) = drawn(Bare) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(quiet) = rendered(raw("headless effects"), PostEffected { running: false }) else {
return;
};
let Some(same) = rendered(raw("headless effects"), PostEffected { running: true }) else {
return;
};
assert_eq!(
quiet, none,
"a set no frame draws from costs the frame nothing"
);
assert_eq!(
same, none,
"and an effect that writes back what it was passed changes nothing"
);
}
#[test]
fn a_vignette_darkens_a_corner_by_the_fraction_it_was_passed() {
let Some(plain) = rendered(raw("headless effects"), Bare) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(vignetted) = rendered(raw("headless effects"), Vignetted) else {
return;
};
let kept = |at: (u32, u32)| {
let read = |pixels: &[u8]| f32::from(pixel(pixels, at.0, at.1)[0]);
read(&vignetted) / read(&plain)
};
let middle = kept((SIDE / 2, SIDE / 2));
assert!(
(middle - 1.0).abs() < 0.02,
"the middle is as deep as it was, got {middle}"
);
let corner = kept((0, 0));
assert!(
(corner - (1.0 - VIGNETTE)).abs() < 0.02,
"and the corner keeps the fraction the effect was passed, got {corner}"
);
}
#[test]
fn two_effects_of_one_stage_run_in_the_order_their_set_names_them() {
let Some(pixels) = rendered(raw("headless effects"), Sequenced) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(plain) = rendered(raw("headless effects"), Bare) else {
return;
};
let read = |pixels: &[u8]| pixel(pixels, SIDE / 2, SIDE / 2)[0] as f32 / 255.0;
let lifted_then_halved = (read(&plain) + LIFTED) * 0.5;
let halved_then_lifted = read(&plain) * 0.5 + LIFTED;
assert!(
(read(&pixels) - lifted_then_halved).abs() < 0.01,
"the set names the lift first, got {} against {lifted_then_halved} and \
{halved_then_lifted}",
read(&pixels)
);
}
#[test]
fn a_lit_stage_effect_reads_the_light_the_screen_cannot_hold_and_its_own_output_blooms() {
let lit = |bloom, screened| rendered(raw("headless effects"), Emissive { bloom, screened });
let Some(kept) = lit(0.0, true) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(spread) = lit(0.5, true) else {
return;
};
let outside = (SIDE / 2 + SIDE / 6, SIDE / 2);
assert!(
pixel(&kept, SIDE / 2, SIDE / 2)[0] > 200,
"the effect kept the light past what the screen shows"
);
assert_eq!(
pixel(&kept, outside.0, outside.1),
[0, 0, 0, u8::MAX],
"and nothing of it where the draw does not reach"
);
assert!(
pixel(&spread, outside.0, outside.1)[0] > 8,
"which the chain then spreads past the draw's own edge"
);
}
#[test]
fn the_scene_depth_an_effect_reads_is_nearer_under_a_draw_than_where_nothing_was_drawn() {
for antialiasing in [true, false] {
let config = raw("headless effects").with_antialiasing(antialiasing);
let Some(pixels) = rendered(config, Depths) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let under = pixel(&pixels, SIDE / 2, SIDE / 2)[0];
let beside = pixel(&pixels, 1, 1)[0];
assert_eq!(
beside,
u8::MAX,
"nothing was drawn in the corner, so the view reaches its own far clip there \
({antialiasing})"
);
assert!(
under > 0 && under < beside / 2,
"and the cube stands well in front of it, got {under} ({antialiasing})"
);
}
}
#[test]
fn an_effect_that_does_not_compile_stops_startup_naming_it() {
let Some(error) = refused(Cracking) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert!(error.contains("Shattered"), "{error}");
assert!(error.contains("nowhere"), "and names what the shader read");
}
#[cfg(feature = "ui")]
mod ui {
use super::*;
const PANE: u32 = 64;
const BADGE: f32 = 32.0;
const INSIDE: (u32, u32) = (16, 16);
struct Badge;
impl Game for Badge {
type Meshes = CubeSet;
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.ui(|ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32.0, 32.0)),
0.0,
egui::Color32::RED,
);
});
}
}
struct Glassed<C: PostEffects> {
over: PhantomData<C>,
}
impl Game for Glassed<Glassing> {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Glassing;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
badge(ctx);
ctx.set_post_effect(Glass);
}
}
impl Game for Glassed<Halving> {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = Halving;
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
badge(ctx);
ctx.set_post_effect(Halve);
}
}
fn badge<G: Game>(ctx: &mut FrameContext<'_, G>) {
ctx.ui(|ui| {
ui.painter().rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(BADGE, BADGE)),
0.0,
egui::Color32::RED,
);
});
}
fn glassed<G: Game>(game: G) -> Option<Vec<u8>> {
sized(Config::new("headless glass"), UVec2::splat(PANE), game)
}
#[test]
fn an_effect_over_the_ui_darkens_what_one_under_it_leaves() {
let Some(over) = glassed(Glassed::<Glassing> { over: PhantomData }) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(under) = glassed(Glassed::<Halving> { over: PhantomData }) else {
return;
};
let at = |pixels: &[u8], (x, y): (u32, u32)| {
let offset = ((y * PANE + x) * 4) as usize;
pixels[offset]
};
assert!(
at(&under, INSIDE) > 248,
"the badge is drawn after the effect under it, so it stands whole"
);
let kept = f32::from(at(&over, INSIDE)) / f32::from(at(&under, INSIDE));
assert!(
(kept - 0.5).abs() < 0.02,
"and the effect over it halves the badge's own pixels, got {kept}"
);
}
#[test]
fn the_ui_layer_is_drawn_over_the_scene() {
let Ok(mut session) = Session::new(Config::new("headless ui"), UVec2::splat(64), |_ctx| {
Ok(Badge)
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
let pixels = session.pixels().expect("the target reads back");
let pixel = |x: usize, y: usize| {
let at = (y * 64 + x) * 4;
[pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
};
let [red, green, blue, _] = pixel(16, 16);
assert!(
red > 248 && green < 16 && blue < 16,
"the badge covers the top left, and no curve is between it and \
the target, got {red} {green} {blue}"
);
let background = pixel(63, 0);
assert_eq!(pixel(48, 48), background, "the rest is the clear color");
}
struct Wash;
impl Game for Wash {
type Meshes = CubeSet;
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.ui(|ui| {
let painter = ui.painter();
painter.rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(64.0, 64.0)),
0.0,
egui::Color32::from_gray(128),
);
painter.rect_filled(
egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32.0, 64.0)),
0.0,
egui::Color32::from_rgba_unmultiplied(255, 255, 255, 128),
);
});
}
}
#[test]
fn the_ui_layer_blends_in_the_space_its_colors_are_authored_in() {
let Ok(mut session) = Session::new(Config::new("headless ui blend"), UVec2::splat(64), {
|_ctx| Ok(Wash)
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
let pixels = session.pixels().expect("the target reads back");
let grey = |x: usize, y: usize| pixels[(y * 64 + x) * 4];
assert_eq!(
grey(48, 32),
128,
"an opaque UI color reaches the target as it was authored"
);
let washed = grey(16, 32);
assert!(
(191..=192).contains(&washed),
"white at half alpha over grey leaves 192, got {washed}"
);
}
}