mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! Surface styles: WGSL of a game's own, compiled into the engine's
//! forward shader.
//!
//! A surface style is a type: its fields are the values its WGSL reads, its
//! [`PASS`](SurfaceStyle::PASS) is where its draws land in the frame, and
//! its [`SURFACE`](SurfaceStyle::SURFACE) and
//! [`DISPLACE`](SurfaceStyle::DISPLACE) are the WGSL it runs. Name it in
//! [`Game::SurfaceStyles`](crate::Game::SurfaceStyles) through
//! [`surface_styles!`](crate::surface_styles), draw with it through
//! [`Instance::surface_style`](crate::mesh::Instance::surface_style), and
//! pass it its values with
//! [`set_surface_style`](crate::FrameContext::set_surface_style).
//!
//! # What the engine declares
//!
//! A style's WGSL is compiled into the forward shader, which declares
//! what each of the two is passed:
//!
//! ```wgsl
//! struct Surface {
//!     color: vec4<f32>,     // the base color, tint and texel together
//!     normal: vec3<f32>,    // world space, unit length
//!     emissive: vec3<f32>,  // the light this surface adds of its own
//!     world: vec3<f32>,     // where the fragment is, in world space
//!     uv: vec2<f32>,        // held within the draw's own frame window
//! }
//!
//! struct Placed {
//!     world: vec3<f32>,     // where the transform placed this vertex
//!     normal: vec3<f32>,    // world space, unit length
//!     uv: vec2<f32>,        // the raw window, held within none
//!     local: vec3<f32>,     // where the mesh was built, in object space
//! }
//! ```
//!
//! The engine reads `color`, `normal` and `emissive` back from a
//! `Surface`, and nothing else: `world` and `uv` are there to read. It
//! lights by the `normal` as it is returned, so a style that turns one
//! keeps it unit length.
//!
//! # What a style declares
//!
//! Either of the two, or neither:
//!
//! ```wgsl
//! fn surface(surface: Surface) -> Surface;   // SURFACE, fragment stage
//! fn displace(placed: Placed) -> vec3<f32>;  // DISPLACE, vertex stage
//! ```
//!
//! `None` on either leaves the engine's own code there: a style with
//! neither compiles and draws exactly as the built-in look does, which is
//! what a style declared for its [`DrawPass`] alone is for. `displace`
//! returns how far to move the vertex in world space.
//!
//! A style's values are bound as `style`, in a WGSL struct named after the
//! Rust type. A style is [`Default`] because a frame that never passes it
//! values draws with the default value of every field — where a
//! [`PostEffect`](crate::PostEffect) is not, since an effect no frame
//! submits never runs at all.
//!
//! A styled draw lands in its style's own pass whatever its material
//! holds. In [`DrawPass::Translucent`] the `color.a` a style returns is
//! what the draw is blended over the frame by; in [`DrawPass::Cutout`] a
//! returned `color.a` under `0.5` drops the texel, whatever the draw's own
//! material declares; in [`DrawPass::Additive`] the draw is added and that
//! alpha is dropped.
//!
//! A style may declare whatever else it needs beside the two: its own
//! code, its own constant values, its own struct types. The forward shader
//! it is compiled into declares over `100` names of its own, and declaring
//! any of them again stops startup under the style's name; the ones a
//! style would reach for first are `Surface`, `Placed`,
//! `Fragment`, `surface_of`, `received`, `shading_of`, `scaled_by`,
//! `held_inside`, `placed`, `columns`, `cofactor`, `unit`, `lit`,
//! `shaded`, `dropped`, `styled`, `displaced`, `THRESHOLD`, `HALF`,
//! `frame`, `lights`, `base_color`, `shading`, `relief` and
//! `emissive_map`. Every name the frame's own sky declares starts with
//! `sky`, `sky_light` and `sky_reflection` among them.
//!
//! A game may name any number of styles, each one pipeline built at
//! startup.

use crate::shader_values::{Sealed, ShaderValues};

/// The engine's own shader, with [`SEAM`] where a style's code goes.
const FORWARD: &str = include_str!("renderer/forward.wgsl");

/// The line every stitch replaces.
const SEAM: &str = "// mirage-engine:style";

/// The bind group a style's values are read through.
pub(crate) const GROUP: u32 = 3;

/// The vertex seam: the vertex where the engine placed it, and where a
/// style's own code moves it to.
const PLACED: &str = "fn displaced(placed: Placed) -> vec3<f32> {\n    return placed.world;\n}\n";
const DISPLACED: &str =
    "fn displaced(placed: Placed) -> vec3<f32> {\n    return placed.world + displace(placed);\n}\n";

