mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a game draws and plays of the sources its assets came out of.

use super::*;

/// The example's mesh, read from the working directory a test runs in.
const MODEL: &str = "examples/assets/hello.glb";

/// The two colors of the checker in [`MODEL`], each over one cell four
/// texels wide — a quarter of the target's width apart.
const CHECKER: [[u8; 3]; 2] = [[74, 97, 122], [232, 227, 209]];

/// The test tone, read from the working directory a test runs in.
const TONE: &str = "tests/assets/sweep.ogg";

meshes! { enum AsAuthoredSet { AsAuthored } }
meshes! { enum BeaconSet { Beacon } }

/// The mesh's two materials, spelled out by hand: the derive names
/// `::mirage_engine`, which the engine's own tests are not.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Face {
    Panels,
    Caps,
}

impl Part for Face {
    fn from_name(name: &str) -> Option<Self> {
        match name {
            "Beacon Panels" => Some(Self::Panels),
            "Beacon Caps" => Some(Self::Caps),
            _ => None,
        }
    }

    fn all() -> Vec<Self> {
        vec![Self::Panels, Self::Caps]
    }

    fn index(&self) -> u32 {
        match self {
            Self::Panels => 0,
            Self::Caps => 1,
        }
    }
}

/// A vocabulary whose one mesh comes out of [`MODEL`] and nowhere else.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Beacon;

impl Catalog for Beacon {
    fn catalog() -> Vec<Self> {
        vec![Self]
    }
}

impl Mesh<Face> for Beacon {
    fn build(&self, assets: &Assets) -> MeshData<Face> {
        assets.mesh("beacon")
    }
}

/// Draws the mesh's front face over the whole target, unlit and without
/// a tint, so every pixel of it is a texel of its own texture.
impl Game for Beacon {
    type Meshes = BeaconSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    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, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        ctx.draw(
            Self.at(Vec3::ZERO)
                .material_of(Face::Panels, Material::color(Color::WHITE)),
        );
    }
}

/// The same mesh with no parts of its own, so every slot of it is
/// anonymous.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct AsAuthored;

impl Catalog for AsAuthored {
    fn catalog() -> Vec<Self> {
        vec![Self]
    }
}

impl Mesh for AsAuthored {
    fn build(&self, assets: &Assets) -> MeshData {
        assets.mesh("beacon")
    }
}

impl Game for AsAuthored {
    type Meshes = AsAuthoredSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    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, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        ctx.draw(Self.at(Vec3::ZERO));
    }
}

/// That mesh drawn red over every slot, textures and all.
struct EveryRed;

impl Game for EveryRed {
    type Meshes = AsAuthoredSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    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, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        ctx.draw(
            AsAuthored
                .at(Vec3::ZERO)
                .material(Material::color(Color::rgb(1.0, 0.0, 0.0))),
        );
    }
}

/// The middle of the checker cell `column` cells across the top row.
fn cell(pixels: &[u8], column: u32) -> [u8; 3] {
    let [red, green, blue, _] = pixel(pixels, SIDE / 4 * column + SIDE / 8, SIDE / 8, SIDE);
    [red, green, blue]
}

fn model() -> Config {
    raw("headless model").with_assets([MODEL])
}

#[test]
fn a_loaded_model_is_drawn_through_the_texture_it_came_with() {
    let Some(pixels) = rendered(model(), Beacon) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    assert_eq!(
        [cell(&pixels, 0), cell(&pixels, 1)],
        CHECKER,
        "neighbouring cells alternate"
    );
    assert_eq!(cell(&pixels, 2), CHECKER[0], "and the pattern repeats");
}

#[test]
fn an_anonymous_slot_still_draws_with_what_the_file_gave_it() {
    let Some(pixels) = rendered(model(), AsAuthored) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    assert_ne!(cell(&pixels, 0), [0; 3], "a part no draw can name is drawn");
    assert_ne!(
        cell(&pixels, 0),
        cell(&pixels, 1),
        "through the texture the file gave it, which no draw asked for"
    );
}

