mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a session draws, read back pixel by pixel.

use core::marker::PhantomData;

use super::*;
use crate::math::{Quat, Vec2, Vec3, Vec4};
use crate::mesh::{Cube, Frame, Mesh, MeshData, Part, Plane, Quad, Sheet, Sphere, Vertex};
use crate::meshes;

use crate::{
    Assets, ButtonBinding, Camera, Catalog, Color, Cursor, DrawPass, EffectStage, FrameContext,
    Holds, Key, Light, Material, MouseButton, NoInputActions, NoSkyboxes, NoSounds, PostEffect,
    PostEffects, Projection, ReliefData, ShaderValues, ShadingData, SkyboxData, Skyboxes,
    SoundData, Sounds, Startup, SurfaceStyle, TextureData, TickContext, Tonemap, Transform, View,
    post_effects, surface_styles,
};

/// The camera every styled square below is drawn from: one meter square
/// of the plane it lies in.
const FLAT: Camera = Camera::new(
    View::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO),
    Projection::orthographic(1.0),
);

/// The side of the square target `center` readings are taken from.
const SIDE: u32 = 32;

/// A light and a tint dim enough that a highlight over them stays under
/// white rather than clipping.
const DIM: Color = Color::rgb(0.3, 0.3, 0.3);

/// The side of the target the bloom chain is read off, wide enough for
/// a few mips.
const WIDE: u32 = 64;

/// The color of the square behind.
const BEHIND: [u8; 4] = [u8::MAX, 0, 0, u8::MAX];

/// And of the one in front, wherever its texture has any opacity.
const IN_FRONT: [u8; 4] = [0, 0, u8::MAX, u8::MAX];

/// The side of the target a cast scene is read off, over [`ACROSS`]
/// meters of world.
const YARD: u32 = 64;

/// World the ground of a cast scene covers.
const ACROSS: f32 = 8.0;

meshes! { enum CubeSet { Cube } }
meshes! { enum CutSet { Cut } }
meshes! { enum GroundSet { Ground } }
meshes! { enum QuadSet { Quad } }

/// A configuration that leaves the light a frame drew where it is, so a
/// test can read exact colors back.
fn raw(title: &str) -> Config {
    Config::new(title).with_tonemap(Tonemap::Off)
}

/// One step of `game` read back off a target `SIDE` pixels square, or
/// `None` where the machine has no usable graphics adapter.
fn rendered<G: Game>(config: Config, game: G) -> Option<Vec<u8>> {
    sized(config, UVec2::splat(SIDE), game)
}

/// One step of `game` read back off a `size`-sized target.
fn sized<G: Game>(config: Config, size: UVec2, game: G) -> Option<Vec<u8>> {
    let mut session = started(config, size, |_ctx| Ok(game))?;

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

/// A session `init` starts, or `None` where this machine has no usable
/// graphics adapter to have started one.
///
/// Every other startup error fails the test that started it, so that a
/// test over what a session draws cannot pass by never drawing.
fn started<G: Game>(
    config: Config,
    size: UVec2,
    init: impl FnOnce(&mut InitContext<'_, G>) -> Result<G, Error>,
) -> Option<Session<G>> {
    match Session::new(config, size, init) {
        Ok(session) => Some(session),
        Err(error) if error.to_string().starts_with(NO_ADAPTER) => None,
        Err(error) => panic!("the session did not start: {error}"),
    }
}

/// The color that leaves in the middle of the target.
fn center<G: Game>(game: G) -> Option<[u8; 4]> {
    Some(middle(&rendered(raw("headless center"), game)?))
}

/// The pixel `x` across and `y` down a `SIDE`-sized target.
fn pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
    let at = ((y * SIDE + x) * 4) as usize;
    [pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
}

/// The pixel in the middle of a `SIDE`-sized target.
fn middle(pixels: &[u8]) -> [u8; 4] {
    pixel(pixels, SIDE / 2, SIDE / 2)
}

/// How many pixels of a target what it draws covers: the ones the frame's
/// own sky, which its corner holds, did not leave.
fn drawn_pixels(pixels: &[u8]) -> usize {
    let sky = &pixels[..4];

    pixels.chunks(4).filter(|pixel| *pixel != sky).count()
}

/// A square per entry filling most of the view, at its own depth along
/// `Z` and with its own material, drawn in that order; each is held a
/// margin short of the view's own edge, so the target's corner is
/// always the frame's own sky.
struct Panes {
    panes: Vec<(f32, Material)>,
}

impl Panes {
    fn new(panes: Vec<(f32, Material)>) -> Self {
        Self { panes }
    }
}

impl Game for Panes {
    type Meshes = QuadSet;
    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 * 2.0, Vec3::ZERO),
            Projection::orthographic(1.0),
        ));
        for &(depth, material) in &self.panes {
            ctx.draw(
                Quad.at(Transform::from_scale_rotation_translation(
                    Vec3::new(0.9, 0.9, 1.0),
                    Quat::IDENTITY,
                    Vec3::Z * depth,
                ))
                .material(material),
            );
        }
    }
}

/// The color `across` pixels along the target's middle row.
fn column(pixels: &[u8], across: u32) -> [u8; 4] {
    let at = ((SIDE / 2 * SIDE + across) * 4) as usize;
    [pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
}

/// A square with a hole in it, and one with none.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Cut {
    Holed,
    Solid,
}

impl Catalog for Cut {
    fn catalog() -> Vec<Self> {
        vec![Self::Holed, Self::Solid]
    }
}

impl Mesh for Cut {
    fn build(&self, assets: &Assets) -> MeshData {
        let square = Quad.build(assets);
        match self {
            // Four texels, so that neither half is read through the
            // blend across the two of them in the middle.
            Self::Holed => {
                let texels = [[0; 4], [0; 4], IN_FRONT, IN_FRONT];
                square.with_texture(TextureData::rgba8(UVec2::new(4, 1), texels.concat()))
            }
            Self::Solid => square,
        }
    }
}

/// Everything a cast scene draws.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Ground {
    Floor,
    Blocker,
}

impl Catalog for Ground {
    fn catalog() -> Vec<Self> {
        vec![Self::Floor, Self::Blocker]
    }
}

impl Mesh for Ground {
    fn build(&self, assets: &Assets) -> MeshData {
        match self {
            Self::Floor => Plane.build(assets),
            Self::Blocker => Cube.build(assets),
        }
    }
}

/// The pixel `x` across and `y` down a `WIDE`-sized target.
fn wide_pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
    let at = ((y * WIDE + x) * 4) as usize;
    [pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
}

mod assets;
mod cache;
mod culling;
#[cfg(feature = "ui")]
mod fonts;
mod image;
mod machines;
mod maps;
mod materials;
mod passes;
mod planes;
mod poses;
mod post_effects;
mod session;
mod shadows;
mod skyboxes;
mod sprites;
mod styles;
#[cfg(feature = "ui")]
mod text;