1use crate::geometry::Geometry;
12use phyz_math::{Mat3, Vec3};
13
14#[derive(Debug, Clone, Copy)]
16pub struct Ray {
17 pub origin: Vec3,
19 pub dir: Vec3,
21}
22
23impl Ray {
24 pub fn new(origin: Vec3, dir: Vec3) -> Option<Self> {
29 let n = dir.norm();
30 if !n.is_finite() || n < 1e-12 {
31 return None;
32 }
33 Some(Self {
34 origin,
35 dir: dir / n,
36 })
37 }
38
39 pub fn at(&self, t: f64) -> Vec3 {
41 self.origin + self.dir * t
42 }
43}
44
45#[derive(Debug, Clone, Copy)]
47pub struct RayHit {
48 pub distance: f64,
50 pub point: Vec3,
52 pub normal: Vec3,
54}
55
56const T_EPS: f64 = 1e-9;
58
59pub fn ray_intersect(geom: &Geometry, pos: &Vec3, rot: &Mat3, ray: &Ray) -> Option<RayHit> {
65 let inv = rot.transpose();
67 let o = inv.mul_vec(ray.origin - *pos);
68 let d = inv.mul_vec(ray.dir);
69
70 let (t, local_normal) = match geom {
71 Geometry::Sphere { radius } => sphere_hit(&o, &d, *radius)?,
72 Geometry::Box { half_extents } => box_hit(&o, &d, half_extents)?,
73 Geometry::Cylinder { radius, height } => cylinder_hit(&o, &d, *radius, *height * 0.5)?,
74 Geometry::Capsule { radius, length } => capsule_hit(&o, &d, *radius, *length * 0.5)?,
77 Geometry::Plane { normal } => plane_hit(&o, &d, normal)?,
78 Geometry::Mesh { vertices, faces } => mesh_hit(&o, &d, vertices, faces)?,
79 };
80
81 Some(RayHit {
82 distance: t,
83 point: ray.at(t),
84 normal: rot.mul_vec(local_normal),
85 })
86}
87
88pub fn ray_cast<'a, I>(shapes: I, ray: &Ray, max_distance: f64) -> Option<(usize, RayHit)>
91where
92 I: IntoIterator<Item = (&'a Geometry, Vec3, Mat3)>,
93{
94 let mut best: Option<(usize, RayHit)> = None;
95 for (idx, (geom, pos, rot)) in shapes.into_iter().enumerate() {
96 let Some(hit) = ray_intersect(geom, &pos, &rot, ray) else {
97 continue;
98 };
99 if hit.distance > max_distance {
100 continue;
101 }
102 if best.is_none_or(|(_, b)| hit.distance < b.distance) {
103 best = Some((idx, hit));
104 }
105 }
106 best
107}
108
109fn nearest_root(t0: f64, t1: f64) -> Option<f64> {
111 let (lo, hi) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
112 if lo >= T_EPS {
113 Some(lo)
114 } else if hi >= T_EPS {
115 Some(hi)
116 } else {
117 None
118 }
119}
120
121fn sphere_hit(o: &Vec3, d: &Vec3, radius: f64) -> Option<(f64, Vec3)> {
122 let b = o.dot(d);
124 let c = o.norm_sq() - radius * radius;
125 let disc = b * b - c;
126 if disc < 0.0 {
127 return None;
128 }
129 let sq = disc.sqrt();
130 let t = nearest_root(-b - sq, -b + sq)?;
131 let p = *o + *d * t;
132 Some((t, p / radius))
133}
134
135fn box_hit(o: &Vec3, d: &Vec3, half: &Vec3) -> Option<(f64, Vec3)> {
136 let mut t_min = f64::NEG_INFINITY;
138 let mut t_max = f64::INFINITY;
139 let mut min_axis = 0usize;
140 let mut max_axis = 0usize;
141 let mut min_sign = 1.0;
142 let mut max_sign = 1.0;
143
144 let oa = [o.x, o.y, o.z];
145 let da = [d.x, d.y, d.z];
146 let ha = [half.x, half.y, half.z];
147
148 for axis in 0..3 {
149 if da[axis].abs() < 1e-12 {
150 if oa[axis] < -ha[axis] || oa[axis] > ha[axis] {
152 return None;
153 }
154 continue;
155 }
156 let inv = 1.0 / da[axis];
157 let mut t1 = (-ha[axis] - oa[axis]) * inv;
158 let mut t2 = (ha[axis] - oa[axis]) * inv;
159 let mut s1 = -1.0;
160 let mut s2 = 1.0;
161 if t1 > t2 {
162 std::mem::swap(&mut t1, &mut t2);
163 std::mem::swap(&mut s1, &mut s2);
164 }
165 if t1 > t_min {
166 t_min = t1;
167 min_axis = axis;
168 min_sign = s1;
169 }
170 if t2 < t_max {
171 t_max = t2;
172 max_axis = axis;
173 max_sign = s2;
174 }
175 if t_min > t_max {
176 return None;
177 }
178 }
179
180 let (t, axis, sign) = if t_min >= T_EPS {
181 (t_min, min_axis, min_sign)
182 } else if t_max >= T_EPS {
183 (t_max, max_axis, max_sign)
184 } else {
185 return None;
186 };
187
188 let mut n = Vec3::zeros();
189 match axis {
190 0 => n.x = sign,
191 1 => n.y = sign,
192 _ => n.z = sign,
193 }
194 Some((t, n))
195}
196
197fn cylinder_side(o: &Vec3, d: &Vec3, r: f64, half_h: f64) -> Option<(f64, Vec3)> {
200 let a = d.x * d.x + d.y * d.y;
201 if a < 1e-12 {
202 return None; }
204 let b = o.x * d.x + o.y * d.y;
205 let c = o.x * o.x + o.y * o.y - r * r;
206 let disc = b * b - a * c;
207 if disc < 0.0 {
208 return None;
209 }
210 let sq = disc.sqrt();
211 for t in [(-b - sq) / a, (-b + sq) / a] {
212 if t < T_EPS {
213 continue;
214 }
215 let z = o.z + d.z * t;
216 if z.abs() <= half_h {
217 let p = *o + *d * t;
218 return Some((t, Vec3::new(p.x / r, p.y / r, 0.0)));
219 }
220 }
221 None
222}
223
224fn cylinder_caps(o: &Vec3, d: &Vec3, r: f64, half_h: f64) -> Option<(f64, Vec3)> {
226 if d.z.abs() < 1e-12 {
227 return None;
228 }
229 let mut best: Option<(f64, Vec3)> = None;
230 for sign in [-1.0, 1.0] {
231 let t = (sign * half_h - o.z) / d.z;
232 if t < T_EPS {
233 continue;
234 }
235 let p = *o + *d * t;
236 if p.x * p.x + p.y * p.y <= r * r && best.is_none_or(|(bt, _)| t < bt) {
237 best = Some((t, Vec3::new(0.0, 0.0, sign)));
238 }
239 }
240 best
241}
242
243fn cylinder_hit(o: &Vec3, d: &Vec3, r: f64, half_h: f64) -> Option<(f64, Vec3)> {
244 match (
245 cylinder_side(o, d, r, half_h),
246 cylinder_caps(o, d, r, half_h),
247 ) {
248 (Some(a), Some(b)) => Some(if a.0 <= b.0 { a } else { b }),
249 (Some(a), None) => Some(a),
250 (None, b) => b,
251 }
252}
253
254fn capsule_hit(o: &Vec3, d: &Vec3, r: f64, half_h: f64) -> Option<(f64, Vec3)> {
255 let mut best: Option<(f64, Vec3)> = cylinder_side(o, d, r, half_h);
256
257 for sign in [-1.0, 1.0] {
259 let centre = Vec3::new(0.0, 0.0, sign * half_h);
260 let oc = *o - centre;
261 let b = oc.dot(d);
262 let c = oc.norm_sq() - r * r;
263 let disc = b * b - c;
264 if disc < 0.0 {
265 continue;
266 }
267 let sq = disc.sqrt();
268 for t in [-b - sq, -b + sq] {
269 if t < T_EPS {
270 continue;
271 }
272 let p = *o + *d * t;
273 if (p.z - sign * half_h) * sign < 0.0 {
274 continue; }
276 if best.is_none_or(|(bt, _)| t < bt) {
277 best = Some((t, (p - centre) / r));
278 }
279 break;
280 }
281 }
282 best
283}
284
285fn plane_hit(o: &Vec3, d: &Vec3, normal: &Vec3) -> Option<(f64, Vec3)> {
286 let n = normal.normalize();
287 let denom = d.dot(n);
288 if denom.abs() < 1e-12 {
289 return None; }
291 let t = -o.dot(n) / denom;
293 if t < T_EPS {
294 return None;
295 }
296 Some((t, if denom < 0.0 { n } else { -n }))
298}
299
300fn mesh_hit(o: &Vec3, d: &Vec3, vertices: &[Vec3], faces: &[[usize; 3]]) -> Option<(f64, Vec3)> {
301 let mut best: Option<(f64, Vec3)> = None;
302 for f in faces {
303 let (Some(&v0), Some(&v1), Some(&v2)) =
304 (vertices.get(f[0]), vertices.get(f[1]), vertices.get(f[2]))
305 else {
306 continue;
307 };
308 if let Some(t) = triangle_hit(o, d, &v0, &v1, &v2)
309 && best.is_none_or(|(bt, _)| t < bt)
310 {
311 let n = (v1 - v0).cross(v2 - v0);
312 let n = n.normalize();
313 best = Some((t, if n.dot(d) < 0.0 { n } else { -n }));
314 }
315 }
316 best
317}
318
319fn triangle_hit(o: &Vec3, d: &Vec3, v0: &Vec3, v1: &Vec3, v2: &Vec3) -> Option<f64> {
321 const EPS: f64 = 1e-12;
322 let e1 = *v1 - *v0;
323 let e2 = *v2 - *v0;
324 let p = d.cross(e2);
325 let det = e1.dot(p);
326 if det.abs() < EPS {
327 return None;
328 }
329 let inv_det = 1.0 / det;
330 let tvec = *o - *v0;
331 let u = tvec.dot(p) * inv_det;
332 if !(0.0..=1.0).contains(&u) {
333 return None;
334 }
335 let q = tvec.cross(e1);
336 let v = d.dot(q) * inv_det;
337 if v < 0.0 || u + v > 1.0 {
338 return None;
339 }
340 let t = e2.dot(q) * inv_det;
341 if t < T_EPS { None } else { Some(t) }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 const EPS: f64 = 1e-9;
349
350 fn ray(o: [f64; 3], d: [f64; 3]) -> Ray {
351 Ray::new(Vec3::new(o[0], o[1], o[2]), Vec3::new(d[0], d[1], d[2])).unwrap()
352 }
353
354 #[test]
355 fn zero_direction_rejected() {
356 assert!(Ray::new(Vec3::zeros(), Vec3::zeros()).is_none());
357 }
358
359 #[test]
360 fn sphere_known_distance() {
361 let g = Geometry::Sphere { radius: 1.0 };
364 let hit = ray_intersect(
365 &g,
366 &Vec3::new(5.0, 0.0, 0.0),
367 &Mat3::identity(),
368 &ray([0.0; 3], [1.0, 0.0, 0.0]),
369 )
370 .expect("ray should hit sphere");
371 assert!((hit.distance - 4.0).abs() < EPS, "got {}", hit.distance);
372 assert!((hit.point.x - 4.0).abs() < EPS);
373 assert!((hit.normal.x + 1.0).abs() < EPS);
375 }
376
377 #[test]
378 fn sphere_tangent_miss() {
379 let g = Geometry::Sphere { radius: 1.0 };
380 let hit = ray_intersect(
382 &g,
383 &Vec3::new(5.0, 1.5, 0.0),
384 &Mat3::identity(),
385 &ray([0.0; 3], [1.0, 0.0, 0.0]),
386 );
387 assert!(hit.is_none());
388 }
389
390 #[test]
391 fn sphere_behind_ray_misses() {
392 let g = Geometry::Sphere { radius: 1.0 };
393 let hit = ray_intersect(
394 &g,
395 &Vec3::new(-5.0, 0.0, 0.0),
396 &Mat3::identity(),
397 &ray([0.0; 3], [1.0, 0.0, 0.0]),
398 );
399 assert!(hit.is_none());
400 }
401
402 #[test]
403 fn ray_from_inside_sphere_exits() {
404 let g = Geometry::Sphere { radius: 2.0 };
405 let hit = ray_intersect(
406 &g,
407 &Vec3::zeros(),
408 &Mat3::identity(),
409 &ray([0.0; 3], [1.0, 0.0, 0.0]),
410 )
411 .unwrap();
412 assert!((hit.distance - 2.0).abs() < EPS);
413 }
414
415 #[test]
416 fn box_face_and_corner() {
417 let g = Geometry::Box {
418 half_extents: Vec3::new(1.0, 1.0, 1.0),
419 };
420 let hit = ray_intersect(
421 &g,
422 &Vec3::new(0.0, 0.0, 10.0),
423 &Mat3::identity(),
424 &ray([0.0; 3], [0.0, 0.0, 1.0]),
425 )
426 .unwrap();
427 assert!((hit.distance - 9.0).abs() < EPS, "got {}", hit.distance);
428 assert!((hit.normal.z + 1.0).abs() < EPS);
429
430 let rot = Mat3::rotation_z(std::f64::consts::FRAC_PI_4);
433 let hit = ray_intersect(
434 &g,
435 &Vec3::new(10.0, 0.0, 0.0),
436 &rot,
437 &ray([0.0; 3], [1.0, 0.0, 0.0]),
438 )
439 .unwrap();
440 assert!(
441 (hit.distance - (10.0 - 2f64.sqrt())).abs() < 1e-9,
442 "got {}",
443 hit.distance
444 );
445 }
446
447 #[test]
448 fn cylinder_side_and_cap() {
449 let g = Geometry::Cylinder {
450 radius: 0.5,
451 height: 2.0,
452 };
453 let hit = ray_intersect(
455 &g,
456 &Vec3::new(3.0, 0.0, 0.0),
457 &Mat3::identity(),
458 &ray([0.0; 3], [1.0, 0.0, 0.0]),
459 )
460 .unwrap();
461 assert!((hit.distance - 2.5).abs() < EPS, "got {}", hit.distance);
462
463 let hit = ray_intersect(
465 &g,
466 &Vec3::new(0.0, 0.0, 5.0),
467 &Mat3::identity(),
468 &ray([0.0; 3], [0.0, 0.0, 1.0]),
469 )
470 .unwrap();
471 assert!((hit.distance - 4.0).abs() < EPS, "got {}", hit.distance);
472 assert!((hit.normal.z + 1.0).abs() < EPS);
473
474 assert!(
476 ray_intersect(
477 &g,
478 &Vec3::new(3.0, 0.0, 0.0),
479 &Mat3::identity(),
480 &ray([0.0, 0.0, 1.5], [1.0, 0.0, 0.0]),
481 )
482 .is_none()
483 );
484 }
485
486 #[test]
487 fn capsule_cap_is_rounded() {
488 let g = Geometry::Capsule {
491 radius: 0.5,
492 length: 2.0,
493 };
494 let hit = ray_intersect(
495 &g,
496 &Vec3::zeros(),
497 &Mat3::identity(),
498 &ray([0.0, 0.0, 5.0], [0.0, 0.0, -1.0]),
499 )
500 .unwrap();
501 assert!((hit.distance - 3.5).abs() < EPS, "got {}", hit.distance);
502 }
503
504 #[test]
505 fn plane_hit_distance() {
506 let g = Geometry::Plane {
507 normal: Vec3::new(0.0, 0.0, 1.0),
508 };
509 let hit = ray_intersect(
510 &g,
511 &Vec3::zeros(),
512 &Mat3::identity(),
513 &ray([0.0, 0.0, 3.0], [0.0, 0.0, -1.0]),
514 )
515 .unwrap();
516 assert!((hit.distance - 3.0).abs() < EPS);
517 assert!((hit.normal.z - 1.0).abs() < EPS);
518 }
519
520 #[test]
521 fn mesh_triangle_hit() {
522 let g = Geometry::Mesh {
523 vertices: vec![
524 Vec3::new(-1.0, -1.0, 0.0),
525 Vec3::new(1.0, -1.0, 0.0),
526 Vec3::new(0.0, 1.0, 0.0),
527 ],
528 faces: vec![[0, 1, 2]],
529 };
530 let hit = ray_intersect(
531 &g,
532 &Vec3::new(0.0, 0.0, 2.0),
533 &Mat3::identity(),
534 &ray([0.0, 0.0, 10.0], [0.0, 0.0, -1.0]),
535 )
536 .unwrap();
537 assert!((hit.distance - 8.0).abs() < EPS, "got {}", hit.distance);
538 }
539
540 #[test]
541 fn ray_cast_picks_nearest_within_range() {
542 let near = Geometry::Sphere { radius: 0.5 };
543 let far = Geometry::Sphere { radius: 0.5 };
544 let shapes = vec![
545 (&far, Vec3::new(8.0, 0.0, 0.0), Mat3::identity()),
546 (&near, Vec3::new(3.0, 0.0, 0.0), Mat3::identity()),
547 ];
548 let r = ray([0.0; 3], [1.0, 0.0, 0.0]);
549
550 let (idx, hit) = ray_cast(shapes.clone(), &r, 100.0).unwrap();
551 assert_eq!(idx, 1);
552 assert!((hit.distance - 2.5).abs() < EPS);
553
554 assert!(ray_cast(shapes.clone(), &r, 1.0).is_none());
556 let (idx, _) = ray_cast(shapes, &r, 3.0).unwrap();
559 assert_eq!(idx, 1);
560 }
561}