mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a slot's shading, emissive and relief maps change texel by texel.

use super::*;

/// The size of a map split across the square below.
const SPLIT: UVec2 = UVec2::new(2, 1);

/// The texel that scales no lane and casts the material's light whole.
const WHOLE: [u8; 4] = [u8::MAX; 4];

/// A normal channel half a right angle off the middle, and the middle
/// itself, which is no turn at all along that axis.
const TURNED: u8 = 218;
const STRAIGHT: u8 = 128;

/// The four lights the tests below draw that square under, each half a
/// right angle off the view: one over it, one under it, one on the
/// camera's own right and one on its left.
const FROM_ABOVE: Vec3 = Vec3::new(0.0, -1.0, -1.0);
const FROM_BELOW: Vec3 = Vec3::new(0.0, 1.0, -1.0);
const FROM_THE_RIGHT: Vec3 = Vec3::new(-1.0, 0.0, -1.0);
const FROM_THE_LEFT: Vec3 = Vec3::new(1.0, 0.0, -1.0);

meshes! { enum MappedSet { Mapped } }
meshes! { enum PerTexelSet { PerTexel } }

/// A shading map whose left half scales nothing and whose right half is
/// `right`.
fn shading(right: [u8; 4]) -> ShadingData {
    ShadingData::rgba8(SPLIT, [WHOLE, right].concat())
}

/// A square whose slot holds one map beside its color, split across the
/// square's two halves; its color is one white texel, read from the
/// nearest, so each half reads its own texel of that map alone.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum PerTexel {
    /// No map at all: both halves draw with the material's own lanes.
    Bare,
    /// The right half at no roughness.
    Roughness,
    /// The right half casting no light of its own.
    Emissive,
}

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

impl Mesh for PerTexel {
    fn build(&self, assets: &Assets) -> MeshData {
        let square = Quad
            .build(assets)
            .with_texture(TextureData::rgba8(UVec2::ONE, WHOLE.to_vec()).pixelated());
        match self {
            Self::Bare => square,
            Self::Roughness => square.with_shading(shading([u8::MAX, 0, u8::MAX, u8::MAX])),
            Self::Emissive => square.with_emissive_map(TextureData::rgba8(
                SPLIT,
                [WHOLE, [0, 0, 0, u8::MAX]].concat(),
            )),
        }
    }
}

/// That square filling the view, drawn with the material the test chooses,
/// with a light straight at it where the test sets one, and whatever
/// bloom it set.
struct Split {
    mesh: PerTexel,
    material: Material,
    lit: bool,
    bloom: f32,
}

impl Split {
    /// The square drawn in the dark, which every test below lights its
    /// own way.
    fn dark(mesh: PerTexel, material: Material) -> Self {
        Self {
            mesh,
            material,
            lit: false,
            bloom: 0.0,
        }
    }
}

impl Game for Split {
    type Meshes = PerTexelSet;
    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.set_bloom(self.bloom);
        // Replaces the default environment's light, with one of the
        // test's own where it sets one.
        let color = if self.lit { DIM } else { Color::BLACK };
        ctx.light(Light::directional(Vec3::NEG_Z, color));
        ctx.draw(self.mesh.at(Vec3::ZERO).material(self.material));
    }
}

/// One step of that square, read back off a `WIDE`-sized target.
fn split(square: Split) -> Option<Vec<u8>> {
    sized(raw("headless per-texel"), UVec2::splat(WIDE), square)
}

/// The color in the middle of the square's `half`: the half the map's
/// first texel covers, then the half its second covers.
fn half(pixels: &[u8], half: u32) -> [u8; 4] {
    wide_pixel(pixels, WIDE / 4 + half * WIDE / 2, WIDE / 2)
}

#[test]
fn a_shading_maps_roughness_splits_one_slots_highlight() {
    let sheen = |mesh| {
        split(Split {
            lit: true,
            ..Split::dark(mesh, Material::lit(DIM))
        })
    };
    let Some(mapped) = sheen(PerTexel::Roughness) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(bare) = sheen(PerTexel::Bare) else {
        return;
    };

    assert_eq!(
        half(&mapped, 0),
        half(&bare, 0),
        "the half the map leaves whole draws at the material's own roughness"
    );
    assert!(
        half(&mapped, 1)[0] > half(&bare, 1)[0],
        "and the half it sharpens takes a highlight the other half has none of, got {:?} \
         against {:?}",
        half(&mapped, 1),
        half(&bare, 1)
    );
}

