mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
use crate::Color;

/// The alpha a cutout material is drawn from; `forward.wgsl` drops what
/// lands under it.
pub(crate) const CUTOUT: f32 = 0.5;

/// A surface's shading: a tint, how strongly lights affect it, and the light
/// it adds of its own.
///
/// A tint alpha under `1.0` draws the surface in the transparent pass:
/// sorted back to front, blended over what is behind it, and never written
/// to depth.
///
/// Set as a slot's default, or per draw with
/// [`Instance::material`](crate::mesh::Instance::material).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Material {
    tint: Color,
    litness: f32,
    emissive: Color,
    roughness: f32,
    metallic: f32,
    cutout: bool,
    additive: bool,
}

impl Material {
    /// A flat color; lights do not affect it.
    pub const fn color(color: Color) -> Self {
        Self::new(color, 0.0)
    }

    /// A color fully lit by the frame's lights.
    pub const fn lit(color: Color) -> Self {
        Self::new(color, 1.0)
    }

    /// A color between flat and lit by `litness`, a fraction clamped to
    /// `0.0..=1.0`.
    pub fn shaded(color: Color, litness: f32) -> Self {
        Self::new(color, litness.clamp(0.0, 1.0))
    }

    /// Adds light of its own to the surface; [`Color::BLACK`] by default.
    ///
    /// Required if you want a surface bright on its own: values past `1.0`
    /// are what [`FrameContext::set_bloom`](crate::FrameContext::set_bloom)
    /// spreads. In the transparent pass the tint's alpha scales everything
    /// the surface draws, this light too — fade one or the other, not both.
    #[must_use]
    pub const fn emissive(mut self, color: Color) -> Self {
        self.emissive = color;
        self
    }

    /// The surface's roughness, a fraction clamped to `0.0..=1.0` and
    /// `1.0` by default: the factor a `.glb` material declares, held per
    /// draw.
    #[must_use]
    pub fn roughness(mut self, roughness: f32) -> Self {
        self.roughness = roughness.clamp(0.0, 1.0);
        self
    }

    /// The surface's metallic, a fraction clamped to `0.0..=1.0` and `0.0`
    /// by default: the factor a `.glb` material declares, held per draw.
    #[must_use]
    pub fn metallic(mut self, metallic: f32) -> Self {
        self.metallic = metallic.clamp(0.0, 1.0);
        self
    }

    /// Drops the texels where `tint × texture` alpha lands under `0.5`, and
    /// draws the rest as opaque.
    ///
    /// Required if you want a sprite drawn with no blending at its edges: a
    /// cutout draw writes depth and is not sorted. A tint alpha under `1.0`
    /// still draws it in the transparent pass, where the same texels are
    /// dropped.
    #[must_use]
    pub const fn cutout(mut self) -> Self {
        self.cutout = true;
        self
    }

    /// Adds what the draw would be to what is behind it, instead of drawing
    /// over it.
    ///
    /// Required if you want a draw that only ever adds light and never
    /// darkens what it covers: it is drawn after the transparent pass, in
    /// the order it was submitted, is never written to depth, and casts no
    /// shadow. The tint's alpha scales everything it adds — the
    /// [`emissive`](Material::emissive) light too: fade one or the other,
    /// not both. The texture's alpha scales each texel the same way: an
    /// empty texel adds nothing, and one half covered adds half of what it
    /// holds. A tint past `1.0` scales what the texture holds past it: the
    /// draw adds light in the shape and color of its own texture. A flat
    /// [`emissive`](Material::emissive) adds one color over every texel
    /// instead. A material that also set [`cutout`](Material::cutout) drops
    /// nothing.
    #[must_use]
    pub const fn additive(mut self) -> Self {
        self.additive = true;
        self
    }

    /// The color shading multiplies the lighting by.
    pub const fn tint(&self) -> Color {
        self.tint
    }

    /// The fraction of lit shading applied: `0.0` flat, `1.0` fully lit.
    pub const fn litness(&self) -> f32 {
        self.litness
    }

