Skip to main content

post_effects/
post-effects.rs

1//! Three full-screen passes over a small lit scene: a `Vignette` that
2//! darkens toward the frame's corners, a `Grain` whose grain is an
3//! integer-hash of the tick count so it stays repeatable, and
4//! `Scanlines` that darken every other pixel row. A slider each drives
5//! its strength from `0.0` to `1.0`.
6
7use mirage_engine::prelude::*;
8
9/// Ground plane's side length, in meters.
10const GROUND_SIZE: f32 = 10.0;
11
12/// The glowing cube's position and edge length, in meters.
13const GLOW_POSITION: Vec3 = Vec3::new(0.0, 0.6, 0.0);
14const GLOW_SIZE: f32 = 0.9;
15const GLOW_COLOR: Color = Color::rgb(4.0, 2.2, 0.6);
16
17/// A sphere either side of the glowing cube, and its radius.
18const SPHERE_POSITIONS: [Vec3; 2] = [Vec3::new(-1.6, 0.5, 0.4), Vec3::new(1.6, 0.5, -0.4)];
19const SPHERE_SUBDIVISIONS: u32 = 3;
20
21const SUN_DIRECTION: Vec3 = Vec3::new(0.5, -1.0, -0.3);
22const SUN_COLOR: Color = Color::rgb(0.85, 0.8, 0.7);
23
24const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.15);
25const SPHERE_COLOR: Color = Color::rgb(0.5, 0.52, 0.55);
26
27/// Bloom the frame draws at, so [`GLOW_COLOR`] past `1.0` scatters.
28const SCENE_BLOOM: f32 = 0.5;
29
30meshes! { enum Shape { Plane, Cube, Sphere } }
31
32/// The values the WGSL `Vignette` reads: how far it darkens toward the
33/// frame's corners.
34#[derive(ShaderValues)]
35struct Vignette {
36    strength: f32,
37}
38
39impl PostEffect for Vignette {
40    const STAGE: EffectStage = EffectStage::ToneMapped;
41    const SHADER: &'static str = include_str!("post_effects_vignette.wgsl");
42}
43
44/// The values the WGSL `Grain` reads: how much grain it draws, and the
45/// seed its integer-hash is drawn from.
46#[derive(ShaderValues)]
47struct Grain {
48    strength: f32,
49    seed: u32,
50}
51
52impl PostEffect for Grain {
53    const STAGE: EffectStage = EffectStage::ToneMapped;
54    const SHADER: &'static str = include_str!("post_effects_grain.wgsl");
55}
56
57/// The values the WGSL `Scanlines` reads: how far it darkens every other
58/// pixel row.
59#[derive(ShaderValues)]
60struct Scanlines {
61    strength: f32,
62}
63
64impl PostEffect for Scanlines {
65    const STAGE: EffectStage = EffectStage::ToneMapped;
66    const SHADER: &'static str = include_str!("post_effects_scanlines.wgsl");
67}
68
69post_effects! { enum Look { Vignette, Grain, Scanlines } }
70
71struct PostEffectEffects {
72    ticks: u32,
73    vignette: f32,
74    grain: f32,
75    scanlines: f32,
76}
77
78impl PostEffectEffects {
79    fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
80        Ok(Self {
81            ticks: 0,
82            vignette: 0.5,
83            grain: 0.08,
84            scanlines: 0.3,
85        })
86    }
87
88    /// The scene every frame draws: a sun over a ground plane, a glowing
89    /// cube bloom scatters from, and a sphere either side of it.
90    fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
91        ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
92
93        ctx.draw(
94            Plane
95                .at(Transform::from_scale(Vec3::new(
96                    GROUND_SIZE,
97                    1.0,
98                    GROUND_SIZE,
99                )))
100                .material(Material::lit(GROUND_COLOR)),
101        );
102        ctx.draw(
103            Cube.at(Transform::from_scale_rotation_translation(
104                Vec3::splat(GLOW_SIZE),
105                Quat::IDENTITY,
106                GLOW_POSITION,
107            ))
108            .material(Material::color(Color::BLACK).emissive(GLOW_COLOR)),
109        );
110        for position in SPHERE_POSITIONS {
111            ctx.draw(
112                Sphere {
113                    subdivisions: SPHERE_SUBDIVISIONS,
114                }
115                .at(position)
116                .material(Material::lit(SPHERE_COLOR)),
117            );
118        }
119    }
120
121    /// A slider per effect, and the effects themselves run at the values
122    /// they hold — `Grain`'s seed is [`Self::ticks`], so the frame it
123    /// draws stays repeatable.
124    fn panel(&mut self, ctx: &mut FrameContext<'_, Self>) {
125        ctx.ui(|ui| {
126            ui.add(egui::Slider::new(&mut self.vignette, 0.0..=1.0).text("vignette"));
127            ui.add(egui::Slider::new(&mut self.grain, 0.0..=1.0).text("grain"));
128            ui.add(egui::Slider::new(&mut self.scanlines, 0.0..=1.0).text("scanlines"));
129        });
130
131        ctx.set_post_effect(Vignette {
132            strength: self.vignette,
133        });
134        ctx.set_post_effect(Grain {
135            strength: self.grain,
136            seed: self.ticks,
137        });
138        ctx.set_post_effect(Scanlines {
139            strength: self.scanlines,
140        });
141    }
142}
143
144impl Game for PostEffectEffects {
145    type Meshes = Shape;
146    type Sounds = NoSounds;
147    type InputActions = Key;
148    type Skyboxes = NoSkyboxes;
149    type SurfaceStyles = ();
150    type PostEffects = Look;
151
152    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {
153        self.ticks += 1;
154    }
155
156    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
157        ctx.set_camera(Camera::new(
158            View::look_at(Vec3::new(0.0, 3.4, 4.6), Vec3::new(0.0, 0.2, 0.0)),
159            Projection::perspective(45.0),
160        ));
161        ctx.set_bloom(SCENE_BLOOM);
162
163        self.draw_scene(ctx);
164        self.panel(ctx);
165    }
166}
167
168fn main() {
169    run(
170        Config::new("Mirage: screen effects").with_size(1280, 720),
171        PostEffectEffects::init,
172    );
173}