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
impl Material
Sourcepub const fn color(color: Color) -> Self
pub const fn color(color: Color) -> Self
A flat color; lights do not affect it.
Examples found in repository?
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
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 }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 }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 }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 }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 }Sourcepub const fn lit(color: Color) -> Self
pub const fn lit(color: Color) -> Self
A color fully lit by the frame’s lights.
Examples found in repository?
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
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 }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}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 }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 }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 }Sourcepub fn shaded(color: Color, litness: f32) -> Self
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?
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
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 }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 }Sourcepub const fn emissive(self, color: Color) -> Self
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?
More examples
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 }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 }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 }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 }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 }Sourcepub fn roughness(self, roughness: f32) -> Self
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?
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
Sourcepub fn metallic(self, metallic: f32) -> Self
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?
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 }Sourcepub const fn cutout(self) -> Self
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?
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
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 }Sourcepub const fn additive(self) -> Self
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?
More examples
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 }Trait Implementations§
impl Copy for Material
impl StructuralPartialEq for Material
Auto Trait Implementations§
impl Freeze for Material
impl RefUnwindSafe for Material
impl Send for Material
impl Sync for Material
impl Unpin for Material
impl UnsafeUnpin for Material
impl UnwindSafe for Material
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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> DowncastSync for T
impl<T> DowncastSync for T
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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