mirage-engine 0.1.0

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
//! What draws lying in one plane leave over one another.

use super::*;

/// The colors a hedge in one plane is drawn in: the ones
/// submitted first, then the one submitted after them.
const EARLIER: Color = Color::rgb(1.0, 0.0, 0.0);
const LATER: Color = Color::rgb(0.0, 0.0, 1.0);

/// The target a hedge is read off, and how far down the view the plane
/// its squares lie in is.
const HEDGE_WIDE: u32 = 320;
const HEDGE_TALL: u32 = 180;
const HEDGE_AT: f32 = 10.4;

/// One square of a hedge at full size, in meters, and how far apart along
/// the plane the row places them.
const HEDGE_WIDTH: f32 = 2.0;
const HEDGE_HEIGHT: f32 = 1.9375;
const HEDGE_STEP: f32 = 1.3866667;

/// The ground's side length in a scene of squares lying flat, in
/// meters: small enough that anything one of them casts past its own
/// edges lands on a pixel.
const PAVED_ACROSS: f32 = 2.0;

/// How far off the plane a marker is drawn, in meters.
const RESTING: f32 = 0.01;

/// How far it extends across, in meters.
const MARKER_ACROSS: f32 = 2.0;

/// The color it is drawn in.
const MARKER: Color = Color::rgb(0.0, 1.0, 0.0);

/// How many squares one plane holds where a test draws more of them
/// than a game would.
const PAVED_DEEP: usize = 200;

/// Upright squares on the ground in one plane, each at its own
/// size, its own place along the plane and with its own material, in the
/// order the frame submits them. The viewpoint places them all in the one
/// plane through where they are drawn, so only that order separates them;
/// their sizes differ, so each one computes that plane through arithmetic
/// of its own.
struct Hedge(Vec<(f32, f32, Material)>);

impl Game for Hedge {
    type Meshes = QuadSet;
    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::new(0.0, 9.0, 19.0), Vec3::new(0.0, 0.0, 8.0)),
            Projection::perspective(45.0),
        ));
        for &(scale, along, material) in &self.0 {
            let height = HEDGE_HEIGHT * scale;
            ctx.draw(
                Quad.at(Transform::from_scale_rotation_translation(
                    Vec3::new(HEDGE_WIDTH * scale, height, 1.0),
                    Quat::IDENTITY,
                    Vec3::new(along, height * 0.5, HEDGE_AT),
                ))
                .upright()
                .material(material),
            );
        }
    }
}

impl Hedge {
    /// A row of `count` squares in one plane, every other one of
    /// them smaller as a hedge of sprites is, the last drawn in `LATER`
    /// and every earlier one in `EARLIER`.
    fn row(count: usize) -> Self {
        Self(
            (0..count)
                .map(|step| {
                    let scale = if step % 2 == 0 { 1.0 } else { 0.8 };
                    let color = if step + 1 == count { LATER } else { EARLIER };
                    let along = -HEDGE_AT + step as f32 * HEDGE_STEP;
                    (scale, along, Material::color(color).cutout())
                })
                .collect(),
        )
    }

    /// The last square of that row on its own, which is what it covers
    /// with nothing to take its pixels.
    fn last_of(count: usize) -> Self {
        Self(Self::row(count).0.split_off(count - 1))
    }
}

/// One step of `hedge`, read back with no curve and one sample per pixel,
/// so a color reads back as it was drawn.
fn hedged(hedge: Hedge) -> Option<Vec<u8>> {
    let config = raw("headless coplanar").with_antialiasing(false);
    sized(config, UVec2::new(HEDGE_WIDE, HEDGE_TALL), hedge)
}

/// Where `pixels` is more blue than red and is not `background`, which is
/// where the last square of a hedge was drawn — the background itself
/// may read bluer than red, so a texel matching it is not a draw.
fn bluer(pixels: &[u8], background: [u8; 4]) -> Vec<usize> {
    (0..pixels.len())
        .step_by(4)
        .filter(|&at| {
            let texel = [pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]];
            texel != background && pixels[at + 2] > pixels[at]
        })
        .collect()
}

