lumo/tracer/
camera.rs

1use crate::{Point, Direction, Float, Vec2, rand_utils};
2use glam::IVec2;
3use crate::tracer::{
4    film::FilmSample, ray::Ray,
5    onb::Onb, Color
6};
7
8/// Common configuration for cameras
9pub struct CameraConfig {
10    /// Camera position in world space
11    pub origin: Point,
12    /// Basis of camera space
13    pub camera_basis: Onb,
14    /// Image resolution
15    pub resolution: IVec2,
16    /// Focal length i.e. distance to focal point behind camera
17    pub focal_length: Float,
18    /// Radius of the camera lens
19    pub lens_radius: Float,
20}
21
22impl CameraConfig {
23    /// Creates a new config with the given arguments
24    pub fn new(
25        origin: Point,
26        towards: Point,
27        up: Direction,
28        lens_radius: Float,
29        focal_length: Float,
30        resolution: (i32, i32)
31    ) -> Self {
32        assert!(resolution.0 > 0 && resolution.1 > 0);
33        assert!(lens_radius >= 0.0);
34        assert!(origin != towards);
35        assert!(up.length() != 0.0);
36
37        let forward = (towards - origin).normalize();
38        let right = forward.cross(up).normalize();
39        let down = forward.cross(right);
40
41        // x = right, y = down, z = towards
42        let camera_basis = Onb::new_from_basis(right, down, forward);
43        let (width, height) = resolution;
44
45        Self {
46            lens_radius,
47            focal_length,
48            origin,
49            camera_basis,
50            resolution: IVec2::new(width, height),
51        }
52    }
53}
54
55/// Camera abstraction
56pub enum Camera {
57    /// Perspective camera with configurable vertical field-of-view
58    Perspective(CameraConfig, Float),
59    /// Orthographic camera that preserves angles with configurable image plane scale
60    Orthographic(CameraConfig, Float),
61}
62
63impl Camera {
64    /// Orthographic camera that preserves angles. All rays are cast in the same
65    /// direction but from a plane instead of a single point
66    ///
67    /// # Arguments
68    /// * `origin` - Camera position in world space
69    /// * `towards` - Point in world space the camera is looking at
70    /// * `up` - Up direction of the camera
71    /// * `image_plane_scale` - Scale of the plane rays are cast from
72    /// * `lens_radius` - Radius of the lens for depth of field. Bigger means more profound effect
73    /// * `focal_length` - Distance to the plane of focus for depth of field
74    /// * `width` - Width of the rendered image
75    /// * `height` - Height of the rendered image
76    #[allow(clippy::too_many_arguments)]
77    pub fn orthographic(
78        origin: Point,
79        towards: Point,
80        up: Direction,
81        image_plane_scale: Float,
82        lens_radius: Float,
83        focal_length: Float,
84        width: i32,
85        height: i32,
86    ) -> Self {
87        assert!(image_plane_scale > 0.0);
88
89        Self::Orthographic(
90            CameraConfig::new(
91                origin,
92                towards,
93                up,
94                lens_radius,
95                focal_length,
96                (width, height)
97            ),
98            image_plane_scale,
99        )
100    }
101
102    /// Perspective camera where sense of depth is more profound. Rays are cast
103    /// from a single point towards points on the image plane.
104    ///
105    /// # Arguments
106    /// * `origin` - Camera position in world space
107    /// * `towards` - Point in world space the camera is looking at
108    /// * `up` - Up direction of the camera
109    /// * `vfov` - Vertical field of view of the camera
110    /// * `lens_radius` - Radius of the lens for depth of field. Bigger means more profound effect
111    /// * `focal_length` - Distance to the plane of focus for depth of field
112    /// * `width` - Width of the rendered image
113    /// * `height` - Height of the rendered image
114    #[allow(clippy::too_many_arguments)]
115    pub fn perspective(
116        origin: Point,
117        towards: Point,
118        up: Direction,
119        vfov: Float,
120        lens_radius: Float,
121        focal_length: Float,
122        width: i32,
123        height: i32,
124    ) -> Self {
125        assert!(vfov > 0.0 && vfov < 180.0);
126
127        Self::Perspective(
128            CameraConfig::new(
129                origin,
130                towards,
131                up,
132                lens_radius,
133                focal_length,
134                (width, height)
135            ),
136            vfov.to_radians() / 2.0,
137        )
138    }
139
140    /// The "default" camera. Perspective camera at world space origin
141    /// pointing towards `-z` with `y` as up and vfov at 90° with no DOF
142    pub fn default(width: i32, height: i32) -> Self {
143        Self::perspective(
144            Point::ZERO,
145            Point::NEG_Z,
146            Direction::Y,
147            90.0,
148            0.0,
149            0.0,
150            width,
151            height,
152        )
153    }
154
155    fn get_cfg(&self) -> &CameraConfig {
156        match self {
157            Self::Orthographic(cfg, _) | Self::Perspective(cfg, _) => cfg,
158        }
159    }
160
161    /// Returns the resolution of the image
162    pub fn get_resolution(&self) -> IVec2 {
163        self.get_cfg().resolution
164    }
165
166    /// Adds depth of field to camera space ray and transform to world space ray
167    fn add_dof(xo_local: Point, wi_local: Direction, cfg: &CameraConfig) -> Ray {
168        let (xo_local, wi_local) = if cfg.lens_radius == 0.0 {
169            (xo_local, wi_local)
170        } else {
171            let lens_xy = cfg.lens_radius
172                * rand_utils::square_to_disk(rand_utils::unit_square());
173            let lens_xyz = lens_xy.extend(0.0);
174
175            let focus_distance = cfg.focal_length / wi_local.z;
176
177            let focus_xyz = focus_distance * wi_local;
178
179            (lens_xyz, focus_xyz - lens_xyz)
180        };
181        // refactor camera basis to DAffine3
182        let xo = cfg.origin + cfg.camera_basis.to_world(xo_local);
183        let wi = cfg.camera_basis.to_world(wi_local);
184        Ray::new(xo, wi)
185    }
186
187    /// Generates a ray given a point in raster space `\[0,width\] x \[0,height\]`
188    pub fn generate_ray(&self, raster_xy: Vec2) -> Ray {
189        let resolution = self.get_resolution();
190        let resolution = Vec2::new(
191            resolution.x as Float,
192            resolution.y as Float,
193        );
194        let min_res = resolution.min_element();
195        // raster to screen here
196        let screen_xy = (2.0 * raster_xy - resolution) / min_res;
197
198        match self {
199            Self::Perspective(cfg, vfov_half) => {
200                // is this correct ???
201                let wi_local = screen_xy.extend(
202                    resolution.y / (min_res * vfov_half.tan())
203                ).normalize();
204
205                Self::add_dof(Point::ZERO, wi_local, cfg)
206            }
207            Self::Orthographic(cfg, scale) => {
208                let screen_xyz = screen_xy.extend(0.0);
209                let xo_local = *scale * screen_xyz;
210
211                Self::add_dof(xo_local, Direction::Z, cfg)
212            }
213        }
214    }
215
216    /// Samples a ray leaving from the lens of the camera towards `xi`
217    pub fn sample_towards(&self, xi: Point, rand_sq: Vec2) -> Ray {
218        let cfg = self.get_cfg();
219        let xo_local = rand_utils::square_to_disk(rand_sq).extend(0.0)
220            * cfg.lens_radius;
221        let xo = cfg.origin + cfg.camera_basis.to_world(xo_local);
222
223        let wi = (xi - xo).normalize();
224
225        Ray::new(xo, wi)
226    }
227
228    /// Probability that `ro` towards `xi` got sampled
229    pub fn sample_towards_pdf(&self, ro: &Ray, xi: Point) -> Float {
230        let cfg = self.get_cfg();
231        let xo = ro.origin;
232        let wi = ro.dir;
233        let ng = cfg.camera_basis.to_world(Direction::Z);
234
235        let lens_area = if cfg.lens_radius == 0.0 {
236            1.0
237        } else {
238            cfg.lens_radius * cfg.lens_radius * crate::PI
239        };
240
241        let pdf = xi.distance_squared(xo) / (ng.dot(wi) * lens_area);
242        pdf.max(0.0)
243    }
244
245    /// PDF for `wi` direction.
246    pub fn pdf(&self, wi: Direction) -> Float {
247        let cfg = self.get_cfg();
248        let wi_local = cfg.camera_basis.to_local(wi);
249        let cos_theta = wi_local.z;
250
251        if cos_theta <= 0.0 {
252            0.0
253        } else {
254            let area_coeff = {
255                let res = self.get_resolution();
256                let res = Vec2::new(
257                    res.x as Float,
258                    res.y as Float,
259                );
260                let min_res = res.min_element();
261                let screen_bounds = res / min_res;
262                screen_bounds.x * screen_bounds.y
263            };
264
265            1.0 / (area_coeff * cos_theta * cos_theta * cos_theta)
266        }
267    }
268
269    /// Incident importance for the ray `ro` starting from the camera lens
270    pub fn importance_sample(&self, ro: &Ray) -> FilmSample {
271        match self {
272            Self::Orthographic(..) => unimplemented!(),
273            Self::Perspective(cfg, _) => {
274                let wi = ro.dir;
275                let wi_local = cfg.camera_basis.to_local(wi);
276                let cos_theta = wi_local.z;
277                if cos_theta < 0.0 {
278                    return FilmSample::default();
279                }
280
281                let pdf = self.pdf(wi);
282
283                let color = Color::splat(pdf);
284
285                let fl = if cfg.lens_radius == 0.0 {
286                    1.0 / cos_theta
287                } else {
288                    cfg.focal_length / cos_theta
289                };
290
291                let resolution = self.get_resolution();
292                let resolution = Vec2::new(
293                    resolution.x as Float,
294                    resolution.y as Float,
295                );
296                let min_res = resolution.min_element();
297
298                let focus = ro.at(fl);
299                let focus_local = cfg.camera_basis.to_local(focus) + cfg.origin;
300                let raster_xy = (focus_local.truncate() * min_res + resolution) / 2.0;
301
302                FilmSample::new(color, raster_xy, true)
303            }
304        }
305    }
306}