Skip to main content

Material

Struct Material 

Source
pub struct Material { /* private fields */ }
Expand description

A surface’s shading: a tint, how strongly lights affect it, and the light it adds of its own.

A tint alpha under 1.0 draws the surface in the transparent pass: sorted back to front, blended over what is behind it, and never written to depth.

Set as a slot’s default, or per draw with Instance::material.

Implementations§

Source§

impl Material

Source

pub const fn color(color: Color) -> Self

A flat color; lights do not affect it.

Examples found in repository?
examples/material-playground.rs (line 300)
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    }
More examples
Hide additional examples
examples/sprite-adventure.rs (line 682)
679    fn build(&self, assets: &Assets) -> MeshData {
680        Quad.build(assets)
681            .with_texture(assets.texture(FLAME_SHEET).pixelated())
682            .with_material(Material::color(FLAME_TINT).additive())
683    }
684}
685
686/// The player's sprite, cutout with its own relief, its sheet shared
687/// with `examples/isometric-board.rs`.
688#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
689struct Walker;
690
691impl Mesh for Walker {
692    fn build(&self, assets: &Assets) -> MeshData {
693        Quad.build(assets)
694            .with_texture(assets.texture(WALKER_SHEET).pixelated())
695            .with_relief(assets.relief(WALKER_RELIEF))
696            .with_material(Material::lit(Color::WHITE).cutout())
697    }
698}
699
700/// The cave floor tile.
701#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
702struct CaveFloor;
703
704impl Mesh for CaveFloor {
705    fn build(&self, assets: &Assets) -> MeshData {
706        Plane
707            .build(assets)
708            .with_texture(assets.texture(CAVE_SHEET).pixelated())
709    }
710}
711
712/// The cave wall face.
713#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
714struct CaveWall;
715
716impl Mesh for CaveWall {
717    fn build(&self, assets: &Assets) -> MeshData {
718        Cube.build(assets)
719            .with_texture(assets.texture(CAVE_SHEET).pixelated())
720    }
721}
722
723/// The loaded door, drawn as its source authored it.
724#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
725struct Door;
726
727impl Mesh for Door {
728    fn build(&self, assets: &Assets) -> MeshData {
729        assets.mesh(DOOR_MESH)
730    }
731}
732
733/// The loaded gem, repainted whole per draw so its glow color shifts.
734#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
735struct Gem;
736
737impl Mesh for Gem {
738    fn build(&self, assets: &Assets) -> MeshData {
739        assets.mesh(GEM_MESH)
740    }
741}
742
743// Everything this game can draw: the meshes above, plus the styled water,
744// the dark filling a looked-into mouth's opening, and the door's own frame,
745// which draw the bare engine primitives Plane, Quad and Cube.
746meshes! {
747    enum Shape {
748        Ground, Shore, Crate, Well, WellMouth, Stone, Bush, Rock, Torch,
749        Flame, Walker, CaveFloor, CaveWall, Door, Gem, Plane, Quad, Cube,
750    }
751}
752
753/// The interact click and the gem's chime, shared with the other examples.
754#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
755enum Sound {
756    Interact,
757    Gem,
758}
759
760impl Sounds for Sound {
761    fn build(&self, assets: &Assets) -> SoundData {
762        match self {
763            Sound::Interact => assets.sound("click"),
764            Sound::Gem => assets.sound("win"),
765        }
766    }
767}
768
769// ---------------------------------------------------------------------
770// Input
771// ---------------------------------------------------------------------
772
773/// Player movement: `WASD`, arrows, or a stick — the strongest reading is
774/// kept.
775#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
776enum Move {
777    Walk,
778}
779
780impl InputAxis2Action for Move {
781    fn bindings(&self) -> Vec<Axis2Binding> {
782        match self {
783            Move::Walk => vec![
784                Axis2Binding::from(ButtonAxis2 {
785                    left: Key::A,
786                    right: Key::D,
787                    down: Key::S,
788                    up: Key::W,
789                }),
790                Axis2Binding::from(ButtonAxis2 {
791                    left: Key::Left,
792                    right: Key::Right,
793                    down: Key::Down,
794                    up: Key::Up,
795                }),
796                Axis2Binding::stick(Stick::Left),
797            ],
798        }
799    }
800}
801
802/// The two verbs this game reads as an edge: interacting with the door, and
803/// a reset of the world to every saved key's fallback.
804#[derive(InputButtonAction, Clone, Copy, PartialEq)]
805enum Button {
806    Interact,
807    Reset,
808}
809
810impl InputButtonAction for Button {
811    fn bindings(&self) -> Vec<ButtonBinding> {
812        match self {
813            Button::Interact => vec![Key::E.into(), Pad::West.into()],
814            Button::Reset => vec![Key::R.into()],
815        }
816    }
817}
818
819struct Controls;
820
821impl InputActions for Controls {
822    type Button = Button;
823    type Axis = NoInputAxes;
824    type Axis2 = Move;
825}
826
827// ---------------------------------------------------------------------
828// Save data
829// ---------------------------------------------------------------------
830
831/// The player's last position, read at startup and saved on area
832/// transition and gem pickup.
833#[derive(Saves, Clone, Copy)]
834enum Position {
835    X,
836    Z,
837}
838
839impl SaveKey for Position {
840    type Value = f64;
841
842    fn fallback(&self) -> f64 {
843        match self {
844            Position::X => PLAYER_SPAWN.x as f64,
845            Position::Z => PLAYER_SPAWN.z as f64,
846        }
847    }
848}
849
850/// The area the player is in, and whether the gem is taken.
851#[derive(Saves, Clone, Copy)]
852enum Flag {
853    InCave,
854    GemTaken,
855}
856
857impl SaveKey for Flag {
858    type Value = bool;
859
860    fn fallback(&self) -> bool {
861        false
862    }
863}
864
865// ---------------------------------------------------------------------
866// The player's facing
867// ---------------------------------------------------------------------
868
869/// The player's last facing: also its row in the sheet, top to bottom.
870#[derive(Clone, Copy, PartialEq)]
871enum Facing {
872    Toward = 0,
873    Right = 1,
874    Away = 2,
875    Left = 3,
876}
877
878impl Facing {
879    /// The facing `heading` points in, favoring its larger axis; `None` at
880    /// rest, so the caller can keep the last facing.
881    fn from_heading(heading: Vec2) -> Option<Self> {
882        if heading == Vec2::ZERO {
883            return None;
884        }
885        Some(if heading.x.abs() > heading.y.abs() {
886            if heading.x > 0.0 {
887                Self::Right
888            } else {
889                Self::Left
890            }
891        } else if heading.y > 0.0 {
892            Self::Away
893        } else {
894            Self::Toward
895        })
896    }
897}
898
899// ---------------------------------------------------------------------
900// The game
901// ---------------------------------------------------------------------
902
903/// The ground tile at `col, row`: the path's dirt along [`PATH_COLUMN`],
904/// the verges that edge it, and a hashed grass variant everywhere else.
905fn ground_cell(col: i32, row: i32) -> Frame {
906    let (column, sheet_row) = match col - PATH_COLUMN {
907        0 => (PATH_DIRT + row.rem_euclid(2) as u32, PATH_ROW),
908        -1 => (PATH_WEST_VERGE, PATH_ROW),
909        1 => (PATH_EAST_VERGE, PATH_ROW),
910        _ => (
911            (col * 31 + row * 17).rem_euclid(GROUND_COLUMNS as i32) as u32,
912            GRASS_ROW,
913        ),
914    };
915
916    Sheet::new(UVec2::new(GROUND_COLUMNS, GROUND_ROWS)).cell_at(UVec2::new(column, sheet_row))
917}
918
919/// The stone sheet's plain masonry, laid `tiles` times across: the sampler
920/// wraps, so a window wider than the sheet repeats the course.
921fn masonry(tiles: f32) -> Frame {
922    let course = 1.0 / STONE_ROWS as f32;
923
924    Frame::rect(Vec2::new(0.0, 1.0 - course), Vec2::new(tiles, 1.0))
925}
926
927/// The wall or door's alpha `fraction` of the way from [`SOLID`] to
928/// [`GHOST_ALPHA`].
929fn ghost_alpha(fraction: f32) -> f32 {
930    SOLID + (GHOST_ALPHA - SOLID) * fraction
931}
932
933/// The wall face in column `variant`, windowed to the meters `standing` of
934/// one course, measured up from that course's own base: every row of the
935/// cave sheet below the floor's covers [`WALL_HEIGHT`], so a course keeps
936/// the floor's texels to the meter however it is cut.
937fn cave_wall_face(variant: u32, standing: Range<f32>) -> Frame {
938    let cell = Vec2::new(1.0 / CAVE_COLUMNS as f32, 1.0 / CAVE_ROWS as f32);
939    let left = (variant % CAVE_COLUMNS) as f32 * cell.x;
940    let face = (CAVE_FLOOR_ROW + 1) as f32 * cell.y;
941    let up_from_base = |height: f32| 1.0 - (1.0 - face) * (height / WALL_HEIGHT);
942
943    Frame::rect(
944        Vec2::new(left, up_from_base(standing.end)),
945        Vec2::new(left + cell.x, up_from_base(standing.start)),
946    )
947}
948
949/// The logical point egui paints the physical pixel `pixel` at.
950fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
951    let point = pixel / pixels_per_point;
952    egui::pos2(point.x, point.y)
953}
954
955fn main() {
956    run(
957        Config::new("Mirage: sprite adventure")
958            .with_size(1280, 720)
959            .with_assets([
960                MODEL,
961                WALKER_SOURCE,
962                WALKER_RELIEF_SOURCE,
963                GROUND_SOURCE,
964                BUSH_SOURCE,
965                BUSH_RELIEF_SOURCE,
966                ROCK_SOURCE,
967                ROCK_RELIEF_SOURCE,
968                TORCH_RELIEF_SOURCE,
969                CRATE_SOURCE,
970                WELL_SOURCE,
971                STONE_SOURCE,
972                CAVE_SOURCE,
973                POND_SOURCE,
974                TORCH_SOURCE,
975                FLAME_SOURCE,
976                INTERACT_SOUND,
977                GEM_SOUND,
978            ]),
979        Keep::init,
980    );
981}
982
983struct Keep {
984    area: Area,
985    position: Vec3,
986    previous: Vec3,
987    facing: Facing,
988    walk_ticks: u32,
989    simulated: Duration,
990    door_opening: bool,
991    /// Ticks the door has been opening for, at a cap of
992    /// [`DOOR_SWING_TICKS`]: how long its world prompt reads "opening" once
993    /// it starts.
994    swing_ticks: u32,
995    gem_taken: bool,
996    /// How far the door wall's fade from [`SOLID`] to [`GHOST_ALPHA`] has
997    /// run as of the last tick: `0.0` to `1.0`.
998    ghost: f32,
999    /// Set by the panel's reset button, since its click lands in a frame
1000    /// rather than a tick; read and cleared on the next tick.
1001    reset_requested: bool,
1002}
1003
1004impl Keep {
1005    /// Prepares every startup-cataloged mesh and resumes wherever the last
1006    /// run left the player.
1007    fn init(ctx: &mut InitContext<'_, Keep>) -> Result<Self, Error> {
1008        let startup = ctx.startup();
1009        let gem_taken = startup.saved(Flag::GemTaken);
1010        let area = if startup.saved(Flag::InCave) {
1011            Area::Cave
1012        } else {
1013            Area::Overworld
1014        };
1015        let position = Vec3::new(
1016            startup.saved(Position::X) as f32,
1017            0.0,
1018            startup.saved(Position::Z) as f32,
1019        );
1020
1021        Ok(Self {
1022            area,
1023            position,
1024            previous: position,
1025            facing: Facing::Toward,
1026            walk_ticks: 0,
1027            simulated: Duration::ZERO,
1028            door_opening: gem_taken,
1029            swing_ticks: if gem_taken { DOOR_SWING_TICKS } else { 0 },
1030            gem_taken,
1031            ghost: 0.0,
1032            reset_requested: false,
1033        })
1034    }
1035
1036    fn camera(position: Vec3, offset: Vec3) -> Camera {
1037        Camera::new(
1038            View::look_at(position + offset, position),
1039            Projection::perspective(CAMERA_FOV),
1040        )
1041    }
1042
1043    /// Obstacles from the overworld's props: the crates, turned as they are
1044    /// drawn, the well's rim, the open water the shoreline rings, the
1045    /// mouth's pillars, and each flora's base.
1046    fn overworld_obstacles() -> impl Iterator<Item = Obstacle> + Clone {
1047        CRATE_POSITIONS
1048            .into_iter()
1049            .map(|(x, z, turn)| {
1050                Obstacle::footprint(Vec2::new(x, z), Vec2::splat(CRATE_SIZE * turned_span(turn)))
1051            })
1052            .chain([
1053                Obstacle::footprint(WELL_POSITION.xz(), WELL_SIZE.xz()),
1054                Obstacle::footprint(POND_CENTER.xz(), Vec2::splat(POND_WATER_HALF * 2.0)),
1055            ])
1056            .chain(
1057                ENTRANCE
1058                    .pillars()
1059                    .map(|at| Obstacle::footprint(at.xz(), MOUTH_PILLAR_SIZE.xz())),
1060            )
1061            .chain(FLORA.into_iter().map(|(x, z, rock)| {
1062                let base = if rock { ROCK_FOOTPRINT } else { BUSH_FOOTPRINT };
1063                Obstacle::footprint(Vec2::new(x, z), Vec2::splat(base))
1064            }))
1065    }
1066
1067    /// Obstacles from the cave: the torch posts, its own mouth's pillars,
1068    /// the runs of wall either side of the doorway and of the mouth, and the
1069    /// `door` leaf.
1070    fn cave_obstacles(door: Obstacle) -> impl Iterator<Item = Obstacle> + Clone {
1071        TORCH_POSITIONS
1072            .into_iter()
1073            .map(|(x, z)| Obstacle::footprint(Vec2::new(x, z), Vec2::splat(TORCH_STAND_WIDTH)))
1074            .chain(
1075                EXIT.pillars()
1076                    .map(|at| Obstacle::footprint(at.xz(), MOUTH_PILLAR_SIZE.xz())),
1077            )
1078            .chain(SIDES.into_iter().flat_map(|side| {
1079                [
1080                    Self::wall_run(side, DOOR_Z),
1081                    Self::wall_run(side, CAVE_LIP_Z),
1082                ]
1083            }))
1084            .chain([door])
1085    }
1086
1087    /// One of the two runs of wall either side of a one-tile opening on the
1088    /// room's axis, at `z`.
1089    fn wall_run(side: f32, z: f32) -> Obstacle {
1090        Obstacle::footprint(
1091            Vec2::new(side * (DOORWAY_HALF + DOOR_WALL_END) * 0.5, z),
1092            Vec2::new(DOOR_WALL_END - DOORWAY_HALF, TILE_SIZE),
1093        )
1094    }
1095
1096    /// Obstacle from the door leaf's own footprint: the box over its four
1097    /// corners, swung back against the wall once the door is opened.
1098    fn door_obstacle(&self) -> Obstacle {
1099        let hinge = DOOR_HINGE.xz();
1100        let across = DOOR_THICKNESS * 0.5;
1101        let corner = |along: f32, aside: f32| {
1102            let (x, z) = if self.door_opening {
1103                (aside, -along)
1104            } else {
1105                (along, aside)
1106            };
1107            hinge + Vec3::new(x, 0.0, z).xz()
1108        };
1109
1110        Obstacle::over([
1111            corner(0.0, -across),
1112            corner(0.0, across),
1113            corner(DOOR_WIDTH, -across),
1114            corner(DOOR_WIDTH, across),
1115        ])
1116    }
1117
1118    /// Pushes the player out of every obstacle their circle has walked into,
1119    /// over as many passes as it takes for one to leave them where the last
1120    /// one did — overlapping obstacles need more than one.
1121    fn push_out_of(&mut self, obstacles: impl Iterator<Item = Obstacle> + Clone) {
1122        /// Passes an overlap is given to settle before the frame takes what
1123        /// it has; ones this game builds settle in two.
1124        const PASSES: u32 = 4;
1125
1126        let mut standing = self.position.xz();
1127        for _ in 0..PASSES {
1128            let settled = obstacles.clone().fold(standing, |point, obstacle| {
1129                obstacle.push_out(point, PLAYER_RADIUS)
1130            });
1131            if settled == standing {
1132                break;
1133            }
1134            standing = settled;
1135        }
1136
1137        self.position.x = standing.x;
1138        self.position.z = standing.y;
1139    }
1140
1141    fn tick_overworld(&mut self, ctx: &mut TickContext<'_, Keep>) {
1142        self.push_out_of(Self::overworld_obstacles());
1143        self.position.x = self.position.x.clamp(-CLEARING_HALF, CLEARING_HALF);
1144        self.position.z = self.position.z.clamp(-CLEARING_HALF, CLEARING_HALF);
1145
1146        if ENTRANCE.holds(self.position) {
1147            if ENTRANCE.holds(self.previous) {
1148                self.position.z = self.previous.z;
1149            } else {
1150                self.enter_cave(ctx);
1151            }
1152        }
1153    }
1154
1155    fn tick_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1156        self.push_out_of(Self::cave_obstacles(self.door_obstacle()));
1157        self.position.x = self.position.x.clamp(-CAVE_HALF_WIDTH, CAVE_HALF_WIDTH);
1158        self.position.z = self.position.z.clamp(CAVE_WALK_FAR_Z, CAVE_WALK_NEAR_Z);
1159
1160        let target = if self.position.z < DOOR_WALL_NEAR_Z {
1161            1.0
1162        } else {
1163            0.0
1164        };
1165        let step = 1.0 / GHOST_RAMP_TICKS as f32;
1166        self.ghost += (target - self.ghost).clamp(-step, step);
1167
1168        if !self.door_opening
1169            && ctx.pressed(Button::Interact)
1170            && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS
1171        {
1172            self.door_opening = true;
1173            self.swing_ticks = 0;
1174            ctx.play(Sound::Interact);
1175        }
1176        if self.door_opening && self.swing_ticks < DOOR_SWING_TICKS {
1177            self.swing_ticks += 1;
1178        }
1179
1180        if !self.gem_taken && self.position.distance(GEM_POSITION) < PICKUP_RADIUS {
1181            self.gem_taken = true;
1182            ctx.play(Sound::Gem);
1183            ctx.save(Flag::GemTaken, true);
1184            ctx.save(Position::X, self.position.x as f64);
1185            ctx.save(Position::Z, self.position.z as f64);
1186        }
1187
1188        if EXIT.holds(self.position) {
1189            if EXIT.holds(self.previous) {
1190                self.position.z = self.previous.z;
1191            } else {
1192                self.exit_cave(ctx);
1193            }
1194        }
1195    }
1196
1197    /// Puts the player back at [`PLAYER_SPAWN`] with the cave and the gem
1198    /// returned to their saved fallbacks, all in this tick: a reset saves
1199    /// every key's own fallback, since there is nothing to clear it to.
1200    fn reset(&mut self, ctx: &mut TickContext<'_, Keep>) {
1201        ctx.save(Position::X, Position::X.fallback());
1202        ctx.save(Position::Z, Position::Z.fallback());
1203        ctx.save(Flag::InCave, Flag::InCave.fallback());
1204        ctx.save(Flag::GemTaken, Flag::GemTaken.fallback());
1205
1206        self.area = Area::Overworld;
1207        self.position = PLAYER_SPAWN;
1208        self.previous = PLAYER_SPAWN;
1209        self.gem_taken = false;
1210        self.door_opening = false;
1211        self.swing_ticks = 0;
1212        self.ghost = 0.0;
1213    }
1214
1215    /// Steps into the cave at [`CAVE_SPAWN`], saving the transition.
1216    fn enter_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1217        self.area = Area::Cave;
1218        self.position = CAVE_SPAWN;
1219        self.previous = CAVE_SPAWN;
1220        ctx.save(Flag::InCave, true);
1221        ctx.save(Position::X, CAVE_SPAWN.x as f64);
1222        ctx.save(Position::Z, CAVE_SPAWN.z as f64);
1223    }
1224
1225    /// Steps back out to the mouth at [`RETURN_SPAWN`], saving the
1226    /// transition.
1227    fn exit_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1228        self.area = Area::Overworld;
1229        self.position = RETURN_SPAWN;
1230        self.previous = RETURN_SPAWN;
1231        ctx.save(Flag::InCave, false);
1232        ctx.save(Position::X, RETURN_SPAWN.x as f64);
1233        ctx.save(Position::Z, RETURN_SPAWN.z as f64);
1234    }
1235
1236    fn draw_ground(&self, ctx: &mut FrameContext<'_, Keep>) {
1237        for col in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1238            for row in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1239                ctx.draw(
1240                    Ground
1241                        .at(Vec3::new(
1242                            col as f32 * TILE_SIZE,
1243                            0.0,
1244                            row as f32 * TILE_SIZE,
1245                        ))
1246                        .frame(ground_cell(col, row)),
1247                );
1248            }
1249        }
1250    }
1251
1252    /// Two staggered rows of bushes around the clearing, open where the path
1253    /// leaves it, drawn between the camera and the ground's edge. The rows
1254    /// running along `Z` skip their two ends, which the rows running along
1255    /// `X` already cover.
1256    fn draw_hedgerow(&self, ctx: &mut FrameContext<'_, Keep>) {
1257        for (row, half) in [HEDGE_INNER_HALF, HEDGE_OUTER_HALF].into_iter().enumerate() {
1258            let row = row as i32;
1259            // The inner row covers both corners; the outer one is half a
1260            // span in from each, backing the gaps the inner row leaves.
1261            let spans = ((2.0 * half / HEDGE_STEP).round() as i32).max(1);
1262            let span = 2.0 * half / spans as f32;
1263            let steps = spans - row;
1264            for step in 0..=steps {
1265                let along = -half + (step as f32 + 0.5 * row as f32) * span;
1266                let scale = if (step + row) % 2 == 0 { 1.0 } else { 0.8 };
1267                let (width, height) = (BUSH_WIDTH * scale, BUSH_HEIGHT * scale);
1268                // The path leaves through the rows running along `X`, so only
1269                // those two open around it.
1270                let gated = along.abs() < HEDGE_GATE_HALF;
1271                let corner = step == 0 || step == steps;
1272                let places = [
1273                    (along, -half, gated),
1274                    (along, half, gated),
1275                    (-half, along, corner),
1276                    (half, along, corner),
1277                ];
1278                for (x, z, skip) in places {
1279                    if skip {
1280                        continue;
1281                    }
1282                    ctx.draw(
1283                        Bush.at(Transform::from_scale_rotation_translation(
1284                            Vec3::new(width, height, width),
1285                            Quat::IDENTITY,
1286                            Vec3::new(x, height * 0.5, z),
1287                        ))
1288                        .upright(),
1289                    );
1290                }
1291            }
1292        }
1293    }
1294
1295    /// The pond: a square of styled water, and the shoreline sprite laid over
1296    /// it, which rings the open middle and hides the water's own edges.
1297    fn draw_pond(&self, ctx: &mut FrameContext<'_, Keep>) {
1298        ctx.draw(
1299            Plane
1300                .at(Transform::from_scale_rotation_translation(
1301                    Vec3::splat(POND_WATER_HALF * 2.0),
1302                    Quat::IDENTITY,
1303                    POND_CENTER,
1304                ))
1305                .material(Material::shaded(WATER_COLOR, WATER_LITNESS))
1306                .surface_style::<Water>(),
1307        );
1308        ctx.draw(
1309            Shore
1310                .at(Transform::from_scale_rotation_translation(
1311                    Vec3::splat(POND_HALF * 2.0),
1312                    Quat::IDENTITY,
1313                    Vec3::new(POND_CENTER.x, 0.0, POND_CENTER.z),
1314                ))
1315                .frame(Sheet::new(UVec2::new(POND_CELLS, 1)).cell(POND_SHORE_CELL)),
1316        );
1317    }
1318
1319    fn draw_crates(&self, ctx: &mut FrameContext<'_, Keep>) {
1320        for &(x, z, turn) in &CRATE_POSITIONS {
1321            ctx.draw(Crate.at(Transform::from_scale_rotation_translation(
1322                Vec3::splat(CRATE_SIZE),
1323                Quat::from_rotation_y(turn),
1324                Vec3::new(x, CRATE_SIZE * 0.5, z),
1325            )));
1326        }
1327    }
1328
1329    /// The well: its rim in grey masonry, and the mouth cell laid over the
1330    /// rim's top face.
1331    fn draw_well(&self, ctx: &mut FrameContext<'_, Keep>) {
1332        let cells = Sheet::new(UVec2::new(WELL_CELLS, 1));
1333        ctx.draw(
1334            Well.at(Transform::from_scale_rotation_translation(
1335                WELL_SIZE,
1336                Quat::IDENTITY,
1337                WELL_POSITION + Vec3::Y * (WELL_SIZE.y * 0.5),
1338            ))
1339            .frame(cells.cell(WELL_RIM_CELL)),
1340        );
1341        ctx.draw(
1342            WellMouth
1343                .at(Transform::from_scale_rotation_translation(
1344                    Vec3::new(WELL_SIZE.x, 1.0, WELL_SIZE.z),
1345                    Quat::IDENTITY,
1346                    WELL_POSITION + Vec3::Y * (WELL_SIZE.y + WELL_MOUTH_LIFT),
1347                ))
1348                .frame(cells.cell(WELL_MOUTH_CELL)),
1349        );
1350    }
1351
1352    fn draw_flora(&self, ctx: &mut FrameContext<'_, Keep>) {
1353        for &(x, z, rock) in &FLORA {
1354            let (width, height) = if rock {
1355                (ROCK_WIDTH, ROCK_HEIGHT)
1356            } else {
1357                (BUSH_WIDTH, BUSH_HEIGHT)
1358            };
1359            let standing = Transform::from_scale_rotation_translation(
1360                Vec3::new(width, height, width),
1361                Quat::IDENTITY,
1362                Vec3::new(x, height * 0.5, z),
1363            );
1364            let flora: Instance<Shape, _> = if rock {
1365                Rock.at(standing).into_set()
1366            } else {
1367                Bush.at(standing).into_set()
1368            };
1369            ctx.draw(flora.upright());
1370        }
1371    }
1372
1373    /// One stone box drawn on the ground at `at`, `size` across, sampling
1374    /// the part of the sheet `frame` covers.
1375    fn draw_stone(ctx: &mut FrameContext<'_, Keep>, at: Vec3, size: Vec3, frame: Frame) {
1376        ctx.draw(
1377            Stone
1378                .at(Transform::from_scale_rotation_translation(
1379                    size,
1380                    Quat::IDENTITY,
1381                    at + Vec3::Y * (size.y * 0.5),
1382                ))
1383                .frame(frame),
1384        );
1385    }
1386
1387    /// Two stone pillars drawn where `mouth` blocks the player, each a
1388    /// capital over its own course of masonry, and, on the one the camera
1389    /// looks into, the lintel across their tops and the dark filling
1390    /// the opening under it.
1391    fn draw_mouth(ctx: &mut FrameContext<'_, Keep>, mouth: Mouth) {
1392        for at in mouth.pillars() {
1393            Self::draw_stone(ctx, at, MOUTH_PILLAR_SIZE, Frame::default());
1394        }
1395        if !mouth.looked_into() {
1396            return;
1397        }
1398
1399        Self::draw_stone(
1400            ctx,
1401            mouth.at + Vec3::Y * MOUTH_PILLAR_SIZE.y,
1402            MOUTH_LINTEL_SIZE,
1403            masonry(MOUTH_LINTEL_TILES),
1404        );
1405        ctx.draw(
1406            Quad.at(Transform::from_scale_rotation_translation(
1407                Vec3::new(MOUTH_PILLAR_OFFSET * 2.0, MOUTH_DARK_HEIGHT, 1.0),
1408                Quat::IDENTITY,
1409                mouth.at + Vec3::Y * (MOUTH_DARK_HEIGHT * 0.5),
1410            ))
1411            .material(Material::color(Color::BLACK)),
1412        );
1413    }
examples/isometric-board.rs (line 542)
529    fn draw_reachable_mark(&self, ctx: &mut FrameContext<'_, Board>, tile: (i32, i32)) {
530        let center = tile_center(tile) + Vec3::Y * REACHABLE_MARK_LIFT;
531        ctx.draw(
532            Plane
533                .at(Transform::from_scale_rotation_translation(
534                    Vec3::new(
535                        TILE_SIZE * REACHABLE_MARK_SCALE,
536                        1.0,
537                        TILE_SIZE * REACHABLE_MARK_SCALE,
538                    ),
539                    Quat::IDENTITY,
540                    center,
541                ))
542                .material(Material::color(REACHABLE_MARK)),
543        );
544    }
545
546    /// A mark bright enough to read past the sprite's own tint under the
547    /// selected unit, or a smaller, dim one under the unit whose turn it
548    /// is while nothing is selected — so the current unit reads from the
549    /// ground alone.
550    fn draw_current_mark(&self, ctx: &mut FrameContext<'_, Board>) {
551        let (color, scale) = if self.selected {
552            (CURRENT_MARK, CURRENT_MARK_SCALE)
553        } else {
554            (TURN_MARK, TURN_MARK_SCALE)
555        };
556        let center = tile_center(self.current().tile) + Vec3::Y * REACHABLE_MARK_LIFT;
557        ctx.draw(
558            Plane
559                .at(Transform::from_scale_rotation_translation(
560                    Vec3::new(TILE_SIZE * scale, 1.0, TILE_SIZE * scale),
561                    Quat::IDENTITY,
562                    center,
563                ))
564                .material(Material::color(color)),
565        );
566    }
examples/breakout-game.rs (line 675)
661    fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662        for spark in &self.sparks {
663            let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664            let fade = 1.0 - age;
665            let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666            ctx.draw(
667                Quad.at(Transform::from_scale_rotation_translation(
668                    Vec3::splat(size),
669                    Quat::IDENTITY,
670                    spark.position,
671                ))
672                .billboard()
673                .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674                .material(
675                    Material::color(spark.color.with_alpha(fade))
676                        .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677                        .additive(),
678                ),
679            );
680        }
681    }
682
683    /// Draws the ball's ghost trail, each ghost smaller and more transparent
684    /// than the one ahead of it; each ghost's position interpolates between
685    /// its own last two resolved ticks by the same `alpha` the ball itself
686    /// draws at, and its radius clamps to what the ball's own radius has
687    /// left over its distance from the head, so a ghost still close to the
688    /// ball never draws past its edge.
689    fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690        let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691        for i in 0..TRAIL_LEN {
692            let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693            let age = (i + 1) as f32 / TRAIL_LEN as f32;
694            let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695            let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696                .min((BALL_RADIUS - head.distance(position)).max(0.0));
697            let scale = Vec3::splat(radius * 2.0);
698            ctx.draw(
699                Sphere { subdivisions: 2 }
700                    .at(Transform::from_scale_rotation_translation(
701                        scale,
702                        Quat::IDENTITY,
703                        position,
704                    ))
705                    .material(
706                        Material::color(BALL_GLOW.with_alpha(fade))
707                            .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708                    ),
709            );
710        }
711    }
712
713    /// Draws one held ball for every life past the one in play, set in a
714    /// row alongside the paddle's own path.
715    fn draw_lives(&self, ctx: &mut FrameContext<'_, Breakout>) {
716        let held_lives = self.lives.saturating_sub(1);
717        for slot in 0..held_lives {
718            let z = PADDLE_Z + (slot + 1) as f32 * LIFE_ROW_SPACING;
719            ctx.draw(
720                Sphere { subdivisions: 2 }
721                    .at(Transform::from_scale_rotation_translation(
722                        Vec3::splat(BALL_RADIUS * 2.0),
723                        Quat::IDENTITY,
724                        Vec3::new(LIFE_ROW_X, BALL_RADIUS, z),
725                    ))
726                    .material(
727                        Material::color(BALL_GLOW)
728                            .emissive(BALL_EMISSIVE)
729                            .additive(),
730                    ),
731            );
732        }
733    }
734
735    fn overlay(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
736        let bricks_left = self
737            .bricks
738            .iter()
739            .filter(|brick| brick.hits_remaining > 0)
740            .count();
741        // Read before `ctx.ui` so a rebind changes what the hint reads this
742        // frame too.
743        let move_hint = bindings_text(ctx.bindings(Move::Paddle));
744        let pause_hint = bindings_text(ctx.bindings(Button::Pause));
745        let serve_hint = bindings_text(ctx.bindings(Button::Serve));
746        ctx.ui(|ui| {
747            ui.horizontal(|ui| {
748                ui.label(egui::RichText::new(format!("score {}", self.score)).size(32.0));
749                ui.label(format!("{bricks_left} bricks left"));
750            });
751            ui.label(format!("move: {move_hint} · {pause_hint} to pause"));
752            if self.phase == Phase::Serving {
753                ui.label(format!("{serve_hint} to serve"));
754            }
755        });
756
757        match self.phase {
758            Phase::Serving | Phase::Playing if self.paused => self.menu(ctx, "paused", false),
759            Phase::Won => self.menu(ctx, "you win", true),
760            Phase::Lost => self.menu(ctx, "game over", true),
761            _ => {}
762        }
763    }
764
765    fn menu(&mut self, ctx: &mut FrameContext<'_, Breakout>, title: &str, over: bool) {
766        let mut clicked = false;
767        let mut quit = false;
768
769        // `ctx.ui` cannot borrow `ctx`, so anything the controls list needs is
770        // read first and applied after.
771        let buttons: Vec<(Button, String)> = Button::all()
772            .into_iter()
773            .map(|action| (action, bindings_text(ctx.bindings(action))))
774            .collect();
775        let axes: Vec<(Move, String)> = Move::all()
776            .into_iter()
777            .map(|action| (action, bindings_text(ctx.bindings(action))))
778            .collect();
779        let listening = self.listening;
780        let actuated_button = (!ctx.ui_wants_keyboard())
781            .then(|| ctx.actuated_button())
782            .flatten();
783        let actuated_axis = (!ctx.ui_wants_keyboard())
784            .then(|| ctx.actuated_axis())
785            .flatten();
786        let mut reset = None;
787
788        ctx.ui(|ui| {
789            egui::Window::new(title)
790                .collapsible(false)
791                .resizable(false)
792                .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
793                .show(ui.ctx(), |ui| {
794                    if over {
795                        ui.label(format!("score {}", self.score));
796                    }
797                    if !over {
798                        ui.add(
799                            egui::Slider::new(&mut self.master_volume, 0.0..=1.0).text("volume"),
800                        );
801                        if ui.button("resume").clicked() {
802                            self.paused = false;
803                            clicked = true;
804                        }
805                        ui.separator();
806                        ui.heading("controls");
807                        for (action, text) in &buttons {
808                            controls_row(
809                                ui,
810                                action.name(),
811                                text,
812                                listening == Some(Listening::Button(*action)),
813                                &mut self.listening,
814                                Listening::Button(*action),
815                                &mut reset,
816                            );
817                        }
818                        for (action, text) in &axes {
819                            controls_row(
820                                ui,
821                                action.name(),
822                                text,
823                                listening == Some(Listening::Move(*action)),
824                                &mut self.listening,
825                                Listening::Move(*action),
826                                &mut reset,
827                            );
828                        }
829                    }
830                    if ui.button("restart").clicked() {
831                        self.restart();
832                        clicked = true;
833                    }
834                    if ui.button("quit").clicked() {
835                        quit = true;
836                    }
837                });
838        });
839
840        match (self.listening, actuated_button, actuated_axis) {
841            (Some(Listening::Button(action)), Some(binding), _) => {
842                ctx.rebind(action, vec![binding]);
843                self.listening = None;
844            }
845            (Some(Listening::Move(action)), _, Some(binding)) => {
846                ctx.rebind(action, vec![binding]);
847                self.listening = None;
848            }
849            _ => {}
850        }
851        match reset {
852            Some(Listening::Button(action)) => ctx.rebind(action, action.bindings()),
853            Some(Listening::Move(action)) => ctx.rebind(action, action.bindings()),
854            None => {}
855        }
856
857        if clicked {
858            ctx.play(Sound::Click);
859        }
860        if quit {
861            ctx.close();
862        }
863    }
864
865    /// Sustains both tracks every frame, and the gain goes to whichever the
866    /// game calls for: gameplay music while a round is live, serving
867    /// included, and menu music whenever a menu covers it.
868    ///
869    /// Each fades in over [`MUSIC_CROSSFADE`] and slides every later gain
870    /// over it, which is the crossfade itself; the one at no gain costs no
871    /// voice while its playback goes on under the other.
872    fn sustain_music(&self, ctx: &mut FrameContext<'_, Breakout>) {
873        let playing = !self.paused && matches!(self.phase, Phase::Serving | Phase::Playing);
874        let gain = |wanted: bool| match wanted {
875            true => MUSIC_GAIN,
876            false => 0.0,
877        };
878
879        ctx.sustain(
880            Sound::Music
881                .gain(gain(playing))
882                .fade(MUSIC_CROSSFADE)
883                .glide(MUSIC_CROSSFADE)
884                .loop_from(MUSIC_LOOP_FROM),
885        );
886        ctx.sustain(
887            Sound::MenuMusic
888                .gain(gain(!playing))
889                .fade(MUSIC_CROSSFADE)
890                .glide(MUSIC_CROSSFADE)
891                .loop_from(MENU_MUSIC_LOOP_FROM),
892        );
893    }
894}
895
896/// One action's name, its live bindings, a rebind control that starts
897/// listening for a new one, and a reset to its defaults; cancel is a
898/// button rather than Escape, since Escape is itself a binding a listen
899/// could capture.
900fn controls_row(
901    ui: &mut egui::Ui,
902    name: &str,
903    bindings: &str,
904    listening: bool,
905    target: &mut Option<Listening>,
906    action: Listening,
907    reset: &mut Option<Listening>,
908) {
909    ui.horizontal(|ui| {
910        ui.label(format!("{name}: {bindings}"));
911        if listening {
912            ui.label("listening");
913            if ui.button("cancel").clicked() {
914                *target = None;
915            }
916        } else if ui.button("rebind").clicked() {
917            *target = Some(action);
918        }
919        if ui.button("reset").clicked() {
920            *reset = Some(action);
921        }
922    });
923}
924
925/// The controls-menu text for a live binding list: each alternative,
926/// separated, in the order the player can use them.
927fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
928    bindings
929        .iter()
930        .map(ToString::to_string)
931        .collect::<Vec<_>>()
932        .join(", ")
933}
934
935fn spawn_bricks() -> Vec<Brick> {
936    let cell = BRICK_HALF_WIDTH * 2.0 + BRICK_GAP;
937    let row_span = BRICK_HALF_DEPTH * 2.0 + BRICK_ROW_GAP;
938    let grid_width = cell * BRICK_COLUMNS as f32 - BRICK_GAP;
939    let start_x = -grid_width * 0.5 + BRICK_HALF_WIDTH;
940    let start_z = -COURT_HALF_DEPTH + WALL_THICKNESS + BRICK_HALF_DEPTH + 0.6;
941
942    (0..BRICK_ROWS)
943        .flat_map(|row| {
944            (0..BRICK_COLUMNS).map(move |column| Brick {
945                row,
946                position: Vec3::new(
947                    start_x + column as f32 * cell,
948                    BRICK_HALF_HEIGHT,
949                    start_z + row as f32 * row_span,
950                ),
951                hits_remaining: BRICK_HITS,
952            })
953        })
954        .collect()
955}
956
957impl Game for Breakout {
958    type Meshes = Shape;
959    type Sounds = Sound;
960    type InputActions = Controls;
961    type Skyboxes = NoSkyboxes;
962    type SurfaceStyles = NoSurfaceStyles;
963    type PostEffects = NoPostEffects;
964
965    fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966        if self.paused {
967            return;
968        }
969
970        let dt = ctx.dt().as_secs_f32();
971        self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972        self.brick_flash = (self.brick_flash - dt).max(0.0);
973        self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974        self.step_sparks(dt);
975
976        // Decay runs before the end-screen return below, so the last pulse and
977        // burst do not stay on screen.
978        if matches!(self.phase, Phase::Won | Phase::Lost) {
979            return;
980        }
981
982        let axis = if ctx.ui_wants_keyboard() {
983            0.0
984        } else {
985            ctx.axis(Move::Paddle)
986        };
987        self.step_paddle(axis, dt);
988
989        match self.phase {
990            Phase::Serving => self.hold_ball(ctx),
991            _ => self.step_ball(ctx, dt),
992        }
993    }
994
995    fn frame(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
996        if matches!(self.phase, Phase::Serving | Phase::Playing) && ctx.pressed(Button::Pause) {
997            self.paused = !self.paused;
998        }
999
1000        ctx.set_volume(self.master_volume);
1001        self.sustain_music(ctx);
1002
1003        ctx.set_camera(Self::camera());
1004
1005        let brick_pulse = (self.brick_flash / BRICK_FLASH).clamp(0.0, 1.0);
1006        ctx.set_bloom((BLOOM_BASE + brick_pulse * BLOOM_PULSE_PEAK).clamp(0.0, 1.0));
1007
1008        let life_lost_t = (self.life_lost_flash / LIFE_LOST_FLASH).clamp(0.0, 1.0);
1009        ctx.set_exposure((1.0 - life_lost_t * EXPOSURE_DIP_DEPTH).clamp(0.0, 1.0));
1010
1011        // The tick moves nothing behind a menu, so a frame there draws the last
1012        // step whole rather than interpolating from the one before.
1013        let alpha = match self.phase {
1014            Phase::Serving | Phase::Playing if !self.paused => ctx.alpha(),
1015            _ => 1.0,
1016        };
1017        let paddle_x = self.paddle_prev_x.lerp(self.paddle_x, alpha);
1018        let ball_pos = self.ball_prev.lerp(self.ball_pos, alpha);
1019
1020        ctx.light(Light::point(ball_pos, BALL_GLOW, BALL_LIGHT_RANGE).shadow());
1021
1022        self.draw_court(ctx);
1023        self.draw_bricks(ctx);
1024        self.draw_sparks(ctx);
1025        self.draw_lives(ctx);
1026
1027        ctx.draw(
1028            Paddle
1029                .at(Transform::from_translation(Vec3::new(
1030                    paddle_x,
1031                    PADDLE_HALF_HEIGHT,
1032                    PADDLE_Z,
1033                )))
1034                .material_of(PaddlePart::Face, self.paddle_face_material()),
1035        );
1036
1037        self.draw_trail(ctx, alpha);
1038        ctx.draw(
1039            Sphere { subdivisions: 2 }
1040                .at(Transform::from_scale_rotation_translation(
1041                    Vec3::splat(BALL_RADIUS * 2.0),
1042                    Quat::IDENTITY,
1043                    ball_pos,
1044                ))
1045                .material(
1046                    Material::color(BALL_GLOW)
1047                        .emissive(BALL_EMISSIVE)
1048                        .additive(),
1049                ),
1050        );
1051
1052        self.overlay(ctx);
1053    }
examples/ui-fonts.rs (line 668)
657    fn draw_station(&self, ctx: &mut FrameContext<'_, Self>, station: StationKind) {
658        let look = station.look();
659        let center = station.center();
660        let front_offset =
661            STATION_SIZE.z * 0.5 - STATION_FRONT_SIZE.z * 0.5 + STATION_FRONT_OUTWARD;
662        let front = center - Vec3::new(0.0, 0.0, front_offset);
663        for (size, position, material) in [
664            (STATION_SIZE, center, Material::lit(look.color)),
665            (
666                STATION_FRONT_SIZE,
667                front,
668                Material::color(Color::BLACK).emissive(look.glow),
669            ),
670        ] {
671            ctx.draw(
672                Cube.at(Transform::from_scale_rotation_translation(
673                    size,
674                    Quat::IDENTITY,
675                    position,
676                ))
677                .material(material),
678            );
679        }
680    }
examples/post-effects.rs (line 108)
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    }
Source

