mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a slot's texture and a draw's fade leave on the target.

use super::*;

/// The one texel [`Poster`] samples, sRGB-encoded as a target reads back.
const PAINT: [u8; 4] = [64, 160, 224, u8::MAX];

meshes! { enum PosterSet { Poster } }

/// A square that fills the screen, whose slot samples [`PAINT`]; the
/// same square with no texture draws over a white default instead.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Poster {
    Painted,
    Bare,
}

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

impl Mesh for Poster {
    fn build(&self, assets: &Assets) -> MeshData {
        let square = Quad.build(assets);
        match self {
            Self::Painted => square.with_texture(TextureData::rgba8(UVec2::ONE, PAINT.to_vec())),
            Self::Bare => square,
        }
    }
}

impl Game for Poster {
    type Meshes = PosterSet;
    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.set_camera(Camera::new(
            View::look_at(Vec3::Z, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        ctx.draw(self.at(Vec3::ZERO).material(Material::color(Color::WHITE)));
    }
}

#[test]
fn a_slot_texture_reaches_the_target_and_white_stands_in_without_one() {
    let Some(painted) = center(Poster::Painted) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(bare) = center(Poster::Bare) else {
        return;
    };

    assert_eq!(painted, PAINT, "an unlit white tint shows the texel itself");
    assert_eq!(bare, [u8::MAX; 4], "a slot with no texture samples white");
}

/// The painted square filling the view, drawn with the material its slot
/// was built with and faded by `fade` — or, where the test passes one,
/// with `restated` in place of that material and no fade at all.
struct Faded {
    fade: f32,
    restated: Option<Material>,
}

impl Game for Faded {
    type Meshes = PosterSet;
    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.set_camera(Camera::new(
            View::look_at(Vec3::Z, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        let square = Poster::Painted.at(Vec3::ZERO);
        ctx.draw(match self.restated {
            Some(material) => square.material(material),
            None => square.faded(self.fade),
        });
    }
}

#[test]
fn a_faded_draw_blends_by_its_fade_over_the_material_its_slot_was_built_with() {
    let read = |game| Some(middle(&rendered(raw("headless fade"), game)?));
    let faded = |fade| {
        read(Faded {
            fade,
            restated: None,
        })
    };

    let Some(half) = faded(0.5) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(restated) = read(Faded {
        fade: 1.0,
        restated: Some(Material::lit(Color::WHITE.with_alpha(0.5))),
    }) else {
        return;
    };
    let Some(whole) = faded(1.0) else {
        return;
    };
    let Some(gone) = faded(0.0) else {
        return;
    };

    assert_eq!(half, restated, "a fade is the alpha of the slot it kept");
    assert_ne!(half, whole, "which the material it fades is not drawn at");
    assert!(
        half.iter()
            .zip(gone)
            .zip(whole)
            .all(|((blended, under), over)| (under.min(over)..=under.max(over)).contains(blended)),
        "{half:?} is not between {gone:?} and {whole:?}"
    );
}