1use super::prelude::*;
2use crate::image::prelude::*;
3use crate::trace::material::Material;
4use rand::RngCore;
5
6pub trait Hittable {
8 fn intersection(&self, rng: &mut Box<dyn RngCore>, ray: &Ray) -> Option<HitRecord>;
11}
12
13pub 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
22enum QuadSoln {
24 NoRoots,
25 OneRoot(f64),
26 TwoRoots(f64, f64),
27}
28
29pub struct World {
31 objects: Vec<Box<dyn Hittable + Send + Sync>>,
32}
33
34pub struct Sphere {
36 centre: Vec3,
37 radius: f64,
38 material: Box<dyn Material>,
39}
40
41impl World {
42 pub fn new() -> Self {
44 Self {
45 objects: Vec::new(),
46 }
47 }
48
49 pub fn add_object(&mut self, object: Box<dyn Hittable + Send + Sync>) {
51 self.objects.push(object);
52 }
53
54 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()); match hit {
72 Some(hit) => {
73 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 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}