pub const fn lit(color: Color) -> Self

A color fully lit by the frame’s lights.

Examples found in repository?
examples/material-playground.rs (line 292)
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    }
More examples
Hide additional examples
examples/sprite-adventure.rs (line 586)
582    fn build(&self, assets: &Assets) -> MeshData {
583        Plane
584            .build(assets)
585            .with_texture(assets.texture(POND_SHEET).pixelated())
586            .with_material(Material::lit(Color::WHITE).cutout())
587    }
588}
589
590/// A crate prop, its texture drawn over a cube.
591#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
592struct Crate;
593
594impl Mesh for Crate {
595    fn build(&self, assets: &Assets) -> MeshData {
596        Cube.build(assets)
597            .with_texture(assets.texture(CRATE_TEXTURE).pixelated())
598    }
599}
600
601/// The well's rim.
602#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
603struct Well;
604
605impl Mesh for Well {
606    fn build(&self, assets: &Assets) -> MeshData {
607        Cube.build(assets)
608            .with_texture(assets.texture(WELL_SHEET).pixelated())
609    }
610}
611
612/// The well's mouth, laid flat over the rim's top face.
613#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
614struct WellMouth;
615
616impl Mesh for WellMouth {
617    fn build(&self, assets: &Assets) -> MeshData {
618        Plane
619            .build(assets)
620            .with_texture(assets.texture(WELL_SHEET).pixelated())
621    }
622}
623
624/// A stone box: the mouth's pillars and lintel.
625#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
626struct Stone;
627
628impl Mesh for Stone {
629    fn build(&self, assets: &Assets) -> MeshData {
630        Cube.build(assets)
631            .with_texture(assets.texture(STONE_SHEET).pixelated())
632    }
633}
634
635/// A bush sprite, cutout with its own relief.
636#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
637struct Bush;
638
639impl Mesh for Bush {
640    fn build(&self, assets: &Assets) -> MeshData {
641        Quad.build(assets)
642            .with_texture(assets.texture(BUSH_SPRITE).pixelated())
643            .with_relief(assets.relief(BUSH_RELIEF))
644            .with_material(Material::lit(Color::WHITE).cutout())
645    }
646}
647
648/// A rock sprite, cutout with its own relief.
649#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
650struct Rock;
651
652impl Mesh for Rock {
653    fn build(&self, assets: &Assets) -> MeshData {
654        Quad.build(assets)
655            .with_texture(assets.texture(ROCK_SPRITE).pixelated())
656            .with_relief(assets.relief(ROCK_RELIEF))
657            .with_material(Material::lit(Color::WHITE).cutout())
658    }
659}
660
661/// A torch's post sprite, cutout with its own relief.
662#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
663struct Torch;
664
665impl Mesh for Torch {
666    fn build(&self, assets: &Assets) -> MeshData {
667        Quad.build(assets)
668            .with_texture(assets.texture(TORCH_SPRITE).pixelated())
669            .with_relief(assets.relief(TORCH_RELIEF))
670            .with_material(Material::lit(Color::WHITE).cutout())
671    }
672}
673
674/// A torch's flame sprite, added over the dark rather than lit.
675#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
676struct Flame;
677
678impl Mesh for Flame {
679    fn build(&self, assets: &Assets) -> MeshData {
680        Quad.build(assets)
681            .with_texture(assets.texture(FLAME_SHEET).pixelated())
682            .with_material(Material::color(FLAME_TINT).additive())
683    }
684}
685
686/// The player's sprite, cutout with its own relief, its sheet shared
687/// with `examples/isometric-board.rs`.
688#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
689struct Walker;
690
691impl Mesh for Walker {
692    fn build(&self, assets: &Assets) -> MeshData {
693        Quad.build(assets)
694            .with_texture(assets.texture(WALKER_SHEET).pixelated())
695            .with_relief(assets.relief(WALKER_RELIEF))
696            .with_material(Material::lit(Color::WHITE).cutout())
697    }
examples/stress-preview.rs (line 399)
394    fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
395        let side = (FIELD_RADIUS + FIELD_INNER_RADIUS) * 2.2;
396        ctx.draw(
397            Plane
398                .at(Transform::from_scale(Vec3::new(side, 1.0, side)))
399                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
400        );
401    }
402
403    fn draw_field(&self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
404        for entry in &self.field {
405            let yaw = if self.settings.moving && entry.moving {
406                entry.phase + elapsed * MOVING_SPEED
407            } else {
408                entry.phase
409            };
410            ctx.draw(
411                Rock { seed: entry.seed }.at(Transform::from_scale_rotation_translation(
412                    Vec3::ONE,
413                    Quat::from_rotation_y(yaw),
414                    entry.position,
415                )),
416            );
417        }
418    }
419
420    /// How many field items lie in the camera's view at `window_size`:
421    /// every item where the field holds at most [`MAX_IN_VIEW_SAMPLES`],
422    /// otherwise one item stepped at a time and the count scaled back up
423    /// to the whole field; `true` in the second place where the count
424    /// came from such a step.
425    ///
426    /// Each item is tested on the engine's own workers: a parallel iterator
427    /// reaches them with nothing configured for it.
428    fn count_in_view(&self, camera: &Camera, window_size: UVec2) -> (usize, bool) {
429        let stride = (self.field.len() as u32 / MAX_IN_VIEW_SAMPLES).max(1) as usize;
430        let tested = self.field.par_iter().step_by(stride);
431        let tested_count = self.field.len().div_ceil(stride);
432        let in_view = tested
433            .filter(|entry| Self::in_view(camera, entry.position, window_size))
434            .count();
435        let estimate = in_view
436            .checked_mul(self.field.len())
437            .and_then(|scaled| scaled.checked_div(tested_count))
438            .unwrap_or(in_view);
439        (estimate, stride > 1)
440    }
441
442    /// Whether `position` draws inside `window_size`, the frame's own
443    /// bound of what the camera's view holds.
444    fn in_view(camera: &Camera, position: Vec3, window_size: UVec2) -> bool {
445        camera.pixel_of(position, window_size).is_some_and(|pixel| {
446            pixel.x >= 0.0
447                && pixel.y >= 0.0
448                && pixel.x < window_size.x as f32
449                && pixel.y < window_size.y as f32
450        })
451    }
452
453    /// The load controls, and this frame's own cost, reported below them.
454    fn controls(&mut self, ctx: &mut FrameContext<'_, Self>, camera: &Camera) {
455        let submitted = self.field.len();
456        let seeds = self.applied_seed_count;
457        let average_ms = self.frame_times.average_ms();
458        let fps = if average_ms > 0.0 {
459            1000.0 / average_ms
460        } else {
461            0.0
462        };
463        let elapsed = ctx.elapsed().as_secs_f32();
464        let (in_view, sampled) = self.count_in_view(camera, ctx.window_size());
465
466        ctx.ui(|ui| {
467            egui::Frame::new()
468                .fill(egui::Color32::from_gray(24))
469                .inner_margin(PANEL_PADDING)
470                .corner_radius(f32::from(PANEL_PADDING))
471                .show(ui, |ui| {
472                    ui.add(
473                        egui::Slider::new(
474                            &mut self.settings.instance_count,
475                            MIN_INSTANCE_COUNT..=MAX_INSTANCE_COUNT,
476                        )
477                        .text("instance count"),
478                    );
479                    ui.add(
480                        egui::Slider::new(
481                            &mut self.settings.seed_count,
482                            MIN_SEED_COUNT..=MAX_SEED_COUNT,
483                        )
484                        .text("distinct seeds"),
485                    );
486                    ui.checkbox(&mut self.settings.sun_shadow, "sun shadow");
487                    ui.checkbox(&mut self.settings.moving, "moving fraction");
488                    ui.separator();
489                    ui.label(format!("instances submitted {submitted}"));
490                    if sampled {
491                        ui.label(format!("in view, sampled {in_view}"));
492                    } else {
493                        ui.label(format!("instances in view {in_view}"));
494                    }
495                    ui.label(format!("distinct seeds {seeds}"));
496                    ui.label(format!("frame time {average_ms:.2}ms, {fps:.0} fps"));
497                    ui.label(format!("elapsed {elapsed:.1}s"));
498                });
499        });
500    }
501}
502
503/// `instance_count` field values, each drawing one of `seed_count`
504/// distinct seed values in a cycle, and scattered from
505/// [`FIELD_INNER_RADIUS`] out to [`FIELD_RADIUS`]; each built from an
506/// integer-hash of its own index.
507fn build_field(instance_count: u32, seed_count: u32) -> Vec<FieldEntry> {
508    (0..instance_count)
509        .map(|index| {
510            let angle = hash_unit(index, 0) * core::f32::consts::TAU;
511            let spread = hash_unit(index, 1).sqrt();
512            let distance = FIELD_INNER_RADIUS + spread * (FIELD_RADIUS - FIELD_INNER_RADIUS);
513            FieldEntry {
514                seed: index % seed_count,
515                position: Vec3::new(angle.cos() * distance, 0.0, angle.sin() * distance),
516                phase: hash_unit(index, 2) * core::f32::consts::TAU,
517                moving: index % MOVING_STRIDE == 0,
518            }
519        })
520        .collect()
521}
522
523/// A rock built from `seed`: a cone of [`ROCK_SIDES`] sides, each base
524/// corner and the apex height displaced by an integer-hash of `seed`.
525fn build_rock(seed: u32) -> MeshData {
526    let height = ROCK_HEIGHT * (1.0 + hash_signed(seed, ROCK_SIDES) * ROCK_HEIGHT_DISPLACEMENT);
527    let apex = Vec3::Y * height;
528    let base: Vec<Vec3> = (0..ROCK_SIDES)
529        .map(|corner| {
530            let angle = core::f32::consts::TAU * corner as f32 / ROCK_SIDES as f32;
531            let radius =
532                ROCK_BASE_RADIUS * (1.0 + hash_signed(seed, corner) * ROCK_RADIAL_DISPLACEMENT);
533            Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius)
534        })
535        .collect();
536
537    let mut vertices = Vec::with_capacity(base.len() * 6);
538    let mut indices = Vec::with_capacity(base.len() * 6);
539    for corner in 0..base.len() {
540        let next = (corner + 1) % base.len();
541        push_face(&mut vertices, &mut indices, base[corner], apex, base[next]);
542        push_face(
543            &mut vertices,
544            &mut indices,
545            base[corner],
546            base[next],
547            Vec3::ZERO,
548        );
549    }
550
551    MeshData::new(vertices, indices).with_material(Material::lit(ROCK_COLOR))
552}
examples/flock-parallelism.rs (line 668)
660    fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
661        ctx.draw(
662            Plane
663                .at(Transform::from_scale(Vec3::new(
664                    GROUND_SIZE,
665                    1.0,
666                    GROUND_SIZE,
667                )))
668                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
669        );
670    }
671
672    /// Every butterfly at its own scale, turned to face its velocity, posed
673    /// by the flap machine of its own group and tinted its own color.
674    fn draw_butterflies(&self, ctx: &mut FrameContext<'_, Self>) {
675        for (position, velocity, kind) in self.butterflies.each() {
676            let rotation = Quat::from_rotation_arc(Vec3::Z, Vec3::from(velocity).normalize());
677            ctx.draw(
678                Butterfly
679                    .at(Transform::from_scale_rotation_translation(
680                        Vec3::splat(BUTTERFLY_SCALE),
681                        rotation,
682                        Vec3::from(position),
683                    ))
684                    .posed(&self.flaps[usize::from(kind.flap)])
685                    .material(Material::lit(TINTS[usize::from(kind.tint)])),
686            );
687        }
688    }
examples/breakout-game.rs (line 601)
593    fn paddle_face_material(&self) -> Material {
594        let t = (self.paddle_flash / PADDLE_FLASH).clamp(0.0, 1.0);
595        let flash = PADDLE_FLASH_EMISSIVE.dimmed(t);
596        let emissive = Color::rgb(
597            PADDLE_AMBIENT_EMISSIVE.red + flash.red,
598            PADDLE_AMBIENT_EMISSIVE.green + flash.green,
599            PADDLE_AMBIENT_EMISSIVE.blue + flash.blue,
600        );
601        Material::lit(PADDLE_BASE).emissive(emissive)
602    }
603
604    fn draw_court(&self, ctx: &mut FrameContext<'_, Breakout>) {
605        ctx.draw(
606            Plane
607                .at(Transform::from_scale(Vec3::new(
608                    COURT_HALF_WIDTH * 2.0,
609                    1.0,
610                    COURT_HALF_DEPTH * 2.0,
611                )))
612                .material(Material::lit(FLOOR_COLOR)),
613        );
614
615        let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, COURT_HALF_DEPTH);
616        for side in [-1.0, 1.0] {
617            let x = side * (COURT_HALF_WIDTH - WALL_THICKNESS * 0.5);
618            ctx.draw(
619                Cube.at(Transform::from_scale_rotation_translation(
620                    side_half * 2.0,
621                    Quat::IDENTITY,
622                    Vec3::new(x, side_half.y, 0.0),
623                ))
624                .material(Material::lit(WALL_COLOR)),
625            );
626        }
627
628        let top_half = Vec3::new(COURT_HALF_WIDTH, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
629        ctx.draw(
630            Cube.at(Transform::from_scale_rotation_translation(
631                top_half * 2.0,
632                Quat::IDENTITY,
633                Vec3::new(0.0, top_half.y, -COURT_HALF_DEPTH + WALL_THICKNESS * 0.5),
634            ))
635            .material(Material::lit(WALL_COLOR)),
636        );
637    }
examples/ui-fonts.rs (line 664)
657    fn draw_station(&self, ctx: &mut FrameContext<'_, Self>, station: StationKind) {
658        let look = station.look();
659        let center = station.center();
660        let front_offset =
661            STATION_SIZE.z * 0.5 - STATION_FRONT_SIZE.z * 0.5 + STATION_FRONT_OUTWARD;
662        let front = center - Vec3::new(0.0, 0.0, front_offset);
663        for (size, position, material) in [
664            (STATION_SIZE, center, Material::lit(look.color)),
665            (
666                STATION_FRONT_SIZE,
667                front,
668                Material::color(Color::BLACK).emissive(look.glow),
669            ),
670        ] {
671            ctx.draw(
672                Cube.at(Transform::from_scale_rotation_translation(
673                    size,
674                    Quat::IDENTITY,
675                    position,
676                ))
677                .material(material),
678            );
679        }
680    }
681
682    fn draw_bracket(&self, ctx: &mut FrameContext<'_, Self>, camera: Camera, station: StationKind) {
683        let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
684        let window_size = ctx.window_size();
685        let Some(pixel) = camera.pixel_of(top, window_size) else {
686            return;
687        };
688        let at = logical(pixel, ctx.pixels_per_point());
689
690        let name = ctx.text_layout(station.look().name, egui::FontId::proportional(BODY_SIZE));
691        let (reading_text, number_text) = station.reading(self.elapsed.as_secs_f32());
692        let reading = ctx.text_layout(&reading_text, egui::FontId::monospace(BODY_SIZE));
693        let number = ctx.text_layout(
694            &number_text,
695            egui::FontId::new(NUMBER_SIZE, egui::FontFamily::Name(DISPLAY_FAMILY.into())),
696        );
697
698        ctx.ui(|ui| bracket(ui.painter(), at, name, reading, number));
699    }
700
701    /// A `Prompt` for `Trigger::Hail`, above every `StationKind` but
702    /// `hovered`: what a player presses to reach one, apart from a hover.
703    fn draw_prompts(
704        &self,
705        ctx: &mut FrameContext<'_, Self>,
706        camera: Camera,
707        hovered: Option<StationKind>,
708    ) {
709        let Some(binding) = ctx.bindings(Trigger::Hail).into_iter().next() else {
710            return;
711        };
712        let hint = prompt(&binding);
713        let glyph = ctx.text_layout(&hint.text(), egui::FontId::new(PROMPT_SIZE, hint.family()));
714        let window_size = ctx.window_size();
715        let pixels_per_point = ctx.pixels_per_point();
716
717        ctx.ui(|ui| {
718            let painter = ui.painter();
719            for station in StationKind::ALL {
720                if Some(station) == hovered {
721                    continue;
722                }
723                let top = station.center() + Vec3::Y * (STATION_SIZE.y * 0.5);
724                let Some(pixel) = camera.pixel_of(top, window_size) else {
725                    continue;
726                };
727                let at = logical(pixel, pixels_per_point);
728                let at = egui::pos2(at.x, at.y - PROMPT_LIFT);
729                prompt_at(painter, at, glyph.clone());
730            }
731        });
732    }
733
734    /// The title, a line and the reading, each in a font this game loaded
735    /// rather than egui's own.
736    fn panel(&self, ctx: &mut FrameContext<'_, Self>) {
737        ctx.ui(|ui| {
738            ui.label(styled(
739                "a game's own fonts",
740                egui::FontId::proportional(HEADING_SIZE),
741            ));
742            ui.label(styled(
743                "drawn in Pixel Operator, the game's proportional font",
744                egui::FontId::proportional(BODY_SIZE),
745            ));
746            ui.label(styled(
747                "the readings above each station in Pixel Operator Mono",
748                egui::FontId::monospace(BODY_SIZE),
749            ));
750        });
751    }
752
753    fn draw_dialogue(&self, ctx: &mut FrameContext<'_, Self>) {
754        let Some(dialogue) = &self.dialogue else {
755            return;
756        };
757        let whole = ctx.text_layout(
758            dialogue.current_line(),
759            egui::FontId::proportional(BODY_SIZE),
760        );
761        let size = whole.size();
762        ctx.ui(|ui| dialogue.draw(ui, size));
763    }
764
765    /// The `StationKind` under the pointer, `None` while the UI holds it.
766    fn hovered(ctx: &FrameContext<'_, Self>) -> Option<StationKind> {
767        if ctx.ui_wants_pointer() {
768            return None;
769        }
770        hit_station(
771            ctx.last_camera()
772                .ray_through(ctx.pointer(), ctx.window_size()),
773        )
774    }
775
776    /// A held [`Trigger::Hail`] turns the camera by the pointer's own
777    /// motion; the wheel zooms it.
778    fn steer(&mut self, ctx: &mut FrameContext<'_, Self>) {
779        if !ctx.ui_wants_pointer() && ctx.down(Trigger::Hail) {
780            self.orbit.turn(ctx.axis2(Turn::Look));
781        }
782        let wheel = ctx.axis(Zoom::Wheel);
783        if !ctx.ui_wants_pointer() && wheel != 0.0 {
784            self.orbit.zoom(ZOOM_STEP.powf(wheel));
785        }
786    }
787}
788
789impl Game for WatchRoom {
790    type Meshes = Shape;
791    type Sounds = NoSounds;
792    type InputActions = Controls;
793    type Skyboxes = Sky;
794    type SurfaceStyles = NoSurfaceStyles;
795    type PostEffects = NoPostEffects;
796
797    fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798        self.elapsed += ctx.dt();
799        self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801        if let Some(dialogue) = &mut self.dialogue {
802            dialogue.tick();
803        }
804        if ctx.pressed(Trigger::Close) {
805            self.dialogue = None;
806            self.hailed = None;
807        }
808        if ctx.pressed(Trigger::Sheet) {
809            self.sheet_open = !self.sheet_open;
810        }
811        if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812            self.handle_hail(ctx);
813        }
814    }
815
816    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
817        self.steer(ctx);
818
819        let camera = self.orbit.camera();
820        ctx.set_camera(camera);
821        ctx.set_skybox(Sky::Dusk);
822        ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
823
824        ctx.draw(
825            Plane
826                .at(Transform::from_scale(Vec3::new(
827                    PLATFORM_SIZE,
828                    1.0,
829                    PLATFORM_SIZE,
830                )))
831                .material(Material::lit(PLATFORM_COLOR)),
832        );
833        for station in StationKind::ALL {
834            self.draw_station(ctx, station);
835        }
836
837        let hovered = Self::hovered(ctx);
838        if !self.sheet_open {
839            if let Some(station) = hovered {
840                ctx.set_cursor(Cursor::Pointer);
841                self.draw_bracket(ctx, camera, station);
842            }
843            self.draw_prompts(ctx, camera, hovered);
844        }
845        if self.dialogue.is_some() {
846            self.draw_dialogue(ctx);
847        }
848        if self.sheet_open {
849            ctx.ui(sheet);
850        }
851        self.panel(ctx);
852    }
Source