    /// The light the surface adds of its own.
    pub const fn emission(&self) -> Color {
        self.emissive
    }

    /// The surface's roughness, a fraction.
    pub const fn rough(&self) -> f32 {
        self.roughness
    }

    /// The surface's metallic, a fraction.
    pub const fn metal(&self) -> f32 {
        self.metallic
    }

    /// The same material at `factor` of its tint alpha, which
    /// [`Instance::faded`](crate::mesh::Instance::faded) scales a resolved
    /// slot by.
    pub(crate) fn faded(self, factor: f32) -> Self {
        Self {
            tint: self.tint.with_alpha(self.tint.alpha * factor),
            ..self
        }
    }

    /// Whether a draw of this material belongs in the transparent pass.
    pub(crate) fn translucent(&self) -> bool {
        self.tint.alpha < 1.0
    }

    /// Whether a draw of this material covers anything at all: a tint alpha
    /// of zero leaves nothing to blend over what is drawn, and nothing for a
    /// depth map to take.
    pub(crate) fn covers(&self) -> bool {
        self.tint.alpha > 0.0
    }

    /// Whether a draw of this material drops the texels its alpha leaves
    /// out.
    pub(crate) fn cuts(&self) -> bool {
        self.cutout
    }

    /// Whether a draw of this material adds to what is behind it rather
    /// than drawing over it, whatever its alpha and its cutout are set to.
    pub(crate) fn adds(&self) -> bool {
        self.additive
    }

    const fn new(tint: Color, litness: f32) -> Self {
        Self {
            tint,
            litness,
            emissive: Color::BLACK,
            roughness: 1.0,
            metallic: 0.0,
            cutout: false,
            additive: false,
        }
    }
}

impl Default for Material {
    /// Lit white.
    fn default() -> Self {
        Self::lit(Color::WHITE)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn only_the_materials_asked_to_drop_texels_do() {
        assert!(!Material::lit(Color::WHITE).cuts());
        assert!(Material::lit(Color::WHITE).cutout().cuts());
        assert!(
            Material::lit(Color::WHITE)
                .cutout()
                .emissive(Color::WHITE)
                .cuts(),
            "and keep it through the knobs that follow"
        );
    }

    #[test]
    fn only_the_materials_asked_to_add_what_they_draw_do() {
        assert!(!Material::lit(Color::WHITE).adds());
        assert!(Material::lit(Color::WHITE).additive().adds());
        assert!(
            Material::lit(Color::rgba(1.0, 1.0, 1.0, 0.25))
                .additive()
                .cutout()
                .adds(),
            "whatever the alpha and the cutout of the material around it"
        );
    }

    #[test]
    fn a_material_is_fully_rough_and_no_metal_until_it_is_asked_to_be_otherwise() {
        let white = Material::lit(Color::WHITE);

        assert_eq!(
            (white.rough(), white.metal()),
            (1.0, 0.0),
            "which is the surface that reflects only its base share of the \
             sky, blurred to one color, at every angle"
        );
        assert_eq!(white.roughness(0.25).rough(), 0.25);
        assert_eq!(white.metallic(0.25).metal(), 0.25);
        assert_eq!(
            (white.roughness(4.0).rough(), white.metallic(4.0).metal()),
            (1.0, 1.0),
            "and neither reaches past the sharpest surface there is"
        );
        assert_eq!(
            (white.roughness(-1.0).rough(), white.metallic(-1.0).metal()),
            (0.0, 0.0)
        );
    }

    #[test]
    fn only_a_tint_alpha_under_one_draws_in_the_transparent_pass() {
        assert!(!Material::color(Color::WHITE).translucent());
        assert!(Material::color(Color::rgba(1.0, 1.0, 1.0, 0.999)).translucent());
        assert!(
            !Material::color(Color::rgba(1.0, 1.0, 1.0, f32::NAN)).translucent(),
            "an alpha that is not a number is not under one either"
        );
    }
}