rustic-zen 0.3.0

Photon-Garden raytracer for creating artistic renderings
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::geom::{Matrix, Point, Rect, Vector};
use crate::scene::Light;
use crate::scene::Object;
use crate::spectrum::{wavelength_to_colour, FIRST_WAVELENGTH, LAST_WAVELENGTH};

use rand::prelude::*;
use rand_distr::num_traits::real::Real;

use std::f64::consts::PI;

pub struct RayResult {
    pub origin: Point,
    pub termination: Point,
    pub wavelength: f64,
}

impl RayResult {
    #[cfg(test)]
    pub fn new<A, B>(start: A, end: B, wavelen: f64) -> Self
    where
        A: Into<Point>,
        B: Into<Point>,
    {
        Self {
            origin: start.into(),
            termination: end.into(),
            wavelength: wavelen,
        }
    }

    pub fn color<P>(&self) -> (P, P, P)
    where
        P: Copy + Real,
    {
        wavelength_to_colour(self.wavelength)
    }
}

pub struct Ray<R: Rng + SeedableRng> {
    origin: Point,
    direction: Vector,
    wavelength: f64,
    bounces: u32,
    ray_rng: R,
}

impl<R> Ray<R>
where
    R: Rng + SeedableRng,
{
    /**
     * Creates new ray from light source, sampling the light apropriately.
     */
    pub fn new(light: &Light<R>, rng: &mut R) -> Self {
        let p = light.location.get(rng);
        let cart_x = p.x;
        let cart_y = p.y;
        let polar_angle = light.polar_angle.sample(rng) * (PI / 180.0);
        let polar_dist = light.polar_distance.sample(rng);
        let origin = Point {
            x: cart_x + f64::cos(polar_angle) * polar_dist,
            y: cart_y + f64::sin(polar_angle) * polar_dist,
        };
        let ray_angle = light.ray_angle.sample(rng) * (PI / 180.0);
        // Set Angle
        let direction = Vector {
            x: f64::cos(ray_angle),
            y: f64::sin(ray_angle),
        };
        // Set Colour
        let mut wavelength = 0.0;
        while wavelength > LAST_WAVELENGTH - 1.0 || wavelength < FIRST_WAVELENGTH {
            wavelength = light.wavelength.sample(rng);
        }
        Ray {
            origin,
            direction,
            wavelength,
            bounces: 1000,
            ray_rng: R::seed_from_u64(rng.gen()),
        }
    }

    pub fn collision_list(
        mut self,
        obj_list: &Vec<Object<R>>,
        viewport: Rect,
    ) -> (Option<RayResult>, Option<Self>) {
        let ((hit, normal, alpha), _dist, obj) = obj_list
            .iter()
            .filter_map(|o| {
                o.get_hit(&self.origin, &self.direction, &mut self.ray_rng)
                    .map(|r| (r, self.origin.distance(&r.0), o))
            })
            .filter(|(_, distance, _)| *distance > 3.0)
            .fold(
                (((0.0, 0.0).into(), (0.0, 0.0).into(), 0.0), f64::MAX, None),
                |b, x| if b.1 > x.1 { (x.0, x.1, Some(x.2)) } else { b },
            );

        match obj {
            None => {
                // This gets returned from fold if our hit is invalid, so we try to hit the viewport
                match self.furthest_aabb(viewport) {
                    Some(vp_hit) => (
                        Some(RayResult {
                            origin: self.origin,
                            termination: vp_hit,
                            wavelength: self.wavelength,
                        }),
                        None,
                    ),
                    None => (None, None),
                }
            }
            Some(obj) => {
                // Our hit is valid
                let material_result = obj
                    .process_material(
                        &self.direction.normalized(),
                        &normal.normalized(),
                        self.wavelength,
                        alpha,
                        &mut self.ray_rng,
                    )
                    .map(|v| Ray {
                        origin: hit,
                        direction: v,
                        wavelength: self.wavelength,
                        bounces: self.bounces - 1,
                        ray_rng: R::seed_from_u64(self.ray_rng.gen()),
                    });

                (
                    Some(RayResult {
                        origin: self.origin,
                        termination: hit,
                        wavelength: self.wavelength,
                    }),
                    material_result,
                )
            }
        }
    }

    fn intersect_edge(&self, s1: Point, sd: Vector) -> Option<f64> {
        let mat_a = Matrix {
            a1: sd.x,
            b1: -self.direction.x,
            a2: sd.y,
            b2: -self.direction.y,
        };

        let omega = self.origin - s1;

        let result = match mat_a.inverse() {
            Some(m) => m * omega,
            None => {
                return None; // Probably cos rays are parallel
            }
        };
        if (result.x >= 0.0) && (result.x <= 1.0) && (result.y > 0.0) {
            Some(result.y)
        } else {
            None
        }
    }

    pub fn furthest_aabb(&self, aabb: Rect) -> Option<Point> {
        let mut max_dist: Option<f64> = None;

        let horizontal = Vector {
            x: aabb.top_right().x - aabb.top_left().x,
            y: 0.0,
        };
        let vertical = Vector {
            x: 0.0,
            y: aabb.bottom_left().y - aabb.top_left().y,
        };

        // top
        match self.intersect_edge(aabb.top_left(), horizontal) {
            None => (),
            Some(d) => {
                max_dist = match max_dist {
                    None => Some(d),
                    Some(md) => {
                        if d > md {
                            Some(d)
                        } else {
                            max_dist
                        }
                    }
                };
            }
        }

        // bottom
        match self.intersect_edge(aabb.bottom_left(), horizontal) {
            None => (),
            Some(d) => {
                max_dist = match max_dist {
                    None => Some(d),
                    Some(md) => {
                        if d > md {
                            Some(d)
                        } else {
                            max_dist
                        }
                    }
                };
            }
        }

        // left
        match self.intersect_edge(aabb.top_left(), vertical) {
            None => (),
            Some(d) => {
                max_dist = match max_dist {
                    None => Some(d),
                    Some(md) => {
                        if d > md {
                            Some(d)
                        } else {
                            max_dist
                        }
                    }
                };
            }
        }

        // right
        match self.intersect_edge(aabb.top_right(), vertical) {
            None => (),
            Some(d) => {
                max_dist = match max_dist {
                    None => Some(d),
                    Some(md) => {
                        if d > md {
                            Some(d)
                        } else {
                            max_dist
                        }
                    }
                };
            }
        }

        match max_dist {
            None => {
                return None;
            }
            Some(d) => {
                return Some(Point {
                    x: self.origin.x + d * self.direction.x,
                    y: self.origin.y + d * self.direction.y,
                });
            }
        }
    }
}