pub fn shaded(color: Color, litness: f32) -> Self

A color between flat and lit by litness, a fraction clamped to 0.0..=1.0.

Examples found in repository?
examples/breakout-game.rs (line 654)
639    fn draw_bricks(&self, ctx: &mut FrameContext<'_, Breakout>) {
640        let scale = Vec3::new(
641            BRICK_HALF_WIDTH * 2.0,
642            BRICK_HALF_HEIGHT * 2.0,
643            BRICK_HALF_DEPTH * 2.0,
644        );
645        for brick in self.bricks.iter().filter(|brick| brick.hits_remaining > 0) {
646            let health = f32::from(brick.hits_remaining) / f32::from(BRICK_HITS);
647            let color = BRICK_ROW_COLORS[brick.row].dimmed(0.4 + 0.6 * health);
648            ctx.draw(
649                Cube.at(Transform::from_scale_rotation_translation(
650                    scale,
651                    Quat::IDENTITY,
652                    brick.position,
653                ))
654                .material(Material::shaded(color, health)),
655            );
656        }
657    }
More examples
Hide additional examples
examples/sprite-adventure.rs (line 1305)
1297    fn draw_pond(&self, ctx: &mut FrameContext<'_, Keep>) {
1298        ctx.draw(
1299            Plane
1300                .at(Transform::from_scale_rotation_translation(
1301                    Vec3::splat(POND_WATER_HALF * 2.0),
1302                    Quat::IDENTITY,
1303                    POND_CENTER,
1304                ))
1305                .material(Material::shaded(WATER_COLOR, WATER_LITNESS))
1306                .surface_style::<Water>(),
1307        );
1308        ctx.draw(
1309            Shore
1310                .at(Transform::from_scale_rotation_translation(
1311                    Vec3::splat(POND_HALF * 2.0),
1312                    Quat::IDENTITY,
1313                    Vec3::new(POND_CENTER.x, 0.0, POND_CENTER.z),
1314                ))
1315                .frame(Sheet::new(UVec2::new(POND_CELLS, 1)).cell(POND_SHORE_CELL)),
1316        );
1317    }
1318
1319    fn draw_crates(&self, ctx: &mut FrameContext<'_, Keep>) {
1320        for &(x, z, turn) in &CRATE_POSITIONS {
1321            ctx.draw(Crate.at(Transform::from_scale_rotation_translation(
1322                Vec3::splat(CRATE_SIZE),
1323                Quat::from_rotation_y(turn),
1324                Vec3::new(x, CRATE_SIZE * 0.5, z),
1325            )));
1326        }
1327    }
1328
1329    /// The well: its rim in grey masonry, and the mouth cell laid over the
1330    /// rim's top face.
1331    fn draw_well(&self, ctx: &mut FrameContext<'_, Keep>) {
1332        let cells = Sheet::new(UVec2::new(WELL_CELLS, 1));
1333        ctx.draw(
1334            Well.at(Transform::from_scale_rotation_translation(
1335                WELL_SIZE,
1336                Quat::IDENTITY,
1337                WELL_POSITION + Vec3::Y * (WELL_SIZE.y * 0.5),
1338            ))
1339            .frame(cells.cell(WELL_RIM_CELL)),
1340        );
1341        ctx.draw(
1342            WellMouth
1343                .at(Transform::from_scale_rotation_translation(
1344                    Vec3::new(WELL_SIZE.x, 1.0, WELL_SIZE.z),
1345                    Quat::IDENTITY,
1346                    WELL_POSITION + Vec3::Y * (WELL_SIZE.y + WELL_MOUTH_LIFT),
1347                ))
1348                .frame(cells.cell(WELL_MOUTH_CELL)),
1349        );
1350    }
1351
1352    fn draw_flora(&self, ctx: &mut FrameContext<'_, Keep>) {
1353        for &(x, z, rock) in &FLORA {
1354            let (width, height) = if rock {
1355                (ROCK_WIDTH, ROCK_HEIGHT)
1356            } else {
1357                (BUSH_WIDTH, BUSH_HEIGHT)
1358            };
1359            let standing = Transform::from_scale_rotation_translation(
1360                Vec3::new(width, height, width),
1361                Quat::IDENTITY,
1362                Vec3::new(x, height * 0.5, z),
1363            );
1364            let flora: Instance<Shape, _> = if rock {
1365                Rock.at(standing).into_set()
1366            } else {
1367                Bush.at(standing).into_set()
1368            };
1369            ctx.draw(flora.upright());
1370        }
1371    }
1372
1373    /// One stone box drawn on the ground at `at`, `size` across, sampling
1374    /// the part of the sheet `frame` covers.
1375    fn draw_stone(ctx: &mut FrameContext<'_, Keep>, at: Vec3, size: Vec3, frame: Frame) {
1376        ctx.draw(
1377            Stone
1378                .at(Transform::from_scale_rotation_translation(
1379                    size,
1380                    Quat::IDENTITY,
1381                    at + Vec3::Y * (size.y * 0.5),
1382                ))
1383                .frame(frame),
1384        );
1385    }
1386
1387    /// Two stone pillars drawn where `mouth` blocks the player, each a
1388    /// capital over its own course of masonry, and, on the one the camera
1389    /// looks into, the lintel across their tops and the dark filling
1390    /// the opening under it.
1391    fn draw_mouth(ctx: &mut FrameContext<'_, Keep>, mouth: Mouth) {
1392        for at in mouth.pillars() {
1393            Self::draw_stone(ctx, at, MOUTH_PILLAR_SIZE, Frame::default());
1394        }
1395        if !mouth.looked_into() {
1396            return;
1397        }
1398
1399        Self::draw_stone(
1400            ctx,
1401            mouth.at + Vec3::Y * MOUTH_PILLAR_SIZE.y,
1402            MOUTH_LINTEL_SIZE,
1403            masonry(MOUTH_LINTEL_TILES),
1404        );
1405        ctx.draw(
1406            Quad.at(Transform::from_scale_rotation_translation(
1407                Vec3::new(MOUTH_PILLAR_OFFSET * 2.0, MOUTH_DARK_HEIGHT, 1.0),
1408                Quat::IDENTITY,
1409                mouth.at + Vec3::Y * (MOUTH_DARK_HEIGHT * 0.5),
1410            ))
1411            .material(Material::color(Color::BLACK)),
1412        );
1413    }
1414
1415    fn draw_cave_floor(&self, ctx: &mut FrameContext<'_, Keep>) {
1416        let half = CAVE_HALF_WIDTH as i32;
1417        let near = CAVE_NEAR_Z as i32;
1418        let far = CAVE_FAR_Z as i32;
1419        for col in -half..=half {
1420            for row in far..=near {
1421                let variant = (col * 13 + row * 7).rem_euclid(CAVE_COLUMNS as i32) as u32;
1422                ctx.draw(
1423                    CaveFloor
1424                        .at(Vec3::new(
1425                            col as f32 * TILE_SIZE,
1426                            0.0,
1427                            row as f32 * TILE_SIZE,
1428                        ))
1429                        .frame(
1430                            Sheet::new(UVec2::new(CAVE_COLUMNS, CAVE_ROWS))
1431                                .cell_at(UVec2::new(variant, CAVE_FLOOR_ROW)),
1432                        ),
1433                );
1434            }
1435        }
1436    }
1437
1438    /// The wall drawn at `at` over the meters `standing`, in courses
1439    /// [`WALL_HEIGHT`] tall from the floor up, each cut to the part of it the
1440    /// span leaves; its faces are picked by `seed` and its stone faded to
1441    /// `fade`, which is `1.0` wherever it is solid.
1442    fn draw_wall(
1443        ctx: &mut FrameContext<'_, Keep>,
1444        at: Vec2,
1445        standing: Range<f32>,
1446        seed: i32,
1447        fade: f32,
1448    ) {
1449        for course in 0..WALL_COURSES {
1450            let base = course as f32 * WALL_HEIGHT;
1451            let low = (standing.start - base).max(0.0);
1452            let high = (standing.end - base).min(WALL_HEIGHT);
1453            if high <= low {
1454                continue;
1455            }
1456
1457            let variant = (seed + course).rem_euclid(CAVE_COLUMNS as i32) as u32;
1458            ctx.draw(
1459                CaveWall
1460                    .at(Transform::from_scale_rotation_translation(
1461                        Vec3::new(TILE_SIZE, high - low, TILE_SIZE),
1462                        Quat::IDENTITY,
1463                        Vec3::new(at.x, base + (low + high) * 0.5, at.y),
1464                    ))
1465                    .frame(cave_wall_face(variant, low..high))
1466                    .faded(fade),
1467            );
1468        }
1469    }
1470
1471    /// The room's two side walls and its back wall, full height, and the low
1472    /// wall closing its near end between the side walls and the mouth. The
1473    /// back wall stops short of the corners the side walls already fill, and
1474    /// the near one leaves the mouth's own tile open.
1475    fn draw_cave_walls(&self, ctx: &mut FrameContext<'_, Keep>) {
1476        let half = CAVE_HALF_WIDTH as i32 + 1;
1477        let near = CAVE_NEAR_Z as i32;
1478        let far = CAVE_FAR_Z as i32;
1479
1480        for row in far..=near {
1481            let z = row as f32 * TILE_SIZE;
1482            let west = Vec2::new(-half as f32 * TILE_SIZE, z);
1483            let east = Vec2::new(half as f32 * TILE_SIZE, z);
1484            Self::draw_wall(ctx, west, 0.0..WALL_TOP, row * 5, SOLID);
1485            Self::draw_wall(ctx, east, 0.0..WALL_TOP, row * 5 + 1, SOLID);
1486        }
1487        for col in (-half + 1)..half {
1488            let x = col as f32 * TILE_SIZE;
1489            let back = Vec2::new(x, far as f32 * TILE_SIZE);
1490            Self::draw_wall(ctx, back, 0.0..WALL_TOP, col * 5 + 2, SOLID);
1491            if col != 0 {
1492                let lip = Vec2::new(x, CAVE_LIP_Z);
1493                Self::draw_wall(ctx, lip, 0.0..CAVE_LIP_HEIGHT, col * 5 + 4, SOLID);
1494            }
1495        }
1496    }
1497
1498    /// The wall the door hangs in, run across the room between the side walls
1499    /// with one tile left open on the room's axis for the doorway and stone
1500    /// filling the column over the door. A player behind the wall is drawn
1501    /// through the stacks between them and the camera, at `seen_through`,
1502    /// faded by `ghost`; the rest of it stays solid, and keeps casting.
1503    fn draw_door_wall(ctx: &mut FrameContext<'_, Keep>, seen_through: Option<f32>, ghost: f32) {
1504        let stone = |x: f32| match seen_through {
1505            Some(at) if (x - at).abs() < GHOST_CORRIDOR_HALF => ghost_alpha(ghost),
1506            _ => SOLID,
1507        };
1508        let half = CAVE_HALF_WIDTH as i32;
1509
1510        for col in (-half..=half).filter(|&col| col != 0) {
1511            let x = col as f32 * TILE_SIZE;
1512            Self::draw_wall(
1513                ctx,
1514                Vec2::new(x, DOOR_Z),
1515                0.0..WALL_TOP,
1516                col * 5 + 3,
1517                stone(x),
1518            );
1519        }
1520        Self::draw_wall(
1521            ctx,
1522            Vec2::new(0.0, DOOR_Z),
1523            DOOR_HEIGHT..WALL_TOP,
1524            3,
1525            stone(0.0),
1526        );
1527    }
1528
1529    /// The two torches: an upright cutout post apiece, the flame's loop
1530    /// burning over its binding, and the light that flame casts.
1531    fn draw_torches(&self, ctx: &mut FrameContext<'_, Keep>) {
1532        let elapsed = self.simulated.as_secs_f32();
1533        let loop_cells = Sheet::new(UVec2::new(FLAME_CELLS, 1));
1534
1535        for (index, &(x, z)) in TORCH_POSITIONS.iter().enumerate() {
1536            let base = Vec3::new(x, 0.0, z);
1537            ctx.draw(
1538                Torch
1539                    .at(Transform::from_scale_rotation_translation(
1540                        Vec3::new(TORCH_SPRITE_WIDTH, TORCH_STAND_HEIGHT, 1.0),
1541                        Quat::IDENTITY,
1542                        base + Vec3::Y * (TORCH_STAND_HEIGHT * 0.5),
1543                    ))
1544                    .upright(),
1545            );
1546
1547            let phase = index as f32 * 2.1;
1548            let flicker = (elapsed * FLAME_FLICKER_SPEED + phase).sin();
1549            let flame_pos =
1550                base + Vec3::Y * (TORCH_STAND_HEIGHT + FLAME_LIFT + flicker * FLAME_BOB);
1551
1552            let light_pos = flame_pos + Vec3::new(0.0, TORCH_LIGHT_LIFT, TORCH_LIGHT_STANDOFF);
1553            ctx.light(Light::point(light_pos, TORCH_LIGHT_COLOR, TORCH_LIGHT_RANGE).shadow());
1554            // The pair burn an even share of the loop apart.
1555            let offset = index as u32 * FLAME_CELLS / TORCH_POSITIONS.len() as u32;
1556            ctx.draw(
1557                Flame
1558                    .at(Transform::from_scale_rotation_translation(
1559                        Vec3::splat(FLAME_SIZE),
1560                        Quat::IDENTITY,
1561                        flame_pos,
1562                    ))
1563                    .billboard()
1564                    .roll(flicker * FLAME_ROLL)
1565                    .frame(loop_cells.cell((elapsed * FLAME_RATE) as u32 + offset)),
1566            );
1567        }
1568    }
1569
1570    /// The door at its hinge — swung back against the wall once opened —
1571    /// drawn through alongside its wall, faded by `ghost`.
1572    fn draw_door(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1573        let fade = ghost_alpha(ghost);
1574        let swung = if self.door_opening {
1575            Quat::from_rotation_y(core::f32::consts::FRAC_PI_2)
1576        } else {
1577            Quat::IDENTITY
1578        };
1579
1580        ctx.draw(
1581            Door.at(Transform::from_rotation_translation(swung, DOOR_HINGE))
1582                .material(Material::shaded(DOOR_COLOR, DOOR_LITNESS))
1583                .faded(fade),
1584        );
1585    }
1586
1587    /// The posts and lintel framing the doorway, in a color the stone never
1588    /// is, standing clear of the wall so the opening reads as a door from
1589    /// across the chamber. Glowing of their own while the door is closed and
1590    /// within [`INTERACT_RADIUS`], the cue that it opens.
1591    fn draw_door_frame(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1592        let reachable =
1593            !self.door_opening && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1594        let material =
1595            Material::shaded(DOOR_FRAME_COLOR, DOOR_FRAME_LITNESS).emissive(if reachable {
1596                DOOR_FRAME_GLOW
1597            } else {
1598                Color::BLACK
1599            });
1600        let fade = ghost_alpha(ghost);
1601        let z = DOOR_WALL_NEAR_Z + DOOR_FRAME_STANDOFF;
1602        let jamb_height = DOOR_HEIGHT + DOOR_FRAME_THICKNESS;
1603
1604        for side in SIDES {
1605            ctx.draw(
1606                Cube.at(Transform::from_scale_rotation_translation(
1607                    Vec3::new(DOOR_FRAME_THICKNESS, jamb_height, DOOR_FRAME_THICKNESS),
1608                    Quat::IDENTITY,
1609                    Vec3::new(
1610                        side * (DOORWAY_HALF + DOOR_FRAME_THICKNESS * 0.5),
1611                        jamb_height * 0.5,
1612                        z,
1613                    ),
1614                ))
1615                .material(material)
1616                .faded(fade),
1617            );
1618        }
1619        ctx.draw(
1620            Cube.at(Transform::from_scale_rotation_translation(
1621                Vec3::new(
1622                    DOOR_WIDTH + DOOR_FRAME_THICKNESS * 2.0,
1623                    DOOR_FRAME_THICKNESS,
1624                    DOOR_FRAME_THICKNESS,
1625                ),
1626                Quat::IDENTITY,
1627                Vec3::new(0.0, DOOR_HEIGHT + DOOR_FRAME_THICKNESS * 0.5, z),
1628            ))
1629            .material(material)
1630            .faded(fade),
1631        );
1632    }
1633
1634    /// A world prompt over the door: what opens it while the player is
1635    /// within [`INTERACT_RADIUS`] and it is closed, and that it swings while
1636    /// it does; gone once it has swung [`DOOR_SWING_TICKS`]. Laid out and
1637    /// placed like `examples/animation.rs`'s own prompt.
1638    fn draw_door_prompt(&self, ctx: &mut FrameContext<'_, Keep>, camera: Camera) {
1639        let near = self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1640        let swinging = self.door_opening && self.swing_ticks < DOOR_SWING_TICKS;
1641        let text = if swinging {
1642            "opening"
1643        } else if near && !self.door_opening {
1644            "e opens the door"
1645        } else {
1646            return;
1647        };
1648
1649        let galley = ctx.text_layout(text, egui::FontId::proportional(DOOR_PROMPT_SIZE));
1650        let point = INTERACT_POINT + Vec3::Y * (DOOR_HEIGHT + DOOR_PROMPT_LIFT);
1651        let window_size = ctx.window_size();
1652        let pixels_per_point = ctx.pixels_per_point();
1653        let Some(pixel) = camera.pixel_of(point, window_size) else {
1654            return;
1655        };
1656
1657        ctx.ui(|ui| {
1658            let painter = ui.painter();
1659            let at = logical(pixel, pixels_per_point);
1660            let ink = galley.mesh_bounds;
1661            let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
1662            let backdrop = egui::Rect::from_center_size(
1663                at,
1664                ink.size() + egui::Vec2::splat(DOOR_PROMPT_PADDING * 2.0),
1665            );
1666            painter.rect_filled(
1667                backdrop,
1668                DOOR_PROMPT_PADDING,
1669                egui::Color32::from_black_alpha(DOOR_PROMPT_BACKDROP),
1670            );
1671            painter.galley(pos, galley, DOOR_PROMPT_COLOR);
1672        });
1673    }
1674
1675    /// The gem, spinning and bobbing over the chamber's floor, and the light
1676    /// it casts over it.
1677    fn draw_gem(&self, ctx: &mut FrameContext<'_, Keep>) {
1678        let t = self.simulated.as_secs_f32();
1679        let bob = (t * 2.0).sin() * GEM_BOB_HEIGHT;
1680        ctx.light(
1681            Light::point(
1682                GEM_POSITION + Vec3::Y * (bob + GEM_LIGHT_LIFT),
1683                GEM_LIGHT_COLOR,
1684                GEM_LIGHT_RANGE,
1685            )
1686            .shadow(),
1687        );
1688        ctx.draw(
1689            Gem.at(Transform::from_scale_rotation_translation(
1690                Vec3::ONE,
1691                Quat::from_rotation_y(t * GEM_SPIN_SPEED),
1692                GEM_POSITION + Vec3::Y * bob,
1693            ))
1694            .material(Material::shaded(GEM_COLOR, 0.7).emissive(GEM_COLOR.dimmed(1.6))),
1695        );
1696    }
examples/sound-lab.rs (line 577)
547    fn draw_sources(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
548        for (index, source) in self.sources.iter().enumerate() {
549            let color = SOURCE_COLORS[index];
550            let picked_up = self.dragging == Some(index);
551            let scale = if picked_up { 1.3 } else { 1.0 };
552            let emissive = if source.enabled {
553                Color::rgb(color.red * 3.0, color.green * 3.0, color.blue * 3.0)
554            } else {
555                color.dimmed(0.15)
556            };
557
558            for (radius, ring_color) in [
559                (source.range, RANGE_COLOR),
560                (source.reference, REFERENCE_COLOR),
561            ] {
562                ctx.draw(
563                    Ring.at(Transform::from_scale_rotation_translation(
564                        Vec3::new(radius, 1.0, radius),
565                        Quat::IDENTITY,
566                        Vec3::new(source.position.x, 0.01, source.position.z),
567                    ))
568                    .material(Material::color(ring_color)),
569                );
570            }
571            ctx.draw(
572                Cube.at(Transform::from_scale_rotation_translation(
573                    Vec3::splat(SOURCE_HALF * 2.0 * scale),
574                    Quat::IDENTITY,
575                    source.position,
576                ))
577                .material(Material::shaded(color, 0.6).emissive(emissive)),
578            );
579        }
580    }
Source

