mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a surface style's own code draws.

use super::*;

/// A style that paints every surface it draws with the color it is passed.
#[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;
         }",
    );
}

/// A style that moves what it draws by the offset it is passed.
#[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; }");
}

/// A style whose draws blend back to front however opaque their tint,
/// at half of what they paint; it reads no values of its own.
#[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;
         }",
    );
}

/// A style whose draws drop the texels their alpha leaves out, which
/// its pass requires and their materials never do.
#[derive(Default, ShaderValues)]
struct Punch;

impl SurfaceStyle for Punch {
    const PASS: DrawPass = DrawPass::Cutout;
}

/// A style whose WGSL names something no shader has.
#[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 } }

/// One red square filling the view, painted by [`Paint`] and passed
/// whatever the test set.
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 = NoPostEffects;

    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>(),
        );
    }
}

/// A white square a quarter of the view across, moved by [`Drift`].
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 = NoPostEffects;

    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>(),
        );
    }
}

/// A red square in front of a blue one, both drawn by [`Ghost`] with a
/// tint as opaque as any other, submitted the way round the test sets.
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 = NoPostEffects;

    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);
        }
    }
}

/// A square its texture clears the middle of, over a solid one, both drawn by [`Punch`] with
/// materials that set no cutout of their own.
struct Punched;

impl Game for Punched {
    type Meshes = CutSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = Punching;
    type PostEffects = NoPostEffects;

    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>(),
        );
    }
}

/// A game whose one style does not compile.
struct Broken;

impl Game for Broken {
    type Meshes = QuadSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = Breaking;
    type PostEffects = NoPostEffects;

    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}

    fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}

/// The error startup stopped `game` with, or `None` where the machine
/// has no usable graphics adapter to have started it against.
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"), SIDE);

    session.game_mut().color = None;
    session.step();
    let dropped = middle(&session.pixels().expect("the target reads back"), SIDE);

    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 [red, green, blue, _] = column(pixels, x);
        [red, green, blue]
    };
    let background = |pixels: &[u8]| {
        let [red, green, blue, _] = pixel(pixels, 0, 0, SIDE);
        [red, green, blue]
    };

    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| column(&pixels, x);

    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");
}