Skip to main content

loam_app/session/
camera.rs

1use loam_math::{EuclideanR3, Space};
2use loam_runtime::{Ctx, Eye, Input, Phase, PointerButton, Session, Stores, Views};
3
4type Vec3 = <EuclideanR3 as Space>::Vector;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct FreeCamera {
8    pub eye: Eye,
9}
10
11impl FreeCamera {
12    pub fn look(&mut self, delta: [f32; 2]) {
13        let forward = Vec3::from(self.eye.forward);
14        let yaw = (-forward.x).atan2(-forward.z) - delta[0] * 0.002;
15        let pitch = (forward.y.clamp(-1.0, 1.0).asin() + delta[1] * 0.002).clamp(-1.5, 1.5);
16        let (yaw_sin, yaw_cos) = yaw.sin_cos();
17        let (pitch_sin, pitch_cos) = pitch.sin_cos();
18        let forward = Vec3::new(-yaw_sin * pitch_cos, pitch_sin, -yaw_cos * pitch_cos);
19        let right = Vec3::new(yaw_cos, 0.0, -yaw_sin);
20        self.eye.forward = forward.to_array();
21        self.eye.right = right.to_array();
22        self.eye.up = right.cross(forward).to_array();
23    }
24
25    pub fn travel(&mut self, axes: [f32; 3], distance: f32) {
26        let direction = Vec3::from(self.eye.right) * axes[0]
27            + Vec3::Y * axes[1]
28            + Vec3::from(self.eye.forward) * axes[2];
29        if let Some(direction) = direction.try_normalize() {
30            self.eye.position = (Vec3::from(self.eye.position) + direction * distance).to_array();
31        }
32    }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct Orbit {
37    pub target: [f32; 3],
38    pub yaw: f32,
39    pub pitch: f32,
40    pub distance: f32,
41}
42
43impl Default for Orbit {
44    fn default() -> Self {
45        Self::around([0.0; 3], DEFAULT_DISTANCE)
46    }
47}
48
49impl Orbit {
50    pub fn around(target: [f32; 3], distance: f32) -> Self {
51        Self {
52            target,
53            yaw: 0.0,
54            pitch: 0.0,
55            distance,
56        }
57    }
58
59    pub fn apply(&mut self, input: &Input) {
60        self.drag(input.drag(PointerButton::Secondary));
61        self.zoom(input.scroll[1]);
62    }
63
64    pub fn drag(&mut self, delta: [f32; 2]) {
65        self.yaw -= delta[0] * ORBIT_GAIN;
66        self.pitch = (self.pitch + delta[1] * ORBIT_GAIN).clamp(-PITCH_LIMIT, PITCH_LIMIT);
67    }
68
69    pub fn zoom(&mut self, lines: f32) {
70        self.distance =
71            (self.distance * (-lines * ZOOM_GAIN).exp()).clamp(MIN_DISTANCE, MAX_DISTANCE);
72    }
73
74    pub fn eye(&self) -> Eye {
75        let (yaw_sin, yaw_cos) = self.yaw.sin_cos();
76        let (pitch_sin, pitch_cos) = self.pitch.sin_cos();
77        let offset = Vec3::new(yaw_sin * pitch_cos, -pitch_sin, yaw_cos * pitch_cos);
78        Eye::looking_at(
79            (Vec3::from(self.target) + offset * self.distance).to_array(),
80            self.target,
81            [0.0, 1.0, 0.0],
82        )
83    }
84}
85
86const DEFAULT_DISTANCE: f32 = 6.0;
87const ORBIT_GAIN: f32 = 0.006;
88const PITCH_LIMIT: f32 = 1.5;
89const ZOOM_GAIN: f32 = 0.12;
90const MIN_DISTANCE: f32 = 1.5;
91const MAX_DISTANCE: f32 = 20.0;
92
93pub fn look(views: &mut Views, eye: Eye) {
94    let root = views.root_mut();
95    root.eye = Eye {
96        aspect: root.eye.aspect,
97        ..eye
98    };
99}
100
101pub fn orbit<A: Stores>(
102    session: &mut Session<A>,
103    camera: impl Fn(&mut A) -> &mut Orbit + Send + 'static,
104) {
105    session.system(Phase::Dispatch, "orbit", move |ctx: Ctx<'_, A>| {
106        let orbit = camera(ctx.app);
107        orbit.apply(ctx.input);
108        let eye = orbit.eye();
109        look(ctx.views, eye);
110        Ok(())
111    });
112}
113
114#[cfg(test)]
115mod tests {
116    use std::f32::consts::FRAC_PI_2;
117
118    use super::*;
119
120    loam_runtime::stores! {
121        #[derive(Default)]
122        pub struct Viewer {
123            camera: Value<Orbit>,
124        }
125    }
126
127    #[test]
128    fn a_reset_brings_the_orbit_camera_back_with_the_scene() {
129        use loam_runtime::{Input, Pointer, PointerButton, PointerPhase, Session, SimConfig};
130
131        let mut session = Session::new(Viewer::default(), SimConfig::default());
132        session.app.camera.set(Orbit::around([0.0; 3], 5.0));
133        orbit(&mut session, |app| app.camera.get_mut());
134        session.set_initial().unwrap();
135        session.boundary(Input::default()).unwrap();
136        let root = session.views().root();
137        let boot = session.views().get(root).unwrap().eye.position;
138
139        let drag = Input {
140            pointers: vec![Pointer {
141                id: 0,
142                button: Some(PointerButton::Secondary),
143                ndc: [0.0; 2],
144                delta: [40.0, 0.0],
145                phase: PointerPhase::Moved,
146                time: 0.0,
147            }],
148            ..Input::default()
149        };
150        session.boundary(drag).unwrap();
151        let turned = session.views().get(root).unwrap().eye.position;
152        assert_ne!(turned, boot, "a secondary drag left the orbit where it was");
153
154        session.reset().unwrap();
155        session.boundary(Input::default()).unwrap();
156        assert_eq!(
157            session.views().get(root).unwrap().eye.position,
158            boot,
159            "the reset did not bring the camera back"
160        );
161    }
162
163    #[test]
164    fn freecam_keeps_the_activation_heading_and_does_not_accelerate_diagonally() {
165        let mut camera = FreeCamera {
166            eye: Eye::looking_at([2.0, 1.0, 3.0], [1.0, 1.5, 2.0], [0.0, 1.0, 0.0]),
167        };
168        let before = camera.eye;
169        camera.look([0.0; 2]);
170        assert!((Vec3::from(camera.eye.forward) - Vec3::from(before.forward)).length() < 1e-6);
171        camera.look([0.1, 0.1]);
172        assert!(camera.eye.forward[0] > before.forward[0]);
173        assert!(camera.eye.forward[1] > before.forward[1]);
174        camera.travel([1.0, 0.0, 1.0], 2.0);
175        assert!(
176            ((Vec3::from(camera.eye.position) - Vec3::from(before.position)).length() - 2.0).abs()
177                < 1e-6
178        );
179    }
180
181    #[test]
182    fn looking_at_mirrors_the_eye_basis_or_the_orbit_sits_on_the_wrong_side_of_its_target() {
183        let close = |got: [f32; 3], want: [f32; 3]| {
184            assert!(
185                got.iter().zip(want).all(|(g, w)| (g - w).abs() <= 1e-6),
186                "{got:?} is not {want:?}"
187            );
188        };
189
190        let eye = Eye::looking_at([0.0, 0.0, 5.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
191        close(eye.forward, [0.0, 0.0, -1.0]);
192        close(eye.right, [1.0, 0.0, 0.0]);
193        close(eye.up, [0.0, 1.0, 0.0]);
194
195        let orbit = Orbit {
196            target: [1.0, 2.0, 3.0],
197            yaw: FRAC_PI_2,
198            pitch: 0.0,
199            distance: 4.0,
200        };
201        let turned = orbit.eye();
202        close(turned.position, [5.0, 2.0, 3.0]);
203        close(turned.forward, [-1.0, 0.0, 0.0]);
204        close(turned.right, [0.0, 0.0, -1.0]);
205        close(turned.up, [0.0, 1.0, 0.0]);
206
207        let pitched = Orbit {
208            target: [1.0, 2.0, 3.0],
209            yaw: 0.0,
210            pitch: -FRAC_PI_2,
211            distance: 4.0,
212        }
213        .eye();
214        close(pitched.position, [1.0, 6.0, 3.0]);
215    }
216
217    #[test]
218    fn orbit_zoom_runs_past_its_distance_limits() {
219        let mut orbit = Orbit::around([0.0; 3], 5.0);
220        orbit.zoom(100.0);
221        assert_eq!(orbit.distance, 1.5);
222        orbit.zoom(-100.0);
223        assert_eq!(orbit.distance, 20.0);
224    }
225}