pub const fn emissive(self, color: Color) -> Self

Adds light of its own to the surface; Color::BLACK by default.

Required if you want a surface bright on its own: values past 1.0 are what FrameContext::set_bloom spreads. In the transparent pass the tint’s alpha scales everything the surface draws, this light too — fade one or the other, not both.

Examples found in repository?
examples/material-playground.rs (line 300)
299fn emissive_material() -> Material {
300    Material::color(EMISSIVE_BASE).emissive(EMISSIVE_GLOW)
301}
More examples
Hide additional examples
examples/breakout-game.rs (line 601)
593    fn paddle_face_material(&self) -> Material {
594        let t = (self.paddle_flash / PADDLE_FLASH).clamp(0.0, 1.0);
595        let flash = PADDLE_FLASH_EMISSIVE.dimmed(t);
596        let emissive = Color::rgb(
597            PADDLE_AMBIENT_EMISSIVE.red + flash.red,
598            PADDLE_AMBIENT_EMISSIVE.green + flash.green,
599            PADDLE_AMBIENT_EMISSIVE.blue + flash.blue,
600        );
601        Material::lit(PADDLE_BASE).emissive(emissive)
602    }
603
604    fn draw_court(&self, ctx: &mut FrameContext<'_, Breakout>) {
605        ctx.draw(
606            Plane
607                .at(Transform::from_scale(Vec3::new(
608                    COURT_HALF_WIDTH * 2.0,
609                    1.0,
610                    COURT_HALF_DEPTH * 2.0,
611                )))
612                .material(Material::lit(FLOOR_COLOR)),
613        );
614
615        let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, COURT_HALF_DEPTH);
616        for side in [-1.0, 1.0] {
617            let x = side * (COURT_HALF_WIDTH - WALL_THICKNESS * 0.5);
618            ctx.draw(
619                Cube.at(Transform::from_scale_rotation_translation(
620                    side_half * 2.0,
621                    Quat::IDENTITY,
622                    Vec3::new(x, side_half.y, 0.0),
623                ))
624                .material(Material::lit(WALL_COLOR)),
625            );
626        }
627
628        let top_half = Vec3::new(COURT_HALF_WIDTH, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
629        ctx.draw(
630            Cube.at(Transform::from_scale_rotation_translation(
631                top_half * 2.0,
632                Quat::IDENTITY,
633                Vec3::new(0.0, top_half.y, -COURT_HALF_DEPTH + WALL_THICKNESS * 0.5),
634            ))
635            .material(Material::lit(WALL_COLOR)),
636        );
637    }
638
639    fn draw_bricks(&self, ctx: &mut FrameContext<'_, Breakout>) {
640        let scale = Vec3::new(
641            BRICK_HALF_WIDTH * 2.0,
642            BRICK_HALF_HEIGHT * 2.0,
643            BRICK_HALF_DEPTH * 2.0,
644        );
645        for brick in self.bricks.iter().filter(|brick| brick.hits_remaining > 0) {
646            let health = f32::from(brick.hits_remaining) / f32::from(BRICK_HITS);
647            let color = BRICK_ROW_COLORS[brick.row].dimmed(0.4 + 0.6 * health);
648            ctx.draw(
649                Cube.at(Transform::from_scale_rotation_translation(
650                    scale,
651                    Quat::IDENTITY,
652                    brick.position,
653                ))
654                .material(Material::shaded(color, health)),
655            );
656        }
657    }
658
659    /// Draws the live spark burst: additive, tumbling by roll as they age,
660    /// shrinking and fading out over their lifetime.
661    fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662        for spark in &self.sparks {
663            let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664            let fade = 1.0 - age;
665            let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666            ctx.draw(
667                Quad.at(Transform::from_scale_rotation_translation(
668                    Vec3::splat(size),
669                    Quat::IDENTITY,
670                    spark.position,
671                ))
672                .billboard()
673                .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674                .material(
675                    Material::color(spark.color.with_alpha(fade))
676                        .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677                        .additive(),
678                ),
679            );
680        }
681    }
682
683    /// Draws the ball's ghost trail, each ghost smaller and more transparent
684    /// than the one ahead of it; each ghost's position interpolates between
685    /// its own last two resolved ticks by the same `alpha` the ball itself
686    /// draws at, and its radius clamps to what the ball's own radius has
687    /// left over its distance from the head, so a ghost still close to the
688    /// ball never draws past its edge.
689    fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690        let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691        for i in 0..TRAIL_LEN {
692            let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693            let age = (i + 1) as f32 / TRAIL_LEN as f32;
694            let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695            let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696                .min((BALL_RADIUS - head.distance(position)).max(0.0));
697            let scale = Vec3::splat(radius * 2.0);
698            ctx.draw(
699                Sphere { subdivisions: 2 }
700                    .at(Transform::from_scale_rotation_translation(
701                        scale,
702                        Quat::IDENTITY,
703                        position,
704                    ))
705                    .material(
706                        Material::color(BALL_GLOW.with_alpha(fade))
707                            .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708                    ),
709            );
710        }
711    }
712
713    /// Draws one held ball for every life past the one in play, set in a
714    /// row alongside the paddle's own path.
715    fn draw_lives(&self, ctx: &mut FrameContext<'_, Breakout>) {
716        let held_lives = self.lives.saturating_sub(1);
717        for slot in 0..held_lives {
718            let z = PADDLE_Z + (slot + 1) as f32 * LIFE_ROW_SPACING;
719            ctx.draw(
720                Sphere { subdivisions: 2 }
721                    .at(Transform::from_scale_rotation_translation(
722                        Vec3::splat(BALL_RADIUS * 2.0),
723                        Quat::IDENTITY,
724                        Vec3::new(LIFE_ROW_X, BALL_RADIUS, z),
725                    ))
726                    .material(
727                        Material::color(BALL_GLOW)
728                            .emissive(BALL_EMISSIVE)
729                            .additive(),
730                    ),
731            );
732        }
733    }
734
735    fn overlay(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
736        let bricks_left = self
737            .bricks
738            .iter()
739            .filter(|brick| brick.hits_remaining > 0)
740            .count();
741        // Read before `ctx.ui` so a rebind changes what the hint reads this
742        // frame too.
743        let move_hint = bindings_text(ctx.bindings(Move::Paddle));
744        let pause_hint = bindings_text(ctx.bindings(Button::Pause));
745        let serve_hint = bindings_text(ctx.bindings(Button::Serve));
746        ctx.ui(|ui| {
747            ui.horizontal(|ui| {
748                ui.label(egui::RichText::new(format!("score {}", self.score)).size(32.0));
749                ui.label(format!("{bricks_left} bricks left"));
750            });
751            ui.label(format!("move: {move_hint} · {pause_hint} to pause"));
752            if self.phase == Phase::Serving {
753                ui.label(format!("{serve_hint} to serve"));
754            }
755        });
756
757        match self.phase {
758            Phase::Serving | Phase::Playing if self.paused => self.menu(ctx, "paused", false),
759            Phase::Won => self.menu(ctx, "you win", true),
760            Phase::Lost => self.menu(ctx, "game over", true),
761            _ => {}
762        }
763    }
764
765    fn menu(&mut self, ctx: &mut FrameContext<'_, Breakout>, title: &str, over: bool) {
766        let mut clicked = false;
767        let mut quit = false;
768
769        // `ctx.ui` cannot borrow `ctx`, so anything the controls list needs is
770        // read first and applied after.
771        let buttons: Vec<(Button, String)> = Button::all()
772            .into_iter()
773            .map(|action| (action, bindings_text(ctx.bindings(action))))
774            .collect();
775        let axes: Vec<(Move, String)> = Move::all()
776            .into_iter()
777            .map(|action| (action, bindings_text(ctx.bindings(action))))
778            .collect();
779        let listening = self.listening;
780        let actuated_button = (!ctx.ui_wants_keyboard())
781            .then(|| ctx.actuated_button())
782            .flatten();
783        let actuated_axis = (!ctx.ui_wants_keyboard())
784            .then(|| ctx.actuated_axis())
785            .flatten();
786        let mut reset = None;
787
788        ctx.ui(|ui| {
789            egui::Window::new(title)
790                .collapsible(false)
791                .resizable(false)
792                .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
793                .show(ui.ctx(), |ui| {
794                    if over {
795                        ui.label(format!("score {}", self.score));
796                    }
797                    if !over {
798                        ui.add(
799                            egui::Slider::new(&mut self.master_volume, 0.0..=1.0).text("volume"),
800                        );
801                        if ui.button("resume").clicked() {
802                            self.paused = false;
803                            clicked = true;
804                        }
805                        ui.separator();
806                        ui.heading("controls");
807                        for (action, text) in &buttons {
808                            controls_row(
809                                ui,
810                                action.name(),
811                                text,
812                                listening == Some(Listening::Button(*action)),
813                                &mut self.listening,
814                                Listening::Button(*action),
815                                &mut reset,
816                            );
817                        }
818                        for (action, text) in &axes {
819                            controls_row(
820                                ui,
821                                action.name(),
822                                text,
823                                listening == Some(Listening::Move(*action)),
824                                &mut self.listening,
825                                Listening::Move(*action),
826                                &mut reset,
827                            );
828                        }
829                    }
830                    if ui.button("restart").clicked() {
831                        self.restart();
832                        clicked = true;
833                    }
834                    if ui.button("quit").clicked() {
835                        quit = true;
836                    }
837                });
838        });
839
840        match (self.listening, actuated_button, actuated_axis) {
841            (Some(Listening::Button(action)), Some(binding), _) => {
842                ctx.rebind(action, vec![binding]);
843                self.listening = None;
844            }
845            (Some(Listening::Move(action)), _, Some(binding)) => {
846                ctx.rebind(action, vec![binding]);
847                self.listening = None;
848            }
849            _ => {}
850        }
851        match reset {
852            Some(Listening::Button(action)) => ctx.rebind(action, action.bindings()),
853            Some(Listening::Move(action)) => ctx.rebind(action, action.bindings()),
854            None => {}
855        }
856
857        if clicked {
858            ctx.play(Sound::Click);
859        }
860        if quit {
861            ctx.close();
862        }
863    }
864
865    /// Sustains both tracks every frame, and the gain goes to whichever the
866    /// game calls for: gameplay music while a round is live, serving
867    /// included, and menu music whenever a menu covers it.
868    ///
869    /// Each fades in over [`MUSIC_CROSSFADE`] and slides every later gain
870    /// over it, which is the crossfade itself; the one at no gain costs no
871    /// voice while its playback goes on under the other.
872    fn sustain_music(&self, ctx: &mut FrameContext<'_, Breakout>) {
873        let playing = !self.paused && matches!(self.phase, Phase::Serving | Phase::Playing);
874        let gain = |wanted: bool| match wanted {
875            true => MUSIC_GAIN,
876            false => 0.0,
877        };
878
879        ctx.sustain(
880            Sound::Music
881                .gain(gain(playing))
882                .fade(MUSIC_CROSSFADE)
883                .glide(MUSIC_CROSSFADE)
884                .loop_from(MUSIC_LOOP_FROM),
885        );
886        ctx.sustain(
887            Sound::MenuMusic
888                .gain(gain(!playing))
889                .fade(MUSIC_CROSSFADE)
890                .glide(MUSIC_CROSSFADE)
891                .loop_from(MENU_MUSIC_LOOP_FROM),
892        );
893    }
894}
895
896/// One action's name, its live bindings, a rebind control that starts
897/// listening for a new one, and a reset to its defaults; cancel is a
898/// button rather than Escape, since Escape is itself a binding a listen
899/// could capture.
900fn controls_row(
901    ui: &mut egui::Ui,
902    name: &str,
903    bindings: &str,
904    listening: bool,
905    target: &mut Option<Listening>,
906    action: Listening,
907    reset: &mut Option<Listening>,
908) {
909    ui.horizontal(|ui| {
910        ui.label(format!("{name}: {bindings}"));
911        if listening {
912            ui.label("listening");
913            if ui.button("cancel").clicked() {
914                *target = None;
915            }
916        } else if ui.button("rebind").clicked() {
917            *target = Some(action);
918        }
919        if ui.button("reset").clicked() {
920            *reset = Some(action);
921        }
922    });
923}
924
925/// The controls-menu text for a live binding list: each alternative,
926/// separated, in the order the player can use them.
927fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
928    bindings
929        .iter()
930        .map(ToString::to_string)
931        .collect::<Vec<_>>()
932        .join(", ")
933}
934
935fn spawn_bricks() -> Vec<Brick> {
936    let cell = BRICK_HALF_WIDTH * 2.0 + BRICK_GAP;
937    let row_span = BRICK_HALF_DEPTH * 2.0 + BRICK_ROW_GAP;
938    let grid_width = cell * BRICK_COLUMNS as f32 - BRICK_GAP;
939    let start_x = -grid_width * 0.5 + BRICK_HALF_WIDTH;
940    let start_z = -COURT_HALF_DEPTH + WALL_THICKNESS + BRICK_HALF_DEPTH + 0.6;
941
942    (0..BRICK_ROWS)
943        .flat_map(|row| {
944            (0..BRICK_COLUMNS).map(move |column| Brick {
945                row,
946                position: Vec3::new(
947                    start_x + column as f32 * cell,
948                    BRICK_HALF_HEIGHT,
949                    start_z + row as f32 * row_span,
950                ),
951                hits_remaining: BRICK_HITS,
952            })
953        })
954        .collect()
955}
956
957impl Game for Breakout {
958    type Meshes = Shape;
959    type Sounds = Sound;
960    type InputActions = Controls;
961    type Skyboxes = NoSkyboxes;
962    type SurfaceStyles = NoSurfaceStyles;
963    type PostEffects = NoPostEffects;
964
965    fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966        if self.paused {
967            return;
968        }
969
970        let dt = ctx.dt().as_secs_f32();
971        self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972        self.brick_flash = (self.brick_flash - dt).max(0.0);
973        self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974        self.step_sparks(dt);
975
976        // Decay runs before the end-screen return below, so the last pulse and
977        // burst do not stay on screen.
978        if matches!(self.phase, Phase::Won | Phase::Lost) {
979            return;
980        }
981
982        let axis = if ctx.ui_wants_keyboard() {
983            0.0
984        } else {
985            ctx.axis(Move::Paddle)
986        };
987        self.step_paddle(axis, dt);
988
989        match self.phase {
990            Phase::Serving => self.hold_ball(ctx),
991            _ => self.step_ball(ctx, dt),
992        }
993    }
994
995    fn frame(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
996        if matches!(self.phase, Phase::Serving | Phase::Playing) && ctx.pressed(Button::Pause) {
997            self.paused = !self.paused;
998        }
999
1000        ctx.set_volume(self.master_volume);
1001        self.sustain_music(ctx);
1002
1003        ctx.set_camera(Self::camera());
1004
1005        let brick_pulse = (self.brick_flash / BRICK_FLASH).clamp(0.0, 1.0);
1006        ctx.set_bloom((BLOOM_BASE + brick_pulse * BLOOM_PULSE_PEAK).clamp(0.0, 1.0));
1007
1008        let life_lost_t = (self.life_lost_flash / LIFE_LOST_FLASH).clamp(0.0, 1.0);
1009        ctx.set_exposure((1.0 - life_lost_t * EXPOSURE_DIP_DEPTH).clamp(0.0, 1.0));
1010
1011        // The tick moves nothing behind a menu, so a frame there draws the last
1012        // step whole rather than interpolating from the one before.
1013        let alpha = match self.phase {
1014            Phase::Serving | Phase::Playing if !self.paused => ctx.alpha(),
1015            _ => 1.0,
1016        };
1017        let paddle_x = self.paddle_prev_x.lerp(self.paddle_x, alpha);
1018        let ball_pos = self.ball_prev.lerp(self.ball_pos, alpha);
1019
1020        ctx.light(Light::point(ball_pos, BALL_GLOW, BALL_LIGHT_RANGE).shadow());
1021
1022        self.draw_court(ctx);
1023        self.draw_bricks(ctx);
1024        self.draw_sparks(ctx);
1025        self.draw_lives(ctx);
1026
1027        ctx.draw(
1028            Paddle
1029                .at(Transform::from_translation(Vec3::new(
1030                    paddle_x,
1031                    PADDLE_HALF_HEIGHT,
1032                    PADDLE_Z,
1033                )))
1034                .material_of(PaddlePart::Face, self.paddle_face_material()),
1035        );
1036
1037        self.draw_trail(ctx, alpha);
1038        ctx.draw(
1039            Sphere { subdivisions: 2 }
1040                .at(Transform::from_scale_rotation_translation(
1041                    Vec3::splat(BALL_RADIUS * 2.0),
1042                    Quat::IDENTITY,
1043                    ball_pos,
1044                ))
1045                .material(
1046                    Material::color(BALL_GLOW)
1047                        .emissive(BALL_EMISSIVE)
1048                        .additive(),
1049                ),
1050        );
1051
1052        self.overlay(ctx);
1053    }
examples/ui-fonts.rs (line 668)
657    fn draw_station(&self, ctx: &mut FrameContext<'_, Self>, station: StationKind) {
658        let look = station.look();
659        let center = station.center();
660        let front_offset =
661            STATION_SIZE.z * 0.5 - STATION_FRONT_SIZE.z * 0.5 + STATION_FRONT_OUTWARD;
662        let front = center - Vec3::new(0.0, 0.0, front_offset);
663        for (size, position, material) in [
664            (STATION_SIZE, center, Material::lit(look.color)),
665            (
666                STATION_FRONT_SIZE,
667                front,
668                Material::color(Color::BLACK).emissive(look.glow),
669            ),
670        ] {
671            ctx.draw(
672                Cube.at(Transform::from_scale_rotation_translation(
673                    size,
674                    Quat::IDENTITY,
675                    position,
676                ))
677                .material(material),
678            );
679        }
680    }
examples/isometric-board.rs (line 604)
583    fn draw_sprite(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
584        let position = self.sprite.previous.lerp(self.sprite.position, ctx.alpha());
585        let current = self.turn == Turn::Sprite;
586        let (tint, glow) = if current && self.selected {
587            (SELECTED_TINT, SELECTED_GLOW)
588        } else if current && hover == Hover::CurrentUnit {
589            (HOVER_TINT, HOVER_GLOW)
590        } else if current {
591            (TURN_TINT, TURN_GLOW)
592        } else {
593            (Color::WHITE, Color::BLACK)
594        };
595        ctx.draw(
596            Sprite
597                .at(Transform::from_scale_rotation_translation(
598                    Vec3::new(SPRITE_WIDTH, SPRITE_HEIGHT, 1.0),
599                    Quat::IDENTITY,
600                    position,
601                ))
602                .upright()
603                .frame(sprite_frame(self.sprite.facing_right))
604                .material(Material::lit(tint).cutout().emissive(glow)),
605        );
606    }
607
608    fn draw_block(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
609        let position = self.block.previous.lerp(self.block.position, ctx.alpha());
610        let current = self.turn == Turn::Block;
611        let (color, glow) = if current && self.selected {
612            (SELECTED_TINT, SELECTED_GLOW)
613        } else if current && hover == Hover::CurrentUnit {
614            (HOVER_TINT, HOVER_GLOW)
615        } else if current {
616            (BLOCK_TURN, TURN_GLOW)
617        } else {
618            (BLOCK_IDLE, Color::BLACK)
619        };
620        ctx.draw(
621            Cube.at(Transform::from_scale_rotation_translation(
622                Vec3::splat(BLOCK_SIZE),
623                Quat::IDENTITY,
624                position,
625            ))
626            .material(Material::lit(color).emissive(glow)),
627        );
628    }
examples/post-effects.rs (line 108)
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    }
examples/sound-lab.rs (line 577)
547    fn draw_sources(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
548        for (index, source) in self.sources.iter().enumerate() {
549            let color = SOURCE_COLORS[index];
550            let picked_up = self.dragging == Some(index);
551            let scale = if picked_up { 1.3 } else { 1.0 };
552            let emissive = if source.enabled {
553                Color::rgb(color.red * 3.0, color.green * 3.0, color.blue * 3.0)
554            } else {
555                color.dimmed(0.15)
556            };
557
558            for (radius, ring_color) in [
559                (source.range, RANGE_COLOR),
560                (source.reference, REFERENCE_COLOR),
561            ] {
562                ctx.draw(
563                    Ring.at(Transform::from_scale_rotation_translation(
564                        Vec3::new(radius, 1.0, radius),
565                        Quat::IDENTITY,
566                        Vec3::new(source.position.x, 0.01, source.position.z),
567                    ))
568                    .material(Material::color(ring_color)),
569                );
570            }
571            ctx.draw(
572                Cube.at(Transform::from_scale_rotation_translation(
573                    Vec3::splat(SOURCE_HALF * 2.0 * scale),
574                    Quat::IDENTITY,
575                    source.position,
576                ))
577                .material(Material::shaded(color, 0.6).emissive(emissive)),
578            );
579        }
580    }
Source