#[test]
fn coplanar_cutout_draws_resolve_in_the_order_they_were_submitted() {
    const ROW: usize = 13;

    let Some(alone) = hedged(Hedge::last_of(ROW)) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(pixels) = hedged(Hedge::row(ROW)) else {
        return;
    };
    let Some(again) = hedged(Hedge::row(ROW)) else {
        return;
    };

    // The target's own corner, which no square of the hedge reaches.
    let background = [alone[0], alone[1], alone[2], alone[3]];
    let reached = bluer(&alone, background);
    assert!(!reached.is_empty(), "the last square is drawn");
    let lost = reached
        .iter()
        .filter(|at| pixels[**at + 2] <= pixels[**at])
        .count();
    assert!(
        (0..pixels.len())
            .step_by(4)
            .any(|at| pixels[at] > pixels[at + 2]),
        "and the squares before it are drawn where it leaves room"
    );
    assert_eq!(
        lost,
        0,
        "the last draw takes every pixel it reaches, texel by texel to \
         none of the ones before it: {lost} of {} went to them",
        reached.len()
    );
    assert_eq!(pixels, again, "and takes them again, frame after frame");
}

#[test]
fn a_coplanar_draw_that_writes_no_depth_still_draws_over_the_ones_before_it() {
    // A square of the plane before the post, so the post writes the
    // plane's depth and the flame has a depth to clear.
    let beside = (1.0, -4.0, Material::color(EARLIER).cutout());
    let post = (1.0, 0.0, Material::color(EARLIER).cutout());
    let flame = (0.5, 0.0, Material::color(LATER).additive());

    let Some(alone) = hedged(Hedge(vec![beside, post])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(pixels) = hedged(Hedge(vec![beside, post, flame])) else {
        return;
    };

    let over = (0..alone.len())
        .step_by(4)
        .filter(|at| alone[*at] == u8::MAX)
        .any(|at| pixels[at + 2] > alone[at + 2]);
    assert!(
        over,
        "an additive draw tests a depth it never writes, so only the depth \
         of the plane it lies in clears what the post wrote"
    );
}

/// Squares lying flat in one world plane, each at its own size, place and
/// material, in the order the frame submits them. Each square's own
/// transform lays it in that plane, and their sizes differ, so each one
/// computes that plane through arithmetic of its own.
struct Paving {
    squares: Vec<(f32, Vec3, Material)>,
    camera: Camera,
    /// The light the squares are drawn under, absent where they are
    /// drawn under none.
    sun: Option<Light>,
    /// Whether a marker is drawn on the plane, before the squares.
    marker: bool,
}

impl Game for Paving {
    type Meshes = GroundSet;
    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(self.camera);
        if let Some(sun) = self.sun {
            ctx.light(sun);
        }
        if self.marker {
            ctx.draw(
                Ground::Blocker
                    .at(Transform::from_scale_rotation_translation(
                        Vec3::new(MARKER_ACROSS, RESTING, MARKER_ACROSS),
                        Quat::IDENTITY,
                        Vec3::Y * RESTING * 0.5,
                    ))
                    .material(Material::color(MARKER)),
            );
        }
        for &(across, at, material) in &self.squares {
            ctx.draw(
                Ground::Floor
                    .at(Transform::from_scale_rotation_translation(
                        Vec3::splat(across),
                        Quat::IDENTITY,
                        at,
                    ))
                    .material(material),
            );
        }
    }
}

impl Paving {
    /// The squares seen down a view well off their plane, so
    /// that each pixel of one is at a depth of its own.
    fn tilted(squares: Vec<(f32, Vec3, Material)>) -> Self {
        Self {
            squares,
            camera: Camera::new(
                View::look_at(Vec3::new(0.0, 6.0, 9.0), Vec3::ZERO),
                Projection::perspective(45.0),
            ),
            sun: None,
            marker: false,
        }
    }

    /// The same, with a marker on the plane the squares lie in.
    fn resting(squares: Vec<(f32, Vec3, Material)>) -> Self {
        Self {
            marker: true,
            ..Self::tilted(squares)
        }
    }

    /// The same from straight above, under a sun that casts sideways, so
    /// that anything a square casts past its own edges is drawn beside
    /// it.
    fn shone(squares: Vec<(f32, Vec3, Material)>) -> Self {
        Self {
            squares,
            camera: Camera::new(
                View::look_at(Vec3::Y * 10.0, Vec3::ZERO).with_up(Vec3::NEG_Z),
                Projection::orthographic(PAVED_ACROSS),
            ),
            sun: Some(Light::directional(Vec3::new(1.0, -1.0, 0.0), Color::WHITE).shadow()),
            marker: false,
        }
    }
}

/// One step of `paving`, read back with no curve and one sample per
/// pixel, so a color reads back as it was drawn.
fn paved(paving: Paving) -> Option<Vec<u8>> {
    let config = raw("headless flat")
        .with_antialiasing(false)
        .with_shadow_resolution(512);
    sized(config, UVec2::new(HEDGE_WIDE, HEDGE_TALL), paving)
}