/// The fragment seam: the surface as the engine read it, and as a style's
/// own code paints it.
const READ: &str = "fn styled(base: Surface) -> Surface {\n    return base;\n}\n";
const SURFACED: &str = "fn styled(base: Surface) -> Surface {\n    return surface(base);\n}\n";

/// The pass a style's draws land in, and the way it draws them.
///
/// A styled draw follows its style's pass whatever its material declares; a
/// draw with no style is drawn in the pass its tint alpha and its cutout
/// resolve to. A
/// flagged light's depth maps take the opaque pass and no other.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DrawPass {
    /// Drawn first, in batch order, written to depth, and cast by a
    /// flagged light.
    Opaque,
    /// Drawn after those and into the same depth, dropping the texels its
    /// alpha leaves out.
    Cutout,
    /// Blended over both of those, back to front, leaving depth alone.
    Translucent,
    /// Added over all of them in the order they were submitted, leaving
    /// depth alone.
    Additive,
}

/// A look of a game's own, written in WGSL and stitched into the engine's
/// shader at startup.
///
/// Required if you want a surface the materials cannot draw: implement it
/// on a type whose fields are the values its WGSL reads, and name that type
/// in [`Game::SurfaceStyles`](crate::Game::SurfaceStyles). The engine keeps the instance
/// data, the lighting, the shadows and the curves that take the frame to
/// the screen; the two hooks below are where a style's own code runs.
pub trait SurfaceStyle: ShaderValues + Default {
    /// The pass this style's draws land in, whatever their material
    /// holds; see the module's own docs.
    const PASS: DrawPass;

    /// WGSL declaring `fn surface(surface: Surface) -> Surface`, run in
    /// the fragment stage before the surface is lit; `None` leaves the
    /// engine's own code there. `Surface` is the engine's, and the module's
    /// own docs state what each of its fields holds.
    ///
    /// `Surface` holds the base color as `color`, the world-space
    /// `normal`, the light the surface adds of its own as `emissive`, and
    /// the `world` position and `uv` it is read at. The engine draws with
    /// `color`, `normal` and `emissive`; `world` and `uv` are there only
    /// to read. A draw with a `Frame` of its own reads its texels at this
    /// `uv`, held within that window. The engine lights with the `normal`
    /// as it is returned, so
    /// a style that turns it keeps it unit length.
    const SURFACE: Option<&'static str> = None;

    /// WGSL declaring `fn displace(placed: Placed) -> vec3<f32>`, run in
    /// the vertex stage, which returns how far to move the vertex in world
    /// space; `None` leaves the vertex where the transform placed it.
    ///
    /// `Placed` holds the `world` position the transform placed the
    /// vertex at, its `normal`, its `uv`, and the `local` position the mesh
    /// was built with. A draw with a `Frame` of its own holds the raw
    /// `uv` of that window here; only `surface` reads one held within it.
    /// A light's depth maps take the vertex unmoved, so a
    /// displaced surface casts the shadow of where it was placed, and so
    /// does the sphere the camera holds the draw by: what this moves is
    /// the look, not the draw.
    const DISPLACE: Option<&'static str> = None;
}

/// The styles one game draws with, named together as
/// [`Game::SurfaceStyles`](crate::Game::SurfaceStyles).
///
/// Written by [`surface_styles!`](crate::surface_styles) and never
/// implemented by hand; `()` for a game that draws with the built-in look
/// alone.
pub trait SurfaceStyles: Sealed + 'static {
    /// The styles startup compiles, in the order the set lists them.
    #[doc(hidden)]
    fn declared(into: &mut Declarations);

    /// Seat of the style this set value holds: `0` for the first style the
    /// set names, one more for each after it.
    #[doc(hidden)]
    fn seat(&self) -> u32;

    /// Lays out the values of the style this set value holds.
    #[doc(hidden)]
    fn write(&self, into: &mut Vec<u8>);
}

/// The styles a set declares, in the order it lists them; the set macro
/// fills it and startup compiles what it holds.
#[doc(hidden)]
#[derive(Default)]
pub struct Declarations(Vec<Declaration>);

impl Declarations {
    /// Declares the style at the next seat.
    #[doc(hidden)]
    pub fn declare<S: SurfaceStyle>(&mut self) {
        self.0.push(Declaration::of::<S>());
    }