pub fn roughness(self, roughness: f32) -> Self

The surface’s roughness, a fraction clamped to 0.0..=1.0 and 1.0 by default: the factor a .glb material declares, held per draw.

Examples found in repository?
examples/material-playground.rs (line 292)
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    }
More examples
Hide additional examples
examples/stress-preview.rs (line 399)
394    fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
395        let side = (FIELD_RADIUS + FIELD_INNER_RADIUS) * 2.2;
396        ctx.draw(
397            Plane
398                .at(Transform::from_scale(Vec3::new(side, 1.0, side)))
399                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
400        );
401    }
examples/flock-parallelism.rs (line 668)
660    fn draw_ground(ctx: &mut FrameContext<'_, Self>) {
661        ctx.draw(
662            Plane
663                .at(Transform::from_scale(Vec3::new(
664                    GROUND_SIZE,
665                    1.0,
666                    GROUND_SIZE,
667                )))
668                .material(Material::lit(GROUND_COLOR).roughness(0.9)),
669        );
670    }
Source

pub fn metallic(self, metallic: f32) -> Self

The surface’s metallic, a fraction clamped to 0.0..=1.0 and 0.0 by default: the factor a .glb material declares, held per draw.

Examples found in repository?
examples/material-playground.rs (line 292)
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    }
Source

