Skip to main content

View

Struct View 

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

The world’s viewpoint.

Implementations§

Source§

impl View

Source

pub const fn look_at(eye: Vec3, target: Vec3) -> Self

Looks from eye at target, +Y up.

Examples found in repository?
examples/material-playground.rs (line 789)
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    }
More examples
Hide additional 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/breakout-game.rs (line 584)
582    fn camera() -> Camera {
583        Camera::new(
584            View::look_at(Vec3::new(0.0, 13.5, 12.5), Vec3::new(0.0, 0.0, 0.5)),
585            Projection::perspective(50.0),
586        )
587    }
examples/isometric-board.rs (line 385)
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    }
examples/sprite-adventure.rs (line 1038)
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    }
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    }
Source

pub const fn with_up(self, up: Vec3) -> Self

Sets the view’s up direction; +Y by default.

Source

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}
Source

pub const fn target(&self) -> Vec3

The camera’s target.

Examples found in repository?
examples/sound-lab.rs (line 882)
881fn listener_right(view: View) -> Vec3 {
882    (view.target() - view.eye())
883        .normalize_or_zero()
884        .cross(view.up())
885}
Source

pub const fn up(&self) -> Vec3

Up direction on the screen.

Examples found in repository?
examples/sound-lab.rs (line 884)
881fn listener_right(view: View) -> Vec3 {
882    (view.target() - view.eye())
883        .normalize_or_zero()
884        .cross(view.up())
885}
Source

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§

Source§

impl Clone for View

Source§

fn clone(&self) -> Self

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

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

Performs copy-assignment from source. Read more
Source§

impl Copy for View

Source§

impl Debug for View

Source§

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

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

impl PartialEq for View

Source§

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

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

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

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for 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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

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

Source§

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

Source§

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

Source§

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

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

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

Source§

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

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

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

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

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

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

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

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

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

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

Source§

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

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

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

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

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

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

fn in_current_span(self) -> Instrumented<Self>

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

impl<T> IntoEither for T

Source§

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

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

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

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

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

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

Initializes a with the given initializer. Read more
Source§

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

Dereferences the given pointer. Read more
Source§

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

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

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

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

Source§

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

Source§

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

Source§

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

Source§

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

Source§

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

Source§

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

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

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

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

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

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

Source§

fn to_sample_(self) -> U

Source§

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

Source§

type Error = !

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

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

Source§

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

Source§

impl<T> WasmNotSendSync for T

Source§

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

Source§

impl<T> WithSubscriber for T

Source§

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

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

fn with_current_subscriber(self) -> WithDispatch<Self>

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