#[test]
fn a_draw_repaints_every_slot_of_a_mesh_with_no_parts_through_its_textures() {
    let Some(pixels) = rendered(model(), EveryRed) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    let [first, second] = [cell(&pixels, 0), cell(&pixels, 1)];
    assert_ne!(first, second, "the checker still shows through");
    assert!(
        first[1] == 0 && first[2] == 0 && second[1] == 0 && second[2] == 0,
        "and every cell of it is red: {first:?} {second:?}"
    );
    assert!(
        first[0] > 0 || second[0] > 0,
        "with red where the texture is lit"
    );
}

#[test]
fn a_catalog_naming_an_asset_no_source_loaded_stops_startup() {
    let config = Config::new("headless assets");
    let Err(error) = Session::new(config, UVec2::splat(SIDE), |_ctx| Ok(Beacon)) else {
        panic!("a mesh naming an asset nothing loaded cannot start a game");
    };
    if error.to_string().contains("graphics adapter") {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    }

    assert_eq!(
        error.to_string(),
        "the game's assets did not resolve: no asset is named `beacon`",
        "the missing model alone: its parts are checked against a mesh that loaded"
    );
}

meshes! { enum PaintedSet { Painted } }

/// A generated mesh painted with a texture the build pulls by name: the
/// mesh it returns carries what that build did not get.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Painted;

impl Catalog for Painted {
    fn catalog() -> Vec<Self> {
        vec![Self]
    }
}

impl Mesh for Painted {
    fn build(&self, assets: &Assets) -> MeshData {
        Cube.build(assets).with_texture(assets.texture("nowhere"))
    }
}

impl Game for Painted {
    type Meshes = PaintedSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    type PostEffects = NoPostEffects;

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

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.draw(Self.at(Vec3::ZERO));
    }
}

#[test]
fn a_texture_a_generated_mesh_is_painted_with_stops_startup_where_no_source_holds_it() {
    let config = Config::new("headless painted");
    let Err(error) = Session::new(config, UVec2::splat(SIDE), |_ctx| Ok(Painted)) else {
        panic!("a mesh painted with a texture nothing loaded cannot start a game");
    };
    if error.to_string().contains("graphics adapter") {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    }

    assert_eq!(
        error.to_string(),
        "the game's assets did not resolve: no asset is named `nowhere`",
        "the name the build asked for, carried out of it by the mesh it painted"
    );
}

/// A vocabulary whose one sound comes out of [`TONE`] and nowhere else.
#[derive(Clone, Eq, Hash, PartialEq)]
struct Sweep;

impl Catalog for Sweep {
    fn catalog() -> Vec<Self> {
        vec![Self]
    }
}

impl Sounds for Sweep {
    fn build(&self, assets: &Assets) -> SoundData {
        assets.sound("sweep").streamed()
    }
}

/// A game that draws nothing and plays that sound however it can.
struct Player;

impl Game for Player {
    type Meshes = CubeSet;
    type Sounds = Sweep;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = NoSurfaceStyles;
    type PostEffects = NoPostEffects;

    fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
        ctx.play(Sweep);
    }

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.set_volume(0.8);
        ctx.set_listener(View::look_at(Vec3::Z, Vec3::ZERO));
        ctx.play(Sweep.at(Vec3::X).pitch(1.5));
        ctx.sustain(Sweep.gain(0.4).loop_from(Duration::from_secs(1)));
    }
}

#[test]
fn a_game_plays_through_a_session_that_has_nothing_to_play_to() {
    let config = Config::new("headless sound").with_assets([TONE]);
    let Ok(mut session) = Session::new(config, UVec2::splat(SIDE), |_ctx| Ok(Player)) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    session.tick();
    session.step();
    session.step();
}

#[test]
fn a_catalog_naming_a_sound_no_source_loaded_stops_startup() {
    let config = Config::new("headless sound");
    let Err(error) = Session::new(config, UVec2::splat(SIDE), |_ctx| Ok(Player)) else {
        panic!("a game naming a sound nothing loaded cannot start");
    };
    if error.to_string().contains("graphics adapter") {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    }

    assert_eq!(
        error.to_string(),
        "the game's assets did not resolve: no asset is named `sweep`"
    );
}