Skip to main content

material_playground/
material-playground.rs

1//! One scene, every knob live: `Sky` pairs a whole scene's sky with its
2//! sun. `Dawn`, `Noon`, `Dusk` and `Night` each pair a gradient with a
3//! sun of its own color and direction; six loaded images each pair with
4//! a sun that fits it, from a bright sky's own bright sun to two space
5//! images lit by none; `Default` is the engine's own grey sky and white
6//! sun. The lamp and the spotlight add their own light beside it, and a
7//! material and the post chain read from sliders too.
8//!
9//! Hold the right mouse button and drag to look around, `W`/`A`/`S`/`D`
10//! to move along the view and to its side, `Space`/`Left Shift` up and
11//! down, and the wheel to scale how far each move goes; the `eye` stays
12//! above the ground plane wherever it moves.
13//!
14//! The ground draws the shading, relief and emissive map pairs, left
15//! plain and right checked on or off, the front sphere whose material a
16//! set of sliders resolves new each frame, a row where each sphere's own
17//! roughness rises so what the sky reflects reads apart across it, a
18//! cloth displaced by a wave and drawn from both its sides, and a
19//! pulsing sphere beside three pillars a light can shadow. The controls
20//! stay in a side area of fixed width, not the bare UI layer, so the
21//! flat grey default sky does not read as a slab under a row that spans
22//! the whole window.
23
24use mirage_engine::prelude::*;
25
26/// Ground plane's side length, in meters.
27const GROUND_SIZE: f32 = 18.0;
28
29/// A pair's two halves are drawn this far either side of its row's center,
30/// in meters.
31const PAIR_HALF_SPACING: f32 = 1.0;
32
33/// Depth, along `z`, of each map pair.
34const SHADING_Z: f32 = 3.4;
35const RELIEF_Z: f32 = 1.8;
36const EMISSIVE_Z: f32 = 0.2;
37
38/// A sphere's radius, so its center is a radius above the ground.
39const SPHERE_RADIUS: f32 = 0.5;
40/// An emissive cube's edge length.
41const CUBE_SIZE: f32 = 0.85;
42
43/// The front sphere's position and scale, ahead of every pair and larger,
44/// so its live material reads apart from them.
45const FRONT_POSITION: Vec3 = Vec3::new(0.0, 0.7, -1.6);
46const FRONT_SCALE: f32 = 1.4;
47
48/// Sphere density every built sphere in this scene shares.
49const SPHERE_SUBDIVISIONS: u32 = 3;
50
51/// How many draws the reflection row makes, and the meters between their
52/// centers.
53const REFLECT_ROW_COUNT: usize = 5;
54const REFLECT_ROW_SPACING: f32 = 1.5;
55const REFLECT_ROW_Z: f32 = -1.6;
56const REFLECT_ROW_RADIUS: f32 = 0.55;
57
58const GROUND_COLOR: Color = Color::rgb(0.24, 0.25, 0.22);
59const SHADING_TINT: Color = Color::rgb(0.55, 0.55, 0.6);
60const RELIEF_TINT: Color = Color::rgb(0.5, 0.45, 0.35);
61const EMISSIVE_BASE: Color = Color::rgb(0.04, 0.04, 0.05);
62const EMISSIVE_GLOW: Color = Color::rgb(3.2, 2.2, 0.7);
63
64/// Texel side length of every generated map: coarse enough that each
65/// checker cell reads as a distinct part on a sphere or a cube face.
66const MAP_SIZE: UVec2 = UVec2::new(64, 64);
67
68/// Checker cell width, in texels, for the shading map.
69const SHADING_CELL: u32 = 8;
70/// The shading checker's two states: occlusion, roughness and metallic —
71/// one square low across all three, the other full across all three.
72const SHADING_LOW: [u8; 3] = [70, 40, 15];
73const SHADING_HIGH: [u8; 3] = [255, 225, 235];
74
75/// Checker cell width, in texels, for the emissive map.
76const EMISSIVE_CELL: u32 = 6;
77
78/// Wave count the relief's texture repeats across its map, and the peak
79/// slope of its surface, in height over distance.
80const BUMP_WAVES: f32 = 6.0;
81const BUMP_SLOPE: f32 = 1.15;
82
83/// `BannerCloth`'s width and height, in meters.
84const BANNER_WIDTH: f32 = 1.1;
85const BANNER_HEIGHT: f32 = 0.7;
86
87/// Columns `BannerCloth` splits into, so its wave curves smoothly.
88const BANNER_COLUMNS: u32 = 10;
89
90/// Where the pillars, the pole, the cloth and the pulsing sphere are
91/// placed, added to every one of their own positions: apart from the
92/// pairs and the reflection row, so a light's shadow has clear ground to
93/// land on.
94const OUTPOST: Vec3 = Vec3::new(-4.6, 0.0, -0.8);
95
96const PILLARS: [(Vec3, Vec3); 3] = [
97    (Vec3::new(-1.8, 0.6, -0.6), Vec3::new(0.6, 1.2, 0.6)),
98    (Vec3::new(0.4, 0.4, -1.6), Vec3::new(0.5, 0.8, 0.5)),
99    (Vec3::new(1.7, 0.9, 0.4), Vec3::new(0.55, 1.8, 0.55)),
100];
101
102const POLE_POSITION: Vec3 = Vec3::new(-2.6, 1.0, 0.4);
103const POLE_SCALE: Vec3 = Vec3::new(0.12, 2.0, 0.12);
104const BANNER_MOUNT: Vec3 = Vec3::new(-2.54, 1.55, 0.46);
105
106const FIELD_ORB_POSITION: Vec3 = Vec3::new(1.3, 1.1, 1.6);
107const FIELD_ORB_SCALE: f32 = 0.7;
108
109/// The lamp's fixed position and reach, in meters.
110const LAMP_POSITION: Vec3 = Vec3::new(2.4, 1.4, -3.0);
111const LAMP_RANGE: f32 = 5.0;
112
113/// The spotlight's fixed placement: where it is placed, which way its
114/// cone points, its reach in meters and its width in radians.
115const SPOT_POSITION: Vec3 = Vec3::new(-3.4, 3.0, 1.6);
116const SPOT_DIRECTION: Vec3 = Vec3::new(0.55, -1.0, -1.0);
117const SPOT_RANGE: f32 = 7.0;
118const SPOT_ANGLE: f32 = 0.5;
119
120/// The camera's vertical field of view, in degrees.
121const CAMERA_FOV: f32 = 46.0;
122/// Where the camera starts, and the `yaw` and the pitch, in radians, it
123/// starts turned to.
124const START_EYE: Vec3 = Vec3::new(-0.6, 2.2, 9.0);
125const START_YAW: f32 = 0.0;
126const START_PITCH: f32 = -0.15;
127/// How far short of straight up or down the pitch may turn, in radians,
128/// where a turn alone reads as nothing.
129const PITCH_LIMIT: f32 = 1.5;
130/// The `eye`'s least height above the ground plane, in meters: held above
131/// zero so a move can never take it below.
132const MIN_EYE_HEIGHT: f32 = 0.3;
133/// Radians the pointer's own motion turns the view by, per physical
134/// pixel it crosses, before the axis it reads through bounds it: a drag
135/// across the whole window turns about a quarter turn.
136const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
137/// Meters a move key covers per second, at [`Playground::speed_scale`]'s
138/// own default.
139const MOVE_SPEED: f32 = 4.0;
140/// The factor one full wheel step multiplies the move speed apart from.
141const SPEED_STEP: f32 = 1.5;
142/// The move speed's own least and most, as a factor of [`MOVE_SPEED`].
143const MIN_SPEED_SCALE: f32 = 0.2;
144const MAX_SPEED_SCALE: f32 = 5.0;
145
146/// Bloom this scene starts at, past the engine's own default: enough that
147/// [`EMISSIVE_GLOW`] and the brightest lights scatter right away.
148const START_BLOOM: f32 = 0.25;
149const START_EXPOSURE: f32 = 1.0;
150
151/// The sky a frame that keeps [`Sky::Default`] draws and is lit by: the
152/// same flat grey the engine falls back to when a frame sets none.
153const DEFAULT_SKY: Color = Color::rgb(0.1, 0.1, 0.1);
154
155/// The fraction of its own light each loaded sky lands and reflects,
156/// through [`SkyboxData::lit_by`]: the bright images fixed low, since an
157/// image read too bright under the frame's own lights at its default
158/// `1.0`; the dim images fixed more, since the scene read too dark under
159/// them at the bright images' value.
160const CLEAR_SKY_LIGHT: f32 = 0.35;
161const CLASSIC_SKY_LIGHT: f32 = 0.35;
162const DAWN_SKY_LIGHT: f32 = 0.3;
163const SINISTER_SKY_LIGHT: f32 = 0.6;
164const LIGHT_BLUE_STARS_LIGHT: f32 = 0.8;
165const BLUE_STARS_LIGHT: f32 = 0.8;
166
167/// The shading pair's plain half: a sphere given the shared shading
168/// material and no map.
169#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
170struct ShadingPlain;
171
172impl Mesh for ShadingPlain {
173    fn build(&self, assets: &Assets) -> MeshData {
174        sphere_with_material(assets, shading_material())
175    }
176}
177
178/// The shading pair's mapped half: the same sphere and material, with its
179/// shading map (occlusion, roughness and metallic) baked in.
180#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
181struct ShadingMapped;
182
183impl Mesh for ShadingMapped {
184    fn build(&self, assets: &Assets) -> MeshData {
185        sphere_with_material(assets, shading_material()).with_shading(shading_checker())
186    }
187}
188
189/// The relief pair's plain half.
190#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
191struct ReliefPlain;
192
193impl Mesh for ReliefPlain {
194    fn build(&self, assets: &Assets) -> MeshData {
195        sphere_with_material(assets, relief_material())
196    }
197}
198
199/// The relief pair's mapped half: the same sphere and material, with its
200/// relief map baked in.
201#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
202struct ReliefMapped;
203
204impl Mesh for ReliefMapped {
205    fn build(&self, assets: &Assets) -> MeshData {
206        sphere_with_material(assets, relief_material()).with_relief(relief_bumps())
207    }
208}
209
210/// The emissive pair's plain half.
211#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
212struct EmissivePlain;
213
214impl Mesh for EmissivePlain {
215    fn build(&self, assets: &Assets) -> MeshData {
216        cube_with_material(assets, emissive_material())
217    }
218}
219
220/// The emissive pair's mapped half: the same cube and material, with its
221/// emissive map baked in.
222#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
223struct EmissiveMapped;
224
225impl Mesh for EmissiveMapped {
226    fn build(&self, assets: &Assets) -> MeshData {
227        cube_with_material(assets, emissive_material()).with_emissive_map(emissive_checker())
228    }
229}
230
231/// The front sphere: a draw overrides its material new every frame, in
232/// place of a baked one.
233#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
234struct Front;
235
236impl Mesh for Front {
237    fn build(&self, assets: &Assets) -> MeshData {
238        Sphere {
239            subdivisions: SPHERE_SUBDIVISIONS,
240        }
241        .build(assets)
242    }
243}
244
245/// `BannerCloth`: a mesh split into columns along its span and placed at
246/// its `x = 0` edge, so `Banner`'s wave curves it, not a single flat
247/// quad. Its triangles are built twice: once as authored and once in the
248/// other order, with the normal turned around, so the cloth draws from
249/// both sides however its wave curves it.
250#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
251struct BannerCloth;
252
253impl Mesh for BannerCloth {
254    fn build(&self, _: &Assets) -> MeshData {
255        banner_mesh()
256    }
257}
258
259// Everything this game draws: the ground plane, each map pair's plain and
260// mapped half, the front sphere with its own live material, the built-in
261// primitives the reflection row and the pillars beside it place per draw,
262// and the displaced cloth.
263meshes! {
264    enum Shape {
265        Plane,
266        Sphere,
267        Cube,
268        ShadingPlain,
269        ShadingMapped,
270        ReliefPlain,
271        ReliefMapped,
272        EmissivePlain,
273        EmissiveMapped,
274        Front,
275        BannerCloth,
276    }
277}
278
279fn sphere_with_material(assets: &Assets, material: Material) -> MeshData {
280    Sphere {
281        subdivisions: SPHERE_SUBDIVISIONS,
282    }
283    .build(assets)
284    .with_material(material)
285}
286
287fn cube_with_material(assets: &Assets, material: Material) -> MeshData {
288    Cube.build(assets).with_material(material)
289}
290
291fn shading_material() -> Material {
292    Material::lit(SHADING_TINT).roughness(0.5).metallic(0.5)
293}
294
295fn relief_material() -> Material {
296    Material::lit(RELIEF_TINT).roughness(0.35)
297}
298
299fn emissive_material() -> Material {
300    Material::color(EMISSIVE_BASE).emissive(EMISSIVE_GLOW)
301}
302
303/// A shading map whose checker goes between low occlusion, roughness and
304/// metallic and full occlusion, roughness and metallic, so all three read
305/// apart across [`ShadingMapped`].
306fn shading_checker() -> ShadingData {
307    ShadingData::rgba8(
308        MAP_SIZE,
309        checker_pixels(MAP_SIZE, SHADING_CELL, SHADING_LOW, SHADING_HIGH),
310    )
311}
312
313/// An emissive map whose checker goes between full glow and none, so
314/// [`EMISSIVE_GLOW`] shapes across [`EmissiveMapped`] instead of casting
315/// whole.
316fn emissive_checker() -> TextureData {
317    TextureData::rgba8(
318        MAP_SIZE,
319        checker_pixels(MAP_SIZE, EMISSIVE_CELL, [0, 0, 0], [255, 255, 255]),
320    )
321}
322
323fn checker_pixels(size: UVec2, cell: u32, low: [u8; 3], high: [u8; 3]) -> Vec<u8> {
324    let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
325    for y in 0..size.y {
326        for x in 0..size.x {
327            let on = ((x / cell) + (y / cell)).is_multiple_of(2);
328            let [red, green, blue] = if on { high } else { low };
329            pixels.extend_from_slice(&[red, green, blue, u8::MAX]);
330        }
331    }
332    pixels
333}
334
335/// A relief whose normals turn across a wave that repeats over the map:
336/// each texel's slope comes from the partial derivatives of a
337/// `sin(u) * sin(v)` height field at `BUMP_SLOPE`'s peak, computed at that
338/// texel and not sampled from any other.
339fn relief_bumps() -> ReliefData {
340    let size = MAP_SIZE;
341    let turns = core::f32::consts::TAU * BUMP_WAVES;
342    let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
343    for y in 0..size.y {
344        for x in 0..size.x {
345            let u = (x as f32 + 0.5) / size.x as f32;
346            let v = (y as f32 + 0.5) / size.y as f32;
347            let slope_u = BUMP_SLOPE * (turns * u).cos() * (turns * v).sin();
348            let slope_v = BUMP_SLOPE * (turns * u).sin() * (turns * v).cos();
349            let normal = Vec3::new(-slope_u, -slope_v, 1.0).normalize();
350            let encode = |signed: f32| ((signed * 0.5 + 0.5) * 255.0).round() as u8;
351            pixels.extend_from_slice(&[encode(normal.x), encode(normal.y), encode(normal.z), 0]);
352        }
353    }
354    ReliefData::normals(size, pixels)
355}
356
357/// `BannerCloth`'s vertices and indices, built twice over: the columns as
358/// authored, facing `+Z`, and the same columns again facing `-Z`, their
359/// triangles in the other order so both draw front side out.
360fn banner_mesh() -> MeshData {
361    let mut vertices = Vec::with_capacity(((BANNER_COLUMNS + 1) * 4) as usize);
362    for normal in [Vec3::Z, Vec3::NEG_Z] {
363        for column in 0..=BANNER_COLUMNS {
364            let u = column as f32 / BANNER_COLUMNS as f32;
365            let x = u * BANNER_WIDTH;
366            for v in [0.0, 1.0] {
367                vertices.push(Vertex::new(
368                    Vec3::new(x, -v * BANNER_HEIGHT, 0.0),
369                    normal,
370                    Vec2::new(u, v),
371                ));
372            }
373        }
374    }
375
376    let side = BANNER_COLUMNS + 1;
377    let mut indices = Vec::with_capacity((BANNER_COLUMNS * 12) as usize);
378    for column in 0..BANNER_COLUMNS {
379        let top_left = column * 2;
380        let bottom_left = top_left + 1;
381        let top_right = top_left + 2;
382        let bottom_right = top_left + 3;
383        indices.extend([
384            bottom_left,
385            bottom_right,
386            top_right,
387            bottom_left,
388            top_right,
389            top_left,
390        ]);
391
392        let back = side * 2;
393        indices.extend([
394            back + top_right,
395            back + bottom_right,
396            back + bottom_left,
397            back + top_left,
398            back + top_right,
399            back + bottom_left,
400        ]);
401    }
402
403    MeshData::new(vertices, indices)
404}
405
406/// Displaced by a wave that grows away from its `x = 0` edge; casts the
407/// shadow of where it was placed, unmoved by its own wave. Its one value
408/// is the clock its wave slides on.
409#[derive(Default, ShaderValues)]
410struct Banner {
411    time: f32,
412}
413
414impl SurfaceStyle for Banner {
415    const PASS: DrawPass = DrawPass::Opaque;
416    const DISPLACE: Option<&'static str> = Some(include_str!("material_playground_banner.wgsl"));
417}
418
419/// A surface that reads no light of the scene's own: it draws its own
420/// pulsing tint, added over what is behind it, through the color it pulses
421/// through and the clock the pulse is timed by.
422#[derive(Default, ShaderValues)]
423struct Field {
424    tint: Color,
425    time: f32,
426}
427
428impl SurfaceStyle for Field {
429    const PASS: DrawPass = DrawPass::Additive;
430    const SURFACE: Option<&'static str> = Some(include_str!("material_playground_field.wgsl"));
431}
432
433surface_styles! { enum Looks { Banner, Field } }
434
435/// A whole scene lighting choice: it names a sky and, kept with it, the
436/// sun that lights the scene, so a choice cannot leave the two apart.
437/// `Dawn`, `Noon`, `Dusk` and `Night` each pair a gradient with a sun of
438/// its own color and direction; `Clear`, `Classic`, `ImageDawn` and
439/// `Sinister` each pair a loaded image with a sun that fits it, and
440/// `LightBlueStars` and `BlueStars` pair a loaded space image with none;
441/// `Default` is the engine's own grey sky and white sun.
442///
443/// [`Skyboxes`] proves every value at startup, so it must be [`Eq`] and
444/// [`Hash`] over a fixed [`Skyboxes::catalog`] — a sky and sun a player
445/// set to any color and direction live could never meet, since `f32` is
446/// neither. This fixed, named set is the shape this file chose in its
447/// place: the side area offers it as one row, and shows the chosen sky's
448/// own light and its sun's own strength as text, read only, rather than
449/// controls a game could not build from. See this example's report for
450/// what that choice costs.
451#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
452enum Sky {
453    Dawn,
454    Noon,
455    Dusk,
456    Night,
457    Clear,
458    Classic,
459    ImageDawn,
460    Sinister,
461    LightBlueStars,
462    BlueStars,
463    Default,
464}
465
466impl Sky {
467    const ALL: [Sky; 11] = [
468        Self::Dawn,
469        Self::Noon,
470        Self::Dusk,
471        Self::Night,
472        Self::Clear,
473        Self::Classic,
474        Self::ImageDawn,
475        Self::Sinister,
476        Self::LightBlueStars,
477        Self::BlueStars,
478        Self::Default,
479    ];
480
481    fn name(self) -> &'static str {
482        match self {
483            Self::Dawn => "dawn",
484            Self::Noon => "noon",
485            Self::Dusk => "dusk",
486            Self::Night => "night",
487            Self::Clear => "clear day",
488            Self::Classic => "classic",
489            Self::ImageDawn => "dawn image",
490            Self::Sinister => "sinister night",
491            Self::LightBlueStars => "light blue stars",
492            Self::BlueStars => "blue stars",
493            Self::Default => "default",
494        }
495    }
496
497    /// The fraction of its own light this sky lands and reflects, through
498    /// [`SkyboxData::lit_by`]: fixed per choice, so a bright one does not
499    /// read too bright, and a dark one does not read too dark, under the
500    /// frame's own lights.
501    fn light(self) -> f32 {
502        match self {
503            Self::Dawn => 0.4,
504            Self::Noon => 0.5,
505            Self::Dusk => 0.35,
506            Self::Night => 0.3,
507            Self::Clear => CLEAR_SKY_LIGHT,
508            Self::Classic => CLASSIC_SKY_LIGHT,
509            Self::ImageDawn => DAWN_SKY_LIGHT,
510            Self::Sinister => SINISTER_SKY_LIGHT,
511            Self::LightBlueStars => LIGHT_BLUE_STARS_LIGHT,
512            Self::BlueStars => BLUE_STARS_LIGHT,
513            Self::Default => 1.0,
514        }
515    }
516
517    /// The sun this choice pairs with its sky: direction, color and
518    /// strength resolved together, so a choice cannot leave them apart.
519    /// `None` for the two space images, which pair with no sun at all.
520    fn sun(self) -> Option<(Vec3, Color, f32)> {
521        match self {
522            Self::Dawn => Some((
523                Vec3::new(-1.0, -0.15, 0.05),
524                Color::rgb(1.0, 0.7, 0.45),
525                1.4,
526            )),
527            Self::Noon => Some((
528                Vec3::new(-0.15, -1.0, -0.1),
529                Color::rgb(1.0, 1.0, 0.98),
530                1.6,
531            )),
532            Self::Dusk => Some((
533                Vec3::new(1.0, -0.15, 0.05),
534                Color::rgb(1.0, 0.55, 0.25),
535                1.2,
536            )),
537            Self::Night => Some((
538                Vec3::new(-0.3, -0.7, -0.6),
539                Color::rgb(0.55, 0.65, 0.85),
540                0.15,
541            )),
542            Self::Clear => Some((
543                Vec3::new(-0.2, -1.0, -0.15),
544                Color::rgb(1.0, 0.98, 0.9),
545                1.5,
546            )),
547            Self::Classic => Some((
548                Vec3::new(-0.4, -0.9, -0.2),
549                Color::rgb(1.0, 0.95, 0.85),
550                1.3,
551            )),
552            Self::ImageDawn => Some((Vec3::new(-1.0, -0.2, 0.1), Color::rgb(1.0, 0.75, 0.5), 1.1)),
553            Self::Sinister => Some((Vec3::new(0.4, -0.5, -0.7), Color::rgb(0.4, 0.5, 0.75), 0.1)),
554            Self::LightBlueStars | Self::BlueStars => None,
555            Self::Default => Some((Vec3::new(-0.4, -1.0, -0.6), Color::WHITE, 1.0)),
556        }
557    }
558
559    /// The color the sky reads under the horizon, through
560    /// [`SkyboxData::with_ground`]: the floor as lit under this choice's own
561    /// sun and [`Self::light`], so it moves with them, not only with the
562    /// image. `None` for the gradient skies and `Default`, which need no
563    /// ground, and for the two space images, which hold space below the
564    /// horizon as well.
565    fn ground(self) -> Option<Color> {
566        match self {
567            Self::Clear => Some(Color::rgb(0.501, 0.517, 0.449)),
568            Self::Classic => Some(Color::rgb(0.420, 0.405, 0.379)),
569            Self::ImageDawn => Some(Color::rgb(0.073, 0.053, 0.032)),
570            Self::Sinister => Some(Color::rgb(0.012, 0.014, 0.020)),
571            Self::Dawn
572            | Self::Noon
573            | Self::Dusk
574            | Self::Night
575            | Self::LightBlueStars
576            | Self::BlueStars
577            | Self::Default => None,
578        }
579    }
580}
581
582impl Catalog for Sky {
583    fn catalog() -> Vec<Self> {
584        Self::ALL.to_vec()
585    }
586}
587
588impl Skyboxes for Sky {
589    fn build(&self, assets: &Assets) -> SkyboxData {
590        let sky = match self {
591            Self::Dawn => SkyboxData::gradient(
592                Color::rgb(0.55, 0.55, 0.75),
593                Color::rgb(0.95, 0.6, 0.35),
594                Color::rgb(0.12, 0.08, 0.06),
595            ),
596            Self::Noon => SkyboxData::gradient(
597                Color::rgb(0.2, 0.45, 0.85),
598                Color::rgb(0.75, 0.82, 0.9),
599                Color::rgb(0.3, 0.3, 0.28),
600            ),
601            Self::Dusk => SkyboxData::gradient(
602                Color::rgb(0.18, 0.1, 0.3),
603                Color::rgb(0.85, 0.35, 0.2),
604                Color::rgb(0.03, 0.02, 0.03),
605            ),
606            Self::Night => SkyboxData::gradient(
607                Color::rgb(0.02, 0.02, 0.06),
608                Color::rgb(0.05, 0.05, 0.1),
609                Color::rgb(0.0, 0.0, 0.0),
610            ),
611            Self::Clear => assets.skybox("sky-clear"),
612            Self::Classic => assets.skybox("sky-classic"),
613            Self::ImageDawn => assets.skybox("sky-dawn"),
614            Self::Sinister => assets.skybox("sky-sinister"),
615            Self::LightBlueStars => assets.skybox("sky-stars-lightblue"),
616            Self::BlueStars => assets.skybox("sky-stars-blue"),
617            Self::Default => SkyboxData::gradient(DEFAULT_SKY, DEFAULT_SKY, DEFAULT_SKY),
618        };
619        let sky = match self.ground() {
620            Some(ground) => sky.with_ground(ground),
621            None => sky,
622        };
623
624        sky.lit_by(self.light())
625    }
626}
627
628/// `color` scaled by `strength`, the value a [`Light`] reads.
629fn scaled(color: Color, strength: f32) -> Color {
630    Color::rgb(
631        color.red * strength,
632        color.green * strength,
633        color.blue * strength,
634    )
635}
636
637/// One light's color and strength, held apart from the position that
638/// names it, plus whether it casts.
639#[derive(Clone, Copy)]
640struct Glow {
641    color: Color,
642    strength: f32,
643    shadow: bool,
644}
645
646impl Glow {
647    /// `color` scaled by `strength`, the value a [`Light`] reads.
648    fn scaled(self) -> Color {
649        scaled(self.color, self.strength)
650    }
651}
652
653/// Every key and button this game reads apart from the UI: held, `Look`
654/// turns the camera by the pointer's own motion, `Forward`/`Back`/
655/// `Left`/`Right` move it along the view and to its side, and `Up`/
656/// `Down` move it along the world's own up.
657#[derive(InputButtonAction, Clone, Copy, PartialEq)]
658enum Move {
659    Forward,
660    Back,
661    Left,
662    Right,
663    Up,
664    Down,
665    Look,
666}
667
668impl InputButtonAction for Move {
669    fn bindings(&self) -> Vec<ButtonBinding> {
670        match self {
671            Self::Forward => vec![Key::W.into()],
672            Self::Back => vec![Key::S.into()],
673            Self::Left => vec![Key::A.into()],
674            Self::Right => vec![Key::D.into()],
675            Self::Up => vec![Key::Space.into()],
676            Self::Down => vec![Key::LeftShift.into()],
677            Self::Look => vec![MouseButton::Right.into()],
678        }
679    }
680}
681
682/// The pointer's own motion, read only while [`Move::Look`] is held.
683#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
684enum Turn {
685    Look,
686}
687
688impl InputAxis2Action for Turn {
689    fn bindings(&self) -> Vec<Axis2Binding> {
690        match self {
691            Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
692        }
693    }
694}
695
696/// How far the wheel moved this frame, read to scale the move speed.
697#[derive(InputAxisAction, Clone, Copy, PartialEq)]
698enum Speed {
699    Wheel,
700}
701
702impl InputAxisAction for Speed {
703    fn bindings(&self) -> Vec<AxisBinding> {
704        match self {
705            Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
706        }
707    }
708}
709
710struct Controls;
711
712impl InputActions for Controls {
713    type Button = Move;
714    type Axis = Speed;
715    type Axis2 = Turn;
716}
717
718struct Playground {
719    eye: Vec3,
720    yaw: f32,
721    pitch: f32,
722    speed_scale: f32,
723
724    sky: Sky,
725    sun_shadow: bool,
726
727    lamp: Glow,
728    spotlight: Glow,
729
730    front_tint: Color,
731    front_roughness: f32,
732    front_metallic: f32,
733    shading_map_on: bool,
734    relief_map_on: bool,
735    emissive_map_on: bool,
736
737    exposure: f32,
738    bloom: f32,
739}
740
741impl Playground {
742    fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
743        let _ = ctx;
744        Ok(Self {
745            eye: START_EYE,
746            yaw: START_YAW,
747            pitch: START_PITCH,
748            speed_scale: 1.0,
749
750            sky: Sky::Default,
751            sun_shadow: true,
752
753            lamp: Glow {
754                color: Color::rgb(0.9, 0.55, 0.3),
755                strength: 3.0,
756                shadow: false,
757            },
758            spotlight: Glow {
759                color: Color::rgb(0.4, 0.6, 1.0),
760                strength: 6.0,
761                shadow: true,
762            },
763
764            front_tint: Color::rgb(0.7, 0.25, 0.2),
765            front_roughness: 0.4,
766            front_metallic: 0.0,
767            shading_map_on: true,
768            relief_map_on: true,
769            emissive_map_on: true,
770
771            exposure: START_EXPOSURE,
772            bloom: START_BLOOM,
773        })
774    }
775
776    /// This frame's forward direction, from `yaw` (turning around the
777    /// world's own up) and `pitch` (turning up or down).
778    fn forward(&self) -> Vec3 {
779        Vec3::new(
780            -self.pitch.cos() * self.yaw.sin(),
781            self.pitch.sin(),
782            -self.pitch.cos() * self.yaw.cos(),
783        )
784    }
785
786    /// The camera this frame draws from: `eye` looking along `forward`.
787    fn camera(&self) -> Camera {
788        Camera::new(
789            View::look_at(self.eye, self.eye + self.forward()),
790            Projection::perspective(CAMERA_FOV),
791        )
792    }
793
794    /// A held `Move::Look` (the right mouse button) turns the camera by
795    /// the pointer's own motion, the same way it moves: dragging right
796    /// turns the view right and left turns it left, dragging down turns
797    /// it to look further down at the scene, dragging up back toward the
798    /// horizon. `W`/`A`/`S`/`D` move along the view and to its side,
799    /// `Space`/`Left Shift` up and down, and the wheel scales how far
800    /// each move goes. The `eye` is held above the ground plane wherever
801    /// it moves.
802    fn fly_camera(&mut self, ctx: &mut FrameContext<'_, Self>) {
803        if !ctx.ui_wants_pointer() && ctx.down(Move::Look) {
804            let look = ctx.axis2(Turn::Look);
805            self.yaw -= look.x;
806            self.pitch = (self.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
807        }
808
809        let wheel = ctx.axis(Speed::Wheel);
810        if !ctx.ui_wants_pointer() && wheel != 0.0 {
811            self.speed_scale =
812                (self.speed_scale * SPEED_STEP.powf(wheel)).clamp(MIN_SPEED_SCALE, MAX_SPEED_SCALE);
813        }
814
815        let forward = self.forward();
816        let right = Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin());
817        let mut move_by = Vec3::ZERO;
818        if ctx.down(Move::Forward) {
819            move_by += forward;
820        }
821        if ctx.down(Move::Back) {
822            move_by -= forward;
823        }
824        if ctx.down(Move::Right) {
825            move_by += right;
826        }
827        if ctx.down(Move::Left) {
828            move_by -= right;
829        }
830        if ctx.down(Move::Up) {
831            move_by += Vec3::Y;
832        }
833        if ctx.down(Move::Down) {
834            move_by -= Vec3::Y;
835        }
836        if move_by.length_squared() > 1.0 {
837            move_by = move_by.normalize();
838        }
839
840        self.eye += move_by * MOVE_SPEED * self.speed_scale * ctx.dt().as_secs_f32();
841        self.eye.y = self.eye.y.max(MIN_EYE_HEIGHT);
842    }
843
844    /// The material [`Front`] draws with, resolved new from its sliders
845    /// every frame — the override [`Instance::material`] takes, in place
846    /// of a baked one.
847    fn front_material(&self) -> Material {
848        Material::lit(self.front_tint)
849            .roughness(self.front_roughness)
850            .metallic(self.front_metallic)
851    }
852
853    /// Every draw this game makes: the ground, each map pair, the front
854    /// sphere, the reflection row and the pillars beside it.
855    fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
856        ctx.draw(
857            Plane
858                .at(Transform::from_scale(Vec3::new(
859                    GROUND_SIZE,
860                    1.0,
861                    GROUND_SIZE,
862                )))
863                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
864        );
865
866        Self::draw_pair(
867            ctx,
868            SHADING_Z,
869            SPHERE_RADIUS,
870            ShadingPlain.at(Vec3::ZERO).into_set(),
871            ShadingMapped.at(Vec3::ZERO).into_set(),
872            self.shading_map_on,
873        );
874        Self::draw_pair(
875            ctx,
876            RELIEF_Z,
877            SPHERE_RADIUS,
878            ReliefPlain.at(Vec3::ZERO).into_set(),
879            ReliefMapped.at(Vec3::ZERO).into_set(),
880            self.relief_map_on,
881        );
882        Self::draw_pair(
883            ctx,
884            EMISSIVE_Z,
885            CUBE_SIZE / 2.0,
886            EmissivePlain.at(Vec3::ZERO).into_set(),
887            EmissiveMapped.at(Vec3::ZERO).into_set(),
888            self.emissive_map_on,
889        );
890
891        ctx.draw(
892            Front
893                .at(Transform::from_scale_rotation_translation(
894                    Vec3::splat(FRONT_SCALE),
895                    Quat::IDENTITY,
896                    FRONT_POSITION,
897                ))
898                .material(self.front_material()),
899        );
900
901        self.draw_reflect_row(ctx);
902        self.draw_outpost(ctx);
903    }
904
905    /// One pair at depth `z`, its centers `height` above the ground: `plain`
906    /// on the left always, and on the right `mapped` where `mapped_on` is
907    /// set, `plain` again where it is not — the same position drawing the
908    /// same base material with and without the map.
909    fn draw_pair(
910        ctx: &mut FrameContext<'_, Self>,
911        z: f32,
912        height: f32,
913        plain: Instance<Shape, Looks>,
914        mapped: Instance<Shape, Looks>,
915        mapped_on: bool,
916    ) {
917        ctx.draw(plain.clone().at(Vec3::new(-PAIR_HALF_SPACING, height, z)));
918        let right = if mapped_on { mapped } else { plain };
919        ctx.draw(right.at(Vec3::new(PAIR_HALF_SPACING, height, z)));
920    }
921
922    /// A row of built-in `Sphere` draws at rising roughness, each
923    /// `metallic(1.0)` with its tint white, so what draws is the sky's own
924    /// reflection alone.
925    fn draw_reflect_row(&self, ctx: &mut FrameContext<'_, Self>) {
926        let start = -REFLECT_ROW_SPACING * (REFLECT_ROW_COUNT as f32 - 1.0) / 2.0;
927        for index in 0..REFLECT_ROW_COUNT {
928            let x = start + index as f32 * REFLECT_ROW_SPACING;
929            let roughness = index as f32 / (REFLECT_ROW_COUNT as f32 - 1.0);
930            ctx.draw(
931                Sphere {
932                    subdivisions: SPHERE_SUBDIVISIONS,
933                }
934                .at(Transform::from_scale_rotation_translation(
935                    Vec3::splat(REFLECT_ROW_RADIUS * 2.0),
936                    Quat::IDENTITY,
937                    Vec3::new(x, REFLECT_ROW_RADIUS, REFLECT_ROW_Z),
938                ))
939                .material(
940                    Material::lit(Color::WHITE)
941                        .roughness(roughness)
942                        .metallic(1.0),
943                ),
944            );
945        }
946    }
947
948    /// Three pillars and a pole a light can shadow, beside `Banner`'s
949    /// displaced cloth and `Field`'s pulsing sphere — [`OUTPOST`] moves the
950    /// whole group clear of the rest of the scene.
951    fn draw_outpost(&self, ctx: &mut FrameContext<'_, Self>) {
952        let clock = ctx.elapsed().as_secs_f32();
953
954        for &(position, scale) in &PILLARS {
955            ctx.draw(
956                Cube.at(Transform::from_scale_rotation_translation(
957                    scale,
958                    Quat::IDENTITY,
959                    OUTPOST + position,
960                ))
961                .material(Material::lit(Color::rgb(0.55, 0.5, 0.45))),
962            );
963        }
964
965        ctx.draw(
966            Cube.at(Transform::from_scale_rotation_translation(
967                POLE_SCALE,
968                Quat::IDENTITY,
969                OUTPOST + POLE_POSITION,
970            ))
971            .material(Material::lit(Color::rgb(0.3, 0.24, 0.18))),
972        );
973
974        ctx.set_surface_style(Banner { time: clock });
975        ctx.draw(
976            BannerCloth
977                .at(Transform::from_translation(OUTPOST + BANNER_MOUNT))
978                .material(Material::lit(Color::rgb(0.75, 0.12, 0.12)))
979                .surface_style::<Banner>(),
980        );
981
982        ctx.set_surface_style(Field {
983            tint: Color::rgb(0.25, 0.75, 1.0),
984            time: clock,
985        });
986        ctx.draw(
987            Sphere { subdivisions: 2 }
988                .at(Transform::from_scale_rotation_translation(
989                    Vec3::splat(FIELD_ORB_SCALE),
990                    Quat::IDENTITY,
991                    OUTPOST + FIELD_ORB_POSITION,
992                ))
993                .material(Material::color(Color::BLACK))
994                .surface_style::<Field>(),
995        );
996    }
997
998    /// This frame's sun, lamp and spotlight, each `.shadow()` where its own
999    /// flag is set. The sun's direction, color and strength come from the
1000    /// chosen [`Sky`], and is absent where the choice pairs with none; only
1001    /// its shadow flag is the player's own.
1002    fn lights(&self) -> Vec<Light> {
1003        let lamp = Light::point(LAMP_POSITION, self.lamp.scaled(), LAMP_RANGE);
1004        let spotlight = Light::spot(Spot {
1005            position: SPOT_POSITION,
1006            direction: SPOT_DIRECTION,
1007            color: self.spotlight.scaled(),
1008            range: SPOT_RANGE,
1009            angle: SPOT_ANGLE,
1010        });
1011
1012        let mut lights = vec![
1013            if self.lamp.shadow {
1014                lamp.shadow()
1015            } else {
1016                lamp
1017            },
1018            if self.spotlight.shadow {
1019                spotlight.shadow()
1020            } else {
1021                spotlight
1022            },
1023        ];
1024
1025        if let Some((direction, color, strength)) = self.sky.sun() {
1026            let sun = Light::directional(direction, scaled(color, strength));
1027            lights.push(if self.sun_shadow { sun.shadow() } else { sun });
1028        }
1029
1030        lights
1031    }
1032
1033    /// Every live knob, in a side area of fixed width so the scene stays
1034    /// visible beside it — the default sky reads as flat grey, so a
1035    /// control added straight to the bare UI layer would read as a slab
1036    /// across the window.
1037    fn controls(&mut self, ctx: &mut FrameContext<'_, Self>) {
1038        let tonemap = ctx.config().tonemap();
1039        let antialiasing = ctx.config().antialiasing();
1040        let shadow_resolution = ctx.config().shadow_resolution();
1041
1042        ctx.ui(|ui| {
1043            egui::Panel::right("controls")
1044                .resizable(false)
1045                .default_size(300.0)
1046                .show(ui, |ui| {
1047                    egui::ScrollArea::vertical().show(ui, |ui| {
1048                        ui.label(
1049                            "right mouse button to look, W/A/S/D to move, \
1050                             space/shift up and down, wheel to scale speed",
1051                        );
1052                        ui.separator();
1053                        self.lighting_controls(ui);
1054                        ui.separator();
1055                        self.light_controls(ui);
1056                        ui.separator();
1057                        self.material_controls(ui);
1058                        ui.separator();
1059                        ui.label(format!(
1060                            "tone map {tonemap:?} \u{b7} antialiasing {antialiasing} \u{b7} \
1061                             shadow {shadow_resolution}px: set at startup, not live"
1062                        ));
1063                        ui.add(egui::Slider::new(&mut self.exposure, 0.1..=3.0).text("exposure"));
1064                        ui.add(egui::Slider::new(&mut self.bloom, 0.0..=1.0).text("bloom"));
1065                    });
1066                });
1067        });
1068    }
1069
1070    fn lighting_controls(&mut self, ui: &mut egui::Ui) {
1071        ui.heading("lighting");
1072        for choice in Sky::ALL {
1073            ui.radio_value(&mut self.sky, choice, choice.name());
1074        }
1075        ui.label(format!("sky light {:.2}: set at startup", self.sky.light()));
1076
1077        match self.sky.sun() {
1078            Some((_, color, strength)) => {
1079                color_swatch(ui, "sun", color);
1080                ui.label(format!("sun strength {strength:.2}: set by the choice"));
1081                ui.checkbox(&mut self.sun_shadow, "sun shadow");
1082            }
1083            None => {
1084                ui.label("no sun: set by the choice");
1085            }
1086        }
1087    }
1088
1089    fn light_controls(&mut self, ui: &mut egui::Ui) {
1090        ui.heading("lights");
1091        glow_controls(ui, "lamp", &mut self.lamp);
1092        glow_controls(ui, "spotlight", &mut self.spotlight);
1093    }
1094
1095    fn material_controls(&mut self, ui: &mut egui::Ui) {
1096        ui.heading("material");
1097        color_row(ui, "tint", &mut self.front_tint);
1098        ui.add(egui::Slider::new(&mut self.front_roughness, 0.0..=1.0).text("roughness"));
1099        ui.add(egui::Slider::new(&mut self.front_metallic, 0.0..=1.0).text("metallic"));
1100        ui.checkbox(
1101            &mut self.shading_map_on,
1102            "shading map: occlusion \u{b7} roughness \u{b7} metallic",
1103        );
1104        ui.checkbox(&mut self.relief_map_on, "relief map: bump normals");
1105        ui.checkbox(&mut self.emissive_map_on, "emissive map: per-texel glow");
1106    }
1107}
1108
1109/// One light's own controls: a color, its strength and a shadow flag.
1110fn glow_controls(ui: &mut egui::Ui, label: &str, glow: &mut Glow) {
1111    ui.label(label);
1112    color_row(ui, "color", &mut glow.color);
1113    ui.add(egui::Slider::new(&mut glow.strength, 0.0..=8.0).text("strength"));
1114    ui.checkbox(&mut glow.shadow, "shadow");
1115}
1116
1117/// One named control over `color`'s red, green and blue channels; alpha
1118/// stays `1.0`.
1119fn color_row(ui: &mut egui::Ui, label: &str, color: &mut Color) {
1120    let mut rgb = [color.red, color.green, color.blue];
1121    ui.horizontal(|ui| {
1122        ui.label(label);
1123        if ui.color_edit_button_rgb(&mut rgb).changed() {
1124            *color = Color::rgb(rgb[0], rgb[1], rgb[2]);
1125        }
1126    });
1127}
1128
1129/// `color` shown, not changed: the sun's own color, resolved from the
1130/// chosen [`Sky`] and not a control the player sets on its own.
1131fn color_swatch(ui: &mut egui::Ui, label: &str, color: Color) {
1132    let mut rgb = [color.red, color.green, color.blue];
1133    ui.horizontal(|ui| {
1134        ui.label(label);
1135        ui.add_enabled_ui(false, |ui| {
1136            ui.color_edit_button_rgb(&mut rgb);
1137        });
1138    });
1139}
1140
1141impl Game for Playground {
1142    type Meshes = Shape;
1143    type Sounds = NoSounds;
1144    type InputActions = Controls;
1145    type Skyboxes = Sky;
1146    type SurfaceStyles = Looks;
1147    type PostEffects = ();
1148
1149    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
1150
1151    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
1152        self.fly_camera(ctx);
1153        ctx.set_camera(self.camera());
1154        ctx.set_skybox(self.sky);
1155        for light in self.lights() {
1156            ctx.light(light);
1157        }
1158        ctx.set_exposure(self.exposure);
1159        ctx.set_bloom(self.bloom);
1160
1161        self.draw_scene(ctx);
1162        self.controls(ctx);
1163    }
1164}
1165
1166fn main() {
1167    let config = Config::new("Mirage: material playground")
1168        .with_size(1280, 720)
1169        .with_assets([
1170            "examples/assets/sky-clear.png",
1171            "examples/assets/sky-classic.png",
1172            "examples/assets/sky-dawn.png",
1173            "examples/assets/sky-sinister.png",
1174            "examples/assets/sky-stars-lightblue.png",
1175            "examples/assets/sky-stars-blue.png",
1176        ]);
1177
1178    run(config, Playground::init);
1179}