    /// Everything `S` declares, in seat order.
    pub(crate) fn of<S: SurfaceStyles>() -> Vec<Declaration> {
        let mut declared = Self::default();
        S::declared(&mut declared);
        declared.0
    }
}

/// One style as startup takes it: its own name, the pass its draws land
/// in, the shader it is drawn with, and the values it reads before a frame
/// writes any.
#[derive(Debug)]
pub(crate) struct Declaration {
    pub(crate) name: &'static str,
    pub(crate) pass: DrawPass,
    pub(crate) source: String,
    pub(crate) defaults: Vec<u8>,
}

impl Declaration {
    fn of<S: SurfaceStyle>() -> Self {
        let mut defaults = Vec::new();
        S::default().write(&mut defaults);
        Self {
            name: core::any::type_name::<S>(),
            pass: S::PASS,
            source: stitched(&S::bound(GROUP, "style"), S::SURFACE, S::DISPLACE),
            defaults,
        }
    }
}

/// Which of a game's styles this is, counted in the order its set lists
/// them.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct SurfaceStyleId(pub(crate) u32);

/// The style of a draw: which seat of the set it is, and where it
/// draws.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Styled {
    pub(crate) id: SurfaceStyleId,
    pub(crate) pass: DrawPass,
    /// Whether the style moves the corners it is drawn with, which takes the
    /// draw off the plane its mesh was built in.
    pub(crate) displaces: bool,
}

impl Styled {
    /// The style `T`, at the seat its set holds it at.
    pub(crate) fn at<T: SurfaceStyle>(at: SurfaceStyleId) -> Self {
        Self {
            id: at,
            pass: T::PASS,
            displaces: T::DISPLACE.is_some(),
        }
    }
}

impl SurfaceStyles for () {
    /// Nothing; a game with no styles compiles none.
    fn declared(_into: &mut Declarations) {}

    /// Not reachable: no call that sets a style compiles for a game with
    /// none.
    fn seat(&self) -> u32 {
        0
    }

    /// Not reachable: no call that writes a style's values compiles for a
    /// game with none.
    fn write(&self, _into: &mut Vec<u8>) {}
}

