use super::*;
#[derive(Default, ShaderValues)]
struct Paint {
color: Vec4,
}
impl SurfaceStyle for Paint {
const PASS: DrawPass = DrawPass::Opaque;
const SURFACE: Option<&'static str> = Some(
"fn surface(surface: Surface) -> Surface {
var painted = surface;
painted.color = vec4<f32>(style.color.rgb, surface.color.a);
return painted;
}",
);
}
#[derive(Default, ShaderValues)]
struct Drift {
by: Vec3,
}
impl SurfaceStyle for Drift {
const PASS: DrawPass = DrawPass::Opaque;
const DISPLACE: Option<&'static str> =
Some("fn displace(placed: Placed) -> vec3<f32> { return style.by; }");
}
#[derive(Default, ShaderValues)]
struct Ghost;
impl SurfaceStyle for Ghost {
const PASS: DrawPass = DrawPass::Translucent;
const SURFACE: Option<&'static str> = Some(
"fn surface(surface: Surface) -> Surface {
var faded = surface;
faded.color.a = 0.5;
return faded;
}",
);
}
#[derive(Default, ShaderValues)]
struct Punch;
impl SurfaceStyle for Punch {
const PASS: DrawPass = DrawPass::Cutout;
}
#[derive(Default, ShaderValues)]
struct Cracked;
impl SurfaceStyle for Cracked {
const PASS: DrawPass = DrawPass::Opaque;
const SURFACE: Option<&'static str> =
Some("fn surface(surface: Surface) -> Surface { return nowhere; }");
}
surface_styles! { enum Painting { Paint } }
surface_styles! { enum Drifting { Drift } }
surface_styles! { enum Ghosting { Ghost } }
surface_styles! { enum Punching { Punch } }
surface_styles! { enum Breaking { Cracked } }
struct Painter {
color: Option<Vec4>,
}
impl Game for Painter {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = Painting;
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(FLAT);
if let Some(color) = self.color {
ctx.set_surface_style(Paint { color });
}
ctx.draw(
Quad.at(Vec3::ZERO)
.material(Material::color(Color::rgb(1.0, 0.0, 0.0)))
.surface_style::<Paint>(),
);
}
}
struct Drifter {
by: Vec3,
}
impl Game for Drifter {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = Drifting;
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 * 2.0, Vec3::ZERO),
Projection::orthographic(2.0),
));
ctx.set_surface_style(Drift { by: self.by });
ctx.draw(
Quad.at(Transform::from_scale(Vec3::splat(0.5)))
.material(Material::color(Color::WHITE))
.surface_style::<Drift>(),
);
}
}
struct Ghosts {
far_first: bool,
}
impl Game for Ghosts {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = Ghosting;
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(FLAT);
let pane = |at: Vec3, color| {
Quad.at(at)
.material(Material::color(color))
.surface_style::<Ghost>()
};
let near = pane(Vec3::Z * 0.5, Color::rgb(1.0, 0.0, 0.0));
let far = pane(Vec3::ZERO, Color::rgb(0.0, 0.0, 1.0));
for pane in match self.far_first {
true => [far, near],
false => [near, far],
} {
ctx.draw(pane);
}
}
}
struct Punched;
impl Game for Punched {
type Meshes = CutSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = Punching;
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 * 2.0, Vec3::ZERO),
Projection::orthographic(1.0),
));
ctx.draw(
Cut::Holed
.at(Vec3::ZERO)
.material(Material::color(Color::WHITE))
.surface_style::<Punch>(),
);
ctx.draw(
Cut::Solid
.at(Vec3::NEG_Z * 0.5)
.material(Material::color(Color::rgb(1.0, 0.0, 0.0)))
.surface_style::<Punch>(),
);
}
}
struct Broken;
impl Game for Broken {
type Meshes = QuadSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = Breaking;
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}
pub(super) fn refused<G: Game>(game: G) -> Option<String> {
Session::new(raw("headless styles"), UVec2::splat(SIDE), |_ctx| {
Ok(Painter { color: None })
})
.ok()?;
let started = Session::new(raw("headless styles"), UVec2::splat(SIDE), |_ctx| Ok(game));
Some(started.err().expect("the game does not start").to_string())
}
#[test]
fn a_styles_own_code_paints_the_surface_it_is_handed() {
let painted = |color: Option<Vec4>| center(Painter { color });
let Some(green) = painted(Some(Vec4::new(0.0, 1.0, 0.0, 1.0))) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(unset) = painted(None) else {
return;
};
assert_eq!(green, [0, u8::MAX, 0, u8::MAX], "the tint is painted over");
assert_eq!(
unset,
[0, 0, 0, u8::MAX],
"and a frame that hands it nothing hands it the default value"
);
}
#[test]
fn the_values_a_frame_hands_a_style_last_are_the_ones_it_reads() {
let Ok(mut session) = Session::new(raw("headless styles"), UVec2::splat(SIDE), |_ctx| {
Ok(Painter { color: None })
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.game_mut().color = Some(Vec4::new(0.0, 0.0, 1.0, 1.0));
session.step();
let blue = middle(&session.pixels().expect("the target reads back"));
session.game_mut().color = None;
session.step();
let dropped = middle(&session.pixels().expect("the target reads back"));
assert_eq!(blue, [0, 0, u8::MAX, u8::MAX]);
assert_eq!(
dropped,
[0, 0, 0, u8::MAX],
"values last no longer than the frame that handed them over"
);
}
#[test]
fn a_styles_own_code_moves_what_it_draws() {
let drifted = |by: Vec3| rendered(raw("headless styles"), Drifter { by });
let Some(placed) = drifted(Vec3::ZERO) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(moved) = drifted(Vec3::X * 0.5) else {
return;
};
let at = |pixels: &[u8], x: u32| {
let offset = ((SIDE / 2 * SIDE + x) * 4) as usize;
[pixels[offset], pixels[offset + 1], pixels[offset + 2]]
};
let background = |pixels: &[u8]| {
let bg = pixel(pixels, 0, 0);
[bg[0], bg[1], bg[2]]
};
assert_eq!(at(&placed, SIDE / 2), [u8::MAX; 3], "it is drawn placed");
assert_eq!(at(&placed, 3 * SIDE / 4), background(&placed));
assert_eq!(
at(&moved, SIDE / 2),
background(&moved),
"and the style moves it"
);
assert_eq!(at(&moved, 3 * SIDE / 4), [u8::MAX; 3]);
}
#[test]
fn a_translucent_style_composites_back_to_front_however_opaque_its_tint() {
let Some(near_first) = center(Ghosts { far_first: false }) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(far_first) = center(Ghosts { far_first: true }) else {
return;
};
let [red, _, blue, _] = near_first;
assert!(
red > blue && blue > 0,
"the nearer of the two is blended over the further, got {near_first:?}"
);
assert_eq!(
near_first, far_first,
"whichever way round the frame submitted them"
);
}
#[test]
fn a_cutout_pass_style_drops_the_texels_its_alpha_leaves_out() {
let Some(pixels) = rendered(raw("headless styles"), Punched) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let across = |x: u32| {
let at = ((SIDE / 2 * SIDE + x) * 4) as usize;
[pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
};
assert_eq!(across(SIDE / 4), BEHIND, "the square behind shows through");
assert_eq!(
across(SIDE * 3 / 4),
IN_FRONT,
"and is held out of the rest by the depth the pass wrote"
);
}
#[test]
fn a_style_that_does_not_compile_stops_startup_naming_it() {
let Some(error) = refused(Broken) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert!(error.contains("Cracked"), "{error}");
assert!(error.contains("nowhere"), "and names what the shader read");
}