#[repr(C)]pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}Expand description
A 3-dimensional vector.
Fields§
§x: f32§y: f32§z: f32Implementations§
Source§impl Vec3
impl Vec3
Sourcepub const NEG_INFINITY: Vec3
pub const NEG_INFINITY: Vec3
All f32::NEG_INFINITY.
Sourcepub const USES_CORE_SIMD: bool = false
pub const USES_CORE_SIMD: bool = false
Vec3 uses Rust Portable SIMD
Sourcepub const USES_SCALAR_MATH: bool = true
pub const USES_SCALAR_MATH: bool = true
Vec3 uses scalar math
Sourcepub const USES_WASM_SIMD: bool = false
pub const USES_WASM_SIMD: bool = false
Vec3 uses WebAssembly 128-bit SIMD
pub const USES_WASM32_SIMD: bool = false
Renamed to USES_WASM_SIMD
Sourcepub const fn new(x: f32, y: f32, z: f32) -> Vec3
pub const fn new(x: f32, y: f32, z: f32) -> Vec3
Creates a new vector.
Examples found in repository?
80const ELF_START: Vec3 = Vec3::new(-3.0, 0.0, 4.0);
81
82/// Ground positions of the three hurt patches, and their radius.
83const HURT_PATCHES: [Vec3; 3] = [
84 Vec3::new(1.5, 0.0, -1.0),
85 Vec3::new(-1.5, 0.0, -3.5),
86 Vec3::new(3.0, 0.0, 2.0),
87];
88const HURT_RADIUS: f32 = 0.9;
89/// Hits it takes before the elf reads `Death` instead of `Hit`.
90const FATAL_HITS: u32 = 3;
91
92/// The seat's position, at the ground, and its footprint: measured on the
93/// asset, `sit_down` lowers the pelvis from `0.77` meters to `0.47` meters
94/// and moves it `0.29` meters toward the seat, the feet staying where they
95/// stood, so a block this tall under that landing puts the pelvis on its
96/// top face.
97const SEAT_POSITION: Vec3 = Vec3::new(-3.5, 0.0, -3.0);
98const SEAT_FOOTPRINT: f32 = 1.0;
99const SEAT_HEIGHT: f32 = 0.45;
100/// How tall the figure sits, from the seat's top to its head.
101const SEATED_HEIGHT: f32 = 0.75;
102/// Height from the ground to the seated elf's head.
103const SEAT_HEAD_HEIGHT: f32 = SEAT_HEIGHT + SEATED_HEIGHT;
104/// The gap in front of the seat's own face the elf stands at.
105const SEAT_STAND_CLEARANCE: f32 = 0.05;
106/// Where the elf stands to sit on the seat, at its front face plus
107/// [`SEAT_STAND_CLEARANCE`], and which way it faces there: away from the
108/// seat, so `sit_down` moves the pelvis back onto it.
109const SEAT_SPOT: Vec3 = Vec3::new(
110 SEAT_POSITION.x,
111 0.0,
112 SEAT_POSITION.z + SEAT_FOOTPRINT * 0.5 + SEAT_STAND_CLEARANCE,
113);
114const SEAT_FACING: f32 = 0.0;
115/// The least distance from [`SEAT_POSITION`] `Button::Interact` sits the
116/// elf down at.
117const SEAT_INTERACT_RADIUS: f32 = 1.6;
118
119/// The second elf's fixed position, under a machine scrubbed by its
120/// distance from the first.
121const SCRUBBED_ELF_POSITION: Vec3 = Vec3::new(3.5, 0.0, 4.0);
122/// The distance at and under which the scrubbed elf reads fully seated.
123const SCRUB_NEAR: f32 = 1.5;
124/// The distance at and past which it reads fully standing.
125const SCRUB_FAR: f32 = 5.0;
126
127/// The lamp post's own position, near the seat and its stand.
128const LAMP_POST_POSITION: Vec3 = Vec3::new(-4.9, 0.0, -2.0);
129const LAMP_POST_HEIGHT: f32 = 2.2;
130const LAMP_POST_THICKNESS: f32 = 0.16;
131const LAMP_POST_COLOR: Color = Color::rgb(0.16, 0.14, 0.12);
132/// The lamp's own head, on top of the post, emissive in [`LAMP_LIGHT_COLOR`].
133const LAMP_HEAD_SIZE: f32 = 0.34;
134/// The gap left between the post's own top and the head's bottom face, so
135/// the light sits clear of both meshes rather than inside the head it
136/// would then cast no light from.
137const LAMP_HEAD_GAP: f32 = 0.06;
138/// Past `1.0`, so its glow lands on the ground near it, visible against
139/// the sky, and the head reads bright once bloom spreads it.
140const LAMP_LIGHT_COLOR: Color = Color::rgb(5.5, 4.2, 2.2);
141const LAMP_LIGHT_RANGE: f32 = 6.0;
142
143/// Centered over the three [`HURT_PATCHES`], tall enough for one cone to
144/// reach all of them.
145const SPOT_POSITION: Vec3 = Vec3::new(1.0, 6.0, -0.83);
146const SPOT_DIRECTION: Vec3 = Vec3::NEG_Y;
147/// Past `1.0`, so the cone is visible on the ground against the sky, and
148/// bright enough that a patch inside it reads well past a patch outside.
149const SPOT_COLOR: Color = Color::rgb(11.0, 9.8, 8.2);
150const SPOT_RANGE: f32 = 9.0;
151const SPOT_ANGLE: f32 = 0.85;
152/// The fixture's own edge length, drawn where the cone starts.
153const SPOT_FIXTURE_SIZE: f32 = 0.22;
154const SPOT_FIXTURE_COLOR: Color = Color::rgb(0.2, 0.2, 0.22);
155
156/// The butterfly's own source, next to the other example assets.
157const BUTTERFLY_SOURCE: &str = "examples/assets/butterfly.glb";
158/// The root node the source names the butterfly under.
159const BUTTERFLY_ROOT: &str = "Butterfly";
160/// The point halfway between [`SEAT_POSITION`] and [`LAMP_POST_POSITION`],
161/// the closed path's own center.
162const BUTTERFLY_CENTER: Vec3 = Vec3::new(-4.2, 0.0, -2.5);
163/// The closed path's radius along `x` and `z`, wide enough to loop around
164/// both the seat and the lamp.
165const BUTTERFLY_RADIUS: Vec2 = Vec2::new(1.8, 1.4);
166/// About the lamp's own height.
167const BUTTERFLY_HEIGHT: f32 = LAMP_POST_HEIGHT;
168/// Radians a second around the path; a full loop takes about 14 seconds.
169const BUTTERFLY_ANGULAR_SPEED: f32 = TAU / 14.0;
170/// Past `1.0`, so its glow lands on the ground and the lamp post it passes.
171const BUTTERFLY_LIGHT_COLOR: Color = Color::rgb(1.8, 5.5, 5.0);
172const BUTTERFLY_LIGHT_RANGE: f32 = 3.0;
173/// The butterfly's own small emissive, so it reads bright rather than
174/// dark against the glow it casts.
175const BUTTERFLY_EMISSIVE: Color = Color::rgb(0.6, 1.8, 1.6);
176
177const GROUND_SIZE: f32 = 400.0;
178const GROUND_COLOR: Color = Color::rgb(0.24, 0.30, 0.22);
179const HURT_COLOR: Color = Color::rgb(0.75, 0.12, 0.10);
180const SEAT_COLOR: Color = Color::rgb(0.5, 0.42, 0.3);
181/// Low, near the horizon, and dim.
182const SUN_DIRECTION: Vec3 = Vec3::new(-0.85, -0.18, -0.5);
183const SUN_COLOR: Color = Color::rgb(0.55, 0.32, 0.22);
184const SKY_ZENITH: Color = Color::rgb(0.06, 0.07, 0.2);
185const SKY_HORIZON: Color = Color::rgb(0.55, 0.35, 0.28);
186const SKY_NADIR: Color = Color::rgb(0.05, 0.05, 0.07);
187/// The fraction of its own light the sky lands and reflects: dim, so the
188/// lamp, spotlight and butterfly lights read against it.
189const SKY_LIGHT: f32 = 0.15;
190
191/// The orbit camera's distance behind and height above its target.
192const CAMERA_BACK: f32 = 3.4;
193const CAMERA_UP: f32 = 1.7;
194/// Height above the ground the camera looks at, framing the whole figure.
195const CAMERA_LOOK_HEIGHT: f32 = 0.8;
196const CAMERA_FOV: f32 = 50.0;
197/// Radians the camera orbits, or tilts, per pixel the pointer moves,
198/// chosen so a drag the width, or the height, of the window turns it by
199/// [`CAMERA_YAW_PER_DRAG`], or [`CAMERA_PITCH_PER_DRAG`].
200const CAMERA_YAW_PER_DRAG: f32 = core::f32::consts::PI;
201const CAMERA_PITCH_PER_DRAG: f32 = core::f32::consts::FRAC_PI_3;
202const CAMERA_YAW_SCALE: f32 = CAMERA_YAW_PER_DRAG / WINDOW_WIDTH as f32;
203const CAMERA_PITCH_SCALE: f32 = CAMERA_PITCH_PER_DRAG / WINDOW_HEIGHT as f32;
204/// The range the camera's tilt is held inside, in radians: short of
205/// looking flat along the ground or straight down, either of which would
206/// stop framing the figure.
207const CAMERA_PITCH_RANGE: Range<f32> = -0.4..0.9;
208
209/// The panel's controls, a key and what it does.
210const CONTROLS: [(&str, &str); 10] = [
211 ("mouse", "turns the camera"),
212 ("click", "locks the pointer"),
213 ("escape", "frees the pointer"),
214 ("wasd or arrows", "walk"),
215 ("left shift", "runs"),
216 ("f", "attacks, chains on a second press"),
217 ("space", "jumps"),
218 ("n", "dances while idle"),
219 ("e", "sits on the seat and stands back up"),
220 ("r", "starts a new elf"),
221];
222
223/// The UI's own text color, read over the ground and the sky both.
224const PANEL_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
225/// How much dark a panel or a prompt's own backdrop puts behind its text.
226const PANEL_BACKDROP: u8 = 190;
227/// The panel's own inner margin, around its labels.
228const PANEL_PADDING: i8 = 8;
229/// The size a world-space prompt reads at, in logical points.
230const PROMPT_SIZE: f32 = 15.0;
231/// Height a world-space prompt is lifted over the point it names.
232const PROMPT_LIFT: f32 = 0.35;
233/// Margin a prompt's own backdrop keeps past its galley, in logical points.
234const PROMPT_PADDING: f32 = 4.0;
235
236meshes! { enum Shape { Plane, Cube, Elf, Butterfly } }
237
238/// The one sky this game draws, a gradient set each frame.
239#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
240enum Sky {
241 Day,
242}
243
244impl Skyboxes for Sky {
245 fn build(&self, _assets: &Assets) -> SkyboxData {
246 match self {
247 Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT),
248 }
249 }
250}
251
252#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
253struct Elf;
254
255/// The clips `ElfState` and `ScrubbedState` play, named as the source
256/// names them.
257#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
258enum ElfClip {
259 #[clip("idle")]
260 Idle,
261 #[clip("walk")]
262 Walk,
263 #[clip("jog")]
264 Jog,
265 #[clip("attack")]
266 Attack,
267 #[clip("hit")]
268 Hit,
269 #[clip("death")]
270 Death,
271 #[clip("sit_down")]
272 SitDown,
273 #[clip("sit")]
274 Sit,
275 #[clip("stand_up")]
276 StandUp,
277 #[clip("jump")]
278 Jump,
279 #[clip("dance")]
280 Dance,
281}
282
283impl Mesh<NoParts, ElfClip> for Elf {
284 fn build(&self, assets: &Assets) -> MeshData<NoParts, ElfClip> {
285 assets.model(ELF_ROOT)
286 }
287}
288
289/// What the game fills each tick to move `ElfState` on.
290#[derive(Default)]
291struct ElfInput {
292 /// The elf's speed this tick, a fraction of [`ELF_SPEED`].
293 speed: f32,
294 attack: bool,
295 /// True the tick a hurt patch is stepped onto and [`FATAL_HITS`] have
296 /// not yet landed.
297 hit: bool,
298 /// True the tick a hurt patch is stepped onto the third time, which
299 /// [`FATAL_HITS`] counts.
300 dying: bool,
301 jump: bool,
302 /// True the tick `Jump`'s own height curve returns the elf to the
303 /// ground after it launches.
304 landed: bool,
305 dance: bool,
306 /// `Button::Interact`, read as sitting down, standing back up, or
307 /// nothing, by the state it reaches.
308 interact: bool,
309 /// Whether the elf stands close enough to the cube to sit on it.
310 near_seat: bool,
311}
312
313#[derive(Clone, Copy, Eq, PartialEq, Debug)]
314enum ElfState {
315 Idle,
316 Locomotion,
317 Attack,
318 Hit,
319 Death,
320 SitDown,
321 Sit,
322 StandUp,
323 Jump,
324 Dance,
325}
326
327impl ElfState {
328 /// Whether the elf is on the seat, or on its way onto or off it.
329 fn seated(self) -> bool {
330 matches!(self, Self::SitDown | Self::Sit | Self::StandUp)
331 }
332
333 /// `Locomotion` where `input` reads a walk or a run, `Idle` at rest:
334 /// where a grounded state returns once whatever interrupted it ends.
335 fn grounded(input: &ElfInput) -> Self {
336 match input.speed > WALK_THRESHOLD {
337 true => Self::Locomotion,
338 false => Self::Idle,
339 }
340 }
341}
342
343impl AnimationStates for ElfState {
344 type Clip = ElfClip;
345 type Input = ElfInput;
346
347 fn entry() -> Self {
348 Self::Idle
349 }
350
351 fn motion(&self, input: &ElfInput) -> Motion<ElfClip> {
352 match self {
353 Self::Idle => Motion::looping(ElfClip::Idle),
354 Self::Locomotion => {
355 Motion::blend(ElfClip::Walk, ElfClip::Jog, input.speed).paced(input.speed)
356 }
357 Self::Attack => Motion::once(ElfClip::Attack),
358 Self::Hit => Motion::once(ElfClip::Hit),
359 Self::Death => Motion::once(ElfClip::Death),
360 Self::SitDown => Motion::once(ElfClip::SitDown),
361 Self::Sit => Motion::looping(ElfClip::Sit),
362 Self::StandUp => Motion::once(ElfClip::StandUp),
363 Self::Jump => Motion::once(ElfClip::Jump),
364 Self::Dance => Motion::looping(ElfClip::Dance),
365 }
366 }
367
368 fn next(&self, input: &ElfInput, at: Progress) -> Option<Transition<Self>> {
369 match (self, input) {
370 (Self::Death, _) => None,
371 (_, ElfInput { dying: true, .. }) => Some(Self::Death.fade(DEATH_FADE)),
372 (_, ElfInput { hit: true, .. }) if *self != Self::Hit => {
373 Some(Self::Hit.fade(HIT_ENTER_FADE))
374 }
375 (Self::Hit, _) if at.ended() => Some(ElfState::grounded(input).fade(HIT_EXIT_FADE)),
376 (Self::Attack, ElfInput { attack: true, .. }) if at.past(ATTACK_RELEASE) => Some(
377 Self::Attack
378 .restarted()
379 .entering_at(ATTACK_CHAIN_ENTRY)
380 .fade(ATTACK_CHAIN_FADE),
381 ),
382 (Self::Attack, _) if at.past(ATTACK_RELEASE) => {
383 Some(ElfState::grounded(input).fade(ATTACK_EXIT_FADE))
384 }
385 (Self::SitDown | Self::Sit | Self::StandUp, i) if i.speed > WALK_THRESHOLD => {
386 Some(Self::Locomotion.fade(STAND_EXIT_FADE))
387 }
388 (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { attack: true, .. }) => {
389 Some(Self::Attack.fade(ATTACK_ENTER_FADE))
390 }
391 (Self::SitDown | Self::Sit | Self::StandUp, ElfInput { jump: true, .. }) => {
392 Some(Self::Jump.fade(JUMP_ENTER_FADE))
393 }
394 (Self::SitDown, _) if at.ended() => Some(Self::Sit.at_once()),
395 (Self::Sit, ElfInput { interact: true, .. }) => Some(Self::StandUp.fade(STAND_UP_FADE)),
396 (Self::StandUp, _) if at.ended() => Some(Self::Idle.fade(STAND_EXIT_FADE)),
397 (Self::Jump, ElfInput { landed: true, .. }) => {
398 Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE))
399 }
400 (Self::Jump, _) if at.ended() => Some(ElfState::grounded(input).fade(JUMP_EXIT_FADE)),
401 (
402 Self::Idle | Self::Locomotion,
403 ElfInput {
404 interact: true,
405 near_seat: true,
406 ..
407 },
408 ) => Some(Self::SitDown.fade(SIT_DOWN_FADE)),
409 (Self::Idle | Self::Locomotion, ElfInput { attack: true, .. }) => {
410 Some(Self::Attack.fade(ATTACK_ENTER_FADE))
411 }
412 (Self::Idle | Self::Locomotion, ElfInput { jump: true, .. }) => {
413 Some(Self::Jump.fade(JUMP_ENTER_FADE))
414 }
415 (Self::Idle, ElfInput { dance: true, .. }) => Some(Self::Dance.fade(DANCE_FADE)),
416 (Self::Dance, i) if i.speed > WALK_THRESHOLD => {
417 Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
418 }
419 (Self::Idle, i) if i.speed > WALK_THRESHOLD => {
420 Some(Self::Locomotion.fade(IDLE_LOCOMOTION_FADE))
421 }
422 (Self::Locomotion, i) if i.speed <= WALK_THRESHOLD => {
423 Some(Self::Idle.fade(IDLE_LOCOMOTION_FADE))
424 }
425 _ => None,
426 }
427 }
428}
429
430/// A machine of one state, posed by nothing but the value it scrubs.
431#[derive(Clone, Copy, Eq, PartialEq, Debug)]
432enum ScrubbedState {
433 SitDown,
434}
435
436/// What the game fills each tick to move `ScrubbedState` on.
437#[derive(Default)]
438struct ScrubbedInput {
439 /// How far into sitting down the second elf reads, a fraction in
440 /// `0.0..=1.0`.
441 settled: f32,
442}
443
444impl AnimationStates for ScrubbedState {
445 type Clip = ElfClip;
446 type Input = ScrubbedInput;
447
448 fn entry() -> Self {
449 Self::SitDown
450 }
451
452 fn motion(&self, input: &ScrubbedInput) -> Motion<ElfClip> {
453 Motion::scrubbed(ElfClip::SitDown, input.settled)
454 }
455
456 fn next(&self, _input: &ScrubbedInput, _at: Progress) -> Option<Transition<Self>> {
457 None
458 }
459}
460
461#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
462struct Butterfly;
463
464/// The one clip `FlyingState` plays, named as the source names it.
465#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
466enum ButterflyClip {
467 #[clip("fly")]
468 Fly,
469}
470
471impl Mesh<NoParts, ButterflyClip> for Butterfly {
472 fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
473 assets.model(BUTTERFLY_ROOT)
474 }
475}
476
477/// A machine of one state, looping the butterfly's only clip.
478#[derive(Clone, Copy, Eq, PartialEq, Debug)]
479enum FlyingState {
480 Flying,
481}
482
483impl AnimationStates for FlyingState {
484 type Clip = ButterflyClip;
485 type Input = ();
486
487 fn entry() -> Self {
488 Self::Flying
489 }
490
491 fn motion(&self, _input: &()) -> Motion<ButterflyClip> {
492 Motion::looping(ButterflyClip::Fly)
493 }
494
495 fn next(&self, _input: &(), _at: Progress) -> Option<Transition<Self>> {
496 None
497 }
498}
499
500/// The butterfly's position and the `yaw` it faces, `t` seconds into its
501/// closed loop around [`BUTTERFLY_CENTER`].
502fn butterfly_pose(t: f32) -> (Vec3, f32) {
503 let angle = t * BUTTERFLY_ANGULAR_SPEED;
504 let position = BUTTERFLY_CENTER
505 + Vec3::new(
506 BUTTERFLY_RADIUS.x * angle.cos(),
507 BUTTERFLY_HEIGHT,
508 BUTTERFLY_RADIUS.y * angle.sin(),
509 );
510 let direction = Vec3::new(
511 -BUTTERFLY_RADIUS.x * angle.sin(),
512 0.0,
513 BUTTERFLY_RADIUS.y * angle.cos(),
514 );
515 (position, direction.x.atan2(direction.z))
516}
517
518/// How far into sitting down the second elf reads at `distance` from the
519/// first.
520fn settled_at(distance: f32) -> f32 {
521 1.0 - (distance - SCRUB_NEAR) / (SCRUB_FAR - SCRUB_NEAR)
522}
523
524/// A camera [`CAMERA_BACK`] behind and [`CAMERA_UP`] above `target`, tilted
525/// `pitch` radians and turned `yaw` radians around it, looking at a point
526/// [`CAMERA_LOOK_HEIGHT`] above `target`.
527fn orbit_camera(target: Vec3, yaw: f32, pitch: f32) -> Camera {
528 let look_at = target + Vec3::Y * CAMERA_LOOK_HEIGHT;
529 let base = Vec3::new(0.0, CAMERA_UP, CAMERA_BACK);
530 let offset = Quat::from_rotation_y(yaw) * (Quat::from_rotation_x(pitch) * base);
531 Camera::new(
532 View::look_at(look_at + offset, look_at),
533 Projection::perspective(CAMERA_FOV),
534 )
535}
536
537/// The logical point egui paints the physical pixel `pixel` at.
538fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
539 let point = pixel / pixels_per_point;
540 egui::pos2(point.x, point.y)
541}
542
543#[derive(InputButtonAction, Clone, Copy)]
544enum Button {
545 Run,
546 Attack,
547 Jump,
548 Dance,
549 Interact,
550 Restart,
551 Hold,
552 Release,
553}
554
555impl InputButtonAction for Button {
556 fn bindings(&self) -> Vec<ButtonBinding> {
557 match self {
558 Button::Run => vec![Key::LeftShift.into()],
559 Button::Attack => vec![Key::F.into()],
560 Button::Jump => vec![Key::Space.into()],
561 Button::Dance => vec![Key::N.into()],
562 Button::Interact => vec![Key::E.into()],
563 Button::Restart => vec![Key::R.into()],
564 Button::Hold => vec![MouseButton::Left.into()],
565 Button::Release => vec![Key::Escape.into()],
566 }
567 }
568}
569
570/// The camera's own controls: turned and tilted by how far the pointer
571/// moves sideways and upward each tick.
572#[derive(InputAxisAction, Clone, Copy)]
573enum Axis {
574 CameraYaw,
575 CameraPitch,
576}
577
578impl InputAxisAction for Axis {
579 fn bindings(&self) -> Vec<AxisBinding> {
580 match self {
581 Axis::CameraYaw => {
582 vec![AxisBinding::pointer_delta(PointerDelta::Sideways).scale(CAMERA_YAW_SCALE)]
583 }
584 Axis::CameraPitch => {
585 vec![AxisBinding::pointer_delta(PointerDelta::Up).scale(CAMERA_PITCH_SCALE)]
586 }
587 }
588 }
589}
590
591#[derive(InputAxis2Action, Clone, Copy)]
592enum Move {
593 Walk,
594}
595
596impl InputAxis2Action for Move {
597 fn bindings(&self) -> Vec<Axis2Binding> {
598 match self {
599 Move::Walk => vec![
600 Axis2Binding::from(ButtonAxis2 {
601 left: Key::A,
602 right: Key::D,
603 down: Key::S,
604 up: Key::W,
605 }),
606 Axis2Binding::from(ButtonAxis2 {
607 left: Key::Left,
608 right: Key::Right,
609 down: Key::Down,
610 up: Key::Up,
611 }),
612 ],
613 }
614 }
615}
616
617struct Controls;
618
619impl InputActions for Controls {
620 type Button = Button;
621 type Axis = Axis;
622 type Axis2 = Move;
623}
624
625/// `text` in [`PANEL_TEXT_COLOR`].
626fn panel_text(text: impl Into<String>) -> egui::RichText {
627 egui::RichText::new(text.into()).color(PANEL_TEXT_COLOR)
628}
629
630struct Scene {
631 elf_pos: Vec3,
632 elf_prev: Vec3,
633 elf_yaw: f32,
634 /// Height the elf is lifted over the ground while off the ground,
635 /// integrated in [`Game::tick`] from [`Self::jump_speed`].
636 elf_height: f32,
637 elf_height_prev: f32,
638 /// The elf's own vertical speed while off the ground, in meters a
639 /// second, positive upward.
640 jump_speed: f32,
641 elf_input: ElfInput,
642 elf_animator: Animator<Elf, ElfState>,
643 scrubbed_animator: Animator<Elf, ScrubbedState>,
644 butterfly_animator: Animator<Butterfly, FlyingState>,
645 hits: u32,
646 /// True while a hurt patch already held the elf, so leaving and
647 /// returning to the same patch counts as a new hit.
648 in_patch: bool,
649 /// Whether the pointer is held; a click takes it, escape frees it.
650 holding: bool,
651 /// The camera's turn around the elf, and its tilt, both in radians.
652 camera_yaw: f32,
653 camera_pitch: f32,
654 /// The last state change the panel names.
655 last_event: &'static str,
656}
657
658impl Scene {
659 fn init(_ctx: &mut InitContext<'_, Scene>) -> Result<Self, Error> {
660 Ok(Self {
661 elf_pos: ELF_START,
662 elf_prev: ELF_START,
663 elf_yaw: 0.0,
664 elf_height: 0.0,
665 elf_height_prev: 0.0,
666 jump_speed: 0.0,
667 elf_input: ElfInput::default(),
668 elf_animator: Animator::new(),
669 scrubbed_animator: Animator::new(),
670 butterfly_animator: Animator::new(),
671 hits: 0,
672 in_patch: false,
673 holding: false,
674 camera_yaw: 0.0,
675 camera_pitch: 0.0,
676 last_event: "none yet",
677 })
678 }
679
680 /// Turns the camera by how far the pointer moves sideways and upward,
681 /// [`CAMERA_PITCH_RANGE`] holding how far it tilts.
682 fn steer_camera(&mut self, ctx: &mut FrameContext<'_, Scene>) {
683 self.camera_yaw -= ctx.axis(Axis::CameraYaw);
684 self.camera_pitch = (self.camera_pitch + ctx.axis(Axis::CameraPitch))
685 .clamp(CAMERA_PITCH_RANGE.start, CAMERA_PITCH_RANGE.end);
686 }
687
688 /// Turns `elf_yaw` toward the heading `ctx` reads, relative to the
689 /// camera's own turn, and moves `elf_pos` along it; the speed it moves
690 /// at, a fraction of [`ELF_SPEED`], held at [`WALK_CAP`] until
691 /// `Button::Run` is held.
692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }
707
708 /// Integrates [`Self::elf_height`] under [`GRAVITY`] from
709 /// [`Self::jump_speed`], held at the ground; `true` the tick it
710 /// returns there from above it.
711 fn fall(&mut self, dt: f32) -> bool {
712 let off_ground = self.elf_height > 0.0;
713 self.jump_speed -= GRAVITY * dt;
714 self.elf_height = (self.elf_height + self.jump_speed * dt).max(0.0);
715 if self.elf_height == 0.0 {
716 self.jump_speed = 0.0;
717 }
718 off_ground && self.elf_height == 0.0
719 }
720
721 /// The hurt patch `elf_pos` stands inside, if any.
722 fn patch_underfoot(&self) -> Option<Vec3> {
723 HURT_PATCHES
724 .into_iter()
725 .find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
726 }
727
728 /// Reads the controls and moves the elf, filling [`Self::elf_input`]
729 /// for [`ElfState`] to read.
730 fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731 let grounded = matches!(
732 self.elf_animator.state(),
733 ElfState::Idle | ElfState::Locomotion
734 );
735
736 self.elf_input.speed = self.advance(ctx);
737 self.elf_input.attack = ctx.pressed(Button::Attack);
738 self.elf_input.jump = ctx.pressed(Button::Jump);
739 self.elf_input.dance = ctx.pressed(Button::Dance);
740
741 self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742 self.elf_input.interact = ctx.pressed(Button::Interact);
743 if self.elf_input.interact && grounded && self.elf_input.near_seat {
744 self.elf_pos = SEAT_SPOT;
745 self.elf_yaw = SEAT_FACING;
746 }
747
748 let underfoot = self.patch_underfoot();
749 let entered_patch = underfoot.is_some() && !self.in_patch;
750 self.in_patch = underfoot.is_some();
751 self.hits += u32::from(entered_patch);
752 self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753 self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754 if entered_patch {
755 self.last_event = match self.elf_input.dying {
756 true => "elf died",
757 false => "elf hit",
758 };
759 }
760 }
761
762 /// Starts a new [`Animator`] over the elf's own state, its position and
763 /// hit count reset with it.
764 fn restart_elf(&mut self) {
765 self.elf_animator = Animator::new();
766 self.elf_pos = ELF_START;
767 self.elf_prev = ELF_START;
768 self.elf_yaw = 0.0;
769 self.elf_height = 0.0;
770 self.elf_height_prev = 0.0;
771 self.jump_speed = 0.0;
772 self.elf_input = ElfInput::default();
773 self.hits = 0;
774 self.in_patch = false;
775 self.last_event = "new elf started";
776 }
777
778 fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779 let state = match self.elf_animator.state() {
780 ElfState::Idle => "idle",
781 ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782 ElfState::Locomotion => "walking",
783 ElfState::Attack => "attacking",
784 ElfState::Hit => "hit",
785 ElfState::Death => "dead",
786 ElfState::SitDown => "sitting down",
787 ElfState::Sit => "sitting",
788 ElfState::StandUp => "standing up",
789 ElfState::Jump => "jumping",
790 ElfState::Dance => "dancing",
791 };
792 ctx.ui(|ui| {
793 egui::Frame::new()
794 .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795 .inner_margin(PANEL_PADDING)
796 .corner_radius(f32::from(PANEL_PADDING))
797 .show(ui, |ui| {
798 ui.heading(panel_text(format!("elf is {state}")));
799 ui.label(panel_text(format!(
800 "hits taken {} of the {} red patches hurt for, {}",
801 self.hits, FATAL_HITS, self.last_event
802 )));
803 ui.label(panel_text(match self.elf_animator.transitioning() {
804 true => "fading between clips",
805 false => "one clip playing",
806 }));
807 ui.add_space(f32::from(PANEL_PADDING));
808 egui::Grid::new("controls").show(ui, |ui| {
809 for (key, does) in CONTROLS {
810 ui.label(panel_text(key));
811 ui.label(panel_text(does));
812 ui.end_row();
813 }
814 });
815 });
816 });
817 }
818
819 /// A prompt over the seat, each hurt patch, and the scrubbed elf,
820 /// naming what a player finds there; the seat's own prompt names the
821 /// live binding of `Button::Interact` by its own name, not one fixed
822 /// in the code, and is absent while the elf sits on it.
823 fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824 let sit_key = ctx
825 .bindings(Button::Interact)
826 .into_iter()
827 .next()
828 .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829 let sit = ctx.text_layout(
830 &format!("{sit_key} sits"),
831 egui::FontId::proportional(PROMPT_SIZE),
832 );
833 let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834 let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836 let mut prompts = vec![(
837 SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838 walk_closer,
839 )];
840 if !self.elf_animator.state().seated() {
841 prompts.push((
842 SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843 sit,
844 ));
845 }
846 prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848 let window_size = ctx.window_size();
849 let pixels_per_point = ctx.pixels_per_point();
850 ctx.ui(|ui| {
851 let painter = ui.painter();
852 for (point, galley) in prompts {
853 let Some(pixel) = camera.pixel_of(point, window_size) else {
854 continue;
855 };
856 let at = logical(pixel, pixels_per_point);
857 let ink = galley.mesh_bounds;
858 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859 let backdrop = egui::Rect::from_center_size(
860 at,
861 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862 );
863 painter.rect_filled(
864 backdrop,
865 PROMPT_PADDING,
866 egui::Color32::from_black_alpha(PANEL_BACKDROP),
867 );
868 painter.galley(pos, galley, PANEL_TEXT_COLOR);
869 }
870 });
871 }
872}
873
874impl Game for Scene {
875 type Meshes = Shape;
876 type Sounds = NoSounds;
877 type InputActions = Controls;
878 type Skyboxes = Sky;
879 type SurfaceStyles = NoSurfaceStyles;
880 type PostEffects = NoPostEffects;
881
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }
919
920 fn frame(&mut self, ctx: &mut FrameContext<'_, Scene>) {
921 self.steer_camera(ctx);
922
923 let alpha = ctx.alpha();
924 let elf_pos = self.elf_prev.lerp(self.elf_pos, alpha);
925 let elf_height = self.elf_height_prev + (self.elf_height - self.elf_height_prev) * alpha;
926 let (butterfly_pos, butterfly_yaw) = butterfly_pose(ctx.elapsed().as_secs_f32());
927
928 let camera = orbit_camera(elf_pos, self.camera_yaw, self.camera_pitch);
929 ctx.set_camera(camera);
930 ctx.set_cursor(if self.holding {
931 Cursor::Held
932 } else {
933 Cursor::Arrow
934 });
935 ctx.set_skybox(Sky::Day);
936 ctx.set_exposure(3.0);
937 ctx.set_bloom(0.2);
938 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
939 ctx.light(
940 Light::point(
941 LAMP_POST_POSITION + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP * 0.5),
942 LAMP_LIGHT_COLOR,
943 LAMP_LIGHT_RANGE,
944 )
945 .shadow(),
946 );
947 ctx.light(
948 Light::spot(Spot {
949 position: SPOT_POSITION,
950 direction: SPOT_DIRECTION,
951 color: SPOT_COLOR,
952 range: SPOT_RANGE,
953 angle: SPOT_ANGLE,
954 })
955 .shadow(),
956 );
957 ctx.light(
958 Light::point(butterfly_pos, BUTTERFLY_LIGHT_COLOR, BUTTERFLY_LIGHT_RANGE).shadow(),
959 );
960
961 ctx.draw(
962 Plane
963 .at(Transform::from_scale(Vec3::new(
964 GROUND_SIZE,
965 1.0,
966 GROUND_SIZE,
967 )))
968 .material(Material::lit(GROUND_COLOR)),
969 );
970 for patch in HURT_PATCHES {
971 ctx.draw(
972 Plane
973 .at(Transform::from_scale_rotation_translation(
974 Vec3::splat(HURT_RADIUS * 2.0),
975 Quat::IDENTITY,
976 patch,
977 ))
978 .material(Material::lit(HURT_COLOR)),
979 );
980 }
981 ctx.draw(
982 Cube.at(Transform::from_scale_rotation_translation(
983 Vec3::new(SEAT_FOOTPRINT, SEAT_HEIGHT, SEAT_FOOTPRINT),
984 Quat::IDENTITY,
985 SEAT_POSITION + Vec3::Y * SEAT_HEIGHT * 0.5,
986 ))
987 .material(Material::lit(SEAT_COLOR)),
988 );
989 ctx.draw(
990 Cube.at(Transform::from_scale_rotation_translation(
991 Vec3::new(LAMP_POST_THICKNESS, LAMP_POST_HEIGHT, LAMP_POST_THICKNESS),
992 Quat::IDENTITY,
993 LAMP_POST_POSITION + Vec3::Y * LAMP_POST_HEIGHT * 0.5,
994 ))
995 .material(Material::lit(LAMP_POST_COLOR)),
996 );
997 ctx.draw(
998 Cube.at(Transform::from_scale_rotation_translation(
999 Vec3::splat(LAMP_HEAD_SIZE),
1000 Quat::IDENTITY,
1001 LAMP_POST_POSITION
1002 + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP + LAMP_HEAD_SIZE * 0.5),
1003 ))
1004 .material(Material::color(Color::BLACK).emissive(LAMP_LIGHT_COLOR)),
1005 );
1006 ctx.draw(
1007 Cube.at(Transform::from_scale_rotation_translation(
1008 Vec3::splat(SPOT_FIXTURE_SIZE),
1009 Quat::IDENTITY,
1010 SPOT_POSITION + Vec3::Y * SPOT_FIXTURE_SIZE * 0.5,
1011 ))
1012 .material(Material::lit(SPOT_FIXTURE_COLOR)),
1013 );
1014
1015 ctx.draw(
1016 Elf.at(Transform::from_rotation_translation(
1017 Quat::from_rotation_y(self.elf_yaw),
1018 elf_pos + Vec3::Y * elf_height,
1019 ))
1020 .posed(&self.elf_animator),
1021 );
1022 ctx.draw(
1023 Elf.at(Transform::from_rotation_translation(
1024 Quat::from_rotation_y(core::f32::consts::PI),
1025 SCRUBBED_ELF_POSITION,
1026 ))
1027 .posed(&self.scrubbed_animator),
1028 );
1029 ctx.draw(
1030 Butterfly
1031 .at(Transform::from_rotation_translation(
1032 Quat::from_rotation_y(butterfly_yaw),
1033 butterfly_pos,
1034 ))
1035 .posed(&self.butterfly_animator)
1036 .material(Material::lit(Color::WHITE).emissive(BUTTERFLY_EMISSIVE)),
1037 );
1038
1039 self.draw_prompts(ctx, camera);
1040 self.panel(ctx);
1041 }More examples
152const PLAYER_SPAWN: Vec3 = Vec3::new(0.0, 0.0, 5.0);
153const CAVE_MOUTH: Vec3 = Vec3::new(0.0, 0.0, -6.0);
154/// Landing position for a return from the cave: past the [`ENTRANCE`] band,
155/// so the return does not count as another step through the mouth.
156const RETURN_SPAWN: Vec3 = Vec3::new(
157 CAVE_MOUTH.x,
158 0.0,
159 CAVE_MOUTH.z + MOUTH_CROSSING_INSET + PORTAL_CLEARANCE,
160);
161
162/// The pond's position and half the side of it: the shoreline sprite is
163/// three meters square, the sixteen texels to the meter the game keeps. The
164/// styled water is drawn barely off the ground, clear of z-fighting with it; the
165/// shore draws at the ground's own `y` instead, submitted after it, so
166/// submission order alone separates the two and the shore never shadows the
167/// ground it lies on.
168const POND_CENTER: Vec3 = Vec3::new(4.2, 0.01, -1.5);
169const POND_HALF: f32 = 1.5;
170/// Half the open water that sprite's shoreline rings — the middle three
171/// fifths of its side, both drawn styled and blocking the player: the square
172/// stops well inside the shore, so its own edges never show.
173const POND_WATER_HALF: f32 = POND_HALF * 0.6;
174const WATER_COLOR: Color = Color::rgba(0.06, 0.20, 0.27, 0.92);
175const WATER_LITNESS: f32 = 0.45;
176
177/// Footprint and turn of the two crates beside the spawn: far enough apart
178/// that neither turned box overlaps the other, and clear of the path's verge.
179const CRATE_POSITIONS: [(f32, f32, f32); 2] = [(-2.7, 3.5, 0.5), (-1.2, 2.6, -0.3)];
180const CRATE_SIZE: f32 = 1.0;
181const WELL_POSITION: Vec3 = Vec3::new(2.6, 0.0, 3.2);
182/// The well's rim: width, height, depth in meters, as wide as the two tiles
183/// its mouth is drawn from.
184const WELL_SIZE: Vec3 = Vec3::new(2.0, 0.6, 2.0);
185/// Mouth lift over the rim's top face, clear of z-fighting.
186const WELL_MOUTH_LIFT: f32 = 0.01;
187
188/// Flora position: `x, z`, and whether it is a rock rather than a bush.
189const FLORA: [(f32, f32, bool); 6] = [
190 (-4.2, 1.4, false),
191 (-4.8, -1.8, true),
192 (5.8, 2.6, false),
193 (6.6, -1.0, false),
194 (3.2, -4.8, true),
195 (-2.2, -5.0, true),
196];
197/// Both sprites are two tiles wide — the tileset's own sixteen texels to
198/// the meter, as the ground and player. Height matches each one's own
199/// cropped sheet, so its visible size stays the same.
200const BUSH_WIDTH: f32 = 2.0;
201const BUSH_HEIGHT: f32 = 1.9375;
202const ROCK_WIDTH: f32 = 2.0;
203const ROCK_HEIGHT: f32 = 1.875;
204/// Each one's block: the base under the sprite, well inside the two meters
205/// it is drawn across — a bush's stems, a rock's foot.
206const BUSH_FOOTPRINT: f32 = 0.6;
207const ROCK_FOOTPRINT: f32 = 0.8;
208
209/// The bush rows that ring the clearing, hiding where the ground tiles stop:
210/// an inner and an outer half-extent in meters, the step along each side,
211/// and the gap left either side of the path.
212const HEDGE_INNER_HALF: f32 = 10.4;
213const HEDGE_OUTER_HALF: f32 = 11.8;
214const HEDGE_STEP: f32 = 1.4;
215const HEDGE_GATE_HALF: f32 = 2.2;
216
217/// Walkable extent of the clearing: to the inner hedge row's near face, so
218/// the bushes the player can see are the wall.
219const CLEARING_HALF: f32 = HEDGE_INNER_HALF - BUSH_WIDTH * 0.5;
220
221/// The mouth's flanking pillars, half this far either side of its center,
222/// and the dark opening drawn between them.
223const MOUTH_PILLAR_OFFSET: f32 = 1.1;
224/// Width, height and depth of each of those pillars: the whole stone sheet
225/// at its own texels to the meter, so the capital lands where a capital
226/// goes.
227const MOUTH_PILLAR_SIZE: Vec3 = Vec3::new(1.0, STONE_ROWS as f32 * TILE_SIZE, 1.0);
228/// Whole tiles the stone across the pillars runs: enough to end
229/// past both of them, so no face of it lands in either pillar, and a
230/// whole number so its masonry repeats without a part course.
231const MOUTH_LINTEL_TILES: f32 = 4.0;
232/// That stone's own size: those tiles across, one course tall, and deeper
233/// than the pillars.
234const MOUTH_LINTEL_SIZE: Vec3 = Vec3::new(
235 MOUTH_LINTEL_TILES * TILE_SIZE,
236 TILE_SIZE,
237 MOUTH_PILLAR_SIZE.z + 0.1,
238);
239/// Height of the dark between the pillars, drawn up into the lintel, so a
240/// camera looking down finds stone over it rather than the ground behind.
241const MOUTH_DARK_HEIGHT: f32 = MOUTH_PILLAR_SIZE.y + MOUTH_LINTEL_SIZE.y * 0.5;
242/// Half the clear opening between the pillars' inner faces.
243const MOUTH_OPENING_HALF: f32 = MOUTH_PILLAR_OFFSET - MOUTH_PILLAR_SIZE.x * 0.5;
244/// Distance in front of a mouth for the plane that stepping through its
245/// opening crosses.
246const MOUTH_CROSSING_INSET: f32 = 0.1;
247/// Distance past that plane a portal lands the player.
248const PORTAL_CLEARANCE: f32 = 0.8;
249
250/// The overworld's mouth into the cave, and the one at the cave's near end
251/// that leads back out.
252const ENTRANCE: Mouth = Mouth {
253 at: CAVE_MOUTH,
254 room_side: 1.0,
255};
256const EXIT: Mouth = Mouth {
257 at: CAVE_EXIT,
258 room_side: -1.0,
259};
260
261const SUN_COLOR: Color = Color::rgb(0.92, 0.87, 0.72);
262/// The sun's direction: low enough that props cast shadows about their own
263/// length, and from over the player's left shoulder, which is where the
264/// tileset's own sprites are shaded from.
265const SUN_DIRECTION: Vec3 = Vec3::new(0.6, -0.75, 0.4);
266
267// ---------------------------------------------------------------------
268// Cave layout
269// ---------------------------------------------------------------------
270
271const CAVE_HALF_WIDTH: f32 = 4.0;
272/// Room tiling and walls toward the camera, past the wall closing it: under
273/// the bottom corners of the frame, wherever the player walks.
274const CAVE_NEAR_Z: f32 = 10.0;
275const CAVE_FAR_Z: f32 = -6.0;
276/// Walkable extent of the room at its far end: the back wall's inner face,
277/// less the player's own half-width, so the sprite stops flush against
278/// stone.
279const CAVE_WALK_FAR_Z: f32 = CAVE_FAR_Z + TILE_SIZE * 0.5 + WALKER_WIDTH * 0.5;
280/// Walkable extent at the room's near end: past the plane stepping out
281/// crosses, so the last step through the mouth is never clamped away. The
282/// near wall itself is what keeps the player from the ledge beyond.
283const CAVE_WALK_NEAR_Z: f32 = CAVE_EXIT.z + PLAYER_RADIUS;
284/// As many meters as the wall face is tiles tall, so it samples the sheet
285/// at the floor's own texels to the meter.
286const WALL_HEIGHT: f32 = 3.0;
287/// Wall height: over the camera, so it neither looks over them nor down onto
288/// their tops.
289const WALL_TOP: f32 = CAVE_CAMERA_OFFSET.y + TILE_SIZE * 0.5;
290/// Courses that takes; the last one is cut to what is left of it.
291const WALL_COURSES: i32 = (WALL_TOP / WALL_HEIGHT) as i32 + 1;
292/// The tile row the room's near end is closed along, from each side wall to
293/// the mouth in it.
294const CAVE_LIP_Z: f32 = 5.0;
295/// Height of that wall: one tile, so its top face is at the floor's own
296/// texels to the meter, and low enough that it never hides the player.
297const CAVE_LIP_HEIGHT: f32 = TILE_SIZE;
298/// The cave's own mouth position: in the near wall, its pillars half their
299/// depth past it.
300const CAVE_EXIT: Vec3 = Vec3::new(0.0, 0.0, CAVE_LIP_Z + MOUTH_PILLAR_SIZE.z * 0.5);
301/// Landing position for an entry into the cave: past the [`EXIT`] band, so
302/// the entry does not count as another step through the mouth.
303const CAVE_SPAWN: Vec3 = Vec3::new(
304 CAVE_EXIT.x,
305 0.0,
306 CAVE_EXIT.z - MOUTH_CROSSING_INSET - PORTAL_CLEARANCE,
307);
308
309const DOOR_Z: f32 = 0.0;
310/// The near face of the wall the door hangs in: past it the wall is
311/// between the camera and the player, and is drawn through.
312const DOOR_WALL_NEAR_Z: f32 = DOOR_Z + TILE_SIZE * 0.5;
313/// Alpha for that wall and its door once the player has been behind them
314/// for [`GHOST_RAMP_TICKS`]: opaque enough for stone in a dark room,
315/// translucent enough for the chamber and the player to show through.
316const GHOST_ALPHA: f32 = 0.6;
317/// The fade of a draw nothing is seen through.
318const SOLID: f32 = 1.0;
319/// Ticks the wall and door take to fade between [`SOLID`] and
320/// [`GHOST_ALPHA`].
321const GHOST_RAMP_TICKS: u32 = 6;
322/// Span either side of the player the wall is drawn through: wide enough for
323/// the sight line to the player, no wider, so no more of the light the
324/// stone holds off the chamber passes through it than that line needs.
325const GHOST_CORRIDOR_HALF: f32 = 1.5;
326/// The door's hinge: `tools/keep_fixture.py`'s box is hinged at its own
327/// local origin and spans [`DOOR_WIDTH`] along local `+X`.
328const DOOR_HINGE: Vec3 = Vec3::new(-DOOR_WIDTH * 0.5, 0.0, DOOR_Z);
329/// Width of that box, as the fixture builds it.
330const DOOR_WIDTH: f32 = 1.0;
331/// Thickness of it, as the fixture builds it.
332const DOOR_THICKNESS: f32 = 0.12;
333/// Height of it, as the fixture builds it: the stone over the doorway starts
334/// here.
335const DOOR_HEIGHT: f32 = 1.9;
336/// Half the doorway the door hangs in: one tile wide, on the room's axis.
337const DOORWAY_HALF: f32 = TILE_SIZE * 0.5;
338/// Span from that axis of the wall the door hangs in: to the side walls'
339/// inner faces.
340const DOOR_WALL_END: f32 = CAVE_HALF_WIDTH + TILE_SIZE * 0.5;
341const INTERACT_POINT: Vec3 = Vec3::new(0.0, 0.0, DOOR_Z);
342const INTERACT_RADIUS: f32 = 1.8;
343
344/// The door slab's own color, a deep red the cave's grey stone never is, so
345/// the slab reads as a door rather than more wall.
346const DOOR_COLOR: Color = Color::rgb(0.58, 0.16, 0.09);
347const DOOR_LITNESS: f32 = 0.85;
348/// Thickness of the posts and lintel framing the doorway, and how far in
349/// front of the wall face they stand, clear of z-fighting with it.
350const DOOR_FRAME_THICKNESS: f32 = 0.14;
351const DOOR_FRAME_STANDOFF: f32 = 0.03;
352/// The frame's own color, visible from the cave's own spawn well before a
353/// torch reaches the doorway.
354const DOOR_FRAME_COLOR: Color = Color::rgb(0.85, 0.55, 0.2);
355const DOOR_FRAME_LITNESS: f32 = 0.9;
356/// The frame's own light while the door is closed and within
357/// [`INTERACT_RADIUS`]: the cue that it can be opened.
358const DOOR_FRAME_GLOW: Color = Color::rgb(1.6, 0.9, 0.35);
359
360const GEM_POSITION: Vec3 = Vec3::new(0.0, 0.5, -4.6);
361const GEM_BOB_HEIGHT: f32 = 0.12;
362const GEM_SPIN_SPEED: f32 = 1.4;
363const PICKUP_RADIUS: f32 = 1.0;
364const GEM_COLOR: Color = Color::rgb(0.35, 0.95, 0.85);
365/// The gem's own light: what lights the chamber until the door opens on the
366/// torches, and gone with the gem once it is taken.
367const GEM_LIGHT_COLOR: Color = Color::rgb(0.3, 0.85, 0.78);
368/// Reach of it: short of the door wall, so what the wall casts never depends
369/// on the gem.
370const GEM_LIGHT_RANGE: f32 = 4.0;
371/// Lift over the gem, clear of the gem's own facets, which would otherwise
372/// shadow the chamber from inside it.
373const GEM_LIGHT_LIFT: f32 = 0.7;
374
375/// The two torches' position, `x, z`: flanking the doorway on the near side
376/// of the wall, the side the player arrives on.
377const TORCH_POSITIONS: [(f32, f32); 2] = [(-3.0, 0.6), (3.0, 0.6)];
378const TORCH_STAND_HEIGHT: f32 = 2.0;
379/// Thickness of a torch's post: the four texels the sprite draws it as,
380/// which is also what it blocks the player as.
381const TORCH_STAND_WIDTH: f32 = 0.25;
382/// Width of the sprite around that post: its canvas is twice the post,
383/// transparent either side.
384const TORCH_SPRITE_WIDTH: f32 = TORCH_STAND_WIDTH * 2.0;
385const TORCH_LIGHT_RANGE: f32 = 10.0;
386/// Height above its flame a torch's light is placed, and how far it is
387/// offset from the post toward the room: straight over the post, its own face
388/// turns edge-on to the light and goes dark.
389const TORCH_LIGHT_LIFT: f32 = 0.7;
390const TORCH_LIGHT_STANDOFF: f32 = 0.8;
391const TORCH_LIGHT_COLOR: Color = Color::rgb(1.0, 0.6, 0.28);
392/// Size of one cell of the flame's loop: a tile, as everything else the
393/// tilesets draw.
394const FLAME_SIZE: f32 = TILE_SIZE;
395/// Lift of the flame's center over the post's top: its own half-height, less
396/// the overlap that keeps the two from parting.
397const FLAME_LIFT: f32 = FLAME_SIZE * 0.5 - 0.1;
398/// The flame's tint, past `1.0`: an additive draw's tint scales its sampled
399/// texel, so this lifts the flame's own lit texels out of the cave's dark
400/// without a flat color added over its dark, unlit base.
401const FLAME_TINT: Color = Color::rgb(2.2, 1.5, 0.7);
402/// Speed the loop runs, in cells a second.
403const FLAME_RATE: f32 = 12.0;
404/// Speed the flicker runs, and how far it lifts and rolls the flame.
405const FLAME_FLICKER_SPEED: f32 = 9.0;
406const FLAME_BOB: f32 = 0.03;
407const FLAME_ROLL: f32 = 0.12;
408
409/// How much dark the overlay puts behind its lines, and how far that dark
410/// extends past them — the ground under it is bright enough to hide bare text
411/// without it.
412const HUD_BACKDROP: u8 = 200;
413const HUD_PADDING: i8 = 8;
414
415/// Ticks the door's own world prompt still reads as it swings once opened,
416/// after which it is taken as open and the prompt drops.
417const DOOR_SWING_TICKS: u32 = 24;
418/// Size the door's world prompt reads at, in logical points, and how far
419/// over the doorway it is lifted.
420const DOOR_PROMPT_SIZE: f32 = 15.0;
421const DOOR_PROMPT_LIFT: f32 = 0.3;
422/// Margin the prompt's own backdrop keeps past its galley, in logical
423/// points, and how much dark that backdrop puts behind the text.
424const DOOR_PROMPT_PADDING: f32 = 4.0;
425const DOOR_PROMPT_BACKDROP: u8 = 190;
426const DOOR_PROMPT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
427
428const CAMERA_FOV: f32 = 45.0;
429/// Both cameras look down about forty degrees: shallow enough that the
430/// upright sprites keep close to their full height on screen.
431const OVERWORLD_CAMERA_OFFSET: Vec3 = Vec3::new(0.0, 9.0, 11.0);
432const CAVE_CAMERA_OFFSET: Vec3 = Vec3::new(0.0, 6.5, 7.5);
433
434// ---------------------------------------------------------------------
435// Collision footprints
436// ---------------------------------------------------------------------
437
438/// Excess reach of a square box turned `turn` about `+Y`, along either
439/// ground axis, as a factor of its side.
440fn turned_span(turn: f32) -> f32 {
441 turn.cos().abs() + turn.sin().abs()
442}
443
444/// One drawn prop's block: its footprint in the `x, z` the player walks.
445///
446/// Everything drawn on the ground is one — a box at its own extents,
447/// an upright sprite at the base under it. Pickups, flames, and whatever a
448/// clamp already holds — the hedgerow, the cave's own walls — are not.
449#[derive(Clone, Copy)]
450struct Obstacle {
451 center: Vec2,
452 half: Vec2,
453}
454
455impl Obstacle {
456 /// The footprint of a box `size` across centered on `at`, both
457 /// in the `x, z` the player walks.
458 fn footprint(at: Vec2, size: Vec2) -> Self {
459 Self {
460 center: at,
461 half: size * 0.5,
462 }
463 }
464
465 /// The footprint that just covers `corners` — what a prop the game turns
466 /// blocks the player as.
467 fn over(corners: [Vec2; 4]) -> Self {
468 let [first, rest @ ..] = corners;
469 let low = rest.iter().fold(first, |low, &corner| low.min(corner));
470 let high = rest.iter().fold(first, |high, &corner| high.max(corner));
471
472 Self {
473 center: (low + high) * 0.5,
474 half: (high - low) * 0.5,
475 }
476 }
477
478 /// Result of pushing a circle of `radius` at `point` out of this obstacle
479 /// the shortest way; `point` itself when it is already clear.
480 fn push_out(self, point: Vec2, radius: f32) -> Vec2 {
481 let delta = point - self.center;
482 let escape = self.half + Vec2::splat(radius) - delta.abs();
483
484 if escape.min_element() <= 0.0 {
485 point
486 } else if escape.x < escape.y {
487 let x = self.center.x + delta.x.signum() * (self.half.x + radius);
488 Vec2::new(x, point.y)
489 } else {
490 let z = self.center.y + delta.y.signum() * (self.half.y + radius);
491 Vec2::new(point.x, z)
492 }
493 }
494}
495
496/// A cave mouth's opening: where it is, and which way along `Z` the room
497/// it leads out of lies.
498#[derive(Clone, Copy)]
499struct Mouth {
500 at: Vec3,
501 room_side: f32,
502}
503
504impl Mouth {
505 /// True where `position` lies between the pillars and past the plane
506 /// in front of the opening — where the only two outcomes are stepping
507 /// through and being held.
508 fn holds(&self, position: Vec3) -> bool {
509 let plane = self.at.z + self.room_side * MOUTH_CROSSING_INSET;
510
511 (position.x - self.at.x).abs() < MOUTH_OPENING_HALF
512 && (plane - position.z) * self.room_side > 0.0
513 }
514
515 /// Position of each of the two pillars flanking the opening.
516 fn pillars(&self) -> impl Iterator<Item = Vec3> + Clone {
517 let at = self.at;
518
519 SIDES
520 .into_iter()
521 .map(move |side| at + Vec3::X * (side * MOUTH_PILLAR_OFFSET))
522 }
523
524 /// True when the camera looks into this mouth: it always lies on `+Z`
525 /// of the player, so the mouth whose room lies that way is the one seen
526 /// from the room's side, and the one to fill with a lintel and the dark
527 /// under it. The other is looked through from behind, and leaves its
528 /// opening clear for the room to show through.
529 fn looked_into(&self) -> bool {
530 self.room_side > 0.0
531 }
532}
533
534// ---------------------------------------------------------------------
535// The water style
536// ---------------------------------------------------------------------
537
538/// The pond's whole look, over the one value it reads: how far its ripple
539/// has traveled.
540#[derive(Default, ShaderValues)]
541struct Water {
542 time: f32,
543}
544
545impl SurfaceStyle for Water {
546 const PASS: DrawPass = DrawPass::Translucent;
547 const SURFACE: Option<&'static str> = Some(include_str!("sprite_adventure_water.wgsl"));
548}
549
550surface_styles! { enum Looks { Water } }
551
552// ---------------------------------------------------------------------
553// Meshes
554// ---------------------------------------------------------------------
555
556/// The player's current area; never both drawn in one frame.
557#[derive(Clone, Copy, PartialEq, Eq)]
558enum Area {
559 Overworld,
560 Cave,
561}
562
563/// The overworld's ground tile, its texture the only thing that separates a
564/// draw of it from another.
565#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
566struct Ground;
567
568impl Mesh for Ground {
569 fn build(&self, assets: &Assets) -> MeshData {
570 Plane
571 .build(assets)
572 .with_texture(assets.texture(GROUND_SHEET).pixelated())
573 }
574}
575
576/// The shoreline sprite laid over the pond's styled water, cutout so the
577/// water shows through its cleared middle.
578#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
579struct Shore;
580
581impl Mesh for Shore {
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 }
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 }
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 }
1697
1698 /// The player: upright so it always faces the camera about `+Y`,
1699 /// windowed to its facing's row and the walk cycle's current frame.
1700 fn draw_walker(&self, ctx: &mut FrameContext<'_, Keep>, ground: Vec3) {
1701 let step = if self.walk_ticks > 0 {
1702 (self.walk_ticks / TICKS_PER_WALK_FRAME) % WALKER_COLUMNS
1703 } else {
1704 0
1705 };
1706 let cell = Sheet::new(UVec2::new(WALKER_COLUMNS, WALKER_ROWS))
1707 .cell_at(UVec2::new(step, self.facing as u32));
1708 let size = Vec2::new(WALKER_WIDTH, WALKER_HEIGHT);
1709
1710 ctx.draw(
1711 Walker
1712 .at(Transform::from_scale_rotation_translation(
1713 size.extend(1.0),
1714 Quat::IDENTITY,
1715 ground + Vec3::Y * (WALKER_HEIGHT * 0.5),
1716 ))
1717 .upright()
1718 .frame(cell),
1719 );
1720 }
1721
1722 fn frame_overworld(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1723 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1724 ctx.set_camera(Self::camera(drawn_at, OVERWORLD_CAMERA_OFFSET));
1725 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
1726
1727 self.draw_ground(ctx);
1728 self.draw_hedgerow(ctx);
1729 self.draw_pond(ctx);
1730 self.draw_crates(ctx);
1731 self.draw_well(ctx);
1732 self.draw_flora(ctx);
1733 Self::draw_mouth(ctx, ENTRANCE);
1734 self.draw_walker(ctx, drawn_at);
1735 }
1736
1737 fn frame_cave(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1738 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1739 let camera = Self::camera(drawn_at, CAVE_CAMERA_OFFSET);
1740 ctx.set_camera(camera);
1741
1742 self.draw_cave_floor(ctx);
1743 self.draw_cave_walls(ctx);
1744 Self::draw_door_wall(ctx, (self.ghost > 0.0).then_some(drawn_at.x), self.ghost);
1745 Self::draw_mouth(ctx, EXIT);
1746 self.draw_torches(ctx);
1747 self.draw_door(ctx, self.ghost);
1748 self.draw_door_frame(ctx, self.ghost);
1749 if !self.gem_taken {
1750 self.draw_gem(ctx);
1751 }
1752 self.draw_walker(ctx, drawn_at);
1753 self.draw_door_prompt(ctx, camera);
1754 }
1755
1756 /// Instructions and the door's interact hint — gathered before `ctx.ui`,
1757 /// which cannot read `ctx`.
1758 fn overlay(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1759 let near_door = self.area == Area::Cave
1760 && !self.door_opening
1761 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1762 let gem_taken = self.area == Area::Cave && self.gem_taken;
1763 let mut reset_clicked = false;
1764
1765 ctx.ui(|ui| {
1766 egui::Frame::new()
1767 .fill(egui::Color32::from_black_alpha(HUD_BACKDROP))
1768 .inner_margin(HUD_PADDING)
1769 .corner_radius(f32::from(HUD_PADDING))
1770 .show(ui, |ui| {
1771 ui.visuals_mut().override_text_color = Some(egui::Color32::WHITE);
1772 ui.label("wasd / arrows / stick to walk");
1773 if near_door {
1774 ui.label("e / west button to open the door");
1775 }
1776 if gem_taken {
1777 ui.label("gem recovered");
1778 }
1779 ui.label("kept between runs: position, gem, cave");
1780 ui.label("r to reset world");
1781 if ui.button("reset world").clicked() {
1782 reset_clicked = true;
1783 }
1784 });
1785 });
1786
1787 if reset_clicked {
1788 self.reset_requested = true;
1789 }
1790 }
1791}
1792
1793impl Game for Keep {
1794 type Meshes = Shape;
1795 type Sounds = Sound;
1796 type InputActions = Controls;
1797 type Skyboxes = NoSkyboxes;
1798 type SurfaceStyles = Looks;
1799 type PostEffects = NoPostEffects;
1800
1801 fn tick(&mut self, ctx: &mut TickContext<'_, Keep>) {
1802 self.previous = self.position;
1803
1804 if self.reset_requested || ctx.pressed(Button::Reset) {
1805 self.reset_requested = false;
1806 self.reset(ctx);
1807 return;
1808 }
1809
1810 let heading = if ctx.ui_wants_keyboard() {
1811 Vec2::ZERO
1812 } else {
1813 ctx.axis2(Move::Walk)
1814 };
1815 match Facing::from_heading(heading) {
1816 Some(facing) => {
1817 self.facing = facing;
1818 self.walk_ticks += 1;
1819 }
1820 None => self.walk_ticks = 0,
1821 }
1822 let stride = Vec3::new(heading.x, 0.0, -heading.y) * WALK_SPEED * ctx.dt().as_secs_f32();
1823 self.position += stride;
1824 self.simulated += ctx.dt();
1825
1826 match self.area {
1827 Area::Overworld => self.tick_overworld(ctx),
1828 Area::Cave => self.tick_cave(ctx),
1829 }
1830 }42const STATION_SIZE: Vec3 = Vec3::new(0.6, 0.9, 0.5);
43/// The emissive cube set against the `STATION_SIZE` cube's own `-Z` side.
44const STATION_FRONT_SIZE: Vec3 = Vec3::new(0.42, 0.5, 0.06);
45/// The distance past the `STATION_SIZE` cube's own face the emissive
46/// cube's front keeps, in meters, so the two stay flush at a shallow
47/// angle.
48const STATION_FRONT_OUTWARD: f32 = 0.005;
49
50const RADAR_SWEEP_RATE: f32 = 40.0;
51const REACTOR_RATE: f32 = 0.5;
52const REACTOR_BASE: f32 = 55.0;
53const REACTOR_SWING: f32 = 35.0;
54
55const SUN_DIRECTION: Vec3 = Vec3::new(0.4, -1.0, -0.3);
56const SUN_COLOR: Color = Color::rgb(0.6, 0.62, 0.7);
57
58const CAMERA_FOV: f32 = 42.0;
59const CAMERA_TARGET: Vec3 = Vec3::new(0.0, STATION_SIZE.y * 0.5, 0.0);
60const START_YAW: f32 = 0.4;
61const START_PITCH: f32 = 0.35;
62/// How far short of straight up or down the pitch may turn, in radians.
63const PITCH_LIMIT: f32 = 0.9;
64const START_DISTANCE: f32 = 8.0;
65const MIN_DISTANCE: f32 = 3.0;
66const MAX_DISTANCE: f32 = 14.0;
67/// Radians the camera turns by on its own, per second.
68const AUTO_TURN_RATE: f32 = 0.12;
69/// Radians the pointer's own motion turns the view by, per physical pixel
70/// it crosses, while [`Trigger::Hail`] is held.
71const TURN_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
72/// The factor one full wheel step divides the distance to the target by.
73const ZOOM_STEP: f32 = 1.12;
74
75const BRACKET_MARGIN: f32 = 10.0;
76const BRACKET_STROKE: f32 = 2.0;
77const BRACKET_CORNER: f32 = 7.0;
78/// The gap kept past the line above's own full line height, so a glyph
79/// whose `mesh_bounds` reaches past that height still clears the line
80/// below.
81const STACK_GAP: f32 = 4.0;
82
83/// The gap between a `StationKind`'s own top and the `Prompt` drawn above
84/// it, in logical points.
85const PROMPT_LIFT: f32 = 20.0;
86
87const DIALOGUE_PADDING_X: f32 = 28.0;
88const DIALOGUE_PADDING_Y: f32 = 20.0;
89const DIALOGUE_MARGIN: f32 = 24.0;
90
91/// A sample the pixel font holds no glyph for, so `Proportional` falls
92/// back to egui's own font and `pixel-only` shows the missing glyph box.
93const FALLBACK_SAMPLE: &str = "café λ";
94/// Different widths of glyph together, so a family's own advance shows.
95const FAMILY_SAMPLE: &str = "mill and wall";
96const GRID_SAMPLE: &str = "the quick fox";
97
98meshes! { enum Shape { Cube, Plane } }
99
100/// This game's own sky: a dark, dim gradient, so each `StationKind`'s own
101/// emissive glow still reads as the platform's brightest color.
102#[derive(Catalog, Clone, Copy, Debug, PartialEq, Eq, Hash)]
103enum Sky {
104 Dusk,
105}
106
107impl Skyboxes for Sky {
108 fn build(&self, _assets: &Assets) -> SkyboxData {
109 SkyboxData::gradient(
110 Color::rgb(0.05, 0.06, 0.12),
111 Color::rgb(0.18, 0.12, 0.16),
112 Color::rgb(0.01, 0.01, 0.02),
113 )
114 .lit_by(0.25)
115 }
116}
117
118/// What one `StationKind` is drawn and named as.
119struct StationLook {
120 name: &'static str,
121 color: Color,
122 glow: Color,
123 lines: [&'static str; 2],
124}
125
126#[derive(Clone, Copy, PartialEq, Eq)]
127enum StationKind {
128 Radar,
129 Reactor,
130 Clock,
131}
132
133impl StationKind {
134 const ALL: [Self; 3] = [Self::Radar, Self::Reactor, Self::Clock];
135
136 fn index(self) -> usize {
137 self as usize
138 }
139
140 fn look(self) -> StationLook {
141 match self {
142 Self::Radar => StationLook {
143 name: "radar",
144 color: Color::rgb(0.22, 0.26, 0.30),
145 glow: Color::rgb(0.3, 1.4, 1.1),
146 lines: [
147 "the radar sweeps the dark past the platform for anything that moves",
148 "nothing answers back tonight",
149 ],
150 },
151 Self::Reactor => StationLook {
152 name: "reactor",
153 color: Color::rgb(0.30, 0.22, 0.18),
154 glow: Color::rgb(1.6, 0.7, 0.2),
155 lines: [
156 "the reactor gauge holds steady at a comfortable idle",
157 "plenty of power left for the long watch ahead",
158 ],
159 },
160 Self::Clock => StationLook {
161 name: "clock",
162 color: Color::rgb(0.20, 0.24, 0.22),
163 glow: Color::rgb(0.8, 0.9, 1.6),
164 lines: [
165 "the clock keeps the same count it always has",
166 "the watch ends when it says so and not before",
167 ],
168 },
169 }
170 }
171
172 /// This `StationKind`'s own center, on the platform's edge.
173 fn center(self) -> Vec3 {
174 let angle = self.index() as f32 / Self::ALL.len() as f32 * core::f32::consts::TAU;
175 Vec3::new(
176 STATION_RADIUS * angle.cos(),
177 STATION_SIZE.y * 0.5,
178 STATION_RADIUS * angle.sin(),
179 )
180 }
181
182 fn aabb(self) -> (Vec3, Vec3) {
183 let half = STATION_SIZE * 0.5;
184 (self.center() - half, self.center() + half)
185 }
186
187 /// The reading a `bracket` shows through `Monospace`, and the one
188 /// large number it shows through the `display` family, both from
189 /// `elapsed` seconds.
190 fn reading(self, elapsed: f32) -> (String, String) {
191 match self {
192 Self::Radar => {
193 let bearing = (elapsed * RADAR_SWEEP_RATE).rem_euclid(360.0);
194 (
195 format!("{bearing:.0} degrees bearing"),
196 format!("{bearing:03.0}"),
197 )
198 }
199 Self::Reactor => {
200 let percent = REACTOR_BASE + REACTOR_SWING * (elapsed * REACTOR_RATE).sin();
201 (
202 format!("{percent:.0} percent output"),
203 format!("{percent:.0}%"),
204 )
205 }
206 Self::Clock => {
207 let seconds = elapsed.rem_euclid(60.0);
208 (
209 format!("{seconds:.1} seconds this minute"),
210 format!("{seconds:04.1}"),
211 )
212 }
213 }
214 }
215}
216
217/// The `StationKind` `ray` lies nearest along, if any.
218fn hit_station(ray: Ray) -> Option<StationKind> {
219 StationKind::ALL
220 .into_iter()
221 .filter_map(|station| {
222 let (min, max) = station.aabb();
223 ray.hit_aabb(min, max).map(|distance| (distance, station))
224 })
225 .min_by(|(a, _), (b, _)| a.total_cmp(b))
226 .map(|(_, station)| station)
227}
228
229/// The logical point egui paints `pixel`, a physical pixel, at.
230fn logical(pixel: Vec2, pixels_per_point: f32) -> egui::Pos2 {
231 let point = pixel / pixels_per_point;
232 egui::pos2(point.x, point.y)
233}
234
235/// `text` at `font`, in this game's own text color.
236fn styled(text: impl Into<String>, font: egui::FontId) -> egui::RichText {
237 egui::RichText::new(text.into())
238 .font(font)
239 .color(TEXT_COLOR)
240}
241
242/// The position that draws `galley` with its `mesh_bounds` centered on
243/// `center_x` and its `mesh_bounds`'s own bottom at `bottom`.
244fn ink_bottom_at(galley: &egui::Galley, center_x: f32, bottom: f32) -> egui::Pos2 {
245 let ink = galley.mesh_bounds;
246 egui::pos2(center_x - ink.center().x, bottom - ink.max.y)
247}
248
249/// How a control reads to a player: a glyph the `prompts` family draws,
250/// or a control's own name in the `Proportional` font.
251enum Prompt {
252 Glyph(char),
253 Text(String),
254}
255
256impl Prompt {
257 fn text(&self) -> String {
258 match self {
259 Self::Glyph(glyph) => glyph.to_string(),
260 Self::Text(text) => text.clone(),
261 }
262 }
263
264 fn family(&self) -> egui::FontFamily {
265 match self {
266 Self::Glyph(_) => egui::FontFamily::Name(PROMPT_FAMILY.into()),
267 Self::Text(_) => egui::FontFamily::Proportional,
268 }
269 }
270}
271
272/// `binding` read for a player: a mouse glyph for its left or right
273/// button, or its own `Display` text otherwise, so a key reads as `E`
274/// and a pad button as its name.
275fn prompt(binding: &ButtonBinding) -> Prompt {
276 match binding {
277 ButtonBinding::Mouse(MouseButton::Left) => Prompt::Glyph('\u{E0EC}'),
278 ButtonBinding::Mouse(MouseButton::Right) => Prompt::Glyph('\u{E0F0}'),
279 other => Prompt::Text(other.to_string()),
280 }
281}
282
283/// A name in `corners`, a reading and a number, placed one above the
284/// other upward from `at`, which sits right above a `StationKind`'s top,
285/// one line height clear of it: the name boxed and nearest `at`, the
286/// reading a full line height above it, the number a full line height
287/// above that, so no two lines and no line and the box ever overlap.
288///
289/// Each gap is measured in the line below's own full line height, not its
290/// smaller `mesh_bounds`, so a glyph whose `mesh_bounds` reaches past that
291/// height still clears the line above it. The name's own frame is sized to
292/// its `mesh_bounds`, margin included, and the name centers inside it on
293/// every side. The caller measures every `Galley`; this only draws. Lifts
294/// into another game unchanged.
295fn bracket(
296 painter: &egui::Painter,
297 at: egui::Pos2,
298 name: Arc<egui::Galley>,
299 reading: Arc<egui::Galley>,
300 number: Arc<egui::Galley>,
301) {
302 let frame_bottom = at.y - name.rect.height();
303 let name_ink = name.mesh_bounds;
304 let frame_height = name_ink.height() + BRACKET_MARGIN * 2.0;
305 let frame = egui::Rect::from_min_size(
306 egui::pos2(
307 at.x - name_ink.width() * 0.5 - BRACKET_MARGIN,
308 frame_bottom - frame_height,
309 ),
310 egui::vec2(name_ink.width() + BRACKET_MARGIN * 2.0, frame_height),
311 );
312 let name_pos = ink_bottom_at(&name, at.x, frame_bottom - BRACKET_MARGIN);
313
314 let reading_bottom = frame.top() - STACK_GAP;
315 let reading_pos = ink_bottom_at(&reading, at.x, reading_bottom);
316
317 let number_bottom = reading_pos.y - reading.rect.height() - STACK_GAP;
318 let number_pos = ink_bottom_at(&number, at.x, number_bottom);
319
320 corners(painter, frame);
321 painter.galley(name_pos, name, TEXT_COLOR);
322 painter.galley(reading_pos, reading, TEXT_COLOR);
323 painter.galley(number_pos, number, TEXT_COLOR);
324}
325
326/// `glyph`, its `mesh_bounds` centered on `at`. Lifts into another game
327/// unchanged.
328fn prompt_at(painter: &egui::Painter, at: egui::Pos2, glyph: Arc<egui::Galley>) {
329 let ink = glyph.mesh_bounds;
330 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
331 painter.galley(pos, glyph, TEXT_COLOR);
332}
333
334/// Four short lines at `rect`'s own corners, in place of its sides.
335fn corners(painter: &egui::Painter, rect: egui::Rect) {
336 let stroke = egui::Stroke::new(BRACKET_STROKE, TEXT_COLOR);
337 for (corner, inward) in [
338 (rect.left_top(), egui::vec2(1.0, 1.0)),
339 (rect.right_top(), egui::vec2(-1.0, 1.0)),
340 (rect.left_bottom(), egui::vec2(1.0, -1.0)),
341 (rect.right_bottom(), egui::vec2(-1.0, -1.0)),
342 ] {
343 painter.line_segment(
344 [corner, corner + egui::vec2(inward.x * BRACKET_CORNER, 0.0)],
345 stroke,
346 );
347 painter.line_segment(
348 [corner, corner + egui::vec2(0.0, inward.y * BRACKET_CORNER)],
349 stroke,
350 );
351 }
352}
353
354/// A name shown large and one line typed a glyph a tick, over two lines a
355/// click steps through. Holds no `StationKind` of its own, so it lifts
356/// into another game unchanged.
357struct Dialogue {
358 name: String,
359 lines: [String; 2],
360 line: usize,
361 revealed: usize,
362}
363
364impl Dialogue {
365 fn start(name: impl Into<String>, lines: [String; 2]) -> Self {
366 Self {
367 name: name.into(),
368 lines,
369 line: 0,
370 revealed: 0,
371 }
372 }
373
374 fn current_line(&self) -> &str {
375 &self.lines[self.line]
376 }
377
378 /// Shows one more glyph of the current line, up to its whole length.
379 fn tick(&mut self) {
380 let len = self.current_line().chars().count();
381 self.revealed = (self.revealed + 1).min(len);
382 }
383
384 /// Steps to the next line; `false` where the last line was already
385 /// shown, for the caller to close instead.
386 fn advance(&mut self) -> bool {
387 if self.line + 1 < self.lines.len() {
388 self.line += 1;
389 self.revealed = 0;
390 true
391 } else {
392 false
393 }
394 }
395
396 /// Draws the box at a size `whole_line` set, so it never grows as the
397 /// glyphs of the same line arrive: `set_min_size` inside the window
398 /// holds that size from the frame this window first draws, rather
399 /// than egui's own resize state, which only grows a window to match
400 /// what the frame before it drew.
401 fn draw(&self, ui: &mut egui::Ui, whole_line: egui::Vec2) {
402 let display = egui::FontFamily::Name(DISPLAY_FAMILY.into());
403 let box_size = egui::vec2(
404 whole_line.x + DIALOGUE_PADDING_X,
405 whole_line.y + HEADING_SIZE + DIALOGUE_PADDING_Y,
406 );
407 egui::Window::new("hail")
408 .title_bar(false)
409 .resizable(false)
410 .collapsible(false)
411 .anchor(
412 egui::Align2::CENTER_BOTTOM,
413 egui::vec2(0.0, -DIALOGUE_MARGIN),
414 )
415 .show(ui.ctx(), |ui| {
416 ui.set_min_size(box_size);
417 ui.label(styled(&self.name, egui::FontId::new(HEADING_SIZE, display)));
418 let shown: String = self.current_line().chars().take(self.revealed).collect();
419 ui.label(styled(shown, egui::FontId::proportional(BODY_SIZE)));
420 });
421 }
422}
423
424/// Every family this game loaded, its name at `16` points beside a sample
425/// at `32` in that family; the same sample under `Proportional`, where
426/// egui's own fonts back what the pixel font holds no glyph for, beside
427/// it again under a family that holds only the pixel font; and the pixel
428/// font at `16` points beside itself at `17`, where its own grid stops
429/// holding it crisp.
430fn sheet(ui: &mut egui::Ui) {
431 egui::Window::new("font sheet")
432 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
433 .collapsible(false)
434 .resizable(false)
435 .show(ui.ctx(), |ui| {
436 egui::Grid::new("font sheet grid").show(ui, |ui| {
437 for (label, family) in [
438 ("proportional", egui::FontFamily::Proportional),
439 ("monospace", egui::FontFamily::Monospace),
440 ("display", egui::FontFamily::Name(DISPLAY_FAMILY.into())),
441 ] {
442 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
443 ui.label(styled(
444 FAMILY_SAMPLE,
445 egui::FontId::new(HEADING_SIZE, family),
446 ));
447 ui.end_row();
448 }
449
450 let pixel_only = egui::FontFamily::Name(PIXEL_ONLY_FAMILY.into());
451 for (label, family) in [
452 ("egui's fonts behind", egui::FontFamily::Proportional),
453 ("pixel font alone", pixel_only),
454 ] {
455 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
456 ui.label(styled(
457 FALLBACK_SAMPLE,
458 egui::FontId::new(BODY_SIZE, family),
459 ));
460 ui.end_row();
461 }
462
463 for (label, size) in [("16 points", 16.0), ("17 points", 17.0)] {
464 ui.label(styled(label, egui::FontId::proportional(BODY_SIZE)));
465 ui.label(styled(GRID_SAMPLE, egui::FontId::proportional(size)));
466 ui.end_row();
467 }
468 });
469 });
470}
471
472#[derive(InputButtonAction, Clone, Copy)]
473enum Trigger {
474 Hail,
475 Sheet,
476 Close,
477}
478
479impl InputButtonAction for Trigger {
480 fn bindings(&self) -> Vec<ButtonBinding> {
481 match self {
482 Self::Hail => vec![MouseButton::Left.into()],
483 Self::Sheet => vec![Key::Tab.into()],
484 Self::Close => vec![Key::Escape.into()],
485 }
486 }
487}
488
489/// The pointer's own motion, read only while [`Trigger::Hail`] is held.
490#[derive(InputAxis2Action, Clone, Copy)]
491enum Turn {
492 Look,
493}
494
495impl InputAxis2Action for Turn {
496 fn bindings(&self) -> Vec<Axis2Binding> {
497 match self {
498 Self::Look => vec![Axis2Binding::pointer().scale(TURN_SENSITIVITY)],
499 }
500 }
501}
502
503#[derive(InputAxisAction, Clone, Copy)]
504enum Zoom {
505 Wheel,
506}
507
508impl InputAxisAction for Zoom {
509 fn bindings(&self) -> Vec<AxisBinding> {
510 match self {
511 Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
512 }
513 }
514}
515
516struct Controls;
517
518impl InputActions for Controls {
519 type Button = Trigger;
520 type Axis = Zoom;
521 type Axis2 = Turn;
522}
523
524/// Reads every name in `names` and adds them under `family`, front to
525/// back in the order given, ahead of whatever `family` already held.
526fn install_family(
527 fonts: &mut egui::FontDefinitions,
528 startup: &mut Startup,
529 family: egui::FontFamily,
530 names: &[&str],
531) -> Result<(), Error> {
532 for &name in names {
533 if !fonts.font_data.contains_key(name) {
534 fonts
535 .font_data
536 .insert(name.to_owned(), startup.font(name)?.into());
537 }
538 }
539 let list = fonts.families.entry(family).or_default();
540 for &name in names.iter().rev() {
541 list.insert(0, name.to_owned());
542 }
543 Ok(())
544}
545
546/// The camera around [`CAMERA_TARGET`], its own distance clamped between
547/// [`MIN_DISTANCE`] and [`MAX_DISTANCE`], `yaw` free and `pitch` held to
548/// [`PITCH_LIMIT`]. Holds nothing of the game, so it copies into another
549/// one with its own values.
550struct Orbit {
551 yaw: f32,
552 pitch: f32,
553 distance: f32,
554}
555
556impl Default for Orbit {
557 fn default() -> Self {
558 Self {
559 yaw: START_YAW,
560 pitch: START_PITCH,
561 distance: START_DISTANCE,
562 }
563 }
564}
565
566impl Orbit {
567 fn camera(&self) -> Camera {
568 let direction = Vec3::new(
569 self.pitch.cos() * self.yaw.sin(),
570 self.pitch.sin(),
571 self.pitch.cos() * self.yaw.cos(),
572 );
573 Camera::new(
574 View::look_at(CAMERA_TARGET + direction * self.distance, CAMERA_TARGET),
575 Projection::perspective(CAMERA_FOV),
576 )
577 }
578
579 /// Turns `yaw` by `-by.x` and `pitch` by `by.y`, `pitch` held to its
580 /// limit.
581 fn turn(&mut self, by: Vec2) {
582 self.yaw -= by.x;
583 self.pitch = (self.pitch + by.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
584 }
585
586 /// Divides the distance to [`CAMERA_TARGET`] by `factor`, held to its
587 /// own least and most.
588 fn zoom(&mut self, factor: f32) {
589 self.distance = (self.distance / factor).clamp(MIN_DISTANCE, MAX_DISTANCE);
590 }
591}
592
593struct WatchRoom {
594 elapsed: Duration,
595 orbit: Orbit,
596 hailed: Option<StationKind>,
597 dialogue: Option<Dialogue>,
598 sheet_open: bool,
599}
600
601impl WatchRoom {
602 fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
603 let startup = ctx.startup();
604 let mut fonts = egui::FontDefinitions::default();
605 for (family, names) in [
606 (egui::FontFamily::Proportional, &[PROPORTIONAL_FONT][..]),
607 (egui::FontFamily::Monospace, &[MONOSPACE_FONT][..]),
608 (
609 egui::FontFamily::Name(DISPLAY_FAMILY.into()),
610 &[DISPLAY_FONT][..],
611 ),
612 (
613 egui::FontFamily::Name(PIXEL_ONLY_FAMILY.into()),
614 &[PROPORTIONAL_FONT][..],
615 ),
616 (
617 egui::FontFamily::Name(PROMPT_FAMILY.into()),
618 &[PROMPT_FONT, PROPORTIONAL_FONT][..],
619 ),
620 ] {
621 install_family(&mut fonts, startup, family, names)?;
622 }
623 startup.set_fonts(fonts)?;
624
625 Ok(Self {
626 elapsed: Duration::ZERO,
627 orbit: Orbit::default(),
628 hailed: None,
629 dialogue: None,
630 sheet_open: false,
631 })
632 }
633
634 fn handle_hail(&mut self, ctx: &mut TickContext<'_, Self>) {
635 let ray = ctx
636 .last_camera()
637 .ray_through(ctx.pointer(), ctx.window_size());
638 let Some(station) = hit_station(ray) else {
639 return;
640 };
641
642 if self.hailed != Some(station) {
643 self.hailed = Some(station);
644 let look = station.look();
645 self.dialogue = Some(Dialogue::start(look.name, look.lines.map(str::to_owned)));
646 return;
647 }
648 let Some(dialogue) = &mut self.dialogue else {
649 return;
650 };
651 if !dialogue.advance() {
652 self.dialogue = None;
653 self.hailed = None;
654 }
655 }
656
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 }13const GLOW_POSITION: Vec3 = Vec3::new(0.0, 0.6, 0.0);
14const GLOW_SIZE: f32 = 0.9;
15const GLOW_COLOR: Color = Color::rgb(4.0, 2.2, 0.6);
16
17/// A sphere either side of the glowing cube, and its radius.
18const SPHERE_POSITIONS: [Vec3; 2] = [Vec3::new(-1.6, 0.5, 0.4), Vec3::new(1.6, 0.5, -0.4)];
19const SPHERE_SUBDIVISIONS: u32 = 3;
20
21const SUN_DIRECTION: Vec3 = Vec3::new(0.5, -1.0, -0.3);
22const SUN_COLOR: Color = Color::rgb(0.85, 0.8, 0.7);
23
24const GROUND_COLOR: Color = Color::rgb(0.16, 0.17, 0.15);
25const SPHERE_COLOR: Color = Color::rgb(0.5, 0.52, 0.55);
26
27/// Bloom the frame draws at, so [`GLOW_COLOR`] past `1.0` scatters.
28const SCENE_BLOOM: f32 = 0.5;
29
30meshes! { enum Shape { Plane, Cube, Sphere } }
31
32/// The values the WGSL `Vignette` reads: how far it darkens toward the
33/// frame's corners.
34#[derive(ShaderValues)]
35struct Vignette {
36 strength: f32,
37}
38
39impl PostEffect for Vignette {
40 const STAGE: EffectStage = EffectStage::ToneMapped;
41 const SHADER: &'static str = include_str!("post_effects_vignette.wgsl");
42}
43
44/// The values the WGSL `Grain` reads: how much grain it draws, and the
45/// seed its integer-hash is drawn from.
46#[derive(ShaderValues)]
47struct Grain {
48 strength: f32,
49 seed: u32,
50}
51
52impl PostEffect for Grain {
53 const STAGE: EffectStage = EffectStage::ToneMapped;
54 const SHADER: &'static str = include_str!("post_effects_grain.wgsl");
55}
56
57/// The values the WGSL `Scanlines` reads: how far it darkens every other
58/// pixel row.
59#[derive(ShaderValues)]
60struct Scanlines {
61 strength: f32,
62}
63
64impl PostEffect for Scanlines {
65 const STAGE: EffectStage = EffectStage::ToneMapped;
66 const SHADER: &'static str = include_str!("post_effects_scanlines.wgsl");
67}
68
69post_effects! { enum Look { Vignette, Grain, Scanlines } }
70
71struct PostEffectEffects {
72 ticks: u32,
73 vignette: f32,
74 grain: f32,
75 scanlines: f32,
76}
77
78impl PostEffectEffects {
79 fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
80 Ok(Self {
81 ticks: 0,
82 vignette: 0.5,
83 grain: 0.08,
84 scanlines: 0.3,
85 })
86 }
87
88 /// The scene every frame draws: a sun over a ground plane, a glowing
89 /// cube bloom scatters from, and a sphere either side of it.
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 }
120
121 /// A slider per effect, and the effects themselves run at the values
122 /// they hold — `Grain`'s seed is [`Self::ticks`], so the frame it
123 /// draws stays repeatable.
124 fn panel(&mut self, ctx: &mut FrameContext<'_, Self>) {
125 ctx.ui(|ui| {
126 ui.add(egui::Slider::new(&mut self.vignette, 0.0..=1.0).text("vignette"));
127 ui.add(egui::Slider::new(&mut self.grain, 0.0..=1.0).text("grain"));
128 ui.add(egui::Slider::new(&mut self.scanlines, 0.0..=1.0).text("scanlines"));
129 });
130
131 ctx.set_post_effect(Vignette {
132 strength: self.vignette,
133 });
134 ctx.set_post_effect(Grain {
135 strength: self.grain,
136 seed: self.ticks,
137 });
138 ctx.set_post_effect(Scanlines {
139 strength: self.scanlines,
140 });
141 }
142}
143
144impl Game for PostEffectEffects {
145 type Meshes = Shape;
146 type Sounds = NoSounds;
147 type InputActions = Key;
148 type Skyboxes = NoSkyboxes;
149 type SurfaceStyles = NoSurfaceStyles;
150 type PostEffects = Look;
151
152 fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {
153 self.ticks += 1;
154 }
155
156 fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
157 ctx.set_camera(Camera::new(
158 View::look_at(Vec3::new(0.0, 3.4, 4.6), Vec3::new(0.0, 0.2, 0.0)),
159 Projection::perspective(45.0),
160 ));
161 ctx.set_bloom(SCENE_BLOOM);
162
163 self.draw_scene(ctx);
164 self.panel(ctx);
165 }45const FRONT_POSITION: Vec3 = Vec3::new(0.0, 0.7, -1.6);
46const FRONT_SCALE: f32 = 1.4;
47
48/// Sphere density every built sphere in this scene shares.
49const SPHERE_SUBDIVISIONS: u32 = 3;
50
51/// How many draws the reflection row makes, and the meters between their
52/// centers.
53const REFLECT_ROW_COUNT: usize = 5;
54const REFLECT_ROW_SPACING: f32 = 1.5;
55const REFLECT_ROW_Z: f32 = -1.6;
56const REFLECT_ROW_RADIUS: f32 = 0.55;
57
58const GROUND_COLOR: Color = Color::rgb(0.24, 0.25, 0.22);
59const SHADING_TINT: Color = Color::rgb(0.55, 0.55, 0.6);
60const RELIEF_TINT: Color = Color::rgb(0.5, 0.45, 0.35);
61const EMISSIVE_BASE: Color = Color::rgb(0.04, 0.04, 0.05);
62const EMISSIVE_GLOW: Color = Color::rgb(3.2, 2.2, 0.7);
63
64/// Texel side length of every generated map: coarse enough that each
65/// checker cell reads as a distinct part on a sphere or a cube face.
66const MAP_SIZE: UVec2 = UVec2::new(64, 64);
67
68/// Checker cell width, in texels, for the shading map.
69const SHADING_CELL: u32 = 8;
70/// The shading checker's two states: occlusion, roughness and metallic —
71/// one square low across all three, the other full across all three.
72const SHADING_LOW: [u8; 3] = [70, 40, 15];
73const SHADING_HIGH: [u8; 3] = [255, 225, 235];
74
75/// Checker cell width, in texels, for the emissive map.
76const EMISSIVE_CELL: u32 = 6;
77
78/// Wave count the relief's texture repeats across its map, and the peak
79/// slope of its surface, in height over distance.
80const BUMP_WAVES: f32 = 6.0;
81const BUMP_SLOPE: f32 = 1.15;
82
83/// `BannerCloth`'s width and height, in meters.
84const BANNER_WIDTH: f32 = 1.1;
85const BANNER_HEIGHT: f32 = 0.7;
86
87/// Columns `BannerCloth` splits into, so its wave curves smoothly.
88const BANNER_COLUMNS: u32 = 10;
89
90/// Where the pillars, the pole, the cloth and the pulsing sphere are
91/// placed, added to every one of their own positions: apart from the
92/// pairs and the reflection row, so a light's shadow has clear ground to
93/// land on.
94const OUTPOST: Vec3 = Vec3::new(-4.6, 0.0, -0.8);
95
96const PILLARS: [(Vec3, Vec3); 3] = [
97 (Vec3::new(-1.8, 0.6, -0.6), Vec3::new(0.6, 1.2, 0.6)),
98 (Vec3::new(0.4, 0.4, -1.6), Vec3::new(0.5, 0.8, 0.5)),
99 (Vec3::new(1.7, 0.9, 0.4), Vec3::new(0.55, 1.8, 0.55)),
100];
101
102const POLE_POSITION: Vec3 = Vec3::new(-2.6, 1.0, 0.4);
103const POLE_SCALE: Vec3 = Vec3::new(0.12, 2.0, 0.12);
104const BANNER_MOUNT: Vec3 = Vec3::new(-2.54, 1.55, 0.46);
105
106const FIELD_ORB_POSITION: Vec3 = Vec3::new(1.3, 1.1, 1.6);
107const FIELD_ORB_SCALE: f32 = 0.7;
108
109/// The lamp's fixed position and reach, in meters.
110const LAMP_POSITION: Vec3 = Vec3::new(2.4, 1.4, -3.0);
111const LAMP_RANGE: f32 = 5.0;
112
113/// The spotlight's fixed placement: where it is placed, which way its
114/// cone points, its reach in meters and its width in radians.
115const SPOT_POSITION: Vec3 = Vec3::new(-3.4, 3.0, 1.6);
116const SPOT_DIRECTION: Vec3 = Vec3::new(0.55, -1.0, -1.0);
117const SPOT_RANGE: f32 = 7.0;
118const SPOT_ANGLE: f32 = 0.5;
119
120/// The camera's vertical field of view, in degrees.
121const CAMERA_FOV: f32 = 46.0;
122/// Where the camera starts, and the `yaw` and the pitch, in radians, it
123/// starts turned to.
124const START_EYE: Vec3 = Vec3::new(-0.6, 2.2, 9.0);
125const START_YAW: f32 = 0.0;
126const START_PITCH: f32 = -0.15;
127/// How far short of straight up or down the pitch may turn, in radians,
128/// where a turn alone reads as nothing.
129const PITCH_LIMIT: f32 = 1.5;
130/// The `eye`'s least height above the ground plane, in meters: held above
131/// zero so a move can never take it below.
132const MIN_EYE_HEIGHT: f32 = 0.3;
133/// Radians the pointer's own motion turns the view by, per physical
134/// pixel it crosses, before the axis it reads through bounds it: a drag
135/// across the whole window turns about a quarter turn.
136const LOOK_SENSITIVITY: f32 = core::f32::consts::FRAC_PI_2 / 1280.0;
137/// Meters a move key covers per second, at [`Playground::speed_scale`]'s
138/// own default.
139const MOVE_SPEED: f32 = 4.0;
140/// The factor one full wheel step multiplies the move speed apart from.
141const SPEED_STEP: f32 = 1.5;
142/// The move speed's own least and most, as a factor of [`MOVE_SPEED`].
143const MIN_SPEED_SCALE: f32 = 0.2;
144const MAX_SPEED_SCALE: f32 = 5.0;
145
146/// Bloom this scene starts at, past the engine's own default: enough that
147/// [`EMISSIVE_GLOW`] and the brightest lights scatter right away.
148const START_BLOOM: f32 = 0.25;
149const START_EXPOSURE: f32 = 1.0;
150
151/// The sky a frame that keeps [`Sky::Default`] draws and is lit by: the
152/// same flat grey the engine falls back to when a frame sets none.
153const DEFAULT_SKY: Color = Color::rgb(0.1, 0.1, 0.1);
154
155/// The fraction of its own light each loaded sky lands and reflects,
156/// through [`SkyboxData::lit_by`]: the bright images fixed low, since an
157/// image read too bright under the frame's own lights at its default
158/// `1.0`; the dim images fixed more, since the scene read too dark under
159/// them at the bright images' value.
160const CLEAR_SKY_LIGHT: f32 = 0.35;
161const CLASSIC_SKY_LIGHT: f32 = 0.35;
162const DAWN_SKY_LIGHT: f32 = 0.3;
163const SINISTER_SKY_LIGHT: f32 = 0.6;
164const LIGHT_BLUE_STARS_LIGHT: f32 = 0.8;
165const BLUE_STARS_LIGHT: f32 = 0.8;
166
167/// The shading pair's plain half: a sphere given the shared shading
168/// material and no map.
169#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
170struct ShadingPlain;
171
172impl Mesh for ShadingPlain {
173 fn build(&self, assets: &Assets) -> MeshData {
174 sphere_with_material(assets, shading_material())
175 }
176}
177
178/// The shading pair's mapped half: the same sphere and material, with its
179/// shading map (occlusion, roughness and metallic) baked in.
180#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
181struct ShadingMapped;
182
183impl Mesh for ShadingMapped {
184 fn build(&self, assets: &Assets) -> MeshData {
185 sphere_with_material(assets, shading_material()).with_shading(shading_checker())
186 }
187}
188
189/// The relief pair's plain half.
190#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
191struct ReliefPlain;
192
193impl Mesh for ReliefPlain {
194 fn build(&self, assets: &Assets) -> MeshData {
195 sphere_with_material(assets, relief_material())
196 }
197}
198
199/// The relief pair's mapped half: the same sphere and material, with its
200/// relief map baked in.
201#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
202struct ReliefMapped;
203
204impl Mesh for ReliefMapped {
205 fn build(&self, assets: &Assets) -> MeshData {
206 sphere_with_material(assets, relief_material()).with_relief(relief_bumps())
207 }
208}
209
210/// The emissive pair's plain half.
211#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
212struct EmissivePlain;
213
214impl Mesh for EmissivePlain {
215 fn build(&self, assets: &Assets) -> MeshData {
216 cube_with_material(assets, emissive_material())
217 }
218}
219
220/// The emissive pair's mapped half: the same cube and material, with its
221/// emissive map baked in.
222#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
223struct EmissiveMapped;
224
225impl Mesh for EmissiveMapped {
226 fn build(&self, assets: &Assets) -> MeshData {
227 cube_with_material(assets, emissive_material()).with_emissive_map(emissive_checker())
228 }
229}
230
231/// The front sphere: a draw overrides its material new every frame, in
232/// place of a baked one.
233#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
234struct Front;
235
236impl Mesh for Front {
237 fn build(&self, assets: &Assets) -> MeshData {
238 Sphere {
239 subdivisions: SPHERE_SUBDIVISIONS,
240 }
241 .build(assets)
242 }
243}
244
245/// `BannerCloth`: a mesh split into columns along its span and placed at
246/// its `x = 0` edge, so `Banner`'s wave curves it, not a single flat
247/// quad. Its triangles are built twice: once as authored and once in the
248/// other order, with the normal turned around, so the cloth draws from
249/// both sides however its wave curves it.
250#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
251struct BannerCloth;
252
253impl Mesh for BannerCloth {
254 fn build(&self, _: &Assets) -> MeshData {
255 banner_mesh()
256 }
257}
258
259// Everything this game draws: the ground plane, each map pair's plain and
260// mapped half, the front sphere with its own live material, the built-in
261// primitives the reflection row and the pillars beside it place per draw,
262// and the displaced cloth.
263meshes! {
264 enum Shape {
265 Plane,
266 Sphere,
267 Cube,
268 ShadingPlain,
269 ShadingMapped,
270 ReliefPlain,
271 ReliefMapped,
272 EmissivePlain,
273 EmissiveMapped,
274 Front,
275 BannerCloth,
276 }
277}
278
279fn sphere_with_material(assets: &Assets, material: Material) -> MeshData {
280 Sphere {
281 subdivisions: SPHERE_SUBDIVISIONS,
282 }
283 .build(assets)
284 .with_material(material)
285}
286
287fn cube_with_material(assets: &Assets, material: Material) -> MeshData {
288 Cube.build(assets).with_material(material)
289}
290
291fn shading_material() -> Material {
292 Material::lit(SHADING_TINT).roughness(0.5).metallic(0.5)
293}
294
295fn relief_material() -> Material {
296 Material::lit(RELIEF_TINT).roughness(0.35)
297}
298
299fn emissive_material() -> Material {
300 Material::color(EMISSIVE_BASE).emissive(EMISSIVE_GLOW)
301}
302
303/// A shading map whose checker goes between low occlusion, roughness and
304/// metallic and full occlusion, roughness and metallic, so all three read
305/// apart across [`ShadingMapped`].
306fn shading_checker() -> ShadingData {
307 ShadingData::rgba8(
308 MAP_SIZE,
309 checker_pixels(MAP_SIZE, SHADING_CELL, SHADING_LOW, SHADING_HIGH),
310 )
311}
312
313/// An emissive map whose checker goes between full glow and none, so
314/// [`EMISSIVE_GLOW`] shapes across [`EmissiveMapped`] instead of casting
315/// whole.
316fn emissive_checker() -> TextureData {
317 TextureData::rgba8(
318 MAP_SIZE,
319 checker_pixels(MAP_SIZE, EMISSIVE_CELL, [0, 0, 0], [255, 255, 255]),
320 )
321}
322
323fn checker_pixels(size: UVec2, cell: u32, low: [u8; 3], high: [u8; 3]) -> Vec<u8> {
324 let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
325 for y in 0..size.y {
326 for x in 0..size.x {
327 let on = ((x / cell) + (y / cell)).is_multiple_of(2);
328 let [red, green, blue] = if on { high } else { low };
329 pixels.extend_from_slice(&[red, green, blue, u8::MAX]);
330 }
331 }
332 pixels
333}
334
335/// A relief whose normals turn across a wave that repeats over the map:
336/// each texel's slope comes from the partial derivatives of a
337/// `sin(u) * sin(v)` height field at `BUMP_SLOPE`'s peak, computed at that
338/// texel and not sampled from any other.
339fn relief_bumps() -> ReliefData {
340 let size = MAP_SIZE;
341 let turns = core::f32::consts::TAU * BUMP_WAVES;
342 let mut pixels = Vec::with_capacity((size.x * size.y * 4) as usize);
343 for y in 0..size.y {
344 for x in 0..size.x {
345 let u = (x as f32 + 0.5) / size.x as f32;
346 let v = (y as f32 + 0.5) / size.y as f32;
347 let slope_u = BUMP_SLOPE * (turns * u).cos() * (turns * v).sin();
348 let slope_v = BUMP_SLOPE * (turns * u).sin() * (turns * v).cos();
349 let normal = Vec3::new(-slope_u, -slope_v, 1.0).normalize();
350 let encode = |signed: f32| ((signed * 0.5 + 0.5) * 255.0).round() as u8;
351 pixels.extend_from_slice(&[encode(normal.x), encode(normal.y), encode(normal.z), 0]);
352 }
353 }
354 ReliefData::normals(size, pixels)
355}
356
357/// `BannerCloth`'s vertices and indices, built twice over: the columns as
358/// authored, facing `+Z`, and the same columns again facing `-Z`, their
359/// triangles in the other order so both draw front side out.
360fn banner_mesh() -> MeshData {
361 let mut vertices = Vec::with_capacity(((BANNER_COLUMNS + 1) * 4) as usize);
362 for normal in [Vec3::Z, Vec3::NEG_Z] {
363 for column in 0..=BANNER_COLUMNS {
364 let u = column as f32 / BANNER_COLUMNS as f32;
365 let x = u * BANNER_WIDTH;
366 for v in [0.0, 1.0] {
367 vertices.push(Vertex::new(
368 Vec3::new(x, -v * BANNER_HEIGHT, 0.0),
369 normal,
370 Vec2::new(u, v),
371 ));
372 }
373 }
374 }
375
376 let side = BANNER_COLUMNS + 1;
377 let mut indices = Vec::with_capacity((BANNER_COLUMNS * 12) as usize);
378 for column in 0..BANNER_COLUMNS {
379 let top_left = column * 2;
380 let bottom_left = top_left + 1;
381 let top_right = top_left + 2;
382 let bottom_right = top_left + 3;
383 indices.extend([
384 bottom_left,
385 bottom_right,
386 top_right,
387 bottom_left,
388 top_right,
389 top_left,
390 ]);
391
392 let back = side * 2;
393 indices.extend([
394 back + top_right,
395 back + bottom_right,
396 back + bottom_left,
397 back + top_left,
398 back + top_right,
399 back + bottom_left,
400 ]);
401 }
402
403 MeshData::new(vertices, indices)
404}
405
406/// Displaced by a wave that grows away from its `x = 0` edge; casts the
407/// shadow of where it was placed, unmoved by its own wave. Its one value
408/// is the clock its wave slides on.
409#[derive(Default, ShaderValues)]
410struct Banner {
411 time: f32,
412}
413
414impl SurfaceStyle for Banner {
415 const PASS: DrawPass = DrawPass::Opaque;
416 const DISPLACE: Option<&'static str> = Some(include_str!("material_playground_banner.wgsl"));
417}
418
419/// A surface that reads no light of the scene's own: it draws its own
420/// pulsing tint, added over what is behind it, through the color it pulses
421/// through and the clock the pulse is timed by.
422#[derive(Default, ShaderValues)]
423struct Field {
424 tint: Color,
425 time: f32,
426}
427
428impl SurfaceStyle for Field {
429 const PASS: DrawPass = DrawPass::Additive;
430 const SURFACE: Option<&'static str> = Some(include_str!("material_playground_field.wgsl"));
431}
432
433surface_styles! { enum Looks { Banner, Field } }
434
435/// A whole scene lighting choice: it names a sky and, kept with it, the
436/// sun that lights the scene, so a choice cannot leave the two apart.
437/// `Dawn`, `Noon`, `Dusk` and `Night` each pair a gradient with a sun of
438/// its own color and direction; `Clear`, `Classic`, `ImageDawn` and
439/// `Sinister` each pair a loaded image with a sun that fits it, and
440/// `LightBlueStars` and `BlueStars` pair a loaded space image with none;
441/// `Default` is the engine's own grey sky and white sun.
442///
443/// [`Skyboxes`] proves every value at startup, so it must be [`Eq`] and
444/// [`Hash`] over a fixed [`Skyboxes::catalog`] — a sky and sun a player
445/// set to any color and direction live could never meet, since `f32` is
446/// neither. This fixed, named set is the shape this file chose in its
447/// place: the side area offers it as one row, and shows the chosen sky's
448/// own light and its sun's own strength as text, read only, rather than
449/// controls a game could not build from. See this example's report for
450/// what that choice costs.
451#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
452enum Sky {
453 Dawn,
454 Noon,
455 Dusk,
456 Night,
457 Clear,
458 Classic,
459 ImageDawn,
460 Sinister,
461 LightBlueStars,
462 BlueStars,
463 Default,
464}
465
466impl Sky {
467 const ALL: [Sky; 11] = [
468 Self::Dawn,
469 Self::Noon,
470 Self::Dusk,
471 Self::Night,
472 Self::Clear,
473 Self::Classic,
474 Self::ImageDawn,
475 Self::Sinister,
476 Self::LightBlueStars,
477 Self::BlueStars,
478 Self::Default,
479 ];
480
481 fn name(self) -> &'static str {
482 match self {
483 Self::Dawn => "dawn",
484 Self::Noon => "noon",
485 Self::Dusk => "dusk",
486 Self::Night => "night",
487 Self::Clear => "clear day",
488 Self::Classic => "classic",
489 Self::ImageDawn => "dawn image",
490 Self::Sinister => "sinister night",
491 Self::LightBlueStars => "light blue stars",
492 Self::BlueStars => "blue stars",
493 Self::Default => "default",
494 }
495 }
496
497 /// The fraction of its own light this sky lands and reflects, through
498 /// [`SkyboxData::lit_by`]: fixed per choice, so a bright one does not
499 /// read too bright, and a dark one does not read too dark, under the
500 /// frame's own lights.
501 fn light(self) -> f32 {
502 match self {
503 Self::Dawn => 0.4,
504 Self::Noon => 0.5,
505 Self::Dusk => 0.35,
506 Self::Night => 0.3,
507 Self::Clear => CLEAR_SKY_LIGHT,
508 Self::Classic => CLASSIC_SKY_LIGHT,
509 Self::ImageDawn => DAWN_SKY_LIGHT,
510 Self::Sinister => SINISTER_SKY_LIGHT,
511 Self::LightBlueStars => LIGHT_BLUE_STARS_LIGHT,
512 Self::BlueStars => BLUE_STARS_LIGHT,
513 Self::Default => 1.0,
514 }
515 }
516
517 /// The sun this choice pairs with its sky: direction, color and
518 /// strength resolved together, so a choice cannot leave them apart.
519 /// `None` for the two space images, which pair with no sun at all.
520 fn sun(self) -> Option<(Vec3, Color, f32)> {
521 match self {
522 Self::Dawn => Some((
523 Vec3::new(-1.0, -0.15, 0.05),
524 Color::rgb(1.0, 0.7, 0.45),
525 1.4,
526 )),
527 Self::Noon => Some((
528 Vec3::new(-0.15, -1.0, -0.1),
529 Color::rgb(1.0, 1.0, 0.98),
530 1.6,
531 )),
532 Self::Dusk => Some((
533 Vec3::new(1.0, -0.15, 0.05),
534 Color::rgb(1.0, 0.55, 0.25),
535 1.2,
536 )),
537 Self::Night => Some((
538 Vec3::new(-0.3, -0.7, -0.6),
539 Color::rgb(0.55, 0.65, 0.85),
540 0.15,
541 )),
542 Self::Clear => Some((
543 Vec3::new(-0.2, -1.0, -0.15),
544 Color::rgb(1.0, 0.98, 0.9),
545 1.5,
546 )),
547 Self::Classic => Some((
548 Vec3::new(-0.4, -0.9, -0.2),
549 Color::rgb(1.0, 0.95, 0.85),
550 1.3,
551 )),
552 Self::ImageDawn => Some((Vec3::new(-1.0, -0.2, 0.1), Color::rgb(1.0, 0.75, 0.5), 1.1)),
553 Self::Sinister => Some((Vec3::new(0.4, -0.5, -0.7), Color::rgb(0.4, 0.5, 0.75), 0.1)),
554 Self::LightBlueStars | Self::BlueStars => None,
555 Self::Default => Some((Vec3::new(-0.4, -1.0, -0.6), Color::WHITE, 1.0)),
556 }
557 }
558
559 /// The color the sky reads under the horizon, through
560 /// [`SkyboxData::with_ground`]: the floor as lit under this choice's own
561 /// sun and [`Self::light`], so it moves with them, not only with the
562 /// image. `None` for the gradient skies and `Default`, which need no
563 /// ground, and for the two space images, which hold space below the
564 /// horizon as well.
565 fn ground(self) -> Option<Color> {
566 match self {
567 Self::Clear => Some(Color::rgb(0.501, 0.517, 0.449)),
568 Self::Classic => Some(Color::rgb(0.420, 0.405, 0.379)),
569 Self::ImageDawn => Some(Color::rgb(0.073, 0.053, 0.032)),
570 Self::Sinister => Some(Color::rgb(0.012, 0.014, 0.020)),
571 Self::Dawn
572 | Self::Noon
573 | Self::Dusk
574 | Self::Night
575 | Self::LightBlueStars
576 | Self::BlueStars
577 | Self::Default => None,
578 }
579 }
580}
581
582impl Catalog for Sky {
583 fn catalog() -> Vec<Self> {
584 Self::ALL.to_vec()
585 }
586}
587
588impl Skyboxes for Sky {
589 fn build(&self, assets: &Assets) -> SkyboxData {
590 let sky = match self {
591 Self::Dawn => SkyboxData::gradient(
592 Color::rgb(0.55, 0.55, 0.75),
593 Color::rgb(0.95, 0.6, 0.35),
594 Color::rgb(0.12, 0.08, 0.06),
595 ),
596 Self::Noon => SkyboxData::gradient(
597 Color::rgb(0.2, 0.45, 0.85),
598 Color::rgb(0.75, 0.82, 0.9),
599 Color::rgb(0.3, 0.3, 0.28),
600 ),
601 Self::Dusk => SkyboxData::gradient(
602 Color::rgb(0.18, 0.1, 0.3),
603 Color::rgb(0.85, 0.35, 0.2),
604 Color::rgb(0.03, 0.02, 0.03),
605 ),
606 Self::Night => SkyboxData::gradient(
607 Color::rgb(0.02, 0.02, 0.06),
608 Color::rgb(0.05, 0.05, 0.1),
609 Color::rgb(0.0, 0.0, 0.0),
610 ),
611 Self::Clear => assets.skybox("sky-clear"),
612 Self::Classic => assets.skybox("sky-classic"),
613 Self::ImageDawn => assets.skybox("sky-dawn"),
614 Self::Sinister => assets.skybox("sky-sinister"),
615 Self::LightBlueStars => assets.skybox("sky-stars-lightblue"),
616 Self::BlueStars => assets.skybox("sky-stars-blue"),
617 Self::Default => SkyboxData::gradient(DEFAULT_SKY, DEFAULT_SKY, DEFAULT_SKY),
618 };
619 let sky = match self.ground() {
620 Some(ground) => sky.with_ground(ground),
621 None => sky,
622 };
623
624 sky.lit_by(self.light())
625 }
626}
627
628/// `color` scaled by `strength`, the value a [`Light`] reads.
629fn scaled(color: Color, strength: f32) -> Color {
630 Color::rgb(
631 color.red * strength,
632 color.green * strength,
633 color.blue * strength,
634 )
635}
636
637/// One light's color and strength, held apart from the position that
638/// names it, plus whether it casts.
639#[derive(Clone, Copy)]
640struct Glow {
641 color: Color,
642 strength: f32,
643 shadow: bool,
644}
645
646impl Glow {
647 /// `color` scaled by `strength`, the value a [`Light`] reads.
648 fn scaled(self) -> Color {
649 scaled(self.color, self.strength)
650 }
651}
652
653/// Every key and button this game reads apart from the UI: held, `Look`
654/// turns the camera by the pointer's own motion, `Forward`/`Back`/
655/// `Left`/`Right` move it along the view and to its side, and `Up`/
656/// `Down` move it along the world's own up.
657#[derive(InputButtonAction, Clone, Copy, PartialEq)]
658enum Move {
659 Forward,
660 Back,
661 Left,
662 Right,
663 Up,
664 Down,
665 Look,
666}
667
668impl InputButtonAction for Move {
669 fn bindings(&self) -> Vec<ButtonBinding> {
670 match self {
671 Self::Forward => vec![Key::W.into()],
672 Self::Back => vec![Key::S.into()],
673 Self::Left => vec![Key::A.into()],
674 Self::Right => vec![Key::D.into()],
675 Self::Up => vec![Key::Space.into()],
676 Self::Down => vec![Key::LeftShift.into()],
677 Self::Look => vec![MouseButton::Right.into()],
678 }
679 }
680}
681
682/// The pointer's own motion, read only while [`Move::Look`] is held.
683#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
684enum Turn {
685 Look,
686}
687
688impl InputAxis2Action for Turn {
689 fn bindings(&self) -> Vec<Axis2Binding> {
690 match self {
691 Self::Look => vec![Axis2Binding::pointer().scale(LOOK_SENSITIVITY)],
692 }
693 }
694}
695
696/// How far the wheel moved this frame, read to scale the move speed.
697#[derive(InputAxisAction, Clone, Copy, PartialEq)]
698enum Speed {
699 Wheel,
700}
701
702impl InputAxisAction for Speed {
703 fn bindings(&self) -> Vec<AxisBinding> {
704 match self {
705 Self::Wheel => vec![AxisBinding::from(WheelDelta::Up).scale(4.0)],
706 }
707 }
708}
709
710struct Controls;
711
712impl InputActions for Controls {
713 type Button = Move;
714 type Axis = Speed;
715 type Axis2 = Turn;
716}
717
718struct Playground {
719 eye: Vec3,
720 yaw: f32,
721 pitch: f32,
722 speed_scale: f32,
723
724 sky: Sky,
725 sun_shadow: bool,
726
727 lamp: Glow,
728 spotlight: Glow,
729
730 front_tint: Color,
731 front_roughness: f32,
732 front_metallic: f32,
733 shading_map_on: bool,
734 relief_map_on: bool,
735 emissive_map_on: bool,
736
737 exposure: f32,
738 bloom: f32,
739}
740
741impl Playground {
742 fn init(ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
743 let _ = ctx;
744 Ok(Self {
745 eye: START_EYE,
746 yaw: START_YAW,
747 pitch: START_PITCH,
748 speed_scale: 1.0,
749
750 sky: Sky::Default,
751 sun_shadow: true,
752
753 lamp: Glow {
754 color: Color::rgb(0.9, 0.55, 0.3),
755 strength: 3.0,
756 shadow: false,
757 },
758 spotlight: Glow {
759 color: Color::rgb(0.4, 0.6, 1.0),
760 strength: 6.0,
761 shadow: true,
762 },
763
764 front_tint: Color::rgb(0.7, 0.25, 0.2),
765 front_roughness: 0.4,
766 front_metallic: 0.0,
767 shading_map_on: true,
768 relief_map_on: true,
769 emissive_map_on: true,
770
771 exposure: START_EXPOSURE,
772 bloom: START_BLOOM,
773 })
774 }
775
776 /// This frame's forward direction, from `yaw` (turning around the
777 /// world's own up) and `pitch` (turning up or down).
778 fn forward(&self) -> Vec3 {
779 Vec3::new(
780 -self.pitch.cos() * self.yaw.sin(),
781 self.pitch.sin(),
782 -self.pitch.cos() * self.yaw.cos(),
783 )
784 }
785
786 /// The camera this frame draws from: `eye` looking along `forward`.
787 fn camera(&self) -> Camera {
788 Camera::new(
789 View::look_at(self.eye, self.eye + self.forward()),
790 Projection::perspective(CAMERA_FOV),
791 )
792 }
793
794 /// A held `Move::Look` (the right mouse button) turns the camera by
795 /// the pointer's own motion, the same way it moves: dragging right
796 /// turns the view right and left turns it left, dragging down turns
797 /// it to look further down at the scene, dragging up back toward the
798 /// horizon. `W`/`A`/`S`/`D` move along the view and to its side,
799 /// `Space`/`Left Shift` up and down, and the wheel scales how far
800 /// each move goes. The `eye` is held above the ground plane wherever
801 /// it moves.
802 fn fly_camera(&mut self, ctx: &mut FrameContext<'_, Self>) {
803 if !ctx.ui_wants_pointer() && ctx.down(Move::Look) {
804 let look = ctx.axis2(Turn::Look);
805 self.yaw -= look.x;
806 self.pitch = (self.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
807 }
808
809 let wheel = ctx.axis(Speed::Wheel);
810 if !ctx.ui_wants_pointer() && wheel != 0.0 {
811 self.speed_scale =
812 (self.speed_scale * SPEED_STEP.powf(wheel)).clamp(MIN_SPEED_SCALE, MAX_SPEED_SCALE);
813 }
814
815 let forward = self.forward();
816 let right = Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin());
817 let mut move_by = Vec3::ZERO;
818 if ctx.down(Move::Forward) {
819 move_by += forward;
820 }
821 if ctx.down(Move::Back) {
822 move_by -= forward;
823 }
824 if ctx.down(Move::Right) {
825 move_by += right;
826 }
827 if ctx.down(Move::Left) {
828 move_by -= right;
829 }
830 if ctx.down(Move::Up) {
831 move_by += Vec3::Y;
832 }
833 if ctx.down(Move::Down) {
834 move_by -= Vec3::Y;
835 }
836 if move_by.length_squared() > 1.0 {
837 move_by = move_by.normalize();
838 }
839
840 self.eye += move_by * MOVE_SPEED * self.speed_scale * ctx.dt().as_secs_f32();
841 self.eye.y = self.eye.y.max(MIN_EYE_HEIGHT);
842 }
843
844 /// The material [`Front`] draws with, resolved new from its sliders
845 /// every frame — the override [`Instance::material`] takes, in place
846 /// of a baked one.
847 fn front_material(&self) -> Material {
848 Material::lit(self.front_tint)
849 .roughness(self.front_roughness)
850 .metallic(self.front_metallic)
851 }
852
853 /// Every draw this game makes: the ground, each map pair, the front
854 /// sphere, the reflection row and the pillars beside it.
855 fn draw_scene(&self, ctx: &mut FrameContext<'_, Self>) {
856 ctx.draw(
857 Plane
858 .at(Transform::from_scale(Vec3::new(
859 GROUND_SIZE,
860 1.0,
861 GROUND_SIZE,
862 )))
863 .material(Material::lit(GROUND_COLOR).roughness(0.9)),
864 );
865
866 Self::draw_pair(
867 ctx,
868 SHADING_Z,
869 SPHERE_RADIUS,
870 ShadingPlain.at(Vec3::ZERO).into_set(),
871 ShadingMapped.at(Vec3::ZERO).into_set(),
872 self.shading_map_on,
873 );
874 Self::draw_pair(
875 ctx,
876 RELIEF_Z,
877 SPHERE_RADIUS,
878 ReliefPlain.at(Vec3::ZERO).into_set(),
879 ReliefMapped.at(Vec3::ZERO).into_set(),
880 self.relief_map_on,
881 );
882 Self::draw_pair(
883 ctx,
884 EMISSIVE_Z,
885 CUBE_SIZE / 2.0,
886 EmissivePlain.at(Vec3::ZERO).into_set(),
887 EmissiveMapped.at(Vec3::ZERO).into_set(),
888 self.emissive_map_on,
889 );
890
891 ctx.draw(
892 Front
893 .at(Transform::from_scale_rotation_translation(
894 Vec3::splat(FRONT_SCALE),
895 Quat::IDENTITY,
896 FRONT_POSITION,
897 ))
898 .material(self.front_material()),
899 );
900
901 self.draw_reflect_row(ctx);
902 self.draw_outpost(ctx);
903 }
904
905 /// One pair at depth `z`, its centers `height` above the ground: `plain`
906 /// on the left always, and on the right `mapped` where `mapped_on` is
907 /// set, `plain` again where it is not — the same position drawing the
908 /// same base material with and without the map.
909 fn draw_pair(
910 ctx: &mut FrameContext<'_, Self>,
911 z: f32,
912 height: f32,
913 plain: Instance<Shape, Looks>,
914 mapped: Instance<Shape, Looks>,
915 mapped_on: bool,
916 ) {
917 ctx.draw(plain.clone().at(Vec3::new(-PAIR_HALF_SPACING, height, z)));
918 let right = if mapped_on { mapped } else { plain };
919 ctx.draw(right.at(Vec3::new(PAIR_HALF_SPACING, height, z)));
920 }
921
922 /// A row of built-in `Sphere` draws at rising roughness, each
923 /// `metallic(1.0)` with its tint white, so what draws is the sky's own
924 /// reflection alone.
925 fn draw_reflect_row(&self, ctx: &mut FrameContext<'_, Self>) {
926 let start = -REFLECT_ROW_SPACING * (REFLECT_ROW_COUNT as f32 - 1.0) / 2.0;
927 for index in 0..REFLECT_ROW_COUNT {
928 let x = start + index as f32 * REFLECT_ROW_SPACING;
929 let roughness = index as f32 / (REFLECT_ROW_COUNT as f32 - 1.0);
930 ctx.draw(
931 Sphere {
932 subdivisions: SPHERE_SUBDIVISIONS,
933 }
934 .at(Transform::from_scale_rotation_translation(
935 Vec3::splat(REFLECT_ROW_RADIUS * 2.0),
936 Quat::IDENTITY,
937 Vec3::new(x, REFLECT_ROW_RADIUS, REFLECT_ROW_Z),
938 ))
939 .material(
940 Material::lit(Color::WHITE)
941 .roughness(roughness)
942 .metallic(1.0),
943 ),
944 );
945 }
946 }102const SUN_DIRECTION: Vec3 = Vec3::new(-0.8, -0.55, -0.5);
103const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
104
105const SKY_ZENITH: Color = Color::rgb(0.15, 0.22, 0.42);
106const SKY_HORIZON: Color = Color::rgb(0.7, 0.48, 0.34);
107const SKY_NADIR: Color = Color::rgb(0.2, 0.16, 0.14);
108const SKY_LIGHT: f32 = 0.65;
109/// The sky's own color under the horizon, so a butterfly's own underside and
110/// the ground reflect light from the sky instead of reading black.
111const SKY_GROUND: Color = Color::rgb(0.42, 0.34, 0.28);
112
113/// The camera's height above and distance from the flock's center, each a
114/// fraction of the flock's own radius, so the view frames every size.
115const CAMERA_HEIGHT_FRACTION: f32 = 0.45;
116const CAMERA_DISTANCE_FRACTION: f32 = 2.2;
117/// How far above the flock's center the camera looks, as a fraction of
118/// the flock's own radius, so the flock sits below the panel.
119const CAMERA_AIM_LIFT_FRACTION: f32 = 0.25;
120const CAMERA_FOV: f32 = 75.0;
121/// Radians the camera turns about the flock's center per second.
122const CAMERA_ANGULAR_SPEED: f32 = 0.08;
123
124/// Butterflies a chunk of [`Butterflies::center`]'s fold sums at a time, so the sum
125/// reads the same bits at any worker count.
126const CENTER_CHUNK_SIZE: usize = 1024;
127
128const PANEL_PADDING: i8 = 8;
129
130meshes! { enum Shape { Butterfly, Plane } }
131
132/// The one sky this game draws, a gradient set each frame.
133#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
134enum Sky {
135 Day,
136}
137
138impl Skyboxes for Sky {
139 fn build(&self, _assets: &Assets) -> SkyboxData {
140 match self {
141 Self::Day => SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR)
142 .lit_by(SKY_LIGHT)
143 .with_ground(SKY_GROUND),
144 }
145 }
146}
147
148#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
149struct Butterfly;
150
151/// The one clip a butterfly is posed by, named as the source names it.
152#[derive(Clip, Clone, Debug, PartialEq, Eq, Hash)]
153enum ButterflyClip {
154 #[clip("fly")]
155 Fly,
156}
157
158impl Mesh<NoParts, ButterflyClip> for Butterfly {
159 fn build(&self, assets: &Assets) -> MeshData<NoParts, ButterflyClip> {
160 assets
161 .model(BUTTERFLY_ROOT)
162 .with_texture(greyed(&assets.texture(BUTTERFLY_SKIN)))
163 }
164}
165
166/// `skin` with every texel at its own grey level, its alpha kept, so a tint
167/// colors it whole.
168fn greyed(skin: &TextureData) -> TextureData {
169 let pixels = skin
170 .pixels()
171 .chunks_exact(4)
172 .flat_map(|texel| {
173 let [red, green, blue, alpha] = [texel[0], texel[1], texel[2], texel[3]];
174 let grey =
175 (0.2126 * f32::from(red) + 0.7152 * f32::from(green) + 0.0722 * f32::from(blue))
176 .round() as u8;
177 [grey, grey, grey, alpha]
178 })
179 .collect();
180 TextureData::rgba8(skin.size(), pixels)
181}
182
183/// What one butterfly looks like: the flap group that poses it and the tint
184/// it is drawn in, both its own for the whole run.
185#[derive(Clone, Copy, Default)]
186struct Kind {
187 flap: u8,
188 tint: u8,
189}
190
191impl Kind {
192 /// The kind butterfly `index` is given, by its own integer-hash.
193 fn of(index: u32) -> Self {
194 Self {
195 flap: (hash(index, 4) % FLAP_GROUPS as u32) as u8,
196 tint: (hash(index, 5) % TINTS.len() as u32) as u8,
197 }
198 }
199}
200
201/// A machine of one state, holding the flap at the phase it is given: one
202/// machine per flap group poses every butterfly of that group.
203#[derive(Clone, Copy, Eq, PartialEq, Debug)]
204enum FlapState {
205 Flapping,
206}
207
208impl AnimationStates for FlapState {
209 type Clip = ButterflyClip;
210 type Input = f32;
211
212 fn entry() -> Self {
213 Self::Flapping
214 }
215
216 fn motion(&self, phase: &f32) -> Motion<ButterflyClip> {
217 Motion::scrubbed(ButterflyClip::Fly, *phase)
218 }
219
220 fn next(&self, _phase: &f32, _at: Progress) -> Option<Transition<Self>> {
221 None
222 }
223}
224
225/// The phase flap group `group` holds `flown` seconds into the run: the
226/// flaps its own rate has run, spread about [`FLAP_RATE`] by
227/// [`FLAP_RATE_SPREAD`], the rate rising and falling by [`FLAP_WAVES`] at
228/// the group's own offsets, from the group's own start. The count is the
229/// sum of the rate over the seconds `flown`, taken whole rather than tick by
230/// tick, so no step adds up an error.
231fn flap_phase(group: usize, flown: f32) -> f32 {
232 let share = group as f32 / FLAP_GROUPS as f32;
233 let rate = FLAP_RATE * (1.0 + FLAP_RATE_SPREAD * (share - 0.5));
234 let waved: f32 = FLAP_WAVES
235 .iter()
236 .enumerate()
237 .map(|(wave, &(depth, period))| {
238 let angular = TAU / period;
239 let offset = hash_unit(group as u32, 6 + wave as u32) * TAU;
240 -depth / angular * (angular * flown + offset).cos()
241 })
242 .sum();
243 (rate * (flown + waved) + share).fract()
244}
245
246/// The sphere the flock is bound to: its center, held above the ground by
247/// [`FLOCK_CLEARANCE`], and its radius, sized so each butterfly has
248/// [`WORLD_VOLUME_PER_BUTTERFLY`].
249#[derive(Clone, Copy)]
250struct World {
251 center: Vec3A,
252 radius: f32,
253}
254
255impl World {
256 fn for_flock(count: u32) -> Self {
257 let radius = (count as f32 * WORLD_VOLUME_PER_BUTTERFLY * 3.0 / (4.0 * PI)).cbrt();
258 Self {
259 center: Vec3A::new(0.0, radius + FLOCK_CLEARANCE, 0.0),
260 radius,
261 }
262 }
263}
264
265/// The box of cells the flock is kept in: cubes [`NEIGHBOR_RADIUS`] across,
266/// `side` to an axis, from `least` on every axis.
267#[derive(Clone, Copy)]
268struct Cells {
269 side: usize,
270 least: Vec3A,
271}
272
273impl Cells {
274 /// The box over `world`'s sphere and [`BOX_MARGIN`] past it.
275 fn covering(world: World) -> Self {
276 let reach = world.radius * BOX_MARGIN;
277 let side = ((2.0 * reach) / NEIGHBOR_RADIUS).ceil().max(1.0) as usize;
278 Self {
279 side,
280 least: world.center - Vec3A::splat(reach),
281 }
282 }
283
284 /// How many cells the box holds.
285 fn count(self) -> usize {
286 self.side * self.side * self.side
287 }
288
289 /// The cell `position` lies in, held inside the box on every axis.
290 fn of(self, position: Vec3A) -> usize {
291 let scaled = (position - self.least) / NEIGHBOR_RADIUS;
292 let most = (self.side - 1) as f32;
293 let x = scaled.x.clamp(0.0, most) as usize;
294 let y = scaled.y.clamp(0.0, most) as usize;
295 let z = scaled.z.clamp(0.0, most) as usize;
296 (x * self.side + y) * self.side + z
297 }
298
299 /// `cell` and every cell beside it, at most 27, inside the box.
300 fn around(self, cell: usize) -> impl Iterator<Item = usize> {
301 let side = self.side;
302 let z = cell % side;
303 let y = (cell / side) % side;
304 let x = cell / (side * side);
305 let span = move |at: usize| at.saturating_sub(1)..(at + 2).min(side);
306 span(x).flat_map(move |cx| {
307 span(y).flat_map(move |cy| span(z).map(move |cz| (cx * side + cy) * side + cz))
308 })
309 }
310}
311
312/// The flock: every butterfly's position and velocity in cell order, where each
313/// cell's butterflies start, and the arrays the next tick is written into.
314struct Butterflies {
315 cells: Cells,
316 /// Where each cell's butterflies start, and one more for the end of the last.
317 start: Vec<u32>,
318 position: Vec<Vec3A>,
319 velocity: Vec<Vec3A>,
320 /// Each butterfly's kind, in the same order.
321 kind: Vec<Kind>,
322 next_position: Vec<Vec3A>,
323 next_velocity: Vec<Vec3A>,
324 next_kind: Vec<Kind>,
325 /// The cell each butterfly of the next arrays lies in.
326 next_cell: Vec<u32>,
327}
328
329impl Butterflies {
330 /// `count` butterflies scattered through `world`'s sphere, each with a level
331 /// heading at [`MIN_SPEED`], sorted into cells.
332 fn scattered(count: u32, world: World) -> Self {
333 let cells = Cells::covering(world);
334 let count = count as usize;
335 let mut swarm = Self {
336 cells,
337 start: vec![0; cells.count() + 1],
338 position: vec![Vec3A::ZERO; count],
339 velocity: vec![Vec3A::ZERO; count],
340 kind: vec![Kind::default(); count],
341 next_position: Vec::with_capacity(count),
342 next_velocity: Vec::with_capacity(count),
343 next_kind: Vec::with_capacity(count),
344 next_cell: Vec::with_capacity(count),
345 };
346 for index in 0..count as u32 {
347 let radius = world.radius * hash_unit(index, 0).cbrt();
348 let inclination = hash_unit(index, 1) * PI;
349 let azimuth = hash_unit(index, 2) * TAU;
350 let position = world.center
351 + Vec3A::new(
352 radius * inclination.sin() * azimuth.cos(),
353 radius * inclination.cos(),
354 radius * inclination.sin() * azimuth.sin(),
355 );
356 let heading = hash_unit(index, 3) * TAU;
357 let velocity = Vec3A::new(heading.cos(), 0.0, heading.sin()) * MIN_SPEED;
358 swarm.next_position.push(position);
359 swarm.next_velocity.push(velocity);
360 swarm.next_kind.push(Kind::of(index));
361 swarm.next_cell.push(cells.of(position) as u32);
362 }
363 swarm.sort();
364 swarm
365 }
366
367 /// One tick of `dt` seconds: every cell's butterflies steered against the
368 /// cells around it into the next arrays, on the engine's workers or one
369 /// cell after another, then the next arrays sorted into cells again.
370 fn step(&mut self, dt: f32, world: World, sequential: bool) {
371 let Self {
372 cells,
373 start,
374 position,
375 velocity,
376 kind,
377 next_position,
378 next_velocity,
379 next_kind,
380 next_cell,
381 } = self;
382 let flock = Flying {
383 cells: *cells,
384 start,
385 position,
386 velocity,
387 kind,
388 };
389 let mut out = CellOut::split(&flock, next_position, next_velocity, next_kind, next_cell);
390 let steer = |(cell, out): (usize, &mut CellOut<'_>)| flock.steer(cell, out, dt, world);
391 if sequential {
392 out.iter_mut().enumerate().for_each(steer);
393 } else {
394 out.par_iter_mut().enumerate().for_each(steer);
395 }
396 drop(out);
397 self.sort();
398 }
399
400 /// Sorts the next arrays into the current ones by cell: a count per
401 /// cell, a running total, and a placement in order, so the result reads
402 /// the same at any worker count.
403 fn sort(&mut self) {
404 self.start.iter_mut().for_each(|start| *start = 0);
405 for &cell in &self.next_cell {
406 self.start[cell as usize + 1] += 1;
407 }
408 for cell in 0..self.cells.count() {
409 self.start[cell + 1] += self.start[cell];
410 }
411 let mut fill = self.start.clone();
412 for (index, &cell) in self.next_cell.iter().enumerate() {
413 let at = fill[cell as usize] as usize;
414 fill[cell as usize] += 1;
415 self.position[at] = self.next_position[index];
416 self.velocity[at] = self.next_velocity[index];
417 self.kind[at] = self.next_kind[index];
418 }
419 }
420
421 /// The flock's center: positions summed in fixed chunks and then
422 /// folded in order, the fold that `mirage_engine::rayon`'s own docs
423 /// show, so it reads the same bits at any worker count.
424 fn center(&self) -> Vec3A {
425 if self.position.is_empty() {
426 return Vec3A::ZERO;
427 }
428 let sum: Vec3A = self
429 .position
430 .par_chunks(CENTER_CHUNK_SIZE)
431 .map(|chunk| chunk.iter().copied().sum::<Vec3A>())
432 .collect::<Vec<Vec3A>>()
433 .into_iter()
434 .sum();
435 sum / self.position.len() as f32
436 }
437
438 /// Every butterfly's position, velocity and kind, in cell order.
439 fn each(&self) -> impl Iterator<Item = (Vec3A, Vec3A, Kind)> + '_ {
440 self.position
441 .iter()
442 .zip(&self.velocity)
443 .zip(&self.kind)
444 .map(|((&position, &velocity), &kind)| (position, velocity, kind))
445 }
446}
447
448/// The flock as one tick reads it: the current arrays and the cells they
449/// are sorted by, shared by every cell's step.
450struct Flying<'a> {
451 cells: Cells,
452 start: &'a [u32],
453 position: &'a [Vec3A],
454 velocity: &'a [Vec3A],
455 kind: &'a [Kind],
456}
457
458impl Flying<'_> {
459 /// The butterflies of `cell`, as a range of the arrays.
460 fn range(&self, cell: usize) -> Range<usize> {
461 self.start[cell] as usize..self.start[cell + 1] as usize
462 }
463
464 /// Steers every butterfly of `cell` by separation, alignment and cohesion
465 /// against the butterflies of the cells around it, and back toward the center
466 /// once it leaves `world`'s sphere, writing the next state into `out`.
467 fn steer(&self, cell: usize, out: &mut CellOut<'_>, dt: f32, world: World) {
468 let mut around: [Range<usize>; 27] = core::array::from_fn(|_| 0..0);
469 let mut near_count = 0;
470 for near in self.cells.around(cell) {
471 around[near_count] = self.range(near);
472 near_count += 1;
473 }
474 let around = &around[..near_count];
475
476 for (at, index) in self.range(cell).enumerate() {
477 let position = self.position[index];
478 let velocity = self.velocity[index];
479 let mut separation = Vec3A::ZERO;
480 let mut heading_sum = Vec3A::ZERO;
481 let mut position_sum = Vec3A::ZERO;
482 let mut neighbors = 0u32;
483
484 for near in around {
485 for other in near.clone() {
486 if other == index {
487 continue;
488 }
489 let offset = position - self.position[other];
490 let squared = offset.length_squared();
491 if squared > NEIGHBOR_RADIUS * NEIGHBOR_RADIUS || squared <= f32::EPSILON {
492 continue;
493 }
494 if squared < SEPARATION_RADIUS * SEPARATION_RADIUS {
495 separation += offset / squared.sqrt();
496 }
497 heading_sum += self.velocity[other];
498 position_sum += self.position[other];
499 neighbors += 1;
500 }
501 }
502
503 let mut steering = separation * SEPARATION_WEIGHT;
504 if neighbors > 0 {
505 let share = 1.0 / neighbors as f32;
506 steering += (heading_sum * share - velocity) * ALIGNMENT_WEIGHT
507 + (position_sum * share - position) * COHESION_WEIGHT;
508 }
509 let from_center = position - world.center;
510 if from_center.length() > world.radius {
511 steering -= from_center.normalize() * BOUND_WEIGHT;
512 }
513
514 let next = velocity + steering * dt;
515 let speed = next.length().clamp(MIN_SPEED, MAX_SPEED);
516 let next_velocity = next.normalize_or_zero() * speed;
517 let next_position = position + next_velocity * dt;
518 out.position[at] = next_position;
519 out.velocity[at] = next_velocity;
520 out.kind[at] = self.kind[index];
521 out.cell[at] = self.cells.of(next_position) as u32;
522 }
523 }
524}
525
526/// One cell's share of the next arrays, written by that cell's step alone.
527struct CellOut<'a> {
528 position: &'a mut [Vec3A],
529 velocity: &'a mut [Vec3A],
530 kind: &'a mut [Kind],
531 cell: &'a mut [u32],
532}
533
534impl<'a> CellOut<'a> {
535 /// The next arrays split into one share per cell of `flock`, in cell
536 /// order, each as long as that cell's range.
537 fn split(
538 flock: &Flying<'_>,
539 position: &'a mut Vec<Vec3A>,
540 velocity: &'a mut Vec<Vec3A>,
541 kind: &'a mut Vec<Kind>,
542 cell: &'a mut Vec<u32>,
543 ) -> Vec<Self> {
544 let count = flock.position.len();
545 position.resize(count, Vec3A::ZERO);
546 velocity.resize(count, Vec3A::ZERO);
547 kind.resize(count, Kind::default());
548 cell.resize(count, 0);
549 let mut out = Vec::with_capacity(flock.cells.count());
550 let mut position = position.as_mut_slice();
551 let mut velocity = velocity.as_mut_slice();
552 let mut kind = kind.as_mut_slice();
553 let mut cell = cell.as_mut_slice();
554 for at in 0..flock.cells.count() {
555 let len = flock.range(at).len();
556 let (own, rest) = position.split_at_mut(len);
557 position = rest;
558 let (own_velocity, rest) = velocity.split_at_mut(len);
559 velocity = rest;
560 let (own_kind, rest) = kind.split_at_mut(len);
561 kind = rest;
562 let (own_cell, rest) = cell.split_at_mut(len);
563 cell = rest;
564 out.push(Self {
565 position: own,
566 velocity: own_velocity,
567 kind: own_kind,
568 cell: own_cell,
569 });
570 }
571 out
572 }
573}
574
575/// An integer-hash of `seed` and `salt`.
576fn hash(seed: u32, salt: u32) -> u32 {
577 let mut x = seed ^ salt.wrapping_mul(0x9E37_79B9);
578 x ^= x >> 16;
579 x = x.wrapping_mul(0x7FEB_352D);
580 x ^= x >> 15;
581 x = x.wrapping_mul(0x846C_A68B);
582 x ^= x >> 16;
583 x
584}
585
586/// `hash`, scaled to `0.0..1.0`.
587fn hash_unit(seed: u32, salt: u32) -> f32 {
588 hash(seed, salt) as f32 / u32::MAX as f32
589}
590
591/// The load a player chooses: how many butterflies the flock holds, and whether
592/// the step below runs one cell after another instead of on the engine's
593/// workers.
594struct Settings {
595 flock_size: u32,
596 sequential: bool,
597}
598
599impl Default for Settings {
600 fn default() -> Self {
601 Self {
602 flock_size: DEFAULT_FLOCK_SIZE,
603 sequential: false,
604 }
605 }
606}
607
608struct Flock {
609 settings: Settings,
610 applied_flock_size: u32,
611 world: World,
612 butterflies: Butterflies,
613 flaps: [Animator<Butterfly, FlapState>; FLAP_GROUPS],
614 /// Seconds the ticks have run, which paces the flap.
615 flown: f32,
616 last_tick_ms: f32,
617}
618
619impl Flock {
620 fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
621 let settings = Settings::default();
622 let world = World::for_flock(settings.flock_size);
623 Ok(Self {
624 applied_flock_size: settings.flock_size,
625 butterflies: Butterflies::scattered(settings.flock_size, world),
626 settings,
627 world,
628 flaps: core::array::from_fn(|_| Animator::new()),
629 flown: 0.0,
630 last_tick_ms: 0.0,
631 })
632 }
633
634 /// Rebuilds the flock where the chosen size changed since the last
635 /// frame.
636 fn apply_settings(&mut self) {
637 if self.settings.flock_size == self.applied_flock_size {
638 return;
639 }
640 self.world = World::for_flock(self.settings.flock_size);
641 self.butterflies = Butterflies::scattered(self.settings.flock_size, self.world);
642 self.applied_flock_size = self.settings.flock_size;
643 }
644
645 /// The camera at `elapsed`, turning about `center` at
646 /// [`CAMERA_HEIGHT_FRACTION`] and [`CAMERA_DISTANCE_FRACTION`] of the
647 /// flock's own radius, looking [`CAMERA_AIM_LIFT_FRACTION`] above it.
648 fn camera(center: Vec3, world: World, elapsed: f32) -> Camera {
649 let angle = elapsed * CAMERA_ANGULAR_SPEED;
650 let eye = center
651 + Vec3::new(
652 angle.cos() * world.radius * CAMERA_DISTANCE_FRACTION,
653 world.radius * CAMERA_HEIGHT_FRACTION,
654 angle.sin() * world.radius * CAMERA_DISTANCE_FRACTION,
655 );
656 let aim = center + Vec3::Y * world.radius * CAMERA_AIM_LIFT_FRACTION;
657 Camera::new(View::look_at(eye, aim), Projection::perspective(CAMERA_FOV))
658 }
659
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 }Sourcepub const fn splat(v: f32) -> Vec3
pub const fn splat(v: f32) -> Vec3
Creates a vector with all elements set to v.
Examples found in repository?
44const BLOCK_HALF_EXTENTS: Vec3 = Vec3::splat(BLOCK_SIZE * 0.5);
45
46/// The name `assets.texture` pulls the sheet under, once loaded.
47const SPRITE_TEXTURE: &str = "walker";
48const SPRITE_SOURCE: &str = "examples/assets/walker.png";
49const CLICK_SOURCE: &str = "examples/assets/click.ogg";
50
51/// The texture's grid: rows top to bottom are toward, right, away, and
52/// left; four frames of a walk cycle across each row. The sprite unit
53/// draws the right row always, mirrored across its own width for a left
54/// order, in place of a left row of its own.
55const SPRITE_COLUMNS: u32 = 4;
56const SPRITE_ROWS: u32 = 4;
57const SPRITE_ROW: u32 = 1;
58const SPRITE_COLUMN: u32 = 0;
59
60/// Each rock around the board: position, a `seed` for its mesh, and a
61/// scale for the transform that places it.
62const ROCKS: [(f32, f32, u32, f32); 5] = [
63 (-BOARD_HALF - 1.2, -BOARD_HALF - 0.6, 11, 1.0),
64 (-BOARD_HALF - 0.8, BOARD_HALF + 1.0, 37, 0.8),
65 (BOARD_HALF + 1.4, -BOARD_HALF - 0.2, 58, 1.3),
66 (BOARD_HALF + 1.0, BOARD_HALF + 1.2, 71, 0.9),
67 (0.3, BOARD_HALF + 1.6, 94, 1.1),
68];
69
70/// How far a rock's corner is displaced from its position on a unit
71/// cube, on each axis.
72const ROCK_JITTER: f32 = 0.16;
73
74/// Half the ground's width and depth under the board and its rock ring,
75/// in meters.
76const GROUND_HALF: f32 = BOARD_HALF + 3.0;
77
78/// The ground's surface height, just under the board's own tiles, clear
79/// of a z-fighting seam with them.
80const GROUND_Y: f32 = -0.01;
81
82const LIGHT_TILE: Color = Color::rgb(0.80, 0.76, 0.64);
83const DARK_TILE: Color = Color::rgb(0.55, 0.50, 0.40);
84/// A reachable tile's own mark, smaller than the tile itself so the
85/// checker tone still shows around its edge.
86const REACHABLE_MARK: Color = Color::rgb(0.20, 0.85, 0.35);
87/// The fraction of a tile's own footprint the reachable mark draws at,
88/// small enough that the tile's own checker tone still shows around it.
89const REACHABLE_MARK_SCALE: f32 = 0.45;
90/// The reachable mark's own lift over the tile's surface, clear of
91/// z-fighting with it.
92const REACHABLE_MARK_LIFT: f32 = 0.01;
93/// The mark under the selected unit, its own color bright enough to read
94/// past the sprite's own tint at a distance.
95const CURRENT_MARK: Color = Color::rgb(1.0, 0.2, 0.75);
96const CURRENT_MARK_SCALE: f32 = 0.85;
97/// The mark under the unit whose turn it is while nothing is selected:
98/// smaller and dim next to [`CURRENT_MARK`], a hint rather than a claim.
99const TURN_MARK: Color = Color::rgb(0.85, 0.75, 0.15);
100const TURN_MARK_SCALE: f32 = 0.5;
101/// The tile a hover reads while a unit is selected: reachable, or blocked
102/// by the other unit standing there.
103const HOVER_REACHABLE_TILE: Color = Color::rgb(0.35, 0.75, 0.68);
104const HOVER_BLOCKED_TILE: Color = Color::rgb(0.62, 0.28, 0.26);
105const BLOCK_IDLE: Color = Color::rgb(0.32, 0.42, 0.58);
106/// `BLOCK_IDLE`, scaled toward white to mark the block unit's own turn.
107const BLOCK_TURN: Color = Color::rgb(0.42, 0.54, 0.72);
108const GROUND_COLOR: Color = Color::rgb(0.15, 0.16, 0.13);
109const ROCK_COLOR: Color = Color::rgb(0.42, 0.40, 0.38);
110const SUN_COLOR: Color = Color::rgb(0.95, 0.92, 0.85);
111
112/// The current unit's tint, close to white so the sprite's own texture
113/// still reads under it, and the light it adds on its own, low enough
114/// that the same texture still reads under its bloom too — distinct from
115/// `SELECTED_TINT`, so a hover and a selection never read the same.
116const HOVER_TINT: Color = Color::rgb(0.9, 1.15, 1.15);
117const HOVER_GLOW: Color = Color::rgb(0.02, 0.15, 0.2);
118
119/// The current unit's tint, close to white with more red where
120/// `HOVER_TINT` raises green and blue instead, so the sprite's own
121/// texture still reads under it, and the light it adds on its own,
122/// scaled down the same way `HOVER_GLOW` is — distinct from `HOVER_TINT`.
123const SELECTED_TINT: Color = Color::rgb(1.15, 0.95, 0.85);
124const SELECTED_GLOW: Color = Color::rgb(0.22, 0.11, 0.0);
125
126/// The unit whose turn it is shows this tint and glow before any hover or
127/// selection, so it reads as the one a click selects.
128const TURN_TINT: Color = Color::rgb(1.0, 1.0, 0.82);
129const TURN_GLOW: Color = Color::rgb(0.08, 0.08, 0.02);
130
131/// The fraction of the frame `set_bloom` spreads, so `HOVER_GLOW` and
132/// `SELECTED_GLOW` read as light around the current unit, not only a
133/// larger fill.
134const BLOOM: f32 = 0.35;
135
136/// The size a world-space prompt naming a click's effect reads at, in
137/// logical points.
138const PROMPT_SIZE: f32 = 15.0;
139/// Height a prompt is lifted over the tile it names, clear of the tile's
140/// own top corner under the diagonal view.
141const PROMPT_TILE_LIFT: f32 = 0.55;
142/// Height a prompt is lifted over the unit it names, past its own height.
143const PROMPT_UNIT_LIFT: f32 = 0.25;
144/// Margin a prompt's own backdrop keeps past its galley, in logical points.
145const PROMPT_PADDING: f32 = 4.0;
146/// How much dark a prompt's own backdrop puts behind its text.
147const PROMPT_BACKDROP: u8 = 190;
148const PROMPT_TEXT_COLOR: egui::Color32 = egui::Color32::from_gray(230);
149
150/// Thirty steps a second, half the engine's default rate; movement stays
151/// smooth through `alpha()` interpolation.
152const TICK_INTERVAL: Duration = Duration::from_nanos(33_333_333);
153
154/// Faces of a cube, each a normal with its right and up axes — the same
155/// layout `mesh::Cube` builds from, shared so a rock's corners hold the
156/// same eight positions between the faces that meet there.
157const ROCK_FACES: [(Vec3, Vec3, Vec3); 6] = [
158 (Vec3::X, Vec3::NEG_Z, Vec3::Y),
159 (Vec3::NEG_X, Vec3::Z, Vec3::Y),
160 (Vec3::Y, Vec3::X, Vec3::NEG_Z),
161 (Vec3::NEG_Y, Vec3::X, Vec3::Z),
162 (Vec3::Z, Vec3::X, Vec3::Y),
163 (Vec3::NEG_Z, Vec3::NEG_X, Vec3::Y),
164];
165
166const ROCK_TRIANGLES: [u32; 6] = [0, 1, 2, 0, 2, 3];
167
168fn main() {
169 run(
170 Config::new("Mirage: isometric board")
171 .with_size(1280, 720)
172 .with_assets([SPRITE_SOURCE, CLICK_SOURCE])
173 .with_tick_interval(TICK_INTERVAL),
174 Board::init,
175 );
176}
177
178/// A rock built for its own `seed`; each value is its own mesh.
179#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
180#[catalog(Self { seed: 0 })]
181struct Rock {
182 seed: u32,
183}
184
185impl Mesh for Rock {
186 fn build(&self, _: &Assets) -> MeshData {
187 build_rock(self.seed)
188 }
189}
190
191/// The sprite unit: a quad windowed to the walk sheet's row facing right.
192#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
193struct Sprite;
194
195impl Mesh for Sprite {
196 fn build(&self, assets: &Assets) -> MeshData {
197 Quad.build(assets)
198 .with_texture(assets.texture(SPRITE_TEXTURE).pixelated())
199 }
200}
201
202// Everything else this game draws: the ground and board tiles are the
203// engine's own Plane and Cube, given their color per draw; the block unit
204// draws as a plain Cube too.
205meshes! { enum Shape { Plane, Cube, Rock, Sprite } }
206
207/// The board's own sky: a dim gradient, so the sun stays the scene's
208/// brightest light.
209#[derive(Catalog, Clone, Copy, Debug, PartialEq, Eq, Hash)]
210enum Sky {
211 Day,
212}
213
214impl Skyboxes for Sky {
215 fn build(&self, _assets: &Assets) -> SkyboxData {
216 SkyboxData::gradient(
217 Color::rgb(0.55, 0.75, 0.95),
218 Color::rgb(0.85, 0.90, 0.95),
219 Color::rgb(0.35, 0.33, 0.30),
220 )
221 .lit_by(0.3)
222 }
223}
224
225/// The one sound this game plays.
226#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
227enum Sound {
228 Click,
229}
230
231impl Sounds for Sound {
232 fn build(&self, assets: &Assets) -> SoundData {
233 match self {
234 Sound::Click => assets.sound("click"),
235 }
236 }
237}
238
239/// The one control this game reads: a click, which selects the unit whose
240/// turn it is or orders it to a tile.
241#[derive(InputButtonAction, Clone, Copy)]
242enum Button {
243 Select,
244}
245
246impl InputButtonAction for Button {
247 fn bindings(&self) -> Vec<ButtonBinding> {
248 match self {
249 Button::Select => vec![MouseButton::Left.into()],
250 }
251 }
252}
253
254struct Controls;
255
256impl InputActions for Controls {
257 type Button = Button;
258 type Axis = NoInputAxes;
259 type Axis2 = NoInputAxes2;
260}
261
262/// Whose turn it is: `Sprite` draws the unit with a texture, `Block` the
263/// plain one.
264#[derive(Clone, Copy, PartialEq, Eq)]
265enum Turn {
266 Sprite,
267 Block,
268}
269
270impl Turn {
271 fn other(self) -> Self {
272 match self {
273 Turn::Sprite => Turn::Block,
274 Turn::Block => Turn::Sprite,
275 }
276 }
277}
278
279/// One unit's position on the board.
280struct Unit {
281 tile: (i32, i32),
282 position: Vec3,
283 previous: Vec3,
284 target: Option<Vec3>,
285 /// The order the unit last moved along `+X` under, kept while it
286 /// stays still.
287 facing_right: bool,
288}
289
290impl Unit {
291 fn resting(tile: (i32, i32), lift: f32) -> Self {
292 let position = tile_center(tile) + Vec3::Y * lift;
293 Self {
294 tile,
295 position,
296 previous: position,
297 target: None,
298 facing_right: true,
299 }
300 }
301
302 /// Steps the unit toward its ordered position by one tick's distance;
303 /// returns whether it landed on the position this tick.
304 fn advance(&mut self, dt: Duration) -> bool {
305 let Some(target) = self.target else {
306 return false;
307 };
308 let to_target = target - self.position;
309 let distance = to_target.length();
310 let step = UNIT_SPEED * dt.as_secs_f32();
311 if distance <= step {
312 self.position = target;
313 self.target = None;
314 true
315 } else {
316 self.position += to_target * (step / distance);
317 false
318 }
319 }
320}
321
322/// The vertical lift from a tile's surface to a unit's center, and the
323/// half-extents `Ray::hit_aabb` reads its box by.
324fn unit_geometry(turn: Turn) -> (f32, Vec3) {
325 match turn {
326 Turn::Sprite => (SPRITE_HEIGHT * 0.5, SPRITE_HALF_EXTENTS),
327 Turn::Block => (BLOCK_SIZE * 0.5, BLOCK_HALF_EXTENTS),
328 }
329}
330
331/// The world position a tile's center is at, on the board's surface
332/// plane.
333fn tile_center((col, row): (i32, i32)) -> Vec3 {
334 let half = (BOARD_TILES - 1) as f32 * 0.5;
335 Vec3::new(
336 (col as f32 - half) * TILE_SIZE,
337 0.0,
338 (row as f32 - half) * TILE_SIZE,
339 )
340}
341
342/// The tile `point` falls over, ignoring its height; `None` off the board.
343fn tile_at(point: Vec3) -> Option<(i32, i32)> {
344 let half = (BOARD_TILES - 1) as f32 * 0.5;
345 let col = (point.x / TILE_SIZE + half + 0.5).floor() as i32;
346 let row = (point.z / TILE_SIZE + half + 0.5).floor() as i32;
347 ((0..BOARD_TILES).contains(&col) && (0..BOARD_TILES).contains(&row)).then_some((col, row))
348}
349
350/// Cursor target this frame: nothing, the unit whose turn it is, or a
351/// board tile.
352#[derive(Clone, Copy, PartialEq)]
353enum Hover {
354 None,
355 CurrentUnit,
356 Tile((i32, i32)),
357}
358
359struct Board {
360 sprite: Unit,
361 block: Unit,
362 turn: Turn,
363 selected: bool,
364}
365
366impl Board {
367 fn init(ctx: &mut InitContext<'_, Board>) -> Result<Self, Error> {
368 for &(_, _, seed, _) in &ROCKS {
369 ctx.prepare(Rock { seed });
370 }
371
372 let (sprite_lift, _) = unit_geometry(Turn::Sprite);
373 let (block_lift, _) = unit_geometry(Turn::Block);
374 Ok(Self {
375 sprite: Unit::resting((1, 1), sprite_lift),
376 block: Unit::resting((BOARD_TILES - 2, BOARD_TILES - 2), block_lift),
377 turn: Turn::Sprite,
378 selected: false,
379 })
380 }
381
382 fn camera() -> Camera {
383 let eye = Vec3::new(9.0, 9.0, 9.0);
384 Camera::new(
385 View::look_at(eye, Vec3::ZERO),
386 Projection::orthographic(11.0),
387 )
388 }
389
390 fn current(&self) -> &Unit {
391 match self.turn {
392 Turn::Sprite => &self.sprite,
393 Turn::Block => &self.block,
394 }
395 }
396
397 fn current_mut(&mut self) -> &mut Unit {
398 match self.turn {
399 Turn::Sprite => &mut self.sprite,
400 Turn::Block => &mut self.block,
401 }
402 }
403
404 fn other(&self) -> &Unit {
405 match self.turn {
406 Turn::Sprite => &self.block,
407 Turn::Block => &self.sprite,
408 }
409 }
410
411 /// Resolves a left click: hitting the current unit's box toggles its
412 /// selection; while selected, a ground hit that lands on an open tile
413 /// orders a move there.
414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }
457
458 /// Cursor target, `None` while the UI has the pointer.
459 fn hovered(&self, ctx: &FrameContext<'_, Board>) -> Hover {
460 if ctx.ui_wants_pointer() {
461 return Hover::None;
462 }
463 let ray = ctx
464 .last_camera()
465 .ray_through(ctx.pointer(), ctx.window_size());
466
467 if self.current().target.is_none() {
468 let (_, half) = unit_geometry(self.turn);
469 let center = self.current().position;
470 if ray.hit_aabb(center - half, center + half).is_some() {
471 return Hover::CurrentUnit;
472 }
473 }
474 let Some(distance) = ray.hit_plane(ray::Plane {
475 point: Vec3::ZERO,
476 normal: Vec3::Y,
477 }) else {
478 return Hover::None;
479 };
480 match tile_at(ray.at(distance)) {
481 Some(tile) => Hover::Tile(tile),
482 None => Hover::None,
483 }
484 }
485
486 /// Whether a selected unit could move to `tile`: on the board, and
487 /// standing under neither unit.
488 fn reachable(&self, tile: (i32, i32)) -> bool {
489 tile != self.current().tile && tile != self.other().tile
490 }
491
492 fn draw_board(&self, ctx: &mut FrameContext<'_, Board>, hover: Hover) {
493 let scale = Vec3::new(TILE_SIZE - TILE_GAP, TILE_THICKNESS, TILE_SIZE - TILE_GAP);
494 for col in 0..BOARD_TILES {
495 for row in 0..BOARD_TILES {
496 let tile = (col, row);
497 let center = tile_center(tile) - Vec3::Y * (TILE_THICKNESS * 0.5);
498 let reachable = self.selected && self.reachable(tile);
499 let hovered = self.selected && hover == Hover::Tile(tile);
500 let color = if hovered {
501 if reachable {
502 HOVER_REACHABLE_TILE
503 } else {
504 HOVER_BLOCKED_TILE
505 }
506 } else if (col + row) % 2 == 0 {
507 LIGHT_TILE
508 } else {
509 DARK_TILE
510 };
511 ctx.draw(
512 Cube.at(Transform::from_scale_rotation_translation(
513 scale,
514 Quat::IDENTITY,
515 center,
516 ))
517 .material(Material::lit(color)),
518 );
519 if reachable && !hovered {
520 self.draw_reachable_mark(ctx, tile);
521 }
522 }
523 }
524 }
525
526 /// A mark over a reachable tile, its own tone apart from the
527 /// checker's tone and the hover tone, so the checker still reads
528 /// under it.
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 }
567
568 fn draw_rocks(&self, ctx: &mut FrameContext<'_, Board>) {
569 for &(x, z, seed, scale) in &ROCKS {
570 let angle = hash_signed(seed, 99) * core::f32::consts::PI;
571 ctx.draw(
572 Rock { seed }
573 .at(Transform::from_scale_rotation_translation(
574 Vec3::splat(scale),
575 Quat::from_rotation_y(angle),
576 Vec3::new(x, 0.5 * scale, z),
577 ))
578 .material(Material::lit(ROCK_COLOR)),
579 );
580 }
581 }
582
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 }More examples
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 }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 }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 }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 }
581
582 /// The listener: a cube drawn from the ground up to [`EYE_HEIGHT`],
583 /// an ear pair set on ± `view`'s right, and a marker at the front that
584 /// shows its fixed `-Z` facing.
585 fn draw_listener(&self, ctx: &mut FrameContext<'_, SoundCheck>, view: View) {
586 let head = view.eye();
587 let ground = Vec3::new(head.x, 0.0, head.z);
588
589 ctx.draw(
590 Cube.at(Transform::from_scale_rotation_translation(
591 Vec3::new(LISTENER_WIDTH, head.y, LISTENER_DEPTH),
592 Quat::IDENTITY,
593 ground + Vec3::Y * head.y * 0.5,
594 ))
595 .material(Material::lit(LISTENER_COLOR)),
596 );
597
598 let right = listener_right(view) * EAR_OFFSET;
599 for (offset, color) in [(right, RIGHT_EAR_COLOR), (-right, LEFT_EAR_COLOR)] {
600 ctx.draw(
601 Sphere { subdivisions: 1 }
602 .at(Transform::from_scale_rotation_translation(
603 Vec3::splat(EAR_SIZE),
604 Quat::IDENTITY,
605 head + offset,
606 ))
607 .material(Material::lit(color)),
608 );
609 }
610
611 ctx.draw(
612 Facing
613 .at(Transform::from_scale_rotation_translation(
614 Vec3::splat(FACING_MARKER_SIZE),
615 Quat::IDENTITY,
616 head + Vec3::NEG_Z * (FACING_MARKER_SIZE * 0.5),
617 ))
618 .material(Material::lit(LISTENER_COLOR)),
619 );
620 }
621
622 fn draw_merge_markers(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
623 if !self.merge_demo {
624 return;
625 }
626 for (position, color) in [(MERGE_POS_A, MERGE_COLOR_A), (MERGE_POS_B, MERGE_COLOR_B)] {
627 ctx.draw(
628 Cube.at(Transform::from_scale_rotation_translation(
629 Vec3::splat(SOURCE_HALF * 2.0),
630 Quat::IDENTITY,
631 position,
632 ))
633 .material(Material::lit(color)),
634 );
635 }
636 }
637
638 /// Draws the ring, each cube as dim as the gain its sustain is declared
639 /// at.
640 fn draw_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
641 if !self.ring_demo {
642 return;
643 }
644 for nth in 0..RING_COUNT {
645 let over = 1.0 - nth as f32 / RING_COUNT as f32;
646 ctx.draw(
647 Cube.at(Transform::from_scale_rotation_translation(
648 Vec3::splat(SOURCE_HALF),
649 Quat::IDENTITY,
650 ring_place(nth),
651 ))
652 .material(Material::lit(RING_COLOR.dimmed(over))),
653 );
654 }
655 }Sourcepub fn map<F>(self, f: F) -> Vec3
pub fn map<F>(self, f: F) -> Vec3
Returns a vector containing each element of self modified by a mapping function f.
Sourcepub fn select(mask: BVec3, if_true: Vec3, if_false: Vec3) -> Vec3
pub fn select(mask: BVec3, if_true: Vec3, if_false: Vec3) -> Vec3
Creates a vector from the elements in if_true and if_false, selecting which to use
for each element of self.
A true element in the mask uses the corresponding element from if_true, and false
uses the element from if_false.
Sourcepub const fn from_array(a: [f32; 3]) -> Vec3
pub const fn from_array(a: [f32; 3]) -> Vec3
Creates a new vector from an array.
Sourcepub const fn from_slice(slice: &[f32]) -> Vec3
pub const fn from_slice(slice: &[f32]) -> Vec3
Creates a vector from the first 3 values in slice.
§Panics
Panics if slice is less than 3 elements long.
Sourcepub fn write_to_slice(self, slice: &mut [f32])
pub fn write_to_slice(self, slice: &mut [f32])
Writes the elements of self to the first 3 elements in slice.
§Panics
Panics if slice is less than 3 elements long.
Sourcepub fn truncate(self) -> Vec2
pub fn truncate(self) -> Vec2
Creates a 2D vector from the x and y elements of self, discarding z.
Truncation may also be performed by using self.xy().
Sourcepub fn from_homogeneous(v: Vec4) -> Vec3
pub fn from_homogeneous(v: Vec4) -> Vec3
Projects a homogeneous coordinate to 3D space by performing perspective divide.
§Panics
Will panic if v.w is 0 when glam_assert is enabled.
Sourcepub fn to_homogeneous(self) -> Vec4
pub fn to_homogeneous(self) -> Vec4
Creates a homogeneous coordinate from self, equivalent to self.extend(1.0).
pub fn to_vec3a(self) -> Vec3A
Sourcepub fn dot_into_vec(self, rhs: Vec3) -> Vec3
pub fn dot_into_vec(self, rhs: Vec3) -> Vec3
Returns a vector where every component is the dot product of self and rhs.
Sourcepub fn cross(self, rhs: Vec3) -> Vec3
pub fn cross(self, rhs: Vec3) -> Vec3
Computes the cross product of self and rhs.
Examples found in repository?
557fn push_face(vertices: &mut Vec<Vertex>, indices: &mut Vec<u32>, a: Vec3, b: Vec3, c: Vec3) {
558 let normal = (b - a).cross(c - a).normalize();
559 let uvs = [
560 Vec2::new(0.0, 1.0),
561 Vec2::new(0.5, 0.0),
562 Vec2::new(1.0, 1.0),
563 ];
564 let base = vertices.len() as u32;
565 for (point, uv) in [a, b, c].into_iter().zip(uvs) {
566 vertices.push(Vertex::new(point, normal, uv));
567 }
568 indices.extend([base, base + 1, base + 2]);
569}More examples
188fn facing_marker() -> MeshData {
189 const TIP: Vec3 = Vec3::new(0.0, 0.0, -0.5);
190 const BACK: [Vec3; 4] = [
191 Vec3::new(-0.5, -0.5, 0.5),
192 Vec3::new(0.5, -0.5, 0.5),
193 Vec3::new(0.5, 0.5, 0.5),
194 Vec3::new(-0.5, 0.5, 0.5),
195 ];
196
197 let mut vertices = Vec::with_capacity(BACK.len() * 3);
198 for (corner, next) in BACK.iter().zip(BACK.iter().cycle().skip(1)) {
199 let normal = (next - corner).cross(TIP - corner).normalize();
200 vertices.extend([
201 Vertex::new(*corner, normal, Vec2::new(0.0, 1.0)),
202 Vertex::new(*next, normal, Vec2::new(1.0, 1.0)),
203 Vertex::new(TIP, normal, Vec2::new(0.5, 0.0)),
204 ]);
205 }
206 let indices = (0..vertices.len() as u32).collect();
207 MeshData::new(vertices, indices)
208}
209
210/// Every sound this game plays. [`Sound::Break`] and [`Sound::Pulse`] read the
211/// same source under two names, so sustaining one and playing the other
212/// once never share a voice; [`Sound::Theme`] and [`Sound::ThemeDecoded`] do
213/// the same for the streamed side against the decoded one, since a clip
214/// decodes one way or the other for good, once built.
215#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
216enum Sound {
217 Bounce,
218 Break,
219 Serve,
220 GameOver,
221 Lost,
222 Win,
223 Click,
224 Theme,
225 ThemeDecoded,
226 MenuTheme,
227 Pulse,
228}
229
230impl Sound {
231 /// The alternatives a one-shot play offers.
232 const ONE_SHOTS: [Sound; 7] = [
233 Sound::Bounce,
234 Sound::Break,
235 Sound::Serve,
236 Sound::GameOver,
237 Sound::Lost,
238 Sound::Win,
239 Sound::Click,
240 ];
241
242 /// The alternatives one source's sustain offers.
243 const SOURCE_CHOICES: [Sound; 9] = [
244 Sound::Bounce,
245 Sound::Break,
246 Sound::Serve,
247 Sound::GameOver,
248 Sound::Lost,
249 Sound::Win,
250 Sound::Click,
251 Sound::Theme,
252 Sound::ThemeDecoded,
253 ];
254
255 fn label(self) -> &'static str {
256 match self {
257 Sound::Bounce => "bounce",
258 Sound::Break => "break",
259 Sound::Serve => "serve",
260 Sound::GameOver => "game over",
261 Sound::Lost => "lost",
262 Sound::Win => "win",
263 Sound::Click => "click",
264 Sound::Theme => "theme (streamed)",
265 Sound::ThemeDecoded => "theme (decoded)",
266 Sound::MenuTheme => "menu theme",
267 Sound::Pulse => "pulse",
268 }
269 }
270}
271
272impl Sounds for Sound {
273 fn build(&self, assets: &Assets) -> SoundData {
274 match self {
275 Sound::Bounce => assets.sound("bounce"),
276 Sound::Break => assets.sound("break"),
277 Sound::Serve => assets.sound("serve"),
278 Sound::GameOver => assets.sound("gameover"),
279 Sound::Lost => assets.sound("lost"),
280 Sound::Win => assets.sound("win"),
281 Sound::Click => assets.sound("click"),
282 Sound::Theme => assets.sound("music").streamed(),
283 Sound::ThemeDecoded => assets.sound("music"),
284 Sound::MenuTheme => assets.sound("menu_music").streamed(),
285 Sound::Pulse => assets.sound("break"),
286 }
287 }
288}
289
290/// The one button this game reads: it holds a source down and moves it.
291#[derive(InputButtonAction, Clone, Copy, PartialEq)]
292enum Button {
293 Select,
294}
295
296impl InputButtonAction for Button {
297 fn bindings(&self) -> Vec<ButtonBinding> {
298 match self {
299 Button::Select => vec![MouseButton::Left.into()],
300 }
301 }
302}
303
304/// The listener's walk, in the ground plane.
305#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
306enum Move {
307 Walk,
308}
309
310impl InputAxis2Action for Move {
311 fn bindings(&self) -> Vec<Axis2Binding> {
312 match self {
313 Move::Walk => vec![
314 Axis2Binding::from(ButtonAxis2 {
315 left: Key::A,
316 right: Key::D,
317 down: Key::S,
318 up: Key::W,
319 }),
320 Axis2Binding::stick(Stick::Left),
321 ],
322 }
323 }
324}
325
326struct Controls;
327
328impl InputActions for Controls {
329 type Button = Button;
330 type Axis = NoInputAxes;
331 type Axis2 = Move;
332}
333
334/// One source a drag moves: a cube on the ground, playing a sustained clip
335/// with its own gain, reference, range, and pitch.
336struct Source {
337 position: Vec3,
338 sound: Sound,
339 gain: f32,
340 reference: f32,
341 range: f32,
342 pitch: f32,
343 /// Whether this source sustains at all; off keeps startup silent.
344 enabled: bool,
345}
346
347impl Source {
348 fn new(x: f32, z: f32, sound: Sound, range: f32, enabled: bool) -> Self {
349 Self {
350 position: Vec3::new(x, SOURCE_HEIGHT, z),
351 sound,
352 gain: 0.5,
353 reference: SOURCE_REFERENCE,
354 range,
355 pitch: 1.0,
356 enabled,
357 }
358 }
359
360 /// This source's sustained cue, with the loop point that seeks far
361 /// where its choice needs one.
362 fn cue(&self) -> SoundCue<Sound> {
363 let cue = self
364 .sound
365 .at(self.position)
366 .gain(self.gain)
367 .reference(self.reference)
368 .range(self.range)
369 .pitch(self.pitch);
370 match self.sound {
371 Sound::Theme | Sound::ThemeDecoded => cue.loop_from(THEME_LOOP_FROM),
372 _ => cue,
373 }
374 }
375}
376
377struct SoundCheck {
378 master_volume: f32,
379
380 picked: Sound,
381 one_shot_gain: f32,
382 one_shot_pitch: f32,
383 one_shot_fade: f32,
384 trim_start: f32,
385 trim_end: f32,
386 one_shot_loop_from: f32,
387
388 theme_on: bool,
389 menu_on: bool,
390 pulse_on: bool,
391 cue_fade: f32,
392
393 /// Sustains [`Sound::Click`] at [`MERGE_POS_A`] and [`MERGE_POS_B`]
394 /// both at the default instance: shows the merge each source's own
395 /// instance above keeps clear of.
396 merge_demo: bool,
397
398 /// Declares [`RING_COUNT`] sustains at once, more than the engine
399 /// plays, so that the cap is heard as it allocates by level.
400 ring_demo: bool,
401
402 player: Vec2,
403 player_prev: Vec2,
404 sources: [Source; 3],
405 dragging: Option<usize>,
406
407 /// Every catalog value's length, read once at startup.
408 durations: HashMap<Sound, Duration>,
409}
410
411impl SoundCheck {
412 fn init(ctx: &mut InitContext<'_, SoundCheck>) -> Result<Self, Error> {
413 let durations = ctx.durations();
414
415 let picked = Sound::Bounce;
416 let trim_end = durations.get(&picked).copied().unwrap_or_default();
417
418 Ok(Self {
419 master_volume: 1.0,
420
421 picked,
422 one_shot_gain: 1.0,
423 one_shot_pitch: 1.0,
424 one_shot_fade: SoundCue::<Sound>::DEFAULT_FADE.as_secs_f32(),
425 trim_start: 0.0,
426 trim_end: trim_end.as_secs_f32(),
427 one_shot_loop_from: 0.0,
428
429 theme_on: false,
430 menu_on: false,
431 pulse_on: false,
432 cue_fade: 1.0,
433
434 merge_demo: false,
435 ring_demo: false,
436
437 player: Vec2::ZERO,
438 player_prev: Vec2::ZERO,
439 sources: [
440 Source::new(-2.5, -2.0, Sound::Bounce, 4.0, false),
441 Source::new(2.5, -2.0, Sound::Serve, 4.0, false),
442 Source::new(0.0, 2.8, Sound::Theme, 7.0, true),
443 ],
444 dragging: None,
445
446 durations,
447 })
448 }
449
450 fn camera(player: Vec2) -> Camera {
451 let ground = Vec3::new(player.x, 0.0, player.y);
452 Camera::new(
453 View::look_at(
454 ground + Vec3::new(0.0, CHASE_UP, CHASE_BACK),
455 ground + Vec3::Y * 0.5,
456 ),
457 Projection::perspective(55.0),
458 )
459 }
460
461 fn handle_walk(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
462 self.player_prev = self.player;
463 if ctx.ui_wants_keyboard() {
464 return;
465 }
466 let walk = ctx.axis2(Move::Walk);
467 let world = Vec2::new(walk.x, -walk.y);
468 self.player = (self.player + world * WALK_SPEED * ctx.dt().as_secs_f32())
469 .clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
470 }
471
472 /// Takes hold of the source a click's ray intersects, moves it across
473 /// the floor while the button stays down, and frees it on release.
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }
509
510 fn draw_room(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
511 ctx.draw(
512 Plane
513 .at(Transform::from_scale(Vec3::new(
514 ROOM_HALF * 2.0,
515 1.0,
516 ROOM_HALF * 2.0,
517 )))
518 .material(Material::lit(FLOOR_COLOR)),
519 );
520
521 let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, ROOM_HALF);
522 for side in [-1.0, 1.0] {
523 let x = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
524 ctx.draw(
525 Cube.at(Transform::from_scale_rotation_translation(
526 side_half * 2.0,
527 Quat::IDENTITY,
528 Vec3::new(x, side_half.y, 0.0),
529 ))
530 .material(Material::lit(WALL_COLOR)),
531 );
532 }
533 let end_half = Vec3::new(ROOM_HALF, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
534 for side in [-1.0, 1.0] {
535 let z = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
536 ctx.draw(
537 Cube.at(Transform::from_scale_rotation_translation(
538 end_half * 2.0,
539 Quat::IDENTITY,
540 Vec3::new(0.0, end_half.y, z),
541 ))
542 .material(Material::lit(WALL_COLOR)),
543 );
544 }
545 }
546
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 }
581
582 /// The listener: a cube drawn from the ground up to [`EYE_HEIGHT`],
583 /// an ear pair set on ± `view`'s right, and a marker at the front that
584 /// shows its fixed `-Z` facing.
585 fn draw_listener(&self, ctx: &mut FrameContext<'_, SoundCheck>, view: View) {
586 let head = view.eye();
587 let ground = Vec3::new(head.x, 0.0, head.z);
588
589 ctx.draw(
590 Cube.at(Transform::from_scale_rotation_translation(
591 Vec3::new(LISTENER_WIDTH, head.y, LISTENER_DEPTH),
592 Quat::IDENTITY,
593 ground + Vec3::Y * head.y * 0.5,
594 ))
595 .material(Material::lit(LISTENER_COLOR)),
596 );
597
598 let right = listener_right(view) * EAR_OFFSET;
599 for (offset, color) in [(right, RIGHT_EAR_COLOR), (-right, LEFT_EAR_COLOR)] {
600 ctx.draw(
601 Sphere { subdivisions: 1 }
602 .at(Transform::from_scale_rotation_translation(
603 Vec3::splat(EAR_SIZE),
604 Quat::IDENTITY,
605 head + offset,
606 ))
607 .material(Material::lit(color)),
608 );
609 }
610
611 ctx.draw(
612 Facing
613 .at(Transform::from_scale_rotation_translation(
614 Vec3::splat(FACING_MARKER_SIZE),
615 Quat::IDENTITY,
616 head + Vec3::NEG_Z * (FACING_MARKER_SIZE * 0.5),
617 ))
618 .material(Material::lit(LISTENER_COLOR)),
619 );
620 }
621
622 fn draw_merge_markers(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
623 if !self.merge_demo {
624 return;
625 }
626 for (position, color) in [(MERGE_POS_A, MERGE_COLOR_A), (MERGE_POS_B, MERGE_COLOR_B)] {
627 ctx.draw(
628 Cube.at(Transform::from_scale_rotation_translation(
629 Vec3::splat(SOURCE_HALF * 2.0),
630 Quat::IDENTITY,
631 position,
632 ))
633 .material(Material::lit(color)),
634 );
635 }
636 }
637
638 /// Draws the ring, each cube as dim as the gain its sustain is declared
639 /// at.
640 fn draw_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
641 if !self.ring_demo {
642 return;
643 }
644 for nth in 0..RING_COUNT {
645 let over = 1.0 - nth as f32 / RING_COUNT as f32;
646 ctx.draw(
647 Cube.at(Transform::from_scale_rotation_translation(
648 Vec3::splat(SOURCE_HALF),
649 Quat::IDENTITY,
650 ring_place(nth),
651 ))
652 .material(Material::lit(RING_COLOR.dimmed(over))),
653 );
654 }
655 }
656
657 /// The controls held at the left: master volume, sustained cues, and
658 /// each source's own knobs.
659 fn side_panel(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) {
660 #[cfg(target_arch = "wasm32")]
661 let unlocked = ctx.sound_unlocked();
662
663 let master_volume = &mut self.master_volume;
664 let theme_on = &mut self.theme_on;
665 let menu_on = &mut self.menu_on;
666 let pulse_on = &mut self.pulse_on;
667 let cue_fade = &mut self.cue_fade;
668 let merge_demo = &mut self.merge_demo;
669 let ring_demo = &mut self.ring_demo;
670 let ring_label = format!("cap demo: sustain {RING_COUNT} sounds at once");
671 let ring_note = format!(
672 "each one is quieter than the one before it, so the engine plays the loudest {MAX_VOICES} and the rest go silent without stopping"
673 );
674 let sources = &mut self.sources;
675
676 ctx.ui(|ui| {
677 egui::Panel::left("controls").show(ui, |ui| {
678 egui::ScrollArea::vertical()
679 .auto_shrink([false, false])
680 .show(ui, |ui| {
681 ui.heading("master");
682 ui.add(egui::Slider::new(master_volume, 0.0..=1.5).text("volume"));
683 #[cfg(target_arch = "wasm32")]
684 if !unlocked {
685 ui.label("audio unlocks on the first click or key in the browser");
686 }
687
688 ui.separator();
689 ui.heading("cue lab");
690 ui.label("a checked box is the sustain declaration");
691 ui.label("unchecking fades it out and parks it");
692 ui.checkbox(theme_on, Sound::Theme.label());
693 ui.checkbox(menu_on, Sound::MenuTheme.label());
694 ui.checkbox(pulse_on, Sound::Pulse.label());
695 ui.add(egui::Slider::new(cue_fade, 0.0..=3.0).text("fade (seconds)"));
696
697 ui.separator();
698 ui.heading("spatial lab");
699 ui.label("drag a source's marker on the floor to move it");
700 ui.label(
701 "a source is at full level inside its gold ring and falls to nothing at the white one",
702 );
703 ui.label("red is the right ear (RCA convention), white is the left");
704 ui.label("the point on the listener always faces -Z");
705 for (index, source) in sources.iter_mut().enumerate() {
706 ui.push_id(index, |ui| {
707 ui.separator();
708 ui.label(format!("source {}", index + 1));
709 ui.checkbox(&mut source.enabled, "enabled");
710 egui::ComboBox::from_label("clip")
711 .selected_text(source.sound.label())
712 .show_ui(ui, |ui| {
713 for choice in Sound::SOURCE_CHOICES {
714 ui.selectable_value(
715 &mut source.sound,
716 choice,
717 choice.label(),
718 );
719 }
720 });
721 ui.add(egui::Slider::new(&mut source.gain, 0.0..=2.0).text("gain"));
722 ui.add(
723 egui::Slider::new(&mut source.range, 1.0..=12.0).text("range"),
724 );
725 let range = source.range;
726 ui.add(
727 egui::Slider::new(&mut source.reference, 0.25..=range)
728 .text("reference"),
729 );
730 ui.add(
731 egui::Slider::new(&mut source.pitch, 0.5..=2.0).text("pitch"),
732 );
733 });
734 }
735 ui.separator();
736 ui.label(
737 "each enabled source above sustains at its own instance (0, 1, 2 by position), so the same clip can play at every one without merging into one voice",
738 );
739 ui.checkbox(merge_demo, "merge demo: same clip, both at instance 0");
740 ui.label(
741 "both declarations below target the same clip at the default instance",
742 );
743 ui.label(
744 "only the one declared last is heard, proof of what the sources above avoid",
745 );
746 ui.separator();
747 ui.checkbox(ring_demo, &ring_label);
748 ui.label(&ring_note);
749 ui.label(
750 "walk into the ring, or turn a source up, and what is played changes with what is loudest",
751 );
752 });
753 });
754 });
755 }
756
757 /// Reads what the one-shot controls hold, returning whether `play` and
758 /// `play x32` were pressed this frame — read inside the closure, applied
759 /// after it, since the closure cannot borrow `ctx`.
760 fn one_shot_panel(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) -> (bool, bool) {
761 let mut play_once = false;
762 let mut play_many = false;
763 let durations = &self.durations;
764 let picked = &mut self.picked;
765 let gain = &mut self.one_shot_gain;
766 let pitch = &mut self.one_shot_pitch;
767 let fade = &mut self.one_shot_fade;
768 let trim_start = &mut self.trim_start;
769 let trim_end = &mut self.trim_end;
770 let loop_from = &mut self.one_shot_loop_from;
771 let duration = durations
772 .get(picked)
773 .copied()
774 .unwrap_or_default()
775 .as_secs_f32()
776 .max(0.001);
777
778 ctx.ui(|ui| {
779 egui::Panel::bottom("one-shot").show(ui, |ui| {
780 ui.heading("one-shot lab");
781 egui::ComboBox::from_label("clip")
782 .selected_text(picked.label())
783 .show_ui(ui, |ui| {
784 for choice in Sound::ONE_SHOTS {
785 if ui
786 .selectable_label(*picked == choice, choice.label())
787 .clicked()
788 && *picked != choice
789 {
790 *picked = choice;
791 *trim_start = 0.0;
792 *trim_end = durations
793 .get(&choice)
794 .copied()
795 .unwrap_or_default()
796 .as_secs_f32();
797 *loop_from = 0.0;
798 }
799 }
800 });
801
802 ui.add(egui::Slider::new(gain, 0.0..=2.0).text("gain"));
803 ui.add(egui::Slider::new(pitch, 0.5..=2.0).text("pitch"));
804 ui.add(egui::Slider::new(fade, 0.0..=2.0).text("fade (seconds)"));
805
806 duration_bar(ui, duration, trim_start, trim_end, loop_from);
807 ui.label(
808 "the marker sets loop_from, which a one-shot ignores: only sustain reads it",
809 );
810
811 ui.horizontal(|ui| {
812 play_once = ui.button("play").clicked();
813 play_many = ui.button("play ×32 (overruns the voice cap)").clicked();
814 });
815 });
816 });
817
818 (play_once, play_many)
819 }
820
821 fn one_shot_cue(&self) -> SoundCue<Sound> {
822 self.picked
823 .gain(self.one_shot_gain)
824 .pitch(self.one_shot_pitch)
825 .fade(Duration::from_secs_f32(self.one_shot_fade))
826 .trim_to(
827 Duration::from_secs_f32(self.trim_start),
828 Duration::from_secs_f32(self.trim_end),
829 )
830 .loop_from(Duration::from_secs_f32(self.one_shot_loop_from))
831 }
832
833 /// Declares [`RING_COUNT`] sustains on a ring, each less loud than the
834 /// one before it, so the engine's cap plays the loudest of them and the
835 /// rest hold no voice while their playback goes on.
836 fn sustain_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
837 if !self.ring_demo {
838 return;
839 }
840 for nth in 0..RING_COUNT {
841 let gain = RING_GAIN * (1.0 - nth as f32 / RING_COUNT as f32);
842 ctx.sustain(
843 Sound::Pulse
844 .at(ring_place(nth))
845 .gain(gain)
846 .reference(RING_REFERENCE)
847 .range(RING_RADIUS * 3.0)
848 .instance(nth + 1),
849 );
850 }
851 }
852
853 fn sustain_cues(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
854 let fade = Duration::from_secs_f32(self.cue_fade);
855 if self.theme_on {
856 ctx.sustain(Sound::Theme.gain(0.5).fade(fade));
857 }
858 if self.menu_on {
859 ctx.sustain(Sound::MenuTheme.gain(0.5).fade(fade));
860 }
861 if self.pulse_on {
862 ctx.sustain(Sound::Pulse.gain(0.3).fade(fade));
863 }
864 }
865}
866
867/// Where the `nth` sustain of the ring stands: around the room, starting
868/// behind the listener's back.
869fn ring_place(nth: u32) -> Vec3 {
870 let turn = TAU * nth as f32 / RING_COUNT as f32;
871
872 Vec3::new(
873 turn.sin() * RING_RADIUS,
874 SOURCE_HEIGHT,
875 turn.cos() * RING_RADIUS,
876 )
877}
878
879/// The direction the ear pair is offset along — the same side the
880/// engine's own pan reads.
881fn listener_right(view: View) -> Vec3 {
882 (view.target() - view.eye())
883 .normalize_or_zero()
884 .cross(view.up())
885}724fn build_rock(seed: u32) -> MeshData {
725 let corners: [Vec3; 8] = core::array::from_fn(|index| {
726 let sign = Vec3::new(
727 if index & 1 == 0 { -0.5 } else { 0.5 },
728 if index & 2 == 0 { -0.5 } else { 0.5 },
729 if index & 4 == 0 { -0.5 } else { 0.5 },
730 );
731 sign + corner_offset(seed, index as u32)
732 });
733 let corner_at = |sign: Vec3| corners[corner_index(sign)];
734
735 let mut vertices = Vec::with_capacity(ROCK_FACES.len() * 4);
736 let mut indices = Vec::with_capacity(ROCK_FACES.len() * 6);
737 for (face, &(normal, right, up)) in ROCK_FACES.iter().enumerate() {
738 let quad = [
739 corner_at(normal - right - up),
740 corner_at(normal + right - up),
741 corner_at(normal + right + up),
742 corner_at(normal - right + up),
743 ];
744 let normal = (quad[1] - quad[0]).cross(quad[3] - quad[0]).normalize();
745 let uvs = [
746 Vec2::new(0.0, 1.0),
747 Vec2::new(1.0, 1.0),
748 Vec2::new(1.0, 0.0),
749 Vec2::new(0.0, 0.0),
750 ];
751 vertices.extend(
752 quad.into_iter()
753 .zip(uvs)
754 .map(|(corner, uv)| Vertex::new(corner, normal, uv)),
755 );
756 let base = face as u32 * 4;
757 indices.extend(ROCK_TRIANGLES.map(|index| base + index));
758 }
759 MeshData::new(vertices, indices)
760}Sourcepub fn min(self, rhs: Vec3) -> Vec3
pub fn min(self, rhs: Vec3) -> Vec3
Returns a vector containing the minimum values for each element of self and rhs.
In other words this computes [min(x, rhs.x), min(self.y, rhs.y), ..].
NaN propogation does not follow IEEE 754-2008 semantics for minNum and may differ on different SIMD architectures.
Sourcepub fn max(self, rhs: Vec3) -> Vec3
pub fn max(self, rhs: Vec3) -> Vec3
Returns a vector containing the maximum values for each element of self and rhs.
In other words this computes [max(self.x, rhs.x), max(self.y, rhs.y), ..].
NaN propogation does not follow IEEE 754-2008 semantics for maxNum and may differ on different SIMD architectures.
Sourcepub fn clamp(self, min: Vec3, max: Vec3) -> Vec3
pub fn clamp(self, min: Vec3, max: Vec3) -> Vec3
Component-wise clamping of values, similar to f32::clamp.
Each element in min must be less-or-equal to the corresponding element in max.
NaN propogation does not follow IEEE 754-2008 semantics and may differ on different SIMD architectures.
§Panics
Will panic if min is greater than max when glam_assert is enabled.
Sourcepub fn min_element(self) -> f32
pub fn min_element(self) -> f32
Returns the horizontal minimum of self.
In other words this computes min(x, y, ..).
NaN propogation does not follow IEEE 754-2008 semantics and may differ on different SIMD architectures.
Sourcepub fn max_element(self) -> f32
pub fn max_element(self) -> f32
Returns the horizontal maximum of self.
In other words this computes max(x, y, ..).
NaN propogation does not follow IEEE 754-2008 semantics and may differ on different SIMD architectures.
Sourcepub fn min_position(self) -> usize
pub fn min_position(self) -> usize
Returns the index of the first minimum element of self.
Sourcepub fn max_position(self) -> usize
pub fn max_position(self) -> usize
Returns the index of the first maximum element of self.
Sourcepub fn element_sum(self) -> f32
pub fn element_sum(self) -> f32
Returns the sum of all elements of self.
In other words, this computes self.x + self.y + ...
Sourcepub fn element_product(self) -> f32
pub fn element_product(self) -> f32
Returns the product of all elements of self.
In other words, this computes self.x * self.y * ...
Sourcepub fn cmpeq(self, rhs: Vec3) -> BVec3
pub fn cmpeq(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a == comparison for each element of
self and rhs.
In other words, this computes [self.x == rhs.x, self.y == rhs.y, ..] for all
elements.
Sourcepub fn cmpne(self, rhs: Vec3) -> BVec3
pub fn cmpne(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a != comparison for each element of
self and rhs.
In other words this computes [self.x != rhs.x, self.y != rhs.y, ..] for all
elements.
Sourcepub fn cmpge(self, rhs: Vec3) -> BVec3
pub fn cmpge(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a >= comparison for each element of
self and rhs.
In other words this computes [self.x >= rhs.x, self.y >= rhs.y, ..] for all
elements.
Sourcepub fn cmpgt(self, rhs: Vec3) -> BVec3
pub fn cmpgt(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a > comparison for each element of
self and rhs.
In other words this computes [self.x > rhs.x, self.y > rhs.y, ..] for all
elements.
Sourcepub fn cmple(self, rhs: Vec3) -> BVec3
pub fn cmple(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a <= comparison for each element of
self and rhs.
In other words this computes [self.x <= rhs.x, self.y <= rhs.y, ..] for all
elements.
Sourcepub fn cmplt(self, rhs: Vec3) -> BVec3
pub fn cmplt(self, rhs: Vec3) -> BVec3
Returns a vector mask containing the result of a < comparison for each element of
self and rhs.
In other words this computes [self.x < rhs.x, self.y < rhs.y, ..] for all
elements.
Sourcepub fn abs(self) -> Vec3
pub fn abs(self) -> Vec3
Returns a vector containing the absolute value of each element of self.
Sourcepub fn signum(self) -> Vec3
pub fn signum(self) -> Vec3
Returns a vector with elements representing the sign of self.
1.0if the number is positive,+0.0orINFINITY-1.0if the number is negative,-0.0orNEG_INFINITYNANif the number isNAN
Sourcepub fn copysign(self, rhs: Vec3) -> Vec3
pub fn copysign(self, rhs: Vec3) -> Vec3
Returns a vector with signs of rhs and the magnitudes of self.
Sourcepub fn is_negative_bitmask(self) -> u32
pub fn is_negative_bitmask(self) -> u32
Returns a bitmask with the lowest 3 bits set to the sign bits from the elements of self.
A negative element results in a 1 bit and a positive element in a 0 bit. Element x goes
into the first lowest bit, element y into the second, etc.
An element is negative if it has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
Sourcepub fn is_negative_mask(self) -> BVec3
pub fn is_negative_mask(self) -> BVec3
Returns a mask indicating which components are negative.
An element is negative if it has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
Sourcepub fn is_finite(self) -> bool
pub fn is_finite(self) -> bool
Returns true if, and only if, all elements are finite. If any element is either
NaN, positive or negative infinity, this will return false.
Sourcepub fn is_finite_mask(self) -> BVec3
pub fn is_finite_mask(self) -> BVec3
Performs is_finite on each element of self, returning a vector mask of the results.
In other words, this computes [x.is_finite(), y.is_finite(), ...].
Sourcepub fn is_nan_mask(self) -> BVec3
pub fn is_nan_mask(self) -> BVec3
Performs is_nan on each element of self, returning a vector mask of the results.
In other words, this computes [x.is_nan(), y.is_nan(), ...].
Sourcepub fn length(self) -> f32
pub fn length(self) -> f32
Computes the length of self.
Examples found in repository?
304 fn advance(&mut self, dt: Duration) -> bool {
305 let Some(target) = self.target else {
306 return false;
307 };
308 let to_target = target - self.position;
309 let distance = to_target.length();
310 let step = UNIT_SPEED * dt.as_secs_f32();
311 if distance <= step {
312 self.position = target;
313 self.target = None;
314 true
315 } else {
316 self.position += to_target * (step / distance);
317 false
318 }
319 }More examples
692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }Sourcepub fn length_squared(self) -> f32
pub fn length_squared(self) -> f32
Computes the squared length of self.
This is faster than length() as it avoids a square root operation.
Examples found in repository?
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 }Sourcepub fn length_recip(self) -> f32
pub fn length_recip(self) -> f32
Computes 1.0 / length().
For valid results, self must not be of length zero.
Sourcepub fn distance(self, rhs: Vec3) -> f32
pub fn distance(self, rhs: Vec3) -> f32
Computes the Euclidean distance between two points in space.
Examples found in repository?
722 fn patch_underfoot(&self) -> Option<Vec3> {
723 HURT_PATCHES
724 .into_iter()
725 .find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
726 }
727
728 /// Reads the controls and moves the elf, filling [`Self::elf_input`]
729 /// for [`ElfState`] to read.
730 fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731 let grounded = matches!(
732 self.elf_animator.state(),
733 ElfState::Idle | ElfState::Locomotion
734 );
735
736 self.elf_input.speed = self.advance(ctx);
737 self.elf_input.attack = ctx.pressed(Button::Attack);
738 self.elf_input.jump = ctx.pressed(Button::Jump);
739 self.elf_input.dance = ctx.pressed(Button::Dance);
740
741 self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742 self.elf_input.interact = ctx.pressed(Button::Interact);
743 if self.elf_input.interact && grounded && self.elf_input.near_seat {
744 self.elf_pos = SEAT_SPOT;
745 self.elf_yaw = SEAT_FACING;
746 }
747
748 let underfoot = self.patch_underfoot();
749 let entered_patch = underfoot.is_some() && !self.in_patch;
750 self.in_patch = underfoot.is_some();
751 self.hits += u32::from(entered_patch);
752 self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753 self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754 if entered_patch {
755 self.last_event = match self.elf_input.dying {
756 true => "elf died",
757 false => "elf hit",
758 };
759 }
760 }
761
762 /// Starts a new [`Animator`] over the elf's own state, its position and
763 /// hit count reset with it.
764 fn restart_elf(&mut self) {
765 self.elf_animator = Animator::new();
766 self.elf_pos = ELF_START;
767 self.elf_prev = ELF_START;
768 self.elf_yaw = 0.0;
769 self.elf_height = 0.0;
770 self.elf_height_prev = 0.0;
771 self.jump_speed = 0.0;
772 self.elf_input = ElfInput::default();
773 self.hits = 0;
774 self.in_patch = false;
775 self.last_event = "new elf started";
776 }
777
778 fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779 let state = match self.elf_animator.state() {
780 ElfState::Idle => "idle",
781 ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782 ElfState::Locomotion => "walking",
783 ElfState::Attack => "attacking",
784 ElfState::Hit => "hit",
785 ElfState::Death => "dead",
786 ElfState::SitDown => "sitting down",
787 ElfState::Sit => "sitting",
788 ElfState::StandUp => "standing up",
789 ElfState::Jump => "jumping",
790 ElfState::Dance => "dancing",
791 };
792 ctx.ui(|ui| {
793 egui::Frame::new()
794 .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795 .inner_margin(PANEL_PADDING)
796 .corner_radius(f32::from(PANEL_PADDING))
797 .show(ui, |ui| {
798 ui.heading(panel_text(format!("elf is {state}")));
799 ui.label(panel_text(format!(
800 "hits taken {} of the {} red patches hurt for, {}",
801 self.hits, FATAL_HITS, self.last_event
802 )));
803 ui.label(panel_text(match self.elf_animator.transitioning() {
804 true => "fading between clips",
805 false => "one clip playing",
806 }));
807 ui.add_space(f32::from(PANEL_PADDING));
808 egui::Grid::new("controls").show(ui, |ui| {
809 for (key, does) in CONTROLS {
810 ui.label(panel_text(key));
811 ui.label(panel_text(does));
812 ui.end_row();
813 }
814 });
815 });
816 });
817 }
818
819 /// A prompt over the seat, each hurt patch, and the scrubbed elf,
820 /// naming what a player finds there; the seat's own prompt names the
821 /// live binding of `Button::Interact` by its own name, not one fixed
822 /// in the code, and is absent while the elf sits on it.
823 fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824 let sit_key = ctx
825 .bindings(Button::Interact)
826 .into_iter()
827 .next()
828 .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829 let sit = ctx.text_layout(
830 &format!("{sit_key} sits"),
831 egui::FontId::proportional(PROMPT_SIZE),
832 );
833 let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834 let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836 let mut prompts = vec![(
837 SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838 walk_closer,
839 )];
840 if !self.elf_animator.state().seated() {
841 prompts.push((
842 SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843 sit,
844 ));
845 }
846 prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848 let window_size = ctx.window_size();
849 let pixels_per_point = ctx.pixels_per_point();
850 ctx.ui(|ui| {
851 let painter = ui.painter();
852 for (point, galley) in prompts {
853 let Some(pixel) = camera.pixel_of(point, window_size) else {
854 continue;
855 };
856 let at = logical(pixel, pixels_per_point);
857 let ink = galley.mesh_bounds;
858 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859 let backdrop = egui::Rect::from_center_size(
860 at,
861 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862 );
863 painter.rect_filled(
864 backdrop,
865 PROMPT_PADDING,
866 egui::Color32::from_black_alpha(PANEL_BACKDROP),
867 );
868 painter.galley(pos, galley, PANEL_TEXT_COLOR);
869 }
870 });
871 }
872}
873
874impl Game for Scene {
875 type Meshes = Shape;
876 type Sounds = NoSounds;
877 type InputActions = Controls;
878 type Skyboxes = Sky;
879 type SurfaceStyles = NoSurfaceStyles;
880 type PostEffects = NoPostEffects;
881
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }More examples
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 }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 }
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 }
1697
1698 /// The player: upright so it always faces the camera about `+Y`,
1699 /// windowed to its facing's row and the walk cycle's current frame.
1700 fn draw_walker(&self, ctx: &mut FrameContext<'_, Keep>, ground: Vec3) {
1701 let step = if self.walk_ticks > 0 {
1702 (self.walk_ticks / TICKS_PER_WALK_FRAME) % WALKER_COLUMNS
1703 } else {
1704 0
1705 };
1706 let cell = Sheet::new(UVec2::new(WALKER_COLUMNS, WALKER_ROWS))
1707 .cell_at(UVec2::new(step, self.facing as u32));
1708 let size = Vec2::new(WALKER_WIDTH, WALKER_HEIGHT);
1709
1710 ctx.draw(
1711 Walker
1712 .at(Transform::from_scale_rotation_translation(
1713 size.extend(1.0),
1714 Quat::IDENTITY,
1715 ground + Vec3::Y * (WALKER_HEIGHT * 0.5),
1716 ))
1717 .upright()
1718 .frame(cell),
1719 );
1720 }
1721
1722 fn frame_overworld(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1723 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1724 ctx.set_camera(Self::camera(drawn_at, OVERWORLD_CAMERA_OFFSET));
1725 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
1726
1727 self.draw_ground(ctx);
1728 self.draw_hedgerow(ctx);
1729 self.draw_pond(ctx);
1730 self.draw_crates(ctx);
1731 self.draw_well(ctx);
1732 self.draw_flora(ctx);
1733 Self::draw_mouth(ctx, ENTRANCE);
1734 self.draw_walker(ctx, drawn_at);
1735 }
1736
1737 fn frame_cave(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1738 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1739 let camera = Self::camera(drawn_at, CAVE_CAMERA_OFFSET);
1740 ctx.set_camera(camera);
1741
1742 self.draw_cave_floor(ctx);
1743 self.draw_cave_walls(ctx);
1744 Self::draw_door_wall(ctx, (self.ghost > 0.0).then_some(drawn_at.x), self.ghost);
1745 Self::draw_mouth(ctx, EXIT);
1746 self.draw_torches(ctx);
1747 self.draw_door(ctx, self.ghost);
1748 self.draw_door_frame(ctx, self.ghost);
1749 if !self.gem_taken {
1750 self.draw_gem(ctx);
1751 }
1752 self.draw_walker(ctx, drawn_at);
1753 self.draw_door_prompt(ctx, camera);
1754 }
1755
1756 /// Instructions and the door's interact hint — gathered before `ctx.ui`,
1757 /// which cannot read `ctx`.
1758 fn overlay(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1759 let near_door = self.area == Area::Cave
1760 && !self.door_opening
1761 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1762 let gem_taken = self.area == Area::Cave && self.gem_taken;
1763 let mut reset_clicked = false;
1764
1765 ctx.ui(|ui| {
1766 egui::Frame::new()
1767 .fill(egui::Color32::from_black_alpha(HUD_BACKDROP))
1768 .inner_margin(HUD_PADDING)
1769 .corner_radius(f32::from(HUD_PADDING))
1770 .show(ui, |ui| {
1771 ui.visuals_mut().override_text_color = Some(egui::Color32::WHITE);
1772 ui.label("wasd / arrows / stick to walk");
1773 if near_door {
1774 ui.label("e / west button to open the door");
1775 }
1776 if gem_taken {
1777 ui.label("gem recovered");
1778 }
1779 ui.label("kept between runs: position, gem, cave");
1780 ui.label("r to reset world");
1781 if ui.button("reset world").clicked() {
1782 reset_clicked = true;
1783 }
1784 });
1785 });
1786
1787 if reset_clicked {
1788 self.reset_requested = true;
1789 }
1790 }Sourcepub fn distance_squared(self, rhs: Vec3) -> f32
pub fn distance_squared(self, rhs: Vec3) -> f32
Compute the squared euclidean distance between two points in space.
Sourcepub fn div_euclid(self, rhs: Vec3) -> Vec3
pub fn div_euclid(self, rhs: Vec3) -> Vec3
Returns the element-wise quotient of [Euclidean division] of self by rhs.
Sourcepub fn rem_euclid(self, rhs: Vec3) -> Vec3
pub fn rem_euclid(self, rhs: Vec3) -> Vec3
Returns the element-wise remainder of Euclidean division of self by rhs.
Sourcepub fn normalize(self) -> Vec3
pub fn normalize(self) -> Vec3
Returns self normalized to length 1.0.
For valid results, self must be finite and not of length zero, nor very close to zero.
See also Self::try_normalize() and Self::normalize_or_zero().
§Panics
Will panic if the resulting normalized vector is not finite when glam_assert is enabled.
Examples found in repository?
392 fn launch(&mut self) {
393 self.ball_vel = Vec3::new(0.35, 0.0, -1.0).normalize() * BALL_SPEED;
394 self.phase = Phase::Playing;
395 }
396
397 /// Holds the ball above the paddle while it waits to be served, tracking
398 /// the paddle's own steering, and launches it once the player serves.
399 fn hold_ball(&mut self, ctx: &mut TickContext<'_, Breakout>) {
400 self.ball_prev = self.ball_pos;
401 self.ball_pos.x = self.paddle_x;
402 self.ball_trail = [self.ball_pos; TRAIL_LEN + 1];
403
404 if !ctx.ui_wants_keyboard() && ctx.pressed(Button::Serve) {
405 self.launch();
406 ctx.play(Sound::Serve);
407 }
408 }
409
410 fn step_paddle(&mut self, axis: f32, dt: f32) {
411 self.paddle_prev_x = self.paddle_x;
412 self.paddle_x =
413 (self.paddle_x + axis * PADDLE_SPEED * dt).clamp(-PADDLE_LIMIT, PADDLE_LIMIT);
414 }
415
416 fn step_ball(&mut self, ctx: &mut TickContext<'_, Breakout>, dt: f32) {
417 self.ball_prev = self.ball_pos;
418 self.ball_pos += self.ball_vel * dt;
419
420 self.bounce_walls(ctx);
421 self.bounce_paddle(ctx);
422 self.bounce_bricks(ctx);
423 self.push_trail();
424
425 if self.ball_pos.z - BALL_RADIUS > COURT_HALF_DEPTH {
426 self.lose_life(ctx);
427 }
428 }
429
430 /// Shifts the ghost trail back one slot and records the ball's newly
431 /// resolved position at the front.
432 fn push_trail(&mut self) {
433 self.ball_trail.rotate_right(1);
434 self.ball_trail[0] = self.ball_pos;
435 }
436
437 fn bounce_walls(&mut self, ctx: &mut TickContext<'_, Breakout>) {
438 let left = -COURT_HALF_WIDTH + WALL_THICKNESS;
439 let right = COURT_HALF_WIDTH - WALL_THICKNESS;
440 let top = -COURT_HALF_DEPTH + WALL_THICKNESS;
441
442 let mut hit = false;
443 if self.ball_pos.x - BALL_RADIUS < left {
444 self.ball_pos.x = left + BALL_RADIUS;
445 self.ball_vel.x = self.ball_vel.x.abs();
446 hit = true;
447 } else if self.ball_pos.x + BALL_RADIUS > right {
448 self.ball_pos.x = right - BALL_RADIUS;
449 self.ball_vel.x = -self.ball_vel.x.abs();
450 hit = true;
451 }
452
453 if self.ball_pos.z - BALL_RADIUS < top {
454 self.ball_pos.z = top + BALL_RADIUS;
455 self.ball_vel.z = self.ball_vel.z.abs();
456 hit = true;
457 }
458
459 if hit {
460 ctx.play(Sound::Bounce.pitch(WALL_BOUNCE_PITCH));
461 }
462 }
463
464 /// Bounces the ball off the paddle, steering it by where it landed.
465 fn bounce_paddle(&mut self, ctx: &mut TickContext<'_, Breakout>) {
466 if self.ball_vel.z <= 0.0 {
467 return;
468 }
469 let reach_x = PADDLE_HALF_WIDTH + BALL_RADIUS;
470 let reach_z = PADDLE_HALF_DEPTH + BALL_RADIUS;
471 let dx = self.ball_pos.x - self.paddle_x;
472 let dz = self.ball_pos.z - PADDLE_Z;
473 if dx.abs() > reach_x || dz.abs() > reach_z {
474 return;
475 }
476
477 let offset = (dx / PADDLE_HALF_WIDTH).clamp(-1.0, 1.0);
478 self.ball_vel = Vec3::new(offset, 0.0, -1.0).normalize() * BALL_SPEED;
479 self.ball_pos.z = PADDLE_Z - reach_z;
480 self.paddle_flash = PADDLE_FLASH;
481 ctx.play(Sound::Bounce.pitch(PADDLE_BOUNCE_PITCH));
482 }
483
484 /// Bounces the ball off the nearest overlapping brick, damaging it.
485 fn bounce_bricks(&mut self, ctx: &mut TickContext<'_, Breakout>) {
486 let reach_x = BRICK_HALF_WIDTH + BALL_RADIUS;
487 let reach_z = BRICK_HALF_DEPTH + BALL_RADIUS;
488 let mut broken = None;
489
490 for brick in self
491 .bricks
492 .iter_mut()
493 .filter(|brick| brick.hits_remaining > 0)
494 {
495 let dx = self.ball_pos.x - brick.position.x;
496 let dz = self.ball_pos.z - brick.position.z;
497 if dx.abs() > reach_x || dz.abs() > reach_z {
498 continue;
499 }
500
501 if reach_x - dx.abs() < reach_z - dz.abs() {
502 self.ball_vel.x = if dx < 0.0 {
503 -self.ball_vel.x.abs()
504 } else {
505 self.ball_vel.x.abs()
506 };
507 } else {
508 self.ball_vel.z = if dz < 0.0 {
509 -self.ball_vel.z.abs()
510 } else {
511 self.ball_vel.z.abs()
512 };
513 }
514
515 brick.hits_remaining -= 1;
516 self.score += 10 * (BRICK_ROWS - brick.row) as u32;
517 ctx.play(Sound::Bounce.pitch(BRICK_BOUNCE_PITCH));
518 if brick.hits_remaining == 0 {
519 ctx.play(
520 Sound::BrickBreak
521 .at(brick.position)
522 .reference(BRICK_BREAK_REFERENCE),
523 );
524 self.brick_flash = BRICK_FLASH;
525 broken = Some((brick.position, BRICK_ROW_COLORS[brick.row]));
526 }
527 break;
528 }
529
530 if let Some((position, color)) = broken {
531 self.spawn_sparks(position, color);
532 }
533
534 if self.bricks.iter().all(|brick| brick.hits_remaining == 0) {
535 self.phase = Phase::Won;
536 ctx.play(Sound::LevelClear);
537 }
538 }
539
540 /// Sends [`SPARK_BURST_COUNT`] sparks outward and upward from a broken
541 /// brick's position, spread by index so no randomness is needed.
542 fn spawn_sparks(&mut self, position: Vec3, color: Color) {
543 for i in 0..SPARK_BURST_COUNT {
544 let t = i as f32 / SPARK_BURST_COUNT as f32;
545 let azimuth = t * TAU;
546 let rise = 0.6 + 0.4 * (t * 3.0).fract();
547 let speed = SPARK_SPEED_MIN.lerp(SPARK_SPEED_MAX, (t * 5.0).fract());
548 let direction = Vec3::new(azimuth.cos(), rise, azimuth.sin()).normalize();
549 self.sparks.push(Spark {
550 position,
551 velocity: direction * speed,
552 roll: azimuth,
553 age: 0.0,
554 color,
555 });
556 }
557 }More examples
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 }188fn facing_marker() -> MeshData {
189 const TIP: Vec3 = Vec3::new(0.0, 0.0, -0.5);
190 const BACK: [Vec3; 4] = [
191 Vec3::new(-0.5, -0.5, 0.5),
192 Vec3::new(0.5, -0.5, 0.5),
193 Vec3::new(0.5, 0.5, 0.5),
194 Vec3::new(-0.5, 0.5, 0.5),
195 ];
196
197 let mut vertices = Vec::with_capacity(BACK.len() * 3);
198 for (corner, next) in BACK.iter().zip(BACK.iter().cycle().skip(1)) {
199 let normal = (next - corner).cross(TIP - corner).normalize();
200 vertices.extend([
201 Vertex::new(*corner, normal, Vec2::new(0.0, 1.0)),
202 Vertex::new(*next, normal, Vec2::new(1.0, 1.0)),
203 Vertex::new(TIP, normal, Vec2::new(0.5, 0.0)),
204 ]);
205 }
206 let indices = (0..vertices.len() as u32).collect();
207 MeshData::new(vertices, indices)
208}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 }359 fn handle_camera(&mut self, ctx: &mut FrameContext<'_, Self>, elapsed: f32) {
360 if ctx.ui_wants_pointer() || ctx.ui_wants_keyboard() {
361 return;
362 }
363 let pan = ctx.axis2(Motion::Pan);
364 let wheel = ctx.axis(Height::Wheel);
365 let look = if ctx.down(Drag::Turn) {
366 ctx.axis2(Motion::Look)
367 } else {
368 Vec2::ZERO
369 };
370 if pan == Vec2::ZERO && wheel == 0.0 && look == Vec2::ZERO {
371 return;
372 }
373
374 let player = self.player.get_or_insert_with(|| {
375 let eye = Self::orbit_eye(elapsed);
376 let forward = (Vec3::ZERO - eye).normalize();
377 Player {
378 eye,
379 yaw: (-forward.x).atan2(-forward.z),
380 pitch: forward.y.asin(),
381 }
382 });
383
384 player.yaw -= look.x;
385 player.pitch = (player.pitch + look.y).clamp(-PITCH_LIMIT, PITCH_LIMIT);
386
387 let forward = Vec3::new(-player.yaw.sin(), 0.0, -player.yaw.cos());
388 let right = Vec3::new(player.yaw.cos(), 0.0, -player.yaw.sin());
389 player.eye += (forward * pan.y + right * pan.x) * PAN_SPEED * ctx.dt().as_secs_f32();
390 player.eye.y =
391 (player.eye.y + wheel * WHEEL_STEP).clamp(MIN_CAMERA_HEIGHT, MAX_CAMERA_HEIGHT);
392 }
393
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}
553
554/// One triangle, shaded flat, over `a`, `b`, `c`, in the order that faces
555/// outward: counter-clockwise as seen from the side its own normal points
556/// to.
557fn push_face(vertices: &mut Vec<Vertex>, indices: &mut Vec<u32>, a: Vec3, b: Vec3, c: Vec3) {
558 let normal = (b - a).cross(c - a).normalize();
559 let uvs = [
560 Vec2::new(0.0, 1.0),
561 Vec2::new(0.5, 0.0),
562 Vec2::new(1.0, 1.0),
563 ];
564 let base = vertices.len() as u32;
565 for (point, uv) in [a, b, c].into_iter().zip(uvs) {
566 vertices.push(Vertex::new(point, normal, uv));
567 }
568 indices.extend([base, base + 1, base + 2]);
569}724fn build_rock(seed: u32) -> MeshData {
725 let corners: [Vec3; 8] = core::array::from_fn(|index| {
726 let sign = Vec3::new(
727 if index & 1 == 0 { -0.5 } else { 0.5 },
728 if index & 2 == 0 { -0.5 } else { 0.5 },
729 if index & 4 == 0 { -0.5 } else { 0.5 },
730 );
731 sign + corner_offset(seed, index as u32)
732 });
733 let corner_at = |sign: Vec3| corners[corner_index(sign)];
734
735 let mut vertices = Vec::with_capacity(ROCK_FACES.len() * 4);
736 let mut indices = Vec::with_capacity(ROCK_FACES.len() * 6);
737 for (face, &(normal, right, up)) in ROCK_FACES.iter().enumerate() {
738 let quad = [
739 corner_at(normal - right - up),
740 corner_at(normal + right - up),
741 corner_at(normal + right + up),
742 corner_at(normal - right + up),
743 ];
744 let normal = (quad[1] - quad[0]).cross(quad[3] - quad[0]).normalize();
745 let uvs = [
746 Vec2::new(0.0, 1.0),
747 Vec2::new(1.0, 1.0),
748 Vec2::new(1.0, 0.0),
749 Vec2::new(0.0, 0.0),
750 ];
751 vertices.extend(
752 quad.into_iter()
753 .zip(uvs)
754 .map(|(corner, uv)| Vertex::new(corner, normal, uv)),
755 );
756 let base = face as u32 * 4;
757 indices.extend(ROCK_TRIANGLES.map(|index| base + index));
758 }
759 MeshData::new(vertices, indices)
760}Sourcepub fn try_normalize(self) -> Option<Vec3>
pub fn try_normalize(self) -> Option<Vec3>
Returns self normalized to length 1.0 if possible, else returns None.
In particular, if the input is zero (or very close to zero), or non-finite,
the result of this operation will be None.
See also Self::normalize_or_zero().
Examples found in repository?
692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }Sourcepub fn normalize_or(self, fallback: Vec3) -> Vec3
pub fn normalize_or(self, fallback: Vec3) -> Vec3
Returns self normalized to length 1.0 if possible, else returns a
fallback value.
In particular, if the input is zero (or very close to zero), or non-finite, the result of this operation will be the fallback value.
See also Self::try_normalize().
Sourcepub fn normalize_or_zero(self) -> Vec3
pub fn normalize_or_zero(self) -> Vec3
Returns self normalized to length 1.0 if possible, else returns zero.
In particular, if the input is zero (or very close to zero), or non-finite, the result of this operation will be zero.
See also Self::try_normalize().
Sourcepub fn normalize_and_length(self) -> (Vec3, f32)
pub fn normalize_and_length(self) -> (Vec3, f32)
Returns self normalized to length 1.0 and the length of self.
If self is zero length then (Self::X, 0.0) is returned.
Sourcepub fn is_normalized(self) -> bool
pub fn is_normalized(self) -> bool
Returns whether self is length 1.0 or not.
Uses a precision threshold of approximately 1e-4.
Sourcepub fn project_onto(self, rhs: Vec3) -> Vec3
pub fn project_onto(self, rhs: Vec3) -> Vec3
Returns the vector projection of self onto rhs.
rhs must be of non-zero length.
§Panics
Will panic if rhs is zero length when glam_assert is enabled.
Sourcepub fn reject_from(self, rhs: Vec3) -> Vec3
pub fn reject_from(self, rhs: Vec3) -> Vec3
Returns the vector rejection of self from rhs.
The vector rejection is the vector perpendicular to the projection of self onto
rhs, in rhs words the result of self - self.project_onto(rhs).
rhs must be of non-zero length.
§Panics
Will panic if rhs has a length of zero when glam_assert is enabled.
Sourcepub fn project_onto_normalized(self, rhs: Vec3) -> Vec3
pub fn project_onto_normalized(self, rhs: Vec3) -> Vec3
Returns the vector projection of self onto rhs.
rhs must be normalized.
§Panics
Will panic if rhs is not normalized when glam_assert is enabled.
Sourcepub fn reject_from_normalized(self, rhs: Vec3) -> Vec3
pub fn reject_from_normalized(self, rhs: Vec3) -> Vec3
Returns the vector rejection of self from rhs.
The vector rejection is the vector perpendicular to the projection of self onto
rhs, in rhs words the result of self - self.project_onto(rhs).
rhs must be normalized.
§Panics
Will panic if rhs is not normalized when glam_assert is enabled.
Sourcepub fn round(self) -> Vec3
pub fn round(self) -> Vec3
Returns a vector containing the nearest integer to a number for each element of self.
Round half-way cases away from 0.0.
Sourcepub fn floor(self) -> Vec3
pub fn floor(self) -> Vec3
Returns a vector containing the largest integer less than or equal to a number for each
element of self.
Sourcepub fn ceil(self) -> Vec3
pub fn ceil(self) -> Vec3
Returns a vector containing the smallest integer greater than or equal to a number for
each element of self.
Sourcepub fn trunc(self) -> Vec3
pub fn trunc(self) -> Vec3
Returns a vector containing the integer part each element of self. This means numbers are
always truncated towards zero.
Sourcepub fn step(self, rhs: Vec3) -> Vec3
pub fn step(self, rhs: Vec3) -> Vec3
Returns a vector containing 0.0 if rhs < self and 1.0 otherwise.
Similar to glsl’s step(edge, x), which translates into edge.step(x)
Sourcepub fn saturate(self) -> Vec3
pub fn saturate(self) -> Vec3
Returns a vector containing all elements of self clamped to the range of [0, 1].
Sourcepub fn fract(self) -> Vec3
pub fn fract(self) -> Vec3
Returns a vector containing the fractional part of the vector as self - self.trunc().
Note that this differs from the GLSL implementation of fract which returns
self - self.floor().
Note that this is fast but not precise for large numbers.
Sourcepub fn fract_gl(self) -> Vec3
pub fn fract_gl(self) -> Vec3
Returns a vector containing the fractional part of the vector as self - self.floor().
Note that this differs from the Rust implementation of fract which returns
self - self.trunc().
Note that this is fast but not precise for large numbers.
Sourcepub fn exp(self) -> Vec3
pub fn exp(self) -> Vec3
Returns a vector containing e^self (the exponential function) for each element of
self.
Sourcepub fn ln(self) -> Vec3
pub fn ln(self) -> Vec3
Returns a vector containing the natural logarithm for each element of self.
This returns NaN when the element is negative and negative infinity when the element is zero.
Sourcepub fn log2(self) -> Vec3
pub fn log2(self) -> Vec3
Returns a vector containing the base 2 logarithm for each element of self.
This returns NaN when the element is negative and negative infinity when the element is zero.
Sourcepub fn powf(self, n: f32) -> Vec3
pub fn powf(self, n: f32) -> Vec3
Returns a vector containing each element of self raised to the power of n.
Sourcepub fn sqrt(self) -> Vec3
pub fn sqrt(self) -> Vec3
Returns a vector containing the square root for each element of self.
This returns NaN when the element is negative.
Sourcepub fn sin_cos(self) -> (Vec3, Vec3)
pub fn sin_cos(self) -> (Vec3, Vec3)
Returns a tuple of two vectors containing the sine and cosine for each element of self.
Sourcepub fn recip(self) -> Vec3
pub fn recip(self) -> Vec3
Returns a vector containing the reciprocal 1.0/n of each element of self.
Sourcepub fn lerp(self, rhs: Vec3, s: f32) -> Vec3
pub fn lerp(self, rhs: Vec3, s: f32) -> Vec3
Performs a linear interpolation between self and rhs based on the value s.
When s is 0.0, the result will be equal to self. When s is 1.0, the result
will be equal to rhs. When s is outside of range [0, 1], the result is linearly
extrapolated.
Examples found in repository?
1722 fn frame_overworld(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1723 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1724 ctx.set_camera(Self::camera(drawn_at, OVERWORLD_CAMERA_OFFSET));
1725 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
1726
1727 self.draw_ground(ctx);
1728 self.draw_hedgerow(ctx);
1729 self.draw_pond(ctx);
1730 self.draw_crates(ctx);
1731 self.draw_well(ctx);
1732 self.draw_flora(ctx);
1733 Self::draw_mouth(ctx, ENTRANCE);
1734 self.draw_walker(ctx, drawn_at);
1735 }
1736
1737 fn frame_cave(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1738 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1739 let camera = Self::camera(drawn_at, CAVE_CAMERA_OFFSET);
1740 ctx.set_camera(camera);
1741
1742 self.draw_cave_floor(ctx);
1743 self.draw_cave_walls(ctx);
1744 Self::draw_door_wall(ctx, (self.ghost > 0.0).then_some(drawn_at.x), self.ghost);
1745 Self::draw_mouth(ctx, EXIT);
1746 self.draw_torches(ctx);
1747 self.draw_door(ctx, self.ghost);
1748 self.draw_door_frame(ctx, self.ghost);
1749 if !self.gem_taken {
1750 self.draw_gem(ctx);
1751 }
1752 self.draw_walker(ctx, drawn_at);
1753 self.draw_door_prompt(ctx, camera);
1754 }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 }
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 }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 }920 fn frame(&mut self, ctx: &mut FrameContext<'_, Scene>) {
921 self.steer_camera(ctx);
922
923 let alpha = ctx.alpha();
924 let elf_pos = self.elf_prev.lerp(self.elf_pos, alpha);
925 let elf_height = self.elf_height_prev + (self.elf_height - self.elf_height_prev) * alpha;
926 let (butterfly_pos, butterfly_yaw) = butterfly_pose(ctx.elapsed().as_secs_f32());
927
928 let camera = orbit_camera(elf_pos, self.camera_yaw, self.camera_pitch);
929 ctx.set_camera(camera);
930 ctx.set_cursor(if self.holding {
931 Cursor::Held
932 } else {
933 Cursor::Arrow
934 });
935 ctx.set_skybox(Sky::Day);
936 ctx.set_exposure(3.0);
937 ctx.set_bloom(0.2);
938 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
939 ctx.light(
940 Light::point(
941 LAMP_POST_POSITION + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP * 0.5),
942 LAMP_LIGHT_COLOR,
943 LAMP_LIGHT_RANGE,
944 )
945 .shadow(),
946 );
947 ctx.light(
948 Light::spot(Spot {
949 position: SPOT_POSITION,
950 direction: SPOT_DIRECTION,
951 color: SPOT_COLOR,
952 range: SPOT_RANGE,
953 angle: SPOT_ANGLE,
954 })
955 .shadow(),
956 );
957 ctx.light(
958 Light::point(butterfly_pos, BUTTERFLY_LIGHT_COLOR, BUTTERFLY_LIGHT_RANGE).shadow(),
959 );
960
961 ctx.draw(
962 Plane
963 .at(Transform::from_scale(Vec3::new(
964 GROUND_SIZE,
965 1.0,
966 GROUND_SIZE,
967 )))
968 .material(Material::lit(GROUND_COLOR)),
969 );
970 for patch in HURT_PATCHES {
971 ctx.draw(
972 Plane
973 .at(Transform::from_scale_rotation_translation(
974 Vec3::splat(HURT_RADIUS * 2.0),
975 Quat::IDENTITY,
976 patch,
977 ))
978 .material(Material::lit(HURT_COLOR)),
979 );
980 }
981 ctx.draw(
982 Cube.at(Transform::from_scale_rotation_translation(
983 Vec3::new(SEAT_FOOTPRINT, SEAT_HEIGHT, SEAT_FOOTPRINT),
984 Quat::IDENTITY,
985 SEAT_POSITION + Vec3::Y * SEAT_HEIGHT * 0.5,
986 ))
987 .material(Material::lit(SEAT_COLOR)),
988 );
989 ctx.draw(
990 Cube.at(Transform::from_scale_rotation_translation(
991 Vec3::new(LAMP_POST_THICKNESS, LAMP_POST_HEIGHT, LAMP_POST_THICKNESS),
992 Quat::IDENTITY,
993 LAMP_POST_POSITION + Vec3::Y * LAMP_POST_HEIGHT * 0.5,
994 ))
995 .material(Material::lit(LAMP_POST_COLOR)),
996 );
997 ctx.draw(
998 Cube.at(Transform::from_scale_rotation_translation(
999 Vec3::splat(LAMP_HEAD_SIZE),
1000 Quat::IDENTITY,
1001 LAMP_POST_POSITION
1002 + Vec3::Y * (LAMP_POST_HEIGHT + LAMP_HEAD_GAP + LAMP_HEAD_SIZE * 0.5),
1003 ))
1004 .material(Material::color(Color::BLACK).emissive(LAMP_LIGHT_COLOR)),
1005 );
1006 ctx.draw(
1007 Cube.at(Transform::from_scale_rotation_translation(
1008 Vec3::splat(SPOT_FIXTURE_SIZE),
1009 Quat::IDENTITY,
1010 SPOT_POSITION + Vec3::Y * SPOT_FIXTURE_SIZE * 0.5,
1011 ))
1012 .material(Material::lit(SPOT_FIXTURE_COLOR)),
1013 );
1014
1015 ctx.draw(
1016 Elf.at(Transform::from_rotation_translation(
1017 Quat::from_rotation_y(self.elf_yaw),
1018 elf_pos + Vec3::Y * elf_height,
1019 ))
1020 .posed(&self.elf_animator),
1021 );
1022 ctx.draw(
1023 Elf.at(Transform::from_rotation_translation(
1024 Quat::from_rotation_y(core::f32::consts::PI),
1025 SCRUBBED_ELF_POSITION,
1026 ))
1027 .posed(&self.scrubbed_animator),
1028 );
1029 ctx.draw(
1030 Butterfly
1031 .at(Transform::from_rotation_translation(
1032 Quat::from_rotation_y(butterfly_yaw),
1033 butterfly_pos,
1034 ))
1035 .posed(&self.butterfly_animator)
1036 .material(Material::lit(Color::WHITE).emissive(BUTTERFLY_EMISSIVE)),
1037 );
1038
1039 self.draw_prompts(ctx, camera);
1040 self.panel(ctx);
1041 }Sourcepub fn move_towards(self, rhs: Vec3, d: f32) -> Vec3
pub fn move_towards(self, rhs: Vec3, d: f32) -> Vec3
Moves towards rhs based on the value d.
When d is 0.0, the result will be equal to self. When d is equal to
self.distance(rhs), the result will be equal to rhs. Will not go past rhs.
Sourcepub fn midpoint(self, rhs: Vec3) -> Vec3
pub fn midpoint(self, rhs: Vec3) -> Vec3
Calculates the midpoint between self and rhs.
The midpoint is the average of, or halfway point between, two vectors.
a.midpoint(b) should yield the same result as a.lerp(b, 0.5)
while being slightly cheaper to compute.
Sourcepub fn abs_diff_eq(self, rhs: Vec3, max_abs_diff: f32) -> bool
pub fn abs_diff_eq(self, rhs: Vec3, max_abs_diff: f32) -> bool
Returns true if the absolute difference of all elements between self and rhs is
less than or equal to max_abs_diff.
This can be used to compare if two vectors contain similar elements. It works best when
comparing with a known value. The max_abs_diff that should be used used depends on
the values being compared against.
For more see comparing floating point numbers.
Sourcepub fn clamp_length(self, min: f32, max: f32) -> Vec3
pub fn clamp_length(self, min: f32, max: f32) -> Vec3
Returns a vector with a length no less than min and no more than max.
§Panics
Will panic if min is greater than max, or if either min or max is negative, when glam_assert is enabled.
Sourcepub fn clamp_length_max(self, max: f32) -> Vec3
pub fn clamp_length_max(self, max: f32) -> Vec3
Returns a vector with a length no more than max.
§Panics
Will panic if max is negative when glam_assert is enabled.
Sourcepub fn clamp_length_min(self, min: f32) -> Vec3
pub fn clamp_length_min(self, min: f32) -> Vec3
Returns a vector with a length no less than min.
§Panics
Will panic if min is negative when glam_assert is enabled.
Sourcepub fn mul_add(self, a: Vec3, b: Vec3) -> Vec3
pub fn mul_add(self, a: Vec3, b: Vec3) -> Vec3
Fused multiply-add. Computes (self * a) + b element-wise with only one rounding
error, yielding a more accurate result than an unfused multiply-add.
Using mul_add may be more performant than an unfused multiply-add if the target
architecture has a dedicated fma CPU instruction. However, this is not always true,
and will be heavily dependant on designing algorithms with specific target hardware in
mind.
Sourcepub fn reflect(self, normal: Vec3) -> Vec3
pub fn reflect(self, normal: Vec3) -> Vec3
Returns the reflection vector for a given incident vector self and surface normal
normal.
normal must be normalized.
§Panics
Will panic if normal is not normalized when glam_assert is enabled.
Sourcepub fn refract(self, normal: Vec3, eta: f32) -> Vec3
pub fn refract(self, normal: Vec3, eta: f32) -> Vec3
Returns the refraction direction for a given incident vector self, surface normal
normal and ratio of indices of refraction, eta. When total internal reflection occurs,
a zero vector will be returned.
self and normal must be normalized.
§Panics
Will panic if self or normal is not normalized when glam_assert is enabled.
Sourcepub fn angle_between(self, rhs: Vec3) -> f32
pub fn angle_between(self, rhs: Vec3) -> f32
Returns the angle (in radians) between two vectors in the range [0, +π].
For the full rotation between two vectors as a quaternion, see
Quat::from_rotation_arc.
The inputs do not need to be unit vectors however they must be non-zero.
§Panics
Will panic if self or rhs has zero length when glam_assert is enabled.
Sourcepub fn angle_to(self, rhs: Vec3, axis: Vec3) -> f32
pub fn angle_to(self, rhs: Vec3, axis: Vec3) -> f32
Returns the signed angle (in radians) from self to rhs around axis
in the range [-π, +π].
The axis must be a unit vector. The angle follows the right-hand rule
around axis and can be used with Self::rotate_axis, e.g.
self.rotate_axis(axis, self.angle_to(rhs, axis)) will be equal to rhs.
For the unsigned angle without a reference axis, see Self::angle_between.
The inputs do not need to be unit vectors however they must be non-zero.
§Panics
Will panic if axis is not normalized when glam_assert is enabled.
Will panic if self or rhs has zero length when glam_assert is enabled.
Sourcepub fn rotate_axis(self, axis: Vec3, angle: f32) -> Vec3
pub fn rotate_axis(self, axis: Vec3, angle: f32) -> Vec3
Rotates around axis by angle (in radians).
The axis must be a unit vector.
§Panics
Will panic if axis is not normalized when glam_assert is enabled.
Sourcepub fn rotate_towards(self, rhs: Vec3, max_angle: f32) -> Vec3
pub fn rotate_towards(self, rhs: Vec3, max_angle: f32) -> Vec3
Rotates towards rhs up to max_angle (in radians).
When max_angle is 0.0, the result will be equal to self. When max_angle is equal to
self.angle_between(rhs), the result will be parallel to rhs. If max_angle is negative,
rotates towards the exact opposite of rhs. Will not go past the target.
Sourcepub fn any_orthogonal_vector(self) -> Vec3
pub fn any_orthogonal_vector(self) -> Vec3
Returns some vector that is orthogonal to the given one.
The input vector must be finite and non-zero.
The output vector is not necessarily unit length. For that use
Self::any_orthonormal_vector() instead.
Sourcepub fn any_orthonormal_vector(self) -> Vec3
pub fn any_orthonormal_vector(self) -> Vec3
Returns any unit vector that is orthogonal to the given one.
The input vector must be unit length.
§Panics
Will panic if self is not normalized when glam_assert is enabled.
Sourcepub fn any_orthonormal_pair(self) -> (Vec3, Vec3)
pub fn any_orthonormal_pair(self) -> (Vec3, Vec3)
Given a unit vector return two other vectors that together form a right-handed orthonormal basis. That is, all three vectors are orthogonal to each other and are normalized.
§Panics
Will panic if self is not normalized when glam_assert is enabled.
Sourcepub fn slerp(self, rhs: Vec3, s: f32) -> Vec3
pub fn slerp(self, rhs: Vec3, s: f32) -> Vec3
Performs a spherical linear interpolation between self and rhs based on the value s.
When s is 0.0, the result will be equal to self. When s is 1.0, the result
will be equal to rhs. When s is outside of range [0, 1], the result is linearly
extrapolated.
Sourcepub fn as_i16vec3(self) -> I16Vec3
pub fn as_i16vec3(self) -> I16Vec3
Casts all elements of self to i16.
Sourcepub fn as_u16vec3(self) -> U16Vec3
pub fn as_u16vec3(self) -> U16Vec3
Casts all elements of self to u16.
Sourcepub fn as_i64vec3(self) -> I64Vec3
pub fn as_i64vec3(self) -> I64Vec3
Casts all elements of self to i64.
Sourcepub fn as_u64vec3(self) -> U64Vec3
pub fn as_u64vec3(self) -> U64Vec3
Casts all elements of self to u64.
Sourcepub fn as_isizevec3(self) -> ISizeVec3
pub fn as_isizevec3(self) -> ISizeVec3
Casts all elements of self to isize.
Sourcepub fn as_usizevec3(self) -> USizeVec3
pub fn as_usizevec3(self) -> USizeVec3
Casts all elements of self to usize.
Trait Implementations§
Source§impl AddAssign for Vec3
impl AddAssign for Vec3
Source§fn add_assign(&mut self, rhs: Vec3)
fn add_assign(&mut self, rhs: Vec3)
+= operation. Read moreSource§impl AddAssign<&Vec3> for Vec3
impl AddAssign<&Vec3> for Vec3
Source§fn add_assign(&mut self, rhs: &Vec3)
fn add_assign(&mut self, rhs: &Vec3)
+= operation. Read moreSource§impl AddAssign<&f32> for Vec3
impl AddAssign<&f32> for Vec3
Source§fn add_assign(&mut self, rhs: &f32)
fn add_assign(&mut self, rhs: &f32)
+= operation. Read moreSource§impl AddAssign<f32> for Vec3
impl AddAssign<f32> for Vec3
Source§fn add_assign(&mut self, rhs: f32)
fn add_assign(&mut self, rhs: f32)
+= operation. Read moreimpl Copy for Vec3
Source§impl DivAssign for Vec3
impl DivAssign for Vec3
Source§fn div_assign(&mut self, rhs: Vec3)
fn div_assign(&mut self, rhs: Vec3)
/= operation. Read moreSource§impl DivAssign<&Vec3> for Vec3
impl DivAssign<&Vec3> for Vec3
Source§fn div_assign(&mut self, rhs: &Vec3)
fn div_assign(&mut self, rhs: &Vec3)
/= operation. Read moreSource§impl DivAssign<&f32> for Vec3
impl DivAssign<&f32> for Vec3
Source§fn div_assign(&mut self, rhs: &f32)
fn div_assign(&mut self, rhs: &f32)
/= operation. Read moreSource§impl DivAssign<f32> for Vec3
impl DivAssign<f32> for Vec3
Source§fn div_assign(&mut self, rhs: f32)
fn div_assign(&mut self, rhs: f32)
/= operation. Read moreSource§impl MulAssign for Vec3
impl MulAssign for Vec3
Source§fn mul_assign(&mut self, rhs: Vec3)
fn mul_assign(&mut self, rhs: Vec3)
*= operation. Read moreSource§impl MulAssign<&Vec3> for Vec3
impl MulAssign<&Vec3> for Vec3
Source§fn mul_assign(&mut self, rhs: &Vec3)
fn mul_assign(&mut self, rhs: &Vec3)
*= operation. Read moreSource§impl MulAssign<&f32> for Vec3
impl MulAssign<&f32> for Vec3
Source§fn mul_assign(&mut self, rhs: &f32)
fn mul_assign(&mut self, rhs: &f32)
*= operation. Read moreSource§impl MulAssign<f32> for Vec3
impl MulAssign<f32> for Vec3
Source§fn mul_assign(&mut self, rhs: f32)
fn mul_assign(&mut self, rhs: f32)
*= operation. Read moreimpl Pod for Vec3
Source§impl RemAssign for Vec3
impl RemAssign for Vec3
Source§fn rem_assign(&mut self, rhs: Vec3)
fn rem_assign(&mut self, rhs: Vec3)
%= operation. Read moreSource§impl RemAssign<&Vec3> for Vec3
impl RemAssign<&Vec3> for Vec3
Source§fn rem_assign(&mut self, rhs: &Vec3)
fn rem_assign(&mut self, rhs: &Vec3)
%= operation. Read moreSource§impl RemAssign<&f32> for Vec3
impl RemAssign<&f32> for Vec3
Source§fn rem_assign(&mut self, rhs: &f32)
fn rem_assign(&mut self, rhs: &f32)
%= operation. Read moreSource§impl RemAssign<f32> for Vec3
impl RemAssign<f32> for Vec3
Source§fn rem_assign(&mut self, rhs: f32)
fn rem_assign(&mut self, rhs: f32)
%= operation. Read moreimpl StructuralPartialEq for Vec3
Source§impl SubAssign for Vec3
impl SubAssign for Vec3
Source§fn sub_assign(&mut self, rhs: Vec3)
fn sub_assign(&mut self, rhs: Vec3)
-= operation. Read moreSource§impl SubAssign<&Vec3> for Vec3
impl SubAssign<&Vec3> for Vec3
Source§fn sub_assign(&mut self, rhs: &Vec3)
fn sub_assign(&mut self, rhs: &Vec3)
-= operation. Read moreSource§impl SubAssign<&f32> for Vec3
impl SubAssign<&f32> for Vec3
Source§fn sub_assign(&mut self, rhs: &f32)
fn sub_assign(&mut self, rhs: &f32)
-= operation. Read moreSource§impl SubAssign<f32> for Vec3
impl SubAssign<f32> for Vec3
Source§fn sub_assign(&mut self, rhs: f32)
fn sub_assign(&mut self, rhs: f32)
-= operation. Read moreSource§impl Vec3Swizzles for Vec3
impl Vec3Swizzles for Vec3
type Vec2 = Vec2
type Vec4 = Vec4
fn xx(self) -> Vec2
fn xy(self) -> Vec2
fn with_xy(self, rhs: Vec2) -> Vec3
fn xz(self) -> Vec2
fn with_xz(self, rhs: Vec2) -> Vec3
fn yx(self) -> Vec2
fn with_yx(self, rhs: Vec2) -> Vec3
fn yy(self) -> Vec2
fn yz(self) -> Vec2
fn with_yz(self, rhs: Vec2) -> Vec3
fn zx(self) -> Vec2
fn with_zx(self, rhs: Vec2) -> Vec3
fn zy(self) -> Vec2
fn with_zy(self, rhs: Vec2) -> Vec3
fn zz(self) -> Vec2
fn xxx(self) -> Vec3
fn xxy(self) -> Vec3
fn xxz(self) -> Vec3
fn xyx(self) -> Vec3
fn xyy(self) -> Vec3
fn xzx(self) -> Vec3
fn xzy(self) -> Vec3
fn xzz(self) -> Vec3
fn yxx(self) -> Vec3
fn yxy(self) -> Vec3
fn yxz(self) -> Vec3
fn yyx(self) -> Vec3
fn yyy(self) -> Vec3
fn yyz(self) -> Vec3
fn yzx(self) -> Vec3
fn yzy(self) -> Vec3
fn yzz(self) -> Vec3
fn zxx(self) -> Vec3
fn zxy(self) -> Vec3
fn zxz(self) -> Vec3
fn zyx(self) -> Vec3
fn zyy(self) -> Vec3
fn zyz(self) -> Vec3
fn zzx(self) -> Vec3
fn zzy(self) -> Vec3
fn zzz(self) -> Vec3
fn xxxx(self) -> Vec4
fn xxxy(self) -> Vec4
fn xxxz(self) -> Vec4
fn xxyx(self) -> Vec4
fn xxyy(self) -> Vec4
fn xxyz(self) -> Vec4
fn xxzx(self) -> Vec4
fn xxzy(self) -> Vec4
fn xxzz(self) -> Vec4
fn xyxx(self) -> Vec4
fn xyxy(self) -> Vec4
fn xyxz(self) -> Vec4
fn xyyx(self) -> Vec4
fn xyyy(self) -> Vec4
fn xyyz(self) -> Vec4
fn xyzx(self) -> Vec4
fn xyzy(self) -> Vec4
fn xyzz(self) -> Vec4
fn xzxx(self) -> Vec4
fn xzxy(self) -> Vec4
fn xzxz(self) -> Vec4
fn xzyx(self) -> Vec4
fn xzyy(self) -> Vec4
fn xzyz(self) -> Vec4
fn xzzx(self) -> Vec4
fn xzzy(self) -> Vec4
fn xzzz(self) -> Vec4
fn yxxx(self) -> Vec4
fn yxxy(self) -> Vec4
fn yxxz(self) -> Vec4
fn yxyx(self) -> Vec4
fn yxyy(self) -> Vec4
fn yxyz(self) -> Vec4
fn yxzx(self) -> Vec4
fn yxzy(self) -> Vec4
fn yxzz(self) -> Vec4
fn yyxx(self) -> Vec4
fn yyxy(self) -> Vec4
fn yyxz(self) -> Vec4
fn yyyx(self) -> Vec4
fn yyyy(self) -> Vec4
fn yyyz(self) -> Vec4
fn yyzx(self) -> Vec4
fn yyzy(self) -> Vec4
fn yyzz(self) -> Vec4
fn yzxx(self) -> Vec4
fn yzxy(self) -> Vec4
fn yzxz(self) -> Vec4
fn yzyx(self) -> Vec4
fn yzyy(self) -> Vec4
fn yzyz(self) -> Vec4
fn yzzx(self) -> Vec4
fn yzzy(self) -> Vec4
fn yzzz(self) -> Vec4
fn zxxx(self) -> Vec4
fn zxxy(self) -> Vec4
fn zxxz(self) -> Vec4
fn zxyx(self) -> Vec4
fn zxyy(self) -> Vec4
fn zxyz(self) -> Vec4
fn zxzx(self) -> Vec4
fn zxzy(self) -> Vec4
fn zxzz(self) -> Vec4
fn zyxx(self) -> Vec4
fn zyxy(self) -> Vec4
fn zyxz(self) -> Vec4
fn zyyx(self) -> Vec4
fn zyyy(self) -> Vec4
fn zyyz(self) -> Vec4
fn zyzx(self) -> Vec4
fn zyzy(self) -> Vec4
fn zyzz(self) -> Vec4
fn zzxx(self) -> Vec4
fn zzxy(self) -> Vec4
fn zzxz(self) -> Vec4
fn zzyx(self) -> Vec4
fn zzyy(self) -> Vec4
fn zzyz(self) -> Vec4
fn zzzx(self) -> Vec4
fn zzzy(self) -> Vec4
fn zzzz(self) -> Vec4
fn xyz(self) -> Self
Auto Trait Implementations§
impl Freeze for Vec3
impl RefUnwindSafe for Vec3
impl Send for Vec3
impl Sync for Vec3
impl Unpin for Vec3
impl UnsafeUnpin for Vec3
impl UnwindSafe for Vec3
Blanket Implementations§
impl<T> AnyBitPattern for Twhere
T: Pod,
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> CheckedBitPattern for Twhere
T: AnyBitPattern,
impl<T> CheckedBitPattern for Twhere
T: AnyBitPattern,
Source§type Bits = T
type Bits = T
Self must have the same layout as the specified Bits except for
the possible invalid bit patterns being checked during
is_valid_bit_pattern.Source§fn is_valid_bit_pattern(_bits: &T) -> bool
fn is_valid_bit_pattern(_bits: &T) -> bool
bits
as &Self.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