rpt/
camera.rs

1use rand::{rngs::StdRng, Rng};
2use rand_distr::UnitDisc;
3
4use crate::shape::Ray;
5
6/// A simple thin-lens perspective camera
7#[derive(Copy, Clone, Debug)]
8pub struct Camera {
9    /// Location of the camera
10    pub eye: glm::DVec3,
11
12    /// Direction that the camera is facing
13    pub direction: glm::DVec3,
14
15    /// Direction of "up" for screen, must be orthogonal to `direction`
16    pub up: glm::DVec3,
17
18    /// Field of view in the longer direction as an angle in radians, in (0, pi)
19    pub fov: f64,
20
21    /// Aperture radius for depth-of-field effects
22    pub aperture: f64,
23
24    /// Focal distance, if aperture radius is nonzero
25    pub focal_distance: f64,
26}
27
28impl Default for Camera {
29    fn default() -> Self {
30        Self {
31            eye: glm::vec3(0.0, 0.0, 10.0),
32            direction: glm::vec3(0.0, 0.0, -1.0),
33            up: glm::vec3(0.0, 1.0, 0.0), // we live in a y-up world...
34            fov: std::f64::consts::FRAC_PI_6,
35            aperture: 0.0,
36            focal_distance: 0.0,
37        }
38    }
39}
40
41impl Camera {
42    /// Perspective camera looking at a point, with a given field of view
43    pub fn look_at(eye: glm::DVec3, center: glm::DVec3, up: glm::DVec3, fov: f64) -> Self {
44        let direction = (center - eye).normalize();
45        let up = (up - up.dot(&direction) * direction).normalize();
46        Self {
47            eye,
48            direction,
49            up,
50            fov,
51            aperture: 0.0,
52            focal_distance: 0.0,
53        }
54    }
55
56    /// Focus the camera on a position, with simulated depth-of-field
57    pub fn focus(mut self, focal_point: glm::DVec3, aperture: f64) -> Self {
58        self.focal_distance = (focal_point - self.eye).dot(&self.direction);
59        self.aperture = aperture;
60        self
61    }
62
63    /// Cast a ray, where (x, y) are normalized to the standard [-1, 1] box
64    pub fn cast_ray(&self, x: f64, y: f64, rng: &mut StdRng) -> Ray {
65        // cot(f / 2) = depth / radius
66        let d = (self.fov / 2.0).tan().recip();
67        let right = glm::cross(&self.direction, &self.up).normalize();
68        let mut origin = self.eye;
69        let mut new_dir = d * self.direction + x * right + y * self.up;
70        if self.aperture > 0.0 {
71            // Depth of field
72            let focal_point = origin + new_dir.normalize() * self.focal_distance;
73            let [x, y]: [f64; 2] = rng.sample(UnitDisc);
74            origin += (x * right + y * self.up) * self.aperture;
75            new_dir = focal_point - origin;
76        }
77        Ray {
78            origin,
79            dir: new_dir.normalize(),
80        }
81    }
82}