1use crate::math::Vec3;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Ray {
12 origin: Vec3,
13 direction: Vec3,
14}
15
16impl Ray {
17 pub fn new(origin: Vec3, direction: Vec3) -> Self {
20 Self {
21 origin,
22 direction: direction.normalize_or_zero(),
23 }
24 }
25
26 pub const fn origin(&self) -> Vec3 {
28 self.origin
29 }
30
31 pub const fn direction(&self) -> Vec3 {
33 self.direction
34 }
35
36 pub fn at(&self, t: f32) -> Vec3 {
38 self.origin + self.direction * t
39 }
40
41 pub fn hit_plane(&self, plane: Plane) -> Option<f32> {
44 reached((plane.point - self.origin).dot(plane.normal) / self.direction.dot(plane.normal))
45 }
46
47 pub fn hit_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
52 let to_center = center - self.origin;
53 let length_squared = self.direction.length_squared();
54 let along = to_center.dot(self.direction);
55 let half_chord = (along * along
56 - length_squared * (to_center.length_squared() - radius * radius))
57 .sqrt();
58
59 reached((along - half_chord) / length_squared)
60 .or_else(|| reached((along + half_chord) / length_squared))
61 }
62
63 pub fn hit_aabb(&self, min: Vec3, max: Vec3) -> Option<f32> {
68 let (to_min, to_max) = (
69 (min - self.origin) / self.direction,
70 (max - self.origin) / self.direction,
71 );
72 let (entry, exit) = (
73 to_min.min(to_max).max_element(),
74 to_min.max(to_max).min_element(),
75 );
76
77 if entry > exit {
78 return None;
79 }
80 reached(entry).or_else(|| reached(exit))
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
90pub struct Plane {
91 pub point: Vec3,
93 pub normal: Vec3,
95}
96
97fn reached(t: f32) -> Option<f32> {
100 (t >= 0.0 && t.is_finite()).then_some(t)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn along_x() -> Ray {
109 Ray::new(Vec3::ZERO, Vec3::X)
110 }
111
112 #[test]
113 fn a_ray_is_measured_in_meters_whatever_it_was_built_from() {
114 let ray = Ray::new(Vec3::Y, Vec3::X * 8.0);
115
116 assert_eq!(ray.origin(), Vec3::Y);
117 assert_eq!(ray.direction(), Vec3::X);
118 assert_eq!(ray.at(3.0), Vec3::new(3.0, 1.0, 0.0));
119 }
120
121 #[test]
122 fn a_plane_is_hit_from_either_side_and_never_behind() {
123 let down = Ray::new(Vec3::Y * 5.0, Vec3::NEG_Y);
124
125 assert_eq!(
126 down.hit_plane(Plane {
127 point: Vec3::ZERO,
128 normal: Vec3::Y
129 }),
130 Some(5.0)
131 );
132 assert_eq!(
133 down.hit_plane(Plane {
134 point: Vec3::ZERO,
135 normal: Vec3::NEG_Y
136 }),
137 Some(5.0),
138 "a plane faces both ways"
139 );
140 assert_eq!(
141 down.hit_plane(Plane {
142 point: Vec3::Y * 5.0,
143 normal: Vec3::Y
144 }),
145 Some(0.0),
146 "a ray that starts on one hits it at once"
147 );
148 assert_eq!(
149 down.hit_plane(Plane {
150 point: Vec3::Y * 9.0,
151 normal: Vec3::Y
152 }),
153 None,
154 "the plane behind it is missed"
155 );
156 }
157
158 #[test]
159 fn a_plane_the_ray_cannot_reach_is_never_hit() {
160 let across = along_x();
161
162 assert_eq!(
163 across.hit_plane(Plane {
164 point: Vec3::ZERO,
165 normal: Vec3::Y
166 }),
167 None,
168 "parallel"
169 );
170 assert_eq!(
171 across.hit_plane(Plane {
172 point: Vec3::ZERO,
173 normal: Vec3::ZERO
174 }),
175 None,
176 "a plane with no normal is no plane"
177 );
178 assert_eq!(
179 Ray::new(Vec3::ZERO, Vec3::ZERO).hit_plane(Plane {
180 point: Vec3::X,
181 normal: Vec3::X
182 }),
183 None,
184 "a ray with no direction reaches nothing"
185 );
186 }
187
188 #[test]
189 fn a_sphere_is_hit_at_its_near_side_and_left_from_within() {
190 let ray = along_x();
191
192 assert_eq!(ray.hit_sphere(Vec3::X * 10.0, 2.0), Some(8.0));
193 assert_eq!(
194 ray.hit_sphere(Vec3::ZERO, 2.0),
195 Some(2.0),
196 "from within, where it leaves"
197 );
198 assert_eq!(ray.hit_sphere(Vec3::NEG_X * 10.0, 2.0), None, "behind");
199 assert_eq!(
200 ray.hit_sphere(Vec3::new(10.0, 3.0, 0.0), 2.0),
201 None,
202 "past it"
203 );
204 }
205
206 #[test]
207 fn a_sphere_the_ray_grazes_or_that_has_no_size_still_answers_a_distance() {
208 let ray = along_x();
209
210 assert_eq!(ray.hit_sphere(Vec3::new(5.0, 2.0, 0.0), 2.0), Some(5.0));
211 assert!(
212 ray.hit_sphere(Vec3::X * 5.0, 0.0)
213 .is_none_or(|t| t.is_finite()),
214 "a sphere of no size is a point on the ray or nothing at all"
215 );
216 assert_eq!(
217 ray.hit_sphere(Vec3::X * 5.0, -2.0),
218 Some(3.0),
219 "one turned inside out is the same sphere"
220 );
221 }
222
223 #[test]
224 fn bounds_are_hit_where_the_ray_enters_and_left_where_it_leaves() {
225 let ray = along_x();
226 let (min, max) = (Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
227
228 assert_eq!(ray.hit_aabb(min, max), Some(4.0));
229 assert_eq!(
230 ray.hit_aabb(Vec3::splat(-1.0), Vec3::ONE),
231 Some(1.0),
232 "from within, where it leaves"
233 );
234 assert_eq!(ray.hit_aabb(max, min), Some(4.0), "however they are given");
235 assert_eq!(ray.hit_aabb(-max, -min), None, "behind");
236 assert_eq!(
237 ray.hit_aabb(min + Vec3::Y * 5.0, max + Vec3::Y * 5.0),
238 None,
239 "above"
240 );
241 }
242
243 #[test]
244 fn bounds_with_no_thickness_are_hit_head_on() {
245 let down = Ray::new(Vec3::Y, Vec3::NEG_Y);
246 let (min, max) = (Vec3::new(-1.0, 0.0, -1.0), Vec3::new(1.0, 0.0, 1.0));
247
248 assert_eq!(down.hit_aabb(min, max), Some(1.0));
249 assert!(
250 down.hit_aabb(Vec3::ZERO, Vec3::ZERO)
251 .is_none_or(|t| t.is_finite()),
252 "bounds of no size at all are a point, or nothing"
253 );
254 }
255
256 #[test]
257 fn a_ray_with_no_direction_reaches_nothing() {
258 let still = Ray::new(Vec3::ZERO, Vec3::ZERO);
259 let hits = [
260 still.hit_plane(Plane {
261 point: Vec3::X,
262 normal: Vec3::X,
263 }),
264 still.hit_sphere(Vec3::ZERO, 2.0),
265 still.hit_sphere(Vec3::X * 5.0, 2.0),
266 still.hit_aabb(Vec3::splat(-1.0), Vec3::ONE),
267 ];
268
269 assert!(hits.iter().all(Option::is_none), "not even what it sits in");
270 }
271}