rustic_zen/
ray.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use crate::geom::{Matrix, Point, Rect, Vector};
6use crate::scene::Light;
7use crate::scene::Object;
8use crate::spectrum::{wavelength_to_colour, FIRST_WAVELENGTH, LAST_WAVELENGTH};
9
10use rand::prelude::*;
11use rand_distr::num_traits::real::Real;
12
13use std::f64::consts::PI;
14
15pub struct RayResult {
16    pub origin: Point,
17    pub termination: Point,
18    pub wavelength: f64,
19}
20
21impl RayResult {
22    #[cfg(test)]
23    pub fn new<A, B>(start: A, end: B, wavelen: f64) -> Self
24    where
25        A: Into<Point>,
26        B: Into<Point>,
27    {
28        Self {
29            origin: start.into(),
30            termination: end.into(),
31            wavelength: wavelen,
32        }
33    }
34
35    pub fn color<P>(&self) -> (P, P, P)
36    where
37        P: Copy + Real,
38    {
39        wavelength_to_colour(self.wavelength)
40    }
41}
42
43pub struct Ray<R: Rng + SeedableRng> {
44    origin: Point,
45    direction: Vector,
46    wavelength: f64,
47    bounces: u32,
48    ray_rng: R,
49}
50
51impl<R> Ray<R>
52where
53    R: Rng + SeedableRng,
54{
55    /**
56     * Creates new ray from light source, sampling the light apropriately.
57     */
58    pub fn new(light: &Light<R>, rng: &mut R) -> Self {
59        let p = light.location.get(rng);
60        let cart_x = p.x;
61        let cart_y = p.y;
62        let polar_angle = light.polar_angle.sample(rng) * (PI / 180.0);
63        let polar_dist = light.polar_distance.sample(rng);
64        let origin = Point {
65            x: cart_x + f64::cos(polar_angle) * polar_dist,
66            y: cart_y + f64::sin(polar_angle) * polar_dist,
67        };
68        let ray_angle = light.ray_angle.sample(rng) * (PI / 180.0);
69        // Set Angle
70        let direction = Vector {
71            x: f64::cos(ray_angle),
72            y: f64::sin(ray_angle),
73        };
74        // Set Colour
75        let mut wavelength = 0.0;
76        while wavelength > LAST_WAVELENGTH - 1.0 || wavelength < FIRST_WAVELENGTH {
77            wavelength = light.wavelength.sample(rng);
78        }
79        Ray {
80            origin,
81            direction,
82            wavelength,
83            bounces: 1000,
84            ray_rng: R::seed_from_u64(rng.gen()),
85        }
86    }
87
88    pub fn collision_list(
89        mut self,
90        obj_list: &Vec<Object<R>>,
91        viewport: Rect,
92    ) -> (Option<RayResult>, Option<Self>) {
93        let ((hit, normal, alpha), _dist, obj) = obj_list
94            .iter()
95            .filter_map(|o| {
96                o.get_hit(&self.origin, &self.direction, &mut self.ray_rng)
97                    .map(|r| (r, self.origin.distance(&r.0), o))
98            })
99            .filter(|(_, distance, _)| *distance > 3.0)
100            .fold(
101                (((0.0, 0.0).into(), (0.0, 0.0).into(), 0.0), f64::MAX, None),
102                |b, x| if b.1 > x.1 { (x.0, x.1, Some(x.2)) } else { b },
103            );
104
105        match obj {
106            None => {
107                // This gets returned from fold if our hit is invalid, so we try to hit the viewport
108                match self.furthest_aabb(viewport) {
109                    Some(vp_hit) => (
110                        Some(RayResult {
111                            origin: self.origin,
112                            termination: vp_hit,
113                            wavelength: self.wavelength,
114                        }),
115                        None,
116                    ),
117                    None => (None, None),
118                }
119            }
120            Some(obj) => {
121                // Our hit is valid
122                let material_result = obj
123                    .process_material(
124                        &self.direction.normalized(),
125                        &normal.normalized(),
126                        self.wavelength,
127                        alpha,
128                        &mut self.ray_rng,
129                    )
130                    .map(|v| Ray {
131                        origin: hit,
132                        direction: v,
133                        wavelength: self.wavelength,
134                        bounces: self.bounces - 1,
135                        ray_rng: R::seed_from_u64(self.ray_rng.gen()),
136                    });
137
138                (
139                    Some(RayResult {
140                        origin: self.origin,
141                        termination: hit,
142                        wavelength: self.wavelength,
143                    }),
144                    material_result,
145                )
146            }
147        }
148    }
149
150    fn intersect_edge(&self, s1: Point, sd: Vector) -> Option<f64> {
151        let mat_a = Matrix {
152            a1: sd.x,
153            b1: -self.direction.x,
154            a2: sd.y,
155            b2: -self.direction.y,
156        };
157
158        let omega = self.origin - s1;
159
160        let result = match mat_a.inverse() {
161            Some(m) => m * omega,
162            None => {
163                return None; // Probably cos rays are parallel
164            }
165        };
166        if (result.x >= 0.0) && (result.x <= 1.0) && (result.y > 0.0) {
167            Some(result.y)
168        } else {
169            None
170        }
171    }
172
173    pub fn furthest_aabb(&self, aabb: Rect) -> Option<Point> {
174        let mut max_dist: Option<f64> = None;
175
176        let horizontal = Vector {
177            x: aabb.top_right().x - aabb.top_left().x,
178            y: 0.0,
179        };
180        let vertical = Vector {
181            x: 0.0,
182            y: aabb.bottom_left().y - aabb.top_left().y,
183        };
184
185        // top
186        match self.intersect_edge(aabb.top_left(), horizontal) {
187            None => (),
188            Some(d) => {
189                max_dist = match max_dist {
190                    None => Some(d),
191                    Some(md) => {
192                        if d > md {
193                            Some(d)
194                        } else {
195                            max_dist
196                        }
197                    }
198                };
199            }
200        }
201
202        // bottom
203        match self.intersect_edge(aabb.bottom_left(), horizontal) {
204            None => (),
205            Some(d) => {
206                max_dist = match max_dist {
207                    None => Some(d),
208                    Some(md) => {
209                        if d > md {
210                            Some(d)
211                        } else {
212                            max_dist
213                        }
214                    }
215                };
216            }
217        }
218
219        // left
220        match self.intersect_edge(aabb.top_left(), vertical) {
221            None => (),
222            Some(d) => {
223                max_dist = match max_dist {
224                    None => Some(d),
225                    Some(md) => {
226                        if d > md {
227                            Some(d)
228                        } else {
229                            max_dist
230                        }
231                    }
232                };
233            }
234        }
235
236        // right
237        match self.intersect_edge(aabb.top_right(), vertical) {
238            None => (),
239            Some(d) => {
240                max_dist = match max_dist {
241                    None => Some(d),
242                    Some(md) => {
243                        if d > md {
244                            Some(d)
245                        } else {
246                            max_dist
247                        }
248                    }
249                };
250            }
251        }
252
253        match max_dist {
254            None => {
255                return None;
256            }
257            Some(d) => {
258                return Some(Point {
259                    x: self.origin.x + d * self.direction.x,
260                    y: self.origin.y + d * self.direction.y,
261                });
262            }
263        }
264    }
265}
266
267#[cfg(test)]
268mod test {
269    type RandGen = rand_pcg::Pcg64Mcg;
270
271    use super::Ray;
272    use crate::geom::{Point, Rect};
273    use crate::sampler::Sampler;
274    use crate::scene::Light;
275    use rand::prelude::*;
276
277    fn new_test_light<R>(l: (f64, f64), a: f64) -> Light<R>
278    where
279        R: Rng,
280    {
281        Light::new(l, 1.0, 0.0, 0.0, a, Sampler::new_blackbody(5800.0))
282    }
283
284    #[test]
285    fn new_works() {
286        let mut rng = RandGen::from_entropy();
287
288        let l = Light {
289            power: Sampler::new_const(1.0),
290            location: (100.0, 100.0).into(),
291            polar_angle: Sampler::new_const(360.0),
292            polar_distance: Sampler::new_const(1.0),
293            ray_angle: Sampler::new_const(0.0),
294            wavelength: Sampler::new_const(460.0),
295        };
296
297        let r = Ray::new(&l, &mut rng);
298        assert_eq!(r.origin.x.round(), 101.0);
299        assert_eq!(r.origin.y.round(), 100.0);
300        assert_eq!(r.direction.x.round(), 1.0);
301        assert_eq!(r.direction.y.round(), 0.0);
302        assert_eq!(r.wavelength.round(), 460.0);
303        assert_eq!(r.bounces, 1000);
304    }
305
306    #[test]
307    fn furthest_aabb_hits_horziontal() {
308        let mut rng = RandGen::from_entropy();
309
310        let x_plus_light = new_test_light((0.0, 0.0), 0.0);
311
312        //Firing a ray in x+, 0 from origin
313        let ray = Ray::new(&x_plus_light, &mut rng);
314
315        // wall from 1,-10 to 11, +10 should be in the way
316        let p1 = Point { x: 1.0, y: -10.0 };
317        let p2 = Point { x: 11.0, y: 10.0 };
318        let aabb = Rect::from_points(&p1, &p2);
319
320        let result = ray.furthest_aabb(aabb);
321
322        // check hit!
323        let result = result.expect("Result should have been Some()");
324        assert_eq!(result.x, 11.0);
325        assert_eq!(result.y, 0.0);
326    }
327
328    #[test]
329    fn furthest_aabb_hits_vertical() {
330        let mut rng = RandGen::from_entropy();
331
332        let x_plus_light = new_test_light((0.0, 0.0), 90.0);
333
334        //Firing a ray in 0, +y from origin
335        let ray = Ray::new(&x_plus_light, &mut rng);
336
337        // wall from 1,-10 to 11, +10 should be in the way
338        let p1 = Point { x: -10.0, y: 1.0 };
339        let p2 = Point { x: 10.0, y: 11.0 };
340        let aabb = Rect::from_points(&p1, &p2);
341
342        let result = ray.furthest_aabb(aabb);
343
344        // check hit!
345        let result = result.expect("That shouldn't be None!");
346        assert_eq!(result.x.round(), 0.0);
347        assert_eq!(result.y.round(), 11.0);
348    }
349
350    #[test]
351    fn furthest_aabb_hits_almost_vertical() {
352        let mut rng = RandGen::from_entropy();
353
354        let x_plus_light = new_test_light((0.0, 0.0), 45.0);
355
356        //Firing a diagonal ray +x, +y from origin
357        let ray = Ray::new(&x_plus_light, &mut rng);
358
359        // wall from 1,-10 to 11, +10 should be in the way
360        let p1 = Point { x: -10.0, y: 1.0 };
361        let p2 = Point { x: 20.0, y: 11.0 };
362        let aabb = Rect::from_points(&p1, &p2);
363
364        let result = ray.furthest_aabb(aabb);
365
366        // check hit!
367        assert!(result.is_some());
368        let result = result.expect("Something was meant to be there!");
369        assert_eq!(result.x.round(), 11.0);
370        assert_eq!(result.y.round(), 11.0);
371    }
372
373    #[test]
374    fn furthest_aabb_special_case() {
375        let mut rng = RandGen::from_entropy();
376
377        let x_plus_light = new_test_light((100.0, 700.0), -45.0);
378
379        //Firing a diagonal ray +x, -y from origin
380        let ray = Ray::new(&x_plus_light, &mut rng);
381
382        // wall from 1,-10 to 11, +10 should be in the way
383        let p1 = Point { x: 0.0, y: 0.0 };
384        let p2 = Point {
385            x: 200.0,
386            y: 1000.0,
387        };
388        let aabb = Rect::from_points(&p1, &p2);
389
390        let result = ray.furthest_aabb(aabb);
391
392        // check hit!
393        let result = result.expect("None is not what we wanted");
394        assert_eq!(result.x.round(), 200.0);
395        assert_eq!(result.y.round(), 600.0);
396    }
397}