mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a draw a machine poses reads back.

use super::*;
use crate::mesh::{Clip, NoParts, Posing};
use crate::{AnimationStates, Animator, Motion, Progress, Transition};

/// A model of sixteen joints under the clips `idle`, `attack`, `dead` and
/// `walk`, read from the working directory a test runs in.
const CORGI: &str = "examples/assets/corgi.glb";

/// A model of two meshes over one skin, under the clip `open`.
const CHEST: &str = "examples/assets/chest.glb";

/// The side of the target every scene below is read off.
const TALL: u32 = 64;

/// The span every tick below covers, in seconds: short enough to lie
/// inside the first cycle of any clip these models hold.
const STEP: Duration = Duration::from_millis(100);

/// The camera the first of those is seen from: that model is authored at
/// `100` times the size a meter is and lies out along `+Z`, so the view
/// covers `1000` about where it lies.
const ASIDE: Camera = Camera::new(
    View::look_at(Vec3::new(700.0, 250.0, 900.0), Vec3::new(0.0, 0.0, 197.0)),
    Projection::orthographic(1000.0),
);

/// The camera the second is seen from, over a meter and a half.
const AHEAD: Camera = Camera::new(
    View::look_at(Vec3::new(0.0, 0.8, 2.0), Vec3::new(0.0, 0.3, 0.0)),
    Projection::orthographic(1.5),
);

meshes! { enum CorgiSet { Corgi } }
meshes! { enum ChestSet { Chest } }

/// The clips the first model holds, spelled out by hand: the derive names
/// `::mirage_engine`, which the engine's own tests are not.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Mood {
    Idle,
    Walk,
}

impl Clip for Mood {
    fn from_name(name: &str) -> Option<Self> {
        match name {
            "idle" => Some(Self::Idle),
            "walk" => Some(Self::Walk),
            _ => None,
        }
    }

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

    fn index(&self) -> u32 {
        self.clone() as u32
    }
}

#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Corgi;

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

impl Mesh<NoParts, Mood> for Corgi {
    fn build(&self, assets: &Assets) -> MeshData<NoParts, Mood> {
        assets.model("RootNode")
    }
}

/// The one clip the second model holds.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Lid {
    Open,
}

impl Clip for Lid {
    fn from_name(name: &str) -> Option<Self> {
        match name {
            "open" => Some(Self::Open),
            _ => None,
        }
    }

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

    fn index(&self) -> u32 {
        0
    }
}

#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Chest;

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

impl Mesh<NoParts, Lid> for Chest {
    fn build(&self, assets: &Assets) -> MeshData<NoParts, Lid> {
        assets.model("Chest")
    }
}

/// A machine of one state, playing that clip over and over.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Walking {
    On,
}

impl AnimationStates for Walking {
    type Clip = Mood;
    type Input = ();

    fn entry() -> Self {
        Self::On
    }

    fn motion(&self, _input: &()) -> Motion<Mood> {
        Motion::looping(Mood::Walk)
    }

    fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
        None
    }
}

/// A machine of one state, as far open as the game states.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Opening {
    It,
}

impl AnimationStates for Opening {
    type Clip = Lid;
    type Input = f32;

    fn entry() -> Self {
        Self::It
    }

    fn motion(&self, openness: &f32) -> Motion<Lid> {
        Motion::scrubbed(Lid::Open, *openness)
    }

    fn next(&self, _input: &f32, _at: Progress) -> Option<Transition<Self>> {
        None
    }
}

/// The first model drawn white, posed by a machine the ticks run, or at
/// the one time along `walk` the game states instead.
struct Trotting {
    animator: Animator<Corgi, Walking>,
    at: Option<f32>,
}

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

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

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.set_camera(ASIDE);
        let corgi = Corgi.at(Vec3::ZERO).material(Material::color(Color::WHITE));
        ctx.draw(match self.at {
            Some(at) => corgi.posed_by(Posing::clip(Mood::Walk.index(), at)),
            None => corgi.posed(&self.animator),
        });
    }
}

/// The second model drawn white, as far open as `openness` states, posed
/// by a machine where `at` is absent and at that time along `open` where it
/// is not; in no pose at all where neither states one.
struct Opened {
    animator: Animator<Chest, Opening>,
    openness: Option<f32>,
    at: Option<f32>,
}

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

    fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
        let Some(openness) = self.openness else {
            return;
        };
        ctx.animate(Chest, &mut self.animator, &openness);
    }

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.set_camera(AHEAD);
        let chest = Chest.at(Vec3::ZERO).material(Material::color(Color::WHITE));
        ctx.draw(match (self.at, self.openness) {
            (Some(at), _) => chest.posed_by(Posing::clip(Lid::Open.index(), at)),
            (None, Some(_)) => chest.posed(&self.animator),
            (None, None) => chest,
        });
    }
}