#[test]
fn draws_lying_in_one_world_plane_resolve_in_the_order_they_were_submitted() {
    let ground = (6.0, Vec3::ZERO, Material::color(EARLIER));
    let tile = (2.0, Vec3::new(0.6, 0.0, 0.9), Material::color(LATER));

    let Some(alone) = paved(Paving::tilted(vec![tile])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(pixels) = paved(Paving::tilted(vec![ground, tile])) else {
        return;
    };

    // The target's own corner, which the one tile never reaches.
    let background = [alone[0], alone[1], alone[2], alone[3]];
    let reached = bluer(&alone, background);
    assert!(!reached.is_empty(), "the later square is drawn");
    let lost = reached
        .iter()
        .filter(|at| pixels[**at + 2] <= pixels[**at])
        .count();
    assert!(
        (0..pixels.len())
            .step_by(4)
            .any(|at| pixels[at] > pixels[at + 2]),
        "and the square before it is drawn where it leaves room"
    );
    assert_eq!(
        lost,
        0,
        "the later draw takes every pixel it reaches, texel by texel to \
         none of the ones before it: {lost} of {} went to them",
        reached.len()
    );
}

/// Where `pixels` is more green than red, which is where a marker
/// on a plane was drawn.
fn greener(pixels: &[u8]) -> Vec<usize> {
    (0..pixels.len())
        .step_by(4)
        .filter(|at| pixels[*at + 1] > pixels[*at])
        .collect()
}

#[test]
fn a_marker_resting_on_a_plane_stays_in_front_of_every_draw_lying_in_it() {
    let squares = (0..PAVED_DEEP)
        .map(|step| {
            let last = step + 1 == PAVED_DEEP;
            let across = if last { 8.0 } else { [6.0, 4.0, 2.0][step % 3] };
            let color = if last { LATER } else { EARLIER };
            (across, Vec3::ZERO, Material::color(color))
        })
        .collect::<Vec<_>>();
    let widest = vec![squares[PAVED_DEEP - 1]];

    let Some(alone) = paved(Paving::tilted(widest)) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(standing) = paved(Paving::resting(Vec::new())) else {
        return;
    };
    let Some(pixels) = paved(Paving::resting(squares)) else {
        return;
    };

    // The target's own corner of a scene with no square in it at all,
    // which the widest square drawn alone may otherwise fill.
    let background = [standing[0], standing[1], standing[2], standing[3]];
    let marked = greener(&standing);
    let reached = bluer(&alone, background);
    assert!(!marked.is_empty() && !reached.is_empty(), "both are drawn");
    let swallowed = marked
        .iter()
        .filter(|at| pixels[**at + 1] <= pixels[**at])
        .count();
    let covered = |at: usize| standing[at + 1] > standing[at];
    let lost = reached
        .iter()
        .filter(|at| !covered(**at) && pixels[**at + 2] <= pixels[**at])
        .count();

    assert_eq!(
        swallowed,
        0,
        "a marker resting {RESTING} meters off the plane is drawn over \
         every one of {PAVED_DEEP} draws of it: {swallowed} of {} pixels \
         went to them",
        marked.len()
    );
    assert_eq!(
        lost,
        0,
        "and the last of those draws takes every pixel the marker leaves: \
         {lost} of {} went to the draws before it",
        reached.len()
    );
}

#[test]
fn a_square_lying_in_the_ground_plane_casts_nothing_past_its_own_edges() {
    let ground = (PAVED_ACROSS, Vec3::ZERO, Material::lit(Color::WHITE));
    let lying = (0.5, Vec3::ZERO, Material::lit(EARLIER));

    let Some(clear) = paved(Paving::shone(vec![ground])) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(pixels) = paved(Paving::shone(vec![ground, lying])) else {
        return;
    };
    let Some(alone) = paved(Paving::shone(vec![lying])) else {
        return;
    };

    let changed = |at: usize| pixels[at..at + 4] != clear[at..at + 4];
    let covered = |at: usize| alone[at..at + 3] != [0, 0, 0];
    let over = (0..clear.len())
        .step_by(4)
        .filter(|&at| changed(at))
        .count();
    let past = (0..clear.len())
        .step_by(4)
        .filter(|&at| changed(at) && !covered(at))
        .count();

    assert!(over > 0, "the square draws over the ground it lies in");
    assert_eq!(
        past, 0,
        "and the ground beside it reads the light it read with no square \
         there: {past} pixels of {over} changed past the square's own"
    );
}