/// Writes the set of every style a game draws with: an enum with one
/// variant wrapping each named type, which
/// [`Game::SurfaceStyles`](crate::Game::SurfaceStyles) names.
///
/// Each type is spelled by its own name and must be in scope; a set that
/// holds a type twice does not compile. Seats run in the order the set
/// lists them, and a draw's style is its seat. A `pub` before `enum` makes
/// the enum public, and `///` lines before that are the enum's.
///
/// ```
/// use mirage_engine::prelude::*;
///
/// #[derive(Default, ShaderValues)]
/// struct Water {
///     wave: f32,
/// }
///
/// impl SurfaceStyle for Water {
///     const PASS: DrawPass = DrawPass::Translucent;
/// }
///
/// surface_styles! { enum Looks { Water } }
/// ```
#[macro_export]
macro_rules! surface_styles {
    ($(#[$attribute:meta])* $vis:vis enum $set:ident { $($style:ident),+ $(,)? }) => {
        $(#[$attribute])*
        $vis enum $set {
            $($style($style)),+
        }

        $(
            impl ::core::convert::From<$style> for $set {
                fn from(style: $style) -> Self {
                    Self::$style(style)
                }
            }

            impl $crate::Holds<$style> for $set {}
        )+

        impl $crate::Sealed for $set {}

        impl $crate::SurfaceStyles for $set {
            fn declared(into: &mut $crate::SurfaceStyleDeclarations) {
                $(into.declare::<$style>();)+
            }

            fn seat(&self) -> u32 {
                let mut at = 0;
                $(
                    if ::core::matches!(self, Self::$style(_)) {
                        return at;
                    }
                    at += 1;
                )+
                at
            }

            fn write(&self, into: &mut ::std::vec::Vec<u8>) {
                match self {
                    $(Self::$style(values) => $crate::ShaderValues::write(values, into)),+
                }
            }
        }
    };
}

/// The shader every draw with no style of its own is drawn with: the
/// engine's own, with both seams left where they were.
pub(crate) fn built_in() -> String {
    stitched("", None, None)
}

/// The shader one style is drawn with: the engine's own, with the style's
/// values declared and its code called where each seam marks.
fn stitched(values: &str, surface: Option<&str>, displace: Option<&str>) -> String {
    let mut code = String::from(values);
    for hook in [displace, surface].into_iter().flatten() {
        code.push_str(hook);
        code.push('\n');
    }
    code.push_str(match displace {
        Some(_) => DISPLACED,
        None => PLACED,
    });
    code.push_str(match surface {
        Some(_) => SURFACED,
        None => READ,
    });

    FORWARD.replace(SEAM, &code)
}

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

    /// A style with both hooks and values of its own, written the way a
    /// game writes one.
    #[derive(Default, crate::ShaderValues)]
    struct Water {
        height: f32,
        tint: Color,
    }

    impl SurfaceStyle for Water {
        const PASS: DrawPass = DrawPass::Translucent;
        const SURFACE: Option<&'static str> =
            Some("fn surface(s: Surface) -> Surface { return s; }");
        const DISPLACE: Option<&'static str> =
            Some("fn displace(p: Placed) -> vec3<f32> { return vec3<f32>(0.0); }");
    }

    /// A style that reads no values, which is what a unit struct is for.
    #[derive(Default, crate::ShaderValues)]
    struct Toon;

    impl SurfaceStyle for Toon {
        const PASS: DrawPass = DrawPass::Opaque;
        const SURFACE: Option<&'static str> =
            Some("fn surface(s: Surface) -> Surface { return s; }");
    }

    surface_styles! { enum Looks { Water, Toon } }

    #[test]
    fn the_shader_carries_one_seam_for_a_style_to_be_stitched_into() {
        assert_eq!(FORWARD.matches(SEAM).count(), 1);
    }

    #[test]
    fn a_frame_with_no_style_is_drawn_with_the_seams_left_where_they_were() {
        let source = built_in();

        assert!(!source.contains(SEAM), "the seam itself is replaced");
        assert!(source.contains(PLACED) && source.contains(READ));
        assert!(
            !source.contains("@group(3)"),
            "and nothing of a style is bound"
        );
    }

    #[test]
    fn a_styles_own_code_is_called_from_the_seams_and_its_values_are_bound() {
        let source = Declaration::of::<Water>().source;

        assert!(source.contains(DISPLACED) && source.contains(SURFACED));
        assert!(
            source.contains("fn surface(s: Surface)") && source.contains("fn displace(p: Placed)"),
            "the style's own code is stitched in whole"
        );
        assert!(source.contains("struct Water"));
        assert!(source.contains("@group(3) @binding(0) var<uniform> style: Water;"));
        assert!(
            source.find("struct Water") < source.find("fn surface(s: Surface)"),
            "and the values are declared before the code that reads them"
        );
    }

    #[test]
    fn a_style_with_no_fields_reads_no_values_and_binds_none() {
        let declared = Declaration::of::<Toon>();

        assert!(!declared.source.contains("@group(3)"));
        assert!(declared.source.contains(PLACED), "and it moves no vertex");
        assert!(declared.defaults.is_empty());
    }

    #[test]
    fn a_set_declares_its_styles_in_the_order_it_names_them() {
        let declared = Declarations::of::<Looks>();

        assert_eq!(
            declared.iter().map(|style| style.pass).collect::<Vec<_>>(),
            vec![DrawPass::Translucent, DrawPass::Opaque]
        );
        assert_eq!(
            (
                Looks::from(Water::default()).seat(),
                Looks::from(Toon).seat()
            ),
            (0, 1)
        );
        assert!(Declarations::of::<()>().is_empty());
    }

    #[test]
    fn a_set_value_lays_out_the_values_of_the_style_it_holds() {
        let mut written = Vec::new();
        Looks::from(Water {
            height: 1.5,
            tint: Color::WHITE,
        })
        .write(&mut written);

        assert_eq!(
            f32::from_le_bytes(written[..4].try_into().expect("four bytes")),
            1.5
        );
        assert_eq!(written.len(), 32, "and pads to the block the shader reads");

        let mut none = Vec::new();
        Looks::from(Toon).write(&mut none);
        assert!(none.is_empty(), "where a style reads nothing");
    }

    #[test]
    fn a_styles_defaults_are_its_own_default_value_laid_out() {
        #[derive(crate::ShaderValues)]
        struct Deep {
            height: f32,
        }

        impl Default for Deep {
            fn default() -> Self {
                Self { height: 3.0 }
            }
        }

        impl SurfaceStyle for Deep {
            const PASS: DrawPass = DrawPass::Opaque;
        }

        let mut written = Vec::new();
        Deep::default().write(&mut written);

        assert_eq!(Declaration::of::<Deep>().defaults, written);
        assert_eq!(
            f32::from_le_bytes(written[..4].try_into().expect("four bytes")),
            3.0,
            "which is not the zero a blank buffer would read"
        );
    }
}