/// A session of `game` over `source`, which ticks `STEP` at a time and
/// draws at the instant its ticks have run to.
fn session<G: Game>(title: &str, source: &str, game: G) -> Option<Session<G>> {
    let config = Config::new(title)
        .with_tonemap(Tonemap::Off)
        .with_assets([source])
        .with_tick_interval(STEP);

    started(config, UVec2::splat(TALL), |_ctx| Ok(game))
}

/// What that session draws after `ticks` of it, each one `STEP` long.
fn after<G: Game>(session: &mut Session<G>, ticks: u32) -> Vec<u8> {
    for _ in 0..ticks {
        session.tick();
    }
    session.step();

    session.pixels().expect("the target reads back")
}

/// What `game` draws in the step after one tick of it.
fn drawn<G: Game>(title: &str, source: &str, game: G) -> Option<Vec<u8>> {
    let mut session = session(title, source, game)?;

    Some(after(&mut session, 1))
}

#[test]
fn a_held_machine_draws_one_pose_however_far_past_its_tick_the_frames_draw() {
    let walking = Trotting {
        animator: Animator::new(),
        at: None,
    };
    let Some(mut session) = session("headless machine held", CORGI, walking) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let running = after(&mut session, 3);
    session.game_mut().animator.hold();
    let held = after(&mut session, 1);
    // The steps' own clock had not moved; from here each step takes it past
    // the ticks' clock by a third of a tick more, with no tick between.
    session.set_frame_interval(STEP * 4 + STEP / 3);
    let third = after(&mut session, 0);
    let two_thirds = after(&mut session, 0);

    assert!(drawn_pixels(&running) > 0, "the model is drawn at all");
    assert_eq!(
        third, held,
        "a frame a third of a tick past the tick that held the machine draws \
         the pose that tick left"
    );
    assert_eq!(
        two_thirds, held,
        "and so does the frame after it, further into the tick"
    );
}

#[test]
fn a_machine_poses_a_draw_at_the_instant_the_frame_draws_it() {
    // The clip read at a time at or before its first key, which is the pose
    // a machine starts in whatever time its own first key lies at.
    let Some(start) = drawn(
        "headless machine start",
        CORGI,
        Trotting {
            animator: Animator::new(),
            at: Some(0.0),
        },
    ) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    let walking = Trotting {
        animator: Animator::new(),
        at: None,
    };
    let Some(mut session) = session("headless machine", CORGI, walking) else {
        return;
    };
    let unrun = after(&mut session, 0);
    let first = after(&mut session, 1);
    let later = after(&mut session, 5);

    assert!(drawn_pixels(&start) > 0, "the model is drawn at all");
    assert_eq!(
        unrun, start,
        "a machine no tick has run poses the draw at the start of what it \
         plays, which is that clip read through the engine's own path"
    );
    assert_eq!(
        first, unrun,
        "a tick and the step after it read one instant, so that step draws \
         the pose the tick started the machine in"
    );
    assert_ne!(
        later, first,
        "and the steps that follow draw it further along that clip"
    );
}

#[test]
fn a_scrubbed_machine_poses_a_draw_by_the_value_the_game_states() {
    let closed = Opened {
        animator: Animator::new(),
        openness: Some(0.0),
        at: None,
    };
    let Some(shut) = drawn("headless chest shut", CHEST, closed) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(rest) = drawn(
        "headless chest rest",
        CHEST,
        Opened {
            animator: Animator::new(),
            openness: None,
            at: None,
        },
    ) else {
        return;
    };
    let Some(whole) = drawn(
        "headless chest open",
        CHEST,
        Opened {
            animator: Animator::new(),
            openness: Some(1.0),
            at: None,
        },
    ) else {
        return;
    };
    // A time past the last key reads that key, whatever the clip's own
    // length is.
    let Some(last) = drawn(
        "headless chest last",
        CHEST,
        Opened {
            animator: Animator::new(),
            openness: None,
            at: Some(1e9),
        },
    ) else {
        return;
    };

    assert!(drawn_pixels(&shut) > 0, "the model is drawn at all");
    assert_eq!(shut, rest, "a lid scrubbed to nothing draws the rest pose");
    assert_ne!(shut, whole, "and one scrubbed whole draws another");
    assert_eq!(whole, last, "which is the last key of the clip it scrubs");
}