ranim_core/core_item/
camera_frame.rs

1// MARK: CameraFrame
2
3use glam::{DMat4, DVec3, dvec2};
4
5use crate::{
6    Extract,
7    animation::{AnimationCell, Eval},
8    core_item::CoreItem,
9    prelude::{Alignable, Interpolatable},
10};
11
12/// The data of a camera
13///
14/// The [`CameraFrame`] has a [`CameraFrame::perspective_blend`] property (default is `0.0`),
15/// which is used to blend between orthographic and perspective projection.
16#[derive(Clone, Debug, PartialEq)]
17pub struct CameraFrame {
18    /// The position
19    pub pos: DVec3,
20    /// The up unit vec
21    pub up: DVec3,
22    /// The facing unit vec
23    pub facing: DVec3,
24
25    // far > near
26    /// The near pane
27    pub near: f64,
28    /// The far pane
29    pub far: f64,
30    /// The perspective blend value in [0.0, 1.0]
31    pub perspective_blend: f64,
32
33    /// **Ortho**: Top - Bottom
34    pub frame_height: f64,
35    /// **Ortho**: The scaling factor
36    pub scale: f64,
37
38    /// **Perspective**: The field of view angle, used in perspective projection
39    pub fovy: f64,
40}
41
42impl Extract for CameraFrame {
43    type Target = CoreItem;
44    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
45        buf.push(CoreItem::CameraFrame(self.clone()));
46    }
47}
48
49impl Interpolatable for CameraFrame {
50    fn lerp(&self, target: &Self, t: f64) -> Self {
51        Self {
52            pos: self.pos.lerp(target.pos, t),
53            up: self.up.lerp(target.up, t),
54            facing: self.facing.lerp(target.facing, t),
55            scale: self.scale.lerp(&target.scale, t),
56            fovy: self.fovy.lerp(&target.fovy, t),
57            near: self.near.lerp(&target.near, t),
58            far: self.far.lerp(&target.far, t),
59            frame_height: self.frame_height.lerp(&target.frame_height, t),
60            perspective_blend: self
61                .perspective_blend
62                .lerp(&target.perspective_blend, t)
63                .clamp(0.0, 1.0),
64        }
65    }
66}
67
68impl Alignable for CameraFrame {
69    fn is_aligned(&self, _other: &Self) -> bool {
70        true
71    }
72    fn align_with(&mut self, _other: &mut Self) {}
73}
74
75impl Default for CameraFrame {
76    fn default() -> Self {
77        Self {
78            pos: DVec3::ZERO,
79            up: DVec3::Y,
80            facing: DVec3::NEG_Z,
81
82            near: -1000.0,
83            far: 1000.0,
84            perspective_blend: 0.0,
85
86            scale: 1.0,
87            frame_height: 8.0,
88
89            fovy: std::f64::consts::PI / 2.0,
90        }
91    }
92}
93
94impl CameraFrame {
95    /// Create a new CameraFrame at the origin facing to the negative z-axis and use Y as up vector with default projection settings.
96    pub fn new() -> Self {
97        Self::default()
98    }
99}
100
101impl CameraFrame {
102    /// Set the view matrix of the camera.
103    pub fn set_view_matrix(&mut self, view_matrix: DMat4) {
104        let inv = view_matrix.inverse();
105        self.pos = inv.transform_point3(DVec3::ZERO);
106        self.up = inv.transform_vector3(DVec3::Y).normalize();
107        self.facing = inv.transform_vector3(DVec3::NEG_Z).normalize();
108    }
109
110    /// Set the view matrix of the camera and return the modified `Self`.
111    pub fn with_view_matrix(mut self, view_matrix: DMat4) -> Self {
112        self.set_view_matrix(view_matrix);
113        self
114    }
115
116    /// The view matrix of the camera
117    pub fn view_matrix(&self) -> DMat4 {
118        DMat4::look_to_rh(self.pos, self.facing, self.up)
119    }
120
121    /// Use the given frame size as `left`, `right`, `bottom`, `top` to construct an orthographic matrix
122    pub fn orthographic_mat(&self, aspect_ratio: f64) -> DMat4 {
123        let frame_size = dvec2(self.frame_height * aspect_ratio, self.frame_height);
124        let frame_size = frame_size * self.scale;
125        DMat4::orthographic_rh(
126            -frame_size.x / 2.0,
127            frame_size.x / 2.0,
128            -frame_size.y / 2.0,
129            frame_size.y / 2.0,
130            self.near,
131            self.far,
132        )
133    }
134
135    /// Use the given frame aspect ratio to construct a perspective matrix
136    pub fn perspective_mat(&self, aspect_ratio: f64) -> DMat4 {
137        let near = self.near.max(0.1);
138        let far = self.far.max(near);
139        DMat4::perspective_rh(self.fovy, aspect_ratio, near, far)
140    }
141
142    /// Use the given frame size to construct projection matrix
143    pub fn projection_matrix(&self, aspect_ratio: f64) -> DMat4 {
144        self.orthographic_mat(aspect_ratio)
145            .lerp(&self.perspective_mat(aspect_ratio), self.perspective_blend)
146    }
147
148    /// Use the given frame size to construct view projection matrix
149    pub fn view_projection_matrix(&self, aspect_ratio: f64) -> DMat4 {
150        self.projection_matrix(aspect_ratio) * self.view_matrix()
151    }
152}
153
154impl CameraFrame {
155    /// Create a perspective camera positioned using spherical coordinates (Z-up), looking at the origin.
156    ///
157    /// - `phi`: polar angle from +Z axis in radians (0 = straight up along +Z, π/2 = XY plane)
158    /// - `theta`: azimuth angle in radians (0 = +X direction, π/2 = +Y direction)
159    /// - `distance`: distance from the origin
160    pub fn from_spherical(phi: f64, theta: f64, distance: f64) -> Self {
161        let mut cam = Self {
162            perspective_blend: 1.0,
163            up: DVec3::Z,
164            ..Self::default()
165        };
166        cam.set_spherical(phi, theta, distance, DVec3::ZERO);
167        cam
168    }
169
170    /// Position the camera using spherical coordinates (Z-up) around a target point.
171    ///
172    /// - `phi`: polar angle from +Z axis in radians (0 = straight up along +Z, π/2 = XY plane)
173    /// - `theta`: azimuth angle in radians (0 = +X direction, π/2 = +Y direction)
174    /// - `distance`: distance from `target`
175    /// - `target`: the point the camera looks at
176    pub fn set_spherical(
177        &mut self,
178        phi: f64,
179        theta: f64,
180        distance: f64,
181        target: DVec3,
182    ) -> &mut Self {
183        self.pos = target
184            + DVec3::new(
185                distance * phi.sin() * theta.cos(),
186                distance * phi.sin() * theta.sin(),
187                distance * phi.cos(),
188            );
189        self.facing = (target - self.pos).normalize();
190        self.up = DVec3::Z;
191        self
192    }
193
194    /// Set the camera to look at a target point.
195    pub fn look_at(&mut self, target: DVec3) -> &mut Self {
196        self.facing = (target - self.pos).normalize();
197        self
198    }
199
200    /// Create an orbit animation that rotates the camera around `target`
201    /// by `total_angle` radians in the XY plane (Z-up).
202    ///
203    /// The camera's current position is used to derive the spherical
204    /// coordinates (distance, elevation) which are kept constant during the orbit.
205    ///
206    /// # Example
207    /// ```ignore
208    /// use std::f64::consts::TAU;
209    ///
210    /// let mut cam = CameraFrame::from_spherical(phi, theta, distance);
211    /// let r_cam = r.insert(cam.clone());
212    /// r.timeline_mut(r_cam).play(
213    ///     cam.orbit(DVec3::ZERO, TAU)
214    ///        .with_duration(8.0)
215    ///        .with_rate_func(linear),
216    /// );
217    /// ```
218    pub fn orbit(&mut self, target: DVec3, total_angle: f64) -> AnimationCell<Self> {
219        let offset = self.pos - target;
220        let distance = offset.length();
221        let phi = if distance > 0.0 {
222            (offset.z / distance).acos()
223        } else {
224            0.0
225        };
226        let theta0 = offset.y.atan2(offset.x);
227        let src = self.clone();
228
229        struct Orbit {
230            src: CameraFrame,
231            target: DVec3,
232            distance: f64,
233            phi: f64,
234            theta0: f64,
235            total_angle: f64,
236        }
237
238        impl Eval<CameraFrame> for Orbit {
239            fn eval_alpha(&self, alpha: f64) -> CameraFrame {
240                let theta = self.theta0 + self.total_angle * alpha;
241                let mut result = self.src.clone();
242                result.set_spherical(self.phi, theta, self.distance, self.target);
243                result
244            }
245        }
246
247        Orbit {
248            src,
249            target,
250            distance,
251            phi,
252            theta0,
253            total_angle,
254        }
255        .into_animation_cell()
256        .apply_to(self)
257    }
258}
259
260impl CameraFrame {
261    /// Center the canvas in the frame when [`CameraFrame::perspective_blend`] is `1.0`
262    pub fn center_canvas_in_frame(
263        &mut self,
264        center: DVec3,
265        width: f64,
266        height: f64,
267        up: DVec3,
268        normal: DVec3,
269        aspect_ratio: f64,
270    ) -> &mut Self {
271        let canvas_ratio = height / width;
272        let up = up.normalize();
273        let normal = normal.normalize();
274
275        let height = if aspect_ratio > canvas_ratio {
276            height
277        } else {
278            width / aspect_ratio
279        };
280
281        let distance = height * 0.5 / (0.5 * self.fovy).tan();
282
283        self.up = up;
284        self.pos = center + normal * distance;
285        self.facing = -normal;
286        self
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use glam::dvec3;
294
295    #[test]
296    fn test_set_view_matrix_default() {
297        let camera = CameraFrame::new();
298        let view_matrix = camera.view_matrix();
299
300        let mut new_camera = CameraFrame::new();
301        new_camera.set_view_matrix(view_matrix);
302
303        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
304        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
305        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
306    }
307
308    #[test]
309    fn test_set_view_matrix_translated() {
310        let mut camera = CameraFrame::new();
311        camera.pos = dvec3(5.0, 3.0, -2.0);
312        let view_matrix = camera.view_matrix();
313
314        let mut new_camera = CameraFrame::new();
315        new_camera.set_view_matrix(view_matrix);
316
317        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
318        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
319        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
320    }
321
322    #[test]
323    fn test_set_view_matrix_rotated() {
324        let mut camera = CameraFrame::new();
325        camera.facing = dvec3(1.0, 0.0, 0.0);
326        camera.up = dvec3(0.0, 1.0, 0.0);
327        let view_matrix = camera.view_matrix();
328
329        let mut new_camera = CameraFrame::new();
330        new_camera.set_view_matrix(view_matrix);
331
332        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
333        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
334        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
335    }
336
337    #[test]
338    fn test_set_view_matrix_complex() {
339        let mut camera = CameraFrame::new();
340        camera.pos = dvec3(10.0, 5.0, 3.0);
341        camera.facing = dvec3(1.0, 0.0, 1.0).normalize();
342        camera.up = dvec3(0.0, 1.0, 0.0);
343        let view_matrix = camera.view_matrix();
344
345        let mut new_camera = CameraFrame::new();
346        new_camera.set_view_matrix(view_matrix);
347
348        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
349        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
350        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
351    }
352}