#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ray {
pub origin: [f32; 3],
pub direction: [f32; 3],
}
impl Ray {
pub fn new(origin: [f32; 3], direction: [f32; 3]) -> Self {
Self { origin, direction }
}
pub fn point_at(self, t: f32) -> [f32; 3] {
[
self.origin[0] + t * self.direction[0],
self.origin[1] + t * self.direction[1],
self.origin[2] + t * self.direction[2],
]
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RayHit {
pub t: f32,
pub triangle_index: usize,
pub barycentric: [f32; 3],
pub front_face: bool,
}
pub fn intersect_triangle(
ray: Ray,
p0: [f32; 3],
p1: [f32; 3],
p2: [f32; 3],
t_max: f32,
) -> Option<(f32, f32, f32, bool)> {
let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
let p_vec = cross(ray.direction, e2);
let det = dot(e1, p_vec);
if !det.is_finite() {
return None;
}
let eps = 1e-8_f32;
if det.abs() < eps {
return None;
}
let inv_det = 1.0 / det;
let s = [
ray.origin[0] - p0[0],
ray.origin[1] - p0[1],
ray.origin[2] - p0[2],
];
let u = dot(s, p_vec) * inv_det;
if !u.is_finite() || !(0.0..=1.0).contains(&u) {
return None;
}
let q = cross(s, e1);
let v = dot(ray.direction, q) * inv_det;
if !v.is_finite() || v < 0.0 || u + v > 1.0 {
return None;
}
let t = dot(e2, q) * inv_det;
if !t.is_finite() || t < 0.0 || t > t_max {
return None;
}
Some((t, u, v, det > 0.0))
}
pub fn intersect_aabb(ray: Ray, min: [f32; 3], max: [f32; 3], t_max: f32) -> Option<(f32, f32)> {
let mut t_enter = 0.0_f32;
let mut t_exit = t_max;
for axis in 0..3 {
let o = ray.origin[axis];
let d = ray.direction[axis];
let a = min[axis];
let b = max[axis];
if !o.is_finite() || !d.is_finite() || !a.is_finite() || !b.is_finite() {
return None;
}
if d.abs() < 1e-30 {
if o < a || o > b {
return None;
}
continue;
}
let inv_d = 1.0 / d;
let mut t0 = (a - o) * inv_d;
let mut t1 = (b - o) * inv_d;
if t0 > t1 {
std::mem::swap(&mut t0, &mut t1);
}
if t0 > t_enter {
t_enter = t0;
}
if t1 < t_exit {
t_exit = t1;
}
if t_enter > t_exit {
return None;
}
}
Some((t_enter, t_exit))
}
#[inline]
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn point_at_zero_is_origin() {
let r = Ray::new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
assert_eq!(r.point_at(0.0), [1.0, 2.0, 3.0]);
}
#[test]
fn point_at_one_is_origin_plus_direction() {
let r = Ray::new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
assert_eq!(r.point_at(1.0), [5.0, 7.0, 9.0]);
}
#[test]
fn triangle_centre_hit_from_back_side() {
let r = Ray::new([0.3333, 0.3333, 0.0], [0.0, 0.0, 1.0]);
let hit = intersect_triangle(
r,
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
f32::INFINITY,
)
.expect("centre hit");
assert!((hit.0 - 1.0).abs() < 1e-5, "t = {}", hit.0);
assert!((hit.1 - 0.3333).abs() < 1e-3);
assert!((hit.2 - 0.3333).abs() < 1e-3);
assert!(!hit.3, "ray along the normal hits from the back side");
}
#[test]
fn triangle_front_face_hit_from_above() {
let r = Ray::new([0.3333, 0.3333, 2.0], [0.0, 0.0, -1.0]);
let hit = intersect_triangle(
r,
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
f32::INFINITY,
)
.expect("front hit");
assert!(hit.3, "ray opposing the normal hits the front face");
}
#[test]
fn ray_parallel_to_triangle_misses() {
let r = Ray::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
assert!(intersect_triangle(
r,
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
f32::INFINITY,
)
.is_none());
}
#[test]
fn triangle_outside_simplex_misses() {
let r = Ray::new([2.0, 2.0, 0.0], [0.0, 0.0, 1.0]);
assert!(intersect_triangle(
r,
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
f32::INFINITY,
)
.is_none());
}
#[test]
fn behind_origin_misses() {
let r = Ray::new([0.3333, 0.3333, 2.0], [0.0, 0.0, 1.0]);
assert!(intersect_triangle(
r,
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
f32::INFINITY,
)
.is_none());
}
#[test]
fn t_max_cull() {
let r = Ray::new([0.3333, 0.3333, 0.0], [0.0, 0.0, 1.0]);
assert!(
intersect_triangle(r, [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0], 0.5,)
.is_none()
);
}
#[test]
fn aabb_axis_aligned_through_centre() {
let r = Ray::new([-1.0, 0.5, 0.5], [1.0, 0.0, 0.0]);
let hit = intersect_aabb(r, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], f32::INFINITY).unwrap();
assert!((hit.0 - 1.0).abs() < 1e-6, "enter t = {}", hit.0);
assert!((hit.1 - 2.0).abs() < 1e-6, "exit t = {}", hit.1);
}
#[test]
fn aabb_origin_inside_box() {
let r = Ray::new([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]);
let hit = intersect_aabb(r, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], f32::INFINITY).unwrap();
assert_eq!(hit.0, 0.0, "origin inside → t_enter = 0");
assert!((hit.1 - 0.5).abs() < 1e-6, "exit t = {}", hit.1);
}
#[test]
fn aabb_misses_to_the_side() {
let r = Ray::new([-1.0, 2.0, 0.5], [1.0, 0.0, 0.0]);
assert!(
intersect_aabb(r, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], f32::INFINITY).is_none(),
"ray passes outside the box in y"
);
}
#[test]
fn aabb_axis_parallel_inside_slab_passes() {
let r = Ray::new([0.5, -1.0, 0.5], [0.0, 1.0, 0.0]);
let hit = intersect_aabb(r, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], f32::INFINITY).unwrap();
assert!((hit.0 - 1.0).abs() < 1e-6);
assert!((hit.1 - 2.0).abs() < 1e-6);
}
#[test]
fn aabb_axis_parallel_outside_slab_misses() {
let r = Ray::new([2.0, -1.0, 0.5], [0.0, 1.0, 0.0]);
assert!(intersect_aabb(r, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], f32::INFINITY).is_none());
}
}