#[cfg(test)]
mod test {
    type RandGen = rand_pcg::Pcg64Mcg;

    use super::Ray;
    use crate::geom::{Point, Rect};
    use crate::sampler::Sampler;
    use crate::scene::Light;
    use rand::prelude::*;

    fn new_test_light<R>(l: (f64, f64), a: f64) -> Light<R>
    where
        R: Rng,
    {
        Light::new(l, 1.0, 0.0, 0.0, a, Sampler::new_blackbody(5800.0))
    }

    #[test]
    fn new_works() {
        let mut rng = RandGen::from_entropy();

        let l = Light {
            power: Sampler::new_const(1.0),
            location: (100.0, 100.0).into(),
            polar_angle: Sampler::new_const(360.0),
            polar_distance: Sampler::new_const(1.0),
            ray_angle: Sampler::new_const(0.0),
            wavelength: Sampler::new_const(460.0),
        };

        let r = Ray::new(&l, &mut rng);
        assert_eq!(r.origin.x.round(), 101.0);
        assert_eq!(r.origin.y.round(), 100.0);
        assert_eq!(r.direction.x.round(), 1.0);
        assert_eq!(r.direction.y.round(), 0.0);
        assert_eq!(r.wavelength.round(), 460.0);
        assert_eq!(r.bounces, 1000);
    }

    #[test]
    fn furthest_aabb_hits_horziontal() {
        let mut rng = RandGen::from_entropy();

        let x_plus_light = new_test_light((0.0, 0.0), 0.0);

        //Firing a ray in x+, 0 from origin
        let ray = Ray::new(&x_plus_light, &mut rng);

        // wall from 1,-10 to 11, +10 should be in the way
        let p1 = Point { x: 1.0, y: -10.0 };
        let p2 = Point { x: 11.0, y: 10.0 };
        let aabb = Rect::from_points(&p1, &p2);

        let result = ray.furthest_aabb(aabb);

        // check hit!
        let result = result.expect("Result should have been Some()");
        assert_eq!(result.x, 11.0);
        assert_eq!(result.y, 0.0);
    }

    #[test]
    fn furthest_aabb_hits_vertical() {
        let mut rng = RandGen::from_entropy();

        let x_plus_light = new_test_light((0.0, 0.0), 90.0);

        //Firing a ray in 0, +y from origin
        let ray = Ray::new(&x_plus_light, &mut rng);

        // wall from 1,-10 to 11, +10 should be in the way
        let p1 = Point { x: -10.0, y: 1.0 };
        let p2 = Point { x: 10.0, y: 11.0 };
        let aabb = Rect::from_points(&p1, &p2);

        let result = ray.furthest_aabb(aabb);

        // check hit!
        let result = result.expect("That shouldn't be None!");
        assert_eq!(result.x.round(), 0.0);
        assert_eq!(result.y.round(), 11.0);
    }

    #[test]
    fn furthest_aabb_hits_almost_vertical() {
        let mut rng = RandGen::from_entropy();

        let x_plus_light = new_test_light((0.0, 0.0), 45.0);

        //Firing a diagonal ray +x, +y from origin
        let ray = Ray::new(&x_plus_light, &mut rng);

        // wall from 1,-10 to 11, +10 should be in the way
        let p1 = Point { x: -10.0, y: 1.0 };
        let p2 = Point { x: 20.0, y: 11.0 };
        let aabb = Rect::from_points(&p1, &p2);

        let result = ray.furthest_aabb(aabb);

        // check hit!
        assert!(result.is_some());
        let result = result.expect("Something was meant to be there!");
        assert_eq!(result.x.round(), 11.0);
        assert_eq!(result.y.round(), 11.0);
    }

    #[test]
    fn furthest_aabb_special_case() {
        let mut rng = RandGen::from_entropy();

        let x_plus_light = new_test_light((100.0, 700.0), -45.0);

        //Firing a diagonal ray +x, -y from origin
        let ray = Ray::new(&x_plus_light, &mut rng);

        // wall from 1,-10 to 11, +10 should be in the way
        let p1 = Point { x: 0.0, y: 0.0 };
        let p2 = Point {
            x: 200.0,
            y: 1000.0,
        };
        let aabb = Rect::from_points(&p1, &p2);

        let result = ray.furthest_aabb(aabb);

        // check hit!
        let result = result.expect("None is not what we wanted");
        assert_eq!(result.x.round(), 200.0);
        assert_eq!(result.y.round(), 600.0);
    }
}