bs_trace/trace/
world.rs

1use super::prelude::*;
2use crate::image::prelude::*;
3use crate::trace::material::Material;
4use rand::RngCore;
5
6/// Represents an object that a ray can hit.
7pub trait Hittable {
8    /// Computes the hit parameters for an input ray. Returns `None` if the ray does not hit this
9    /// object.
10    fn intersection(&self, rng: &mut Box<dyn RngCore>, ray: &Ray) -> Option<HitRecord>;
11}
12
13/// The relevant parameters for a ray hitting a hittable object.
14pub struct HitRecord {
15    pub(self) t: f64,
16    pub(self) intersection: Vec3,
17    pub(self) normal: Vec3,
18    pub(self) attenuation: Colour,
19    pub(self) bounce_direction: Option<Vec3>,
20}
21
22/// Real solution(s) for a quadratic equation.
23enum QuadSoln {
24    NoRoots,
25    OneRoot(f64),
26    TwoRoots(f64, f64),
27}
28
29/// A world, consisting of a set of objects that can be hit by rays.
30pub struct World {
31    objects: Vec<Box<dyn Hittable + Send + Sync>>,
32}
33
34/// A sphere, at the given centre with the given radius.
35pub struct Sphere {
36    centre: Vec3,
37    radius: f64,
38    material: Box<dyn Material>,
39}
40
41impl World {
42    /// Creates a new world.
43    pub fn new() -> Self {
44        Self {
45            objects: Vec::new(),
46        }
47    }
48
49    /// Adds the given boxed hittable object to this world.
50    pub fn add_object(&mut self, object: Box<dyn Hittable + Send + Sync>) {
51        self.objects.push(object);
52    }
53
54    /// Traces a ray into the world, and returns the resultant colour.
55    pub fn trace(&self, rng: &mut Box<dyn RngCore>, ray: &Ray) -> Colour {
56        self.do_trace(rng, ray, 0)
57    }
58
59    fn do_trace(&self, rng: &mut Box<dyn RngCore>, ray: &Ray, bounce_count: usize) -> Colour {
60        if bounce_count > 20 {
61            Colour::BLACK
62        } else {
63            let hit = self
64                .objects
65                .iter()
66                .flat_map(|obj| obj.intersection(rng, ray))
67                .filter(|x| !x.t.is_nan())
68                .filter(|x| x.t > 1e-3)
69                .min_by(|x1, x2| x1.t.partial_cmp(&x2.t).unwrap()); // unwrap OK because partial_cmp is None iff nan
70
71            match hit {
72                Some(hit) => {
73                    // this is normal-based colouring
74                    // Colour::from(hit.normal.into_map(|x| (x + 1.0) / 2.0))
75                    let new_origin = hit.intersection;
76                    let new_direction = hit.bounce_direction;
77                    if let Some(new_direction) = new_direction {
78                        let new_ray = Ray::new(new_origin, new_direction);
79                        hit.attenuation * self.do_trace(rng, &new_ray, bounce_count + 1)
80                    } else {
81                        Colour::BLACK
82                    }
83                }
84                None => {
85                    let t = (ray.direction.y() + 1.0) / 2.0;
86                    let c0 = Vec3::new([1.0, 1.0, 1.0]);
87                    let c1 = Vec3::new([0.5, 0.7, 1.0]);
88                    Colour::from(Vec3::pure(1.0 - t) * c0 + Vec3::pure(t) * c1)
89                }
90            }
91        }
92    }
93}
94
95impl Sphere {
96    /// Constructs a new sphere at the given centre with the given radius.
97    pub const fn new(centre: Vec3, radius: f64, material: Box<dyn Material>) -> Self {
98        Self {
99            centre,
100            radius,
101            material,
102        }
103    }
104}
105
106impl Hittable for Sphere {
107    fn intersection(&self, rng: &mut Box<dyn RngCore>, ray: &Ray) -> Option<HitRecord> {
108        let co = self.centre - ray.origin;
109        let a = ray.direction.dot(ray.direction);
110        let b = -2.0 * (ray.direction.dot(co));
111        let c = co.mag_sq() - self.radius * self.radius;
112        let discriminant = b * b - 4.0 * a * c;
113
114        let roots = if discriminant < 0.0 {
115            QuadSoln::NoRoots
116        } else if discriminant == 0.0 {
117            let root = -b / (2.0 * a);
118            QuadSoln::OneRoot(root)
119        } else {
120            let pos_root = (-b + discriminant.sqrt()) / (2.0 * a);
121            let neg_root = (-b - discriminant.sqrt()) / (2.0 * a);
122            QuadSoln::TwoRoots(pos_root, neg_root)
123        };
124
125        let nearest_pos_root = match roots {
126            QuadSoln::NoRoots => None,
127            QuadSoln::OneRoot(root) => {
128                if root < 0.0 {
129                    None
130                } else {
131                    Some(root)
132                }
133            }
134            QuadSoln::TwoRoots(r1, r2) => {
135                let min = r1.min(r2);
136                let max = r1.max(r2);
137
138                if max < 0.0 {
139                    None
140                } else if min < 0.0 {
141                    Some(max)
142                } else {
143                    Some(min)
144                }
145            }
146        }?;
147
148        let intersection = ray.at(nearest_pos_root);
149        let normal = (intersection - self.centre).normalised();
150        let attenuation = self.material.colour();
151        let bounce_direction = self.material.bounce(rng, ray.direction, normal);
152        Some(HitRecord {
153            t: nearest_pos_root,
154            intersection,
155            normal,
156            attenuation,
157            bounce_direction,
158        })
159    }
160}