pub const fn cutout(self) -> Self

Drops the texels where tint × texture alpha lands under 0.5, and draws the rest as opaque.

Required if you want a sprite drawn with no blending at its edges: a cutout draw writes depth and is not sorted. A tint alpha under 1.0 still draws it in the transparent pass, where the same texels are dropped.

Examples found in repository?
examples/sprite-adventure.rs (line 586)
582    fn build(&self, assets: &Assets) -> MeshData {
583        Plane
584            .build(assets)
585            .with_texture(assets.texture(POND_SHEET).pixelated())
586            .with_material(Material::lit(Color::WHITE).cutout())
587    }
588}
589
590/// A crate prop, its texture drawn over a cube.
591#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
592struct Crate;
593
594impl Mesh for Crate {
595    fn build(&self, assets: &Assets) -> MeshData {
596        Cube.build(assets)
597            .with_texture(assets.texture(CRATE_TEXTURE).pixelated())
598    }
599}
600
601/// The well's rim.
602#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
603struct Well;
604
605impl Mesh for Well {
606    fn build(&self, assets: &Assets) -> MeshData {
607        Cube.build(assets)
608            .with_texture(assets.texture(WELL_SHEET).pixelated())
609    }
610}
611
612/// The well's mouth, laid flat over the rim's top face.
613#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
614struct WellMouth;
615
616impl Mesh for WellMouth {
617    fn build(&self, assets: &Assets) -> MeshData {
618        Plane
619            .build(assets)
620            .with_texture(assets.texture(WELL_SHEET).pixelated())
621    }
622}
623
624/// A stone box: the mouth's pillars and lintel.
625#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
626struct Stone;
627
628impl Mesh for Stone {
629    fn build(&self, assets: &Assets) -> MeshData {
630        Cube.build(assets)
631            .with_texture(assets.texture(STONE_SHEET).pixelated())
632    }
633}
634
635/// A bush sprite, cutout with its own relief.
636#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
637struct Bush;
638
639impl Mesh for Bush {
640    fn build(&self, assets: &Assets) -> MeshData {
641        Quad.build(assets)
642            .with_texture(assets.texture(BUSH_SPRITE).pixelated())
643            .with_relief(assets.relief(BUSH_RELIEF))
644            .with_material(Material::lit(Color::WHITE).cutout())
645    }
646}
647
648/// A rock sprite, cutout with its own relief.
649#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
650struct Rock;
651
652impl Mesh for Rock {
653    fn build(&self, assets: &Assets) -> MeshData {
654        Quad.build(assets)
655            .with_texture(assets.texture(ROCK_SPRITE).pixelated())
656            .with_relief(assets.relief(ROCK_RELIEF))
657            .with_material(Material::lit(Color::WHITE).cutout())
658    }
659}
660
661/// A torch's post sprite, cutout with its own relief.
662#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
663struct Torch;
664
665impl Mesh for Torch {
666    fn build(&self, assets: &Assets) -> MeshData {
667        Quad.build(assets)
668            .with_texture(assets.texture(TORCH_SPRITE).pixelated())
669            .with_relief(assets.relief(TORCH_RELIEF))
670            .with_material(Material::lit(Color::WHITE).cutout())
671    }
672}
673
674/// A torch's flame sprite, added over the dark rather than lit.
675#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
676struct Flame;
677
678impl Mesh for Flame {
679    fn build(&self, assets: &Assets) -> MeshData {
680        Quad.build(assets)
681            .with_texture(assets.texture(FLAME_SHEET).pixelated())
682            .with_material(Material::color(FLAME_TINT).additive())
683    }
684}
685
686/// The player's sprite, cutout with its own relief, its sheet shared
687/// with `examples/isometric-board.rs`.
688#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
689struct Walker;
690
691impl Mesh for Walker {
692    fn build(&self, assets: &Assets) -> MeshData {
693        Quad.build(assets)
694            .with_texture(assets.texture(WALKER_SHEET).pixelated())
695            .with_relief(assets.relief(WALKER_RELIEF))
696            .with_material(Material::lit(Color::WHITE).cutout())
697    }
More examples
Hide additional examples
examples/isometric-board.rs (line 604)
583    fn draw_sprite(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
584        let position = self.sprite.previous.lerp(self.sprite.position, ctx.alpha());
585        let current = self.turn == Turn::Sprite;
586        let (tint, glow) = if current && self.selected {
587            (SELECTED_TINT, SELECTED_GLOW)
588        } else if current && hover == Hover::CurrentUnit {
589            (HOVER_TINT, HOVER_GLOW)
590        } else if current {
591            (TURN_TINT, TURN_GLOW)
592        } else {
593            (Color::WHITE, Color::BLACK)
594        };
595        ctx.draw(
596            Sprite
597                .at(Transform::from_scale_rotation_translation(
598                    Vec3::new(SPRITE_WIDTH, SPRITE_HEIGHT, 1.0),
599                    Quat::IDENTITY,
600                    position,
601                ))
602                .upright()
603                .frame(sprite_frame(self.sprite.facing_right))
604                .material(Material::lit(tint).cutout().emissive(glow)),
605        );
606    }
Source

pub const fn additive(self) -> Self

Adds what the draw would be to what is behind it, instead of drawing over it.

Required if you want a draw that only ever adds light and never darkens what it covers: it is drawn after the transparent pass, in the order it was submitted, is never written to depth, and casts no shadow. The tint’s alpha scales everything it adds — the emissive light too: fade one or the other, not both. The texture’s alpha scales each texel the same way: an empty texel adds nothing, and one half covered adds half of what it holds. A tint past 1.0 scales what the texture holds past it: the draw adds light in the shape and color of its own texture. A flat emissive adds one color over every texel instead. A material that also set cutout drops nothing.

Examples found in repository?
examples/sprite-adventure.rs (line 682)
679    fn build(&self, assets: &Assets) -> MeshData {
680        Quad.build(assets)
681            .with_texture(assets.texture(FLAME_SHEET).pixelated())
682            .with_material(Material::color(FLAME_TINT).additive())
683    }
More examples
Hide additional examples
examples/breakout-game.rs (line 677)
661    fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662        for spark in &self.sparks {
663            let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664            let fade = 1.0 - age;
665            let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666            ctx.draw(
667                Quad.at(Transform::from_scale_rotation_translation(
668                    Vec3::splat(size),
669                    Quat::IDENTITY,
670                    spark.position,
671                ))
672                .billboard()
673                .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674                .material(
675                    Material::color(spark.color.with_alpha(fade))
676                        .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677                        .additive(),
678                ),
679            );
680        }
681    }
682
683    /// Draws the ball's ghost trail, each ghost smaller and more transparent
684    /// than the one ahead of it; each ghost's position interpolates between
685    /// its own last two resolved ticks by the same `alpha` the ball itself
686    /// draws at, and its radius clamps to what the ball's own radius has
687    /// left over its distance from the head, so a ghost still close to the
688    /// ball never draws past its edge.
689    fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690        let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691        for i in 0..TRAIL_LEN {
692            let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693            let age = (i + 1) as f32 / TRAIL_LEN as f32;
694            let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695            let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696                .min((BALL_RADIUS - head.distance(position)).max(0.0));
697            let scale = Vec3::splat(radius * 2.0);
698            ctx.draw(
699                Sphere { subdivisions: 2 }
700                    .at(Transform::from_scale_rotation_translation(
701                        scale,
702                        Quat::IDENTITY,
703                        position,
704                    ))
705                    .material(
706                        Material::color(BALL_GLOW.with_alpha(fade))
707                            .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708                    ),
709            );
710        }
711    }
712
713    /// Draws one held ball for every life past the one in play, set in a
714    /// row alongside the paddle's own path.
715    fn draw_lives(&self, ctx: &mut FrameContext<'_, Breakout>) {
716        let held_lives = self.lives.saturating_sub(1);
717        for slot in 0..held_lives {
718            let z = PADDLE_Z + (slot + 1) as f32 * LIFE_ROW_SPACING;
719            ctx.draw(
720                Sphere { subdivisions: 2 }
721                    .at(Transform::from_scale_rotation_translation(
722                        Vec3::splat(BALL_RADIUS * 2.0),
723                        Quat::IDENTITY,
724                        Vec3::new(LIFE_ROW_X, BALL_RADIUS, z),
725                    ))
726                    .material(
727                        Material::color(BALL_GLOW)
728                            .emissive(BALL_EMISSIVE)
729                            .additive(),
730                    ),
731            );
732        }
733    }
734
735    fn overlay(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
736        let bricks_left = self
737            .bricks
738            .iter()
739            .filter(|brick| brick.hits_remaining > 0)
740            .count();
741        // Read before `ctx.ui` so a rebind changes what the hint reads this
742        // frame too.
743        let move_hint = bindings_text(ctx.bindings(Move::Paddle));
744        let pause_hint = bindings_text(ctx.bindings(Button::Pause));
745        let serve_hint = bindings_text(ctx.bindings(Button::Serve));
746        ctx.ui(|ui| {
747            ui.horizontal(|ui| {
748                ui.label(egui::RichText::new(format!("score {}", self.score)).size(32.0));
749                ui.label(format!("{bricks_left} bricks left"));
750            });
751            ui.label(format!("move: {move_hint} · {pause_hint} to pause"));
752            if self.phase == Phase::Serving {
753                ui.label(format!("{serve_hint} to serve"));
754            }
755        });
756
757        match self.phase {
758            Phase::Serving | Phase::Playing if self.paused => self.menu(ctx, "paused", false),
759            Phase::Won => self.menu(ctx, "you win", true),
760            Phase::Lost => self.menu(ctx, "game over", true),
761            _ => {}
762        }
763    }
764
765    fn menu(&mut self, ctx: &mut FrameContext<'_, Breakout>, title: &str, over: bool) {
766        let mut clicked = false;
767        let mut quit = false;
768
769        // `ctx.ui` cannot borrow `ctx`, so anything the controls list needs is
770        // read first and applied after.
771        let buttons: Vec<(Button, String)> = Button::all()
772            .into_iter()
773            .map(|action| (action, bindings_text(ctx.bindings(action))))
774            .collect();
775        let axes: Vec<(Move, String)> = Move::all()
776            .into_iter()
777            .map(|action| (action, bindings_text(ctx.bindings(action))))
778            .collect();
779        let listening = self.listening;
780        let actuated_button = (!ctx.ui_wants_keyboard())
781            .then(|| ctx.actuated_button())
782            .flatten();
783        let actuated_axis = (!ctx.ui_wants_keyboard())
784            .then(|| ctx.actuated_axis())
785            .flatten();
786        let mut reset = None;
787
788        ctx.ui(|ui| {
789            egui::Window::new(title)
790                .collapsible(false)
791                .resizable(false)
792                .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
793                .show(ui.ctx(), |ui| {
794                    if over {
795                        ui.label(format!("score {}", self.score));
796                    }
797                    if !over {
798                        ui.add(
799                            egui::Slider::new(&mut self.master_volume, 0.0..=1.0).text("volume"),
800                        );
801                        if ui.button("resume").clicked() {
802                            self.paused = false;
803                            clicked = true;
804                        }
805                        ui.separator();
806                        ui.heading("controls");
807                        for (action, text) in &buttons {
808                            controls_row(
809                                ui,
810                                action.name(),
811                                text,
812                                listening == Some(Listening::Button(*action)),
813                                &mut self.listening,
814                                Listening::Button(*action),
815                                &mut reset,
816                            );
817                        }
818                        for (action, text) in &axes {
819                            controls_row(
820                                ui,
821                                action.name(),
822                                text,
823                                listening == Some(Listening::Move(*action)),
824                                &mut self.listening,
825                                Listening::Move(*action),
826                                &mut reset,
827                            );
828                        }
829                    }
830                    if ui.button("restart").clicked() {
831                        self.restart();
832                        clicked = true;
833                    }
834                    if ui.button("quit").clicked() {
835                        quit = true;
836                    }
837                });
838        });
839
840        match (self.listening, actuated_button, actuated_axis) {
841            (Some(Listening::Button(action)), Some(binding), _) => {
842                ctx.rebind(action, vec![binding]);
843                self.listening = None;
844            }
845            (Some(Listening::Move(action)), _, Some(binding)) => {
846                ctx.rebind(action, vec![binding]);
847                self.listening = None;
848            }
849            _ => {}
850        }
851        match reset {
852            Some(Listening::Button(action)) => ctx.rebind(action, action.bindings()),
853            Some(Listening::Move(action)) => ctx.rebind(action, action.bindings()),
854            None => {}
855        }
856
857        if clicked {
858            ctx.play(Sound::Click);
859        }
860        if quit {
861            ctx.close();
862        }
863    }
864
865    /// Sustains both tracks every frame, and the gain goes to whichever the
866    /// game calls for: gameplay music while a round is live, serving
867    /// included, and menu music whenever a menu covers it.
868    ///
869    /// Each fades in over [`MUSIC_CROSSFADE`] and slides every later gain
870    /// over it, which is the crossfade itself; the one at no gain costs no
871    /// voice while its playback goes on under the other.
872    fn sustain_music(&self, ctx: &mut FrameContext<'_, Breakout>) {
873        let playing = !self.paused && matches!(self.phase, Phase::Serving | Phase::Playing);
874        let gain = |wanted: bool| match wanted {
875            true => MUSIC_GAIN,
876            false => 0.0,
877        };
878
879        ctx.sustain(
880            Sound::Music
881                .gain(gain(playing))
882                .fade(MUSIC_CROSSFADE)
883                .glide(MUSIC_CROSSFADE)
884                .loop_from(MUSIC_LOOP_FROM),
885        );
886        ctx.sustain(
887            Sound::MenuMusic
888                .gain(gain(!playing))
889                .fade(MUSIC_CROSSFADE)
890                .glide(MUSIC_CROSSFADE)
891                .loop_from(MENU_MUSIC_LOOP_FROM),
892        );
893    }
894}
895
896/// One action's name, its live bindings, a rebind control that starts
897/// listening for a new one, and a reset to its defaults; cancel is a
898/// button rather than Escape, since Escape is itself a binding a listen
899/// could capture.
900fn controls_row(
901    ui: &mut egui::Ui,
902    name: &str,
903    bindings: &str,
904    listening: bool,
905    target: &mut Option<Listening>,
906    action: Listening,
907    reset: &mut Option<Listening>,
908) {
909    ui.horizontal(|ui| {
910        ui.label(format!("{name}: {bindings}"));
911        if listening {
912            ui.label("listening");
913            if ui.button("cancel").clicked() {
914                *target = None;
915            }
916        } else if ui.button("rebind").clicked() {
917            *target = Some(action);
918        }
919        if ui.button("reset").clicked() {
920            *reset = Some(action);
921        }
922    });
923}
924
925/// The controls-menu text for a live binding list: each alternative,
926/// separated, in the order the player can use them.
927fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
928    bindings
929        .iter()
930        .map(ToString::to_string)
931        .collect::<Vec<_>>()
932        .join(", ")
933}
934
935fn spawn_bricks() -> Vec<Brick> {
936    let cell = BRICK_HALF_WIDTH * 2.0 + BRICK_GAP;
937    let row_span = BRICK_HALF_DEPTH * 2.0 + BRICK_ROW_GAP;
938    let grid_width = cell * BRICK_COLUMNS as f32 - BRICK_GAP;
939    let start_x = -grid_width * 0.5 + BRICK_HALF_WIDTH;
940    let start_z = -COURT_HALF_DEPTH + WALL_THICKNESS + BRICK_HALF_DEPTH + 0.6;
941
942    (0..BRICK_ROWS)
943        .flat_map(|row| {
944            (0..BRICK_COLUMNS).map(move |column| Brick {
945                row,
946                position: Vec3::new(
947                    start_x + column as f32 * cell,
948                    BRICK_HALF_HEIGHT,
949                    start_z + row as f32 * row_span,
950                ),
951                hits_remaining: BRICK_HITS,
952            })
953        })
954        .collect()
955}
956
957impl Game for Breakout {
958    type Meshes = Shape;
959    type Sounds = Sound;
960    type InputActions = Controls;
961    type Skyboxes = NoSkyboxes;
962    type SurfaceStyles = NoSurfaceStyles;
963    type PostEffects = NoPostEffects;
964
965    fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966        if self.paused {
967            return;
968        }
969
970        let dt = ctx.dt().as_secs_f32();
971        self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972        self.brick_flash = (self.brick_flash - dt).max(0.0);
973        self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974        self.step_sparks(dt);
975
976        // Decay runs before the end-screen return below, so the last pulse and
977        // burst do not stay on screen.
978        if matches!(self.phase, Phase::Won | Phase::Lost) {
979            return;
980        }
981
982        let axis = if ctx.ui_wants_keyboard() {
983            0.0
984        } else {
985            ctx.axis(Move::Paddle)
986        };
987        self.step_paddle(axis, dt);
988
989        match self.phase {
990            Phase::Serving => self.hold_ball(ctx),
991            _ => self.step_ball(ctx, dt),
992        }
993    }
994
995    fn frame(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
996        if matches!(self.phase, Phase::Serving | Phase::Playing) && ctx.pressed(Button::Pause) {
997            self.paused = !self.paused;
998        }
999
1000        ctx.set_volume(self.master_volume);
1001        self.sustain_music(ctx);
1002
1003        ctx.set_camera(Self::camera());
1004
1005        let brick_pulse = (self.brick_flash / BRICK_FLASH).clamp(0.0, 1.0);
1006        ctx.set_bloom((BLOOM_BASE + brick_pulse * BLOOM_PULSE_PEAK).clamp(0.0, 1.0));
1007
1008        let life_lost_t = (self.life_lost_flash / LIFE_LOST_FLASH).clamp(0.0, 1.0);
1009        ctx.set_exposure((1.0 - life_lost_t * EXPOSURE_DIP_DEPTH).clamp(0.0, 1.0));
1010
1011        // The tick moves nothing behind a menu, so a frame there draws the last
1012        // step whole rather than interpolating from the one before.
1013        let alpha = match self.phase {
1014            Phase::Serving | Phase::Playing if !self.paused => ctx.alpha(),
1015            _ => 1.0,
1016        };
1017        let paddle_x = self.paddle_prev_x.lerp(self.paddle_x, alpha);
1018        let ball_pos = self.ball_prev.lerp(self.ball_pos, alpha);
1019
1020        ctx.light(Light::point(ball_pos, BALL_GLOW, BALL_LIGHT_RANGE).shadow());
1021
1022        self.draw_court(ctx);
1023        self.draw_bricks(ctx);
1024        self.draw_sparks(ctx);
1025        self.draw_lives(ctx);
1026
1027        ctx.draw(
1028            Paddle
1029                .at(Transform::from_translation(Vec3::new(
1030                    paddle_x,
1031                    PADDLE_HALF_HEIGHT,
1032                    PADDLE_Z,
1033                )))
1034                .material_of(PaddlePart::Face, self.paddle_face_material()),
1035        );
1036
1037        self.draw_trail(ctx, alpha);
1038        ctx.draw(
1039            Sphere { subdivisions: 2 }
1040                .at(Transform::from_scale_rotation_translation(
1041                    Vec3::splat(BALL_RADIUS * 2.0),
1042                    Quat::IDENTITY,
1043                    ball_pos,
1044                ))
1045                .material(
1046                    Material::color(BALL_GLOW)
1047                        .emissive(BALL_EMISSIVE)
1048                        .additive(),
1049                ),
1050        );
1051
1052        self.overlay(ctx);
1053    }
Source

pub const fn tint(&self) -> Color

The color shading multiplies the lighting by.

Source

pub const fn litness(&self) -> f32

The fraction of lit shading applied: 0.0 flat, 1.0 fully lit.

Source

pub const fn emission(&self) -> Color

The light the surface adds of its own.

Source

pub const fn rough(&self) -> f32

The surface’s roughness, a fraction.

Source

pub const fn metal(&self) -> f32

The surface’s metallic, a fraction.

Trait Implementations§

Source§

impl Clone for Material

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for Material

Source§

impl Debug for Material

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Material

Source§

fn default() -> Self

Lit white.

Source§

impl PartialEq for Material

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Material

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> SerializableAny for T
where T: 'static + Any + Clone + for<'a> Send + Sync,

Source§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

Source§

fn simd_from(_simd: S, value: T) -> T

Source§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

Source§

fn simd_into(self, simd: S) -> T

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more