#[test]
fn an_emissive_map_casts_the_materials_light_in_its_own_shape_and_blooms_in_it() {
    let glowing = |bloom| {
        split(Split {
            bloom,
            ..Split::dark(
                PerTexel::Emissive,
                Material::lit(Color::BLACK).emissive(Color::rgb(1.0, 0.0, 0.0)),
            )
        })
    };
    let Some(plain) = glowing(0.0) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(bloomed) = glowing(0.5) else {
        return;
    };

    let dropped = half(&plain, 1);

    assert_eq!(
        half(&plain, 0),
        [u8::MAX, dropped[1], dropped[2], u8::MAX],
        "the half the map keeps casts the material's own light over what the \
         surface reflects of the sky"
    );
    assert!(
        dropped[0] > 0 && dropped[0] == dropped[1] && dropped[1] == dropped[2],
        "and the half it drops casts none, leaving one grey reflection of the \
         sky, got {dropped:?}"
    );
    assert!(
        half(&bloomed, 1)[0] > 0,
        "the chain spreads that light into the half that cast none"
    );
    assert!(
        half(&bloomed, 1)[0] < half(&bloomed, 0)[0],
        "which stays darker than the half it came from, got {:?} against {:?}",
        half(&bloomed, 1),
        half(&bloomed, 0)
    );
}

/// A square whose slot holds a relief of one texel: one facing
/// straight out of the surface, one whose normal points up the map, and
/// one whose normal points across it.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
enum Mapped {
    Flat,
    Up,
    Across,
}

impl Catalog for Mapped {
    fn catalog() -> Vec<Self> {
        vec![Self::Flat, Self::Up, Self::Across]
    }
}

impl Mesh for Mapped {
    fn build(&self, assets: &Assets) -> MeshData {
        // A whole alpha byte no draw reads: these declare no depth.
        let normals =
            |across, up, out| ReliefData::normals(UVec2::ONE, vec![across, up, out, u8::MAX]);
        Quad.build(assets).with_relief(match self {
            Self::Flat => normals(STRAIGHT, STRAIGHT, u8::MAX),
            Self::Up => normals(STRAIGHT, TURNED, TURNED),
            Self::Across => normals(TURNED, STRAIGHT, TURNED),
        })
    }
}

/// That square across the view, lit by one sun, drawn where its own
/// transform places it or turned to the camera.
struct Relit {
    mesh: Mapped,
    sun: Vec3,
    faced: bool,
}

impl Game for Relit {
    type Meshes = MappedSet;
    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.light(Light::directional(self.sun, Color::WHITE));
        let drawn = self
            .mesh
            .at(Vec3::ZERO)
            .material(Material::lit(Color::WHITE));
        ctx.draw(if self.faced { drawn.upright() } else { drawn });
    }
}

#[test]
fn a_placed_draws_relief_turns_the_light_its_texels_take() {
    let lit = |mesh, sun| {
        center(Relit {
            mesh,
            sun,
            faced: false,
        })
    };
    let Some(above) = lit(Mapped::Up, FROM_ABOVE) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let (Some(below), Some(flat_above), Some(flat_below)) = (
        lit(Mapped::Up, FROM_BELOW),
        lit(Mapped::Flat, FROM_ABOVE),
        lit(Mapped::Flat, FROM_BELOW),
    ) else {
        return;
    };

    assert!(
        flat_above[0].abs_diff(flat_below[0]) <= 1,
        "a texel facing straight out of the square takes the two suns alike, got \
         {flat_above:?} against {flat_below:?}"
    );
    assert!(
        above[0] > flat_above[0],
        "one turned up takes more of the sun over the view, got {above:?} against \
         {flat_above:?}"
    );
    assert!(
        below[0] < flat_below[0],
        "and less of the one under it, got {below:?} against {flat_below:?}"
    );
}

#[test]
fn a_relief_texel_turned_up_the_map_takes_more_of_a_sun_over_it_than_a_flat_one() {
    let lit = |mesh, faced| {
        center(Relit {
            mesh,
            sun: FROM_ABOVE,
            faced,
        })
    };
    let Some(placed) = lit(Mapped::Up, false) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let (Some(placed_flat), Some(faced), Some(faced_flat)) = (
        lit(Mapped::Flat, false),
        lit(Mapped::Up, true),
        lit(Mapped::Flat, true),
    ) else {
        return;
    };

    assert!(
        placed[0] > placed_flat[0],
        "the derivative basis reads the map's own up, got {placed:?} against {placed_flat:?}"
    );
    assert!(
        faced[0] > faced_flat[0],
        "and so do the axes the camera turned, got {faced:?} against {faced_flat:?}"
    );
}

#[test]
fn a_relief_texel_turned_across_the_map_takes_more_of_a_sun_on_the_cameras_right_than_its_left() {
    let lit = |sun, faced| {
        center(Relit {
            mesh: Mapped::Across,
            sun,
            faced,
        })
    };
    let Some(faced_right) = lit(FROM_THE_RIGHT, true) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let (Some(faced_left), Some(placed_right), Some(placed_left)) = (
        lit(FROM_THE_LEFT, true),
        lit(FROM_THE_RIGHT, false),
        lit(FROM_THE_LEFT, false),
    ) else {
        return;
    };

    assert!(
        faced_right[0] > faced_left[0],
        "the axes the camera turned read the map's own across, got {faced_right:?} against \
         {faced_left:?}"
    );
    assert!(
        placed_right[0] > placed_left[0],
        "and so does the derivative basis, got {placed_right:?} against {placed_left:?}"
    );
}