mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! Three full-screen passes over a small lit scene: a `Vignette` that
//! darkens toward the frame's corners, a `Grain` whose grain is an
//! integer-hash of the tick count so it stays repeatable, and
//! `Scanlines` that darken every other pixel row. A slider each drives
//! its strength from `0.0` to `1.0`.

use mirage_engine::prelude::*;

/// Ground plane's side length, in meters.
const GROUND_SIZE: f32 = 10.0;

/// The glowing cube's position and edge length, in meters.
const GLOW_POSITION: Vec3 = Vec3::new(0.0, 0.6, 0.0);
const GLOW_SIZE: f32 = 0.9;
const GLOW_COLOR: Color = Color::rgb(4.0, 2.2, 0.6);

/// A sphere either side of the glowing cube, and its radius.
const SPHERE_POSITIONS: [Vec3; 2] = [Vec3::new(-1.6, 0.5, 0.4), Vec3::new(1.6, 0.5, -0.4)];
const SPHERE_SUBDIVISIONS: u32 = 3;

const SUN_DIRECTION: Vec3 = Vec3::new(0.5, -1.0, -0.3);
const SUN_COLOR: Color = Color::rgb(0.85, 0.8, 0.7);

const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.15);
const SPHERE_COLOR: Color = Color::rgb(0.5, 0.52, 0.55);

/// Bloom the frame draws at, so [`GLOW_COLOR`] past `1.0` scatters.
const SCENE_BLOOM: f32 = 0.5;

meshes! { enum Shape { Plane, Cube, Sphere } }

/// The values the WGSL `Vignette` reads: how far it darkens toward the
/// frame's corners.
#[derive(ShaderValues)]
struct Vignette {
    strength: f32,
}

impl PostEffect for Vignette {
    const STAGE: EffectStage = EffectStage::ToneMapped;
    const SHADER: &'static str = include_str!("post_effects_vignette.wgsl");
}

/// The values the WGSL `Grain` reads: how much grain it draws, and the
/// seed its integer-hash is drawn from.
#[derive(ShaderValues)]
struct Grain {
    strength: f32,
    seed: u32,
}

impl PostEffect for Grain {
    const STAGE: EffectStage = EffectStage::ToneMapped;
    const SHADER: &'static str = include_str!("post_effects_grain.wgsl");
}

/// The values the WGSL `Scanlines` reads: how far it darkens every other
/// pixel row.
#[derive(ShaderValues)]
struct Scanlines {
    strength: f32,
}

impl PostEffect for Scanlines {
    const STAGE: EffectStage = EffectStage::ToneMapped;
    const SHADER: &'static str = include_str!("post_effects_scanlines.wgsl");
}

post_effects! { enum Look { Vignette, Grain, Scanlines } }

struct PostEffectEffects {
    ticks: u32,
    vignette: f32,
    grain: f32,
    scanlines: f32,
}

impl PostEffectEffects {
    fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
        Ok(Self {
            ticks: 0,
            vignette: 0.5,
            grain: 0.08,
            scanlines: 0.3,
        })
    }

    /// The scene every frame draws: a sun over a ground plane, a glowing
    /// cube bloom scatters from, and a sphere either side of it.
    fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
        ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());

        ctx.draw(
            Plane
                .at(Transform::from_scale(Vec3::new(
                    GROUND_SIZE,
                    1.0,
                    GROUND_SIZE,
                )))
                .material(Material::lit(GROUND_COLOR)),
        );
        ctx.draw(
            Cube.at(Transform::from_scale_rotation_translation(
                Vec3::splat(GLOW_SIZE),
                Quat::IDENTITY,
                GLOW_POSITION,
            ))
            .material(Material::color(Color::BLACK).emissive(GLOW_COLOR)),
        );
        for position in SPHERE_POSITIONS {
            ctx.draw(
                Sphere {
                    subdivisions: SPHERE_SUBDIVISIONS,
                }
                .at(position)
                .material(Material::lit(SPHERE_COLOR)),
            );
        }
    }

    /// A slider per effect, and the effects themselves run at the values
    /// they hold — `Grain`'s seed is [`Self::ticks`], so the frame it
    /// draws stays repeatable.
    fn panel(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.ui(|ui| {
            ui.add(egui::Slider::new(&mut self.vignette, 0.0..=1.0).text("vignette"));
            ui.add(egui::Slider::new(&mut self.grain, 0.0..=1.0).text("grain"));
            ui.add(egui::Slider::new(&mut self.scanlines, 0.0..=1.0).text("scanlines"));
        });

        ctx.set_post_effect(Vignette {
            strength: self.vignette,
        });
        ctx.set_post_effect(Grain {
            strength: self.grain,
            seed: self.ticks,
        });
        ctx.set_post_effect(Scanlines {
            strength: self.scanlines,
        });
    }
}

impl Game for PostEffectEffects {
    type Meshes = Shape;
    type Sounds = NoSounds;
    type InputActions = Key;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = ();
    type PostEffects = Look;

    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {
        self.ticks += 1;
    }

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.set_camera(Camera::new(
            View::look_at(Vec3::new(0.0, 3.4, 4.6), Vec3::new(0.0, 0.2, 0.0)),
            Projection::perspective(45.0),
        ));
        ctx.set_bloom(SCENE_BLOOM);

        self.draw_scene(ctx);
        self.panel(ctx);
    }
}

fn main() {
    run(
        Config::new("Mirage: screen effects").with_size(1280, 720),
        PostEffectEffects::init,
    );
}