pub struct View { /* private fields */ }Expand description
The world’s viewpoint.
Implementations§
Source§impl View
impl View
Sourcepub const fn look_at(eye: Vec3, target: Vec3) -> Self
pub const fn look_at(eye: Vec3, target: Vec3) -> Self
Looks from eye at target, +Y up.
Examples found in repository?
More examples
examples/stress-preview.rs (line 290)
288 fn camera(&self) -> Camera {
289 Camera::new(
290 View::look_at(self.eye, self.eye + self.forward()),
291 Projection::perspective(CAMERA_FOV),
292 )
293 }
294}
295
296struct StressPreview {
297 settings: Settings,
298 applied_instance_count: u32,
299 applied_seed_count: u32,
300 field: Vec<FieldEntry>,
301 frame_times: FrameTimer,
302 /// Set at the instant a pan, a wheel step or a drag first moves the
303 /// camera; from then on the orbit never runs again.
304 player: Option<Player>,
305}
306
307impl StressPreview {
308 fn init(_ctx: &mut InitContext<'_, Self>) -> Result<Self, Error> {
309 let settings = Settings::default();
310 let field = build_field(settings.instance_count, settings.seed_count);
311 Ok(Self {
312 applied_instance_count: settings.instance_count,
313 applied_seed_count: settings.seed_count,
314 settings,
315 field,
316 frame_times: FrameTimer::new(),
317 player: None,
318 })
319 }
320
321 /// Rebuilds the field where the instance count or the seed count
322 /// changed since the last frame.
323 fn apply_settings(&mut self) {
324 if self.settings.instance_count == self.applied_instance_count
325 && self.settings.seed_count == self.applied_seed_count
326 {
327 return;
328 }
329 self.field = build_field(self.settings.instance_count, self.settings.seed_count);
330 self.applied_instance_count = self.settings.instance_count;
331 self.applied_seed_count = self.settings.seed_count;
332 }
333
334 /// The camera's place along the orbit at `elapsed`, before the player
335 /// takes it over.
336 fn orbit_eye(elapsed: f32) -> Vec3 {
337 let angle = elapsed * CAMERA_ANGULAR_SPEED;
338 Vec3::new(
339 angle.cos() * CAMERA_ORBIT_RADIUS,
340 CAMERA_HEIGHT,
341 angle.sin() * CAMERA_ORBIT_RADIUS,
342 )
343 }
344
345 /// The frame's camera: the orbit at `elapsed`, or the player's own
346 /// place once they have taken over.
347 fn camera(&self, elapsed: f32) -> Camera {
348 match &self.player {
349 Some(player) => player.camera(),
350 None => Camera::new(
351 View::look_at(Self::orbit_eye(elapsed), Vec3::ZERO),
352 Projection::perspective(CAMERA_FOV),
353 ),
354 }
355 }examples/sound-lab.rs (lines 453-456)
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}
886
887/// The clip's duration, with two trim handles and a loop marker, each moved
888/// by the pointer's own place rather than by a moving total.
889fn duration_bar(
890 ui: &mut egui::Ui,
891 duration: f32,
892 trim_start: &mut f32,
893 trim_end: &mut f32,
894 loop_from: &mut f32,
895) {
896 let size = egui::vec2(ui.available_width().min(420.0), 28.0);
897 let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
898 let painter = ui.painter();
899 painter.rect_filled(rect, 3.0, egui::Color32::from_gray(35));
900
901 let x_of = |seconds: f32| rect.left() + (seconds / duration).clamp(0.0, 1.0) * rect.width();
902 let seconds_of = |x: f32| ((x - rect.left()) / rect.width()).clamp(0.0, 1.0) * duration;
903
904 let span = egui::Rect::from_min_max(
905 egui::pos2(x_of(*trim_start), rect.top()),
906 egui::pos2(x_of(*trim_end), rect.bottom()),
907 );
908 painter.rect_filled(span, 3.0, egui::Color32::from_rgb(70, 120, 95));
909
910 let start_x = x_of(*trim_start);
911 if let Some(x) = drag_handle(
912 ui,
913 rect,
914 "trim-start",
915 start_x,
916 egui::Color32::from_rgb(230, 200, 80),
917 ) {
918 *trim_start = seconds_of(x).min(*trim_end);
919 }
920 let end_x = x_of(*trim_end);
921 if let Some(x) = drag_handle(
922 ui,
923 rect,
924 "trim-end",
925 end_x,
926 egui::Color32::from_rgb(230, 200, 80),
927 ) {
928 *trim_end = seconds_of(x).max(*trim_start);
929 }
930 let loop_x = x_of(*loop_from);
931 if let Some(x) = drag_handle(
932 ui,
933 rect,
934 "loop-from",
935 loop_x,
936 egui::Color32::from_rgb(90, 170, 230),
937 ) {
938 *loop_from = seconds_of(x).clamp(*trim_start, *trim_end);
939 }
940}
941
942/// One round handle at `x`. Returns the pointer's `x` while a drag holds
943/// it.
944fn drag_handle(
945 ui: &mut egui::Ui,
946 bar: egui::Rect,
947 salt: &str,
948 x: f32,
949 color: egui::Color32,
950) -> Option<f32> {
951 let radius = 6.0;
952 let center = egui::pos2(x, bar.center().y);
953 let sense_rect = egui::Rect::from_center_size(center, egui::Vec2::splat(radius * 2.5));
954 let id = ui.id().with(salt);
955 let response = ui.interact(sense_rect, id, egui::Sense::drag());
956 ui.painter().circle_filled(center, radius, color);
957
958 response
959 .dragged()
960 .then(|| response.interact_pointer_pos())
961 .flatten()
962 .map(|pos| pos.x)
963}
964
965impl Game for SoundCheck {
966 type Meshes = Shape;
967 type Sounds = Sound;
968 type InputActions = Controls;
969 type Skyboxes = Sky;
970 type SurfaceStyles = NoSurfaceStyles;
971 type PostEffects = NoPostEffects;
972
973 fn tick(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
974 self.handle_walk(ctx);
975 self.handle_drag(ctx);
976 }
977
978 fn frame(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) {
979 ctx.set_volume(self.master_volume);
980
981 let player = self.player_prev.lerp(self.player, ctx.alpha());
982 let ear = Vec3::new(player.x, EYE_HEIGHT, player.y);
983 let listener = View::look_at(ear, ear + Vec3::NEG_Z);
984 ctx.set_listener(listener);
985
986 ctx.set_camera(Self::camera(player));
987 ctx.set_skybox(Sky::Room);
988 ctx.set_bloom(0.2);
989 ctx.light(Light::directional(Vec3::new(-0.4, -1.0, -0.5), SUN_COLOR).shadow());
990
991 self.draw_room(ctx);
992 self.draw_sources(ctx);
993 self.draw_listener(ctx, listener);
994 self.draw_merge_markers(ctx);
995 self.draw_ring(ctx);
996
997 self.sustain_cues(ctx);
998 for (index, source) in self.sources.iter().enumerate() {
999 if source.enabled {
1000 ctx.sustain(source.cue().instance(index as u32));
1001 }
1002 }
1003 if self.merge_demo {
1004 ctx.sustain(Sound::Click.at(MERGE_POS_A).gain(MERGE_GAIN));
1005 ctx.sustain(Sound::Click.at(MERGE_POS_B).gain(MERGE_GAIN));
1006 }
1007 self.sustain_ring(ctx);
1008
1009 self.side_panel(ctx);
1010 let (play_once, play_many) = self.one_shot_panel(ctx);
1011
1012 if play_once {
1013 ctx.play(self.one_shot_cue());
1014 }
1015 if play_many {
1016 for _ in 0..32 {
1017 ctx.play(self.one_shot_cue());
1018 }
1019 }
1020 }Additional examples can be found in:
Sourcepub const fn eye(&self) -> Vec3
pub const fn eye(&self) -> Vec3
Camera position.
Examples found in repository?
examples/sound-lab.rs (line 586)
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}Sourcepub fn direction(&self) -> Vec3
pub fn direction(&self) -> Vec3
The direction from the View::eye to the View::target, one
meter long; zero where the two are one point.
Trait Implementations§
impl Copy for View
impl StructuralPartialEq for View
Auto Trait Implementations§
impl Freeze for View
impl RefUnwindSafe for View
impl Send for View
impl Sync for View
impl Unpin for View
impl UnsafeUnpin for View
impl UnwindSafe for View
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> 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> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read 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> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more