Skip to main content

ranim_core/core_item/
camera_frame.rs

1// MARK: CameraFrame
2
3use glam::{DMat4, DVec3, dvec2};
4
5use crate::{
6    Extract,
7    animation::{Eval, Placeable},
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(bevy_ecs::component::Component, 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        glam::dcamera::rh::view::look_to_mat4(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        glam::dcamera::rh::proj::directx::orthographic(
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        glam::dcamera::rh::proj::directx::perspective(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    /// r.play(
212    ///     cam.orbit(DVec3::ZERO, TAU)
213    ///         .with_duration(8.0)
214    ///         .with_rate_func(linear),
215    /// );
216    /// ```
217    pub fn orbit(
218        &mut self,
219        target: DVec3,
220        total_angle: f64,
221    ) -> impl Eval<Output = Self> + Placeable + use<> {
222        let offset = self.pos - target;
223        let distance = offset.length();
224        let phi = if distance > 0.0 {
225            (offset.z / distance).acos()
226        } else {
227            0.0
228        };
229        let theta0 = offset.y.atan2(offset.x);
230        let src = self.clone();
231
232        struct Orbit {
233            src: CameraFrame,
234            target: DVec3,
235            distance: f64,
236            phi: f64,
237            theta0: f64,
238            total_angle: f64,
239        }
240
241        impl Eval for Orbit {
242            type Output = CameraFrame;
243
244            fn eval_alpha(&self, alpha: f64) -> Self::Output {
245                let theta = self.theta0 + self.total_angle * alpha;
246                let mut result = self.src.clone();
247                result.set_spherical(self.phi, theta, self.distance, self.target);
248                result
249            }
250        }
251
252        Orbit {
253            src,
254            target,
255            distance,
256            phi,
257            theta0,
258            total_angle,
259        }
260        .apply_to(self)
261    }
262}
263
264impl CameraFrame {
265    /// Center the canvas in the frame when [`CameraFrame::perspective_blend`] is `1.0`
266    pub fn center_canvas_in_frame(
267        &mut self,
268        center: DVec3,
269        width: f64,
270        height: f64,
271        up: DVec3,
272        normal: DVec3,
273        aspect_ratio: f64,
274    ) -> &mut Self {
275        let canvas_ratio = height / width;
276        let up = up.normalize();
277        let normal = normal.normalize();
278
279        let height = if aspect_ratio > canvas_ratio {
280            height
281        } else {
282            width / aspect_ratio
283        };
284
285        let distance = height * 0.5 / (0.5 * self.fovy).tan();
286
287        self.up = up;
288        self.pos = center + normal * distance;
289        self.facing = -normal;
290        self
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use glam::dvec3;
298
299    #[test]
300    fn test_set_view_matrix_default() {
301        let camera = CameraFrame::new();
302        let view_matrix = camera.view_matrix();
303
304        let mut new_camera = CameraFrame::new();
305        new_camera.set_view_matrix(view_matrix);
306
307        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
308        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
309        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
310    }
311
312    #[test]
313    fn test_set_view_matrix_translated() {
314        let mut camera = CameraFrame::new();
315        camera.pos = dvec3(5.0, 3.0, -2.0);
316        let view_matrix = camera.view_matrix();
317
318        let mut new_camera = CameraFrame::new();
319        new_camera.set_view_matrix(view_matrix);
320
321        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
322        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
323        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
324    }
325
326    #[test]
327    fn test_set_view_matrix_rotated() {
328        let mut camera = CameraFrame::new();
329        camera.facing = dvec3(1.0, 0.0, 0.0);
330        camera.up = dvec3(0.0, 1.0, 0.0);
331        let view_matrix = camera.view_matrix();
332
333        let mut new_camera = CameraFrame::new();
334        new_camera.set_view_matrix(view_matrix);
335
336        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
337        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
338        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
339    }
340
341    #[test]
342    fn test_set_view_matrix_complex() {
343        let mut camera = CameraFrame::new();
344        camera.pos = dvec3(10.0, 5.0, 3.0);
345        camera.facing = dvec3(1.0, 0.0, 1.0).normalize();
346        camera.up = dvec3(0.0, 1.0, 0.0);
347        let view_matrix = camera.view_matrix();
348
349        let mut new_camera = CameraFrame::new();
350        new_camera.set_view_matrix(view_matrix);
351
352        assert!(new_camera.pos.distance(camera.pos) < 1e-10);
353        assert!(new_camera.up.angle_between(camera.up) < 1e-10);
354        assert!(new_camera.facing.angle_between(camera.facing) < 1e-10);
355    }
356}