Skip to main content

scenix_math/
ray.rs

1use crate::{Aabb, EPSILON, Sphere, Vec2, Vec3, sqrt};
2
3/// A normalized 3D ray.
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Ray3 {
7    /// Ray origin.
8    pub origin: Vec3,
9    /// Normalized ray direction.
10    pub direction: Vec3,
11}
12
13impl Ray3 {
14    /// Creates a ray and normalizes the direction.
15    #[inline]
16    pub fn new(origin: Vec3, direction: Vec3) -> Self {
17        Self {
18            origin,
19            direction: direction.normalize(),
20        }
21    }
22
23    /// Returns the point at parametric distance `t`.
24    #[inline]
25    pub fn at(self, t: f32) -> Vec3 {
26        self.origin + self.direction * t
27    }
28
29    /// Intersects this ray with an AABB and returns nearest non-negative `t`.
30    pub fn intersect_aabb(self, aabb: Aabb) -> Option<f32> {
31        let mut t_min = 0.0_f32;
32        let mut t_max = f32::INFINITY;
33
34        for axis in 0..3 {
35            let origin = self.origin[axis];
36            let direction = self.direction[axis];
37            let min = aabb.min[axis];
38            let max = aabb.max[axis];
39
40            if direction.abs() <= EPSILON {
41                if origin < min || origin > max {
42                    return None;
43                }
44                continue;
45            }
46
47            let inv = 1.0 / direction;
48            let mut t1 = (min - origin) * inv;
49            let mut t2 = (max - origin) * inv;
50            if t1 > t2 {
51                core::mem::swap(&mut t1, &mut t2);
52            }
53            t_min = t_min.max(t1);
54            t_max = t_max.min(t2);
55            if t_min > t_max {
56                return None;
57            }
58        }
59
60        Some(t_min)
61    }
62
63    /// Intersects this ray with a sphere and returns nearest non-negative `t`.
64    pub fn intersect_sphere(self, center: Vec3, radius: f32) -> Option<f32> {
65        self.intersect_bounding_sphere(Sphere::new(center, radius))
66    }
67
68    /// Intersects this ray with a sphere and returns nearest non-negative `t`.
69    pub fn intersect_bounding_sphere(self, sphere: Sphere) -> Option<f32> {
70        let oc = self.origin - sphere.center;
71        let a = self.direction.dot(self.direction);
72        let b = 2.0 * oc.dot(self.direction);
73        let c = oc.dot(oc) - sphere.radius * sphere.radius;
74        let discriminant = b * b - 4.0 * a * c;
75        if discriminant < 0.0 || a.abs() <= EPSILON {
76            return None;
77        }
78        let root = sqrt(discriminant);
79        let t0 = (-b - root) / (2.0 * a);
80        let t1 = (-b + root) / (2.0 * a);
81        if t0 >= 0.0 {
82            Some(t0)
83        } else if t1 >= 0.0 {
84            Some(t1)
85        } else {
86            None
87        }
88    }
89
90    /// Intersects this ray with a triangle using Moller-Trumbore.
91    ///
92    /// Returns `(t, Vec2::new(u, v))`, where `u` and `v` are barycentric
93    /// coordinates and `w = 1 - u - v`.
94    pub fn intersect_triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, Vec2)> {
95        let edge1 = b - a;
96        let edge2 = c - a;
97        let pvec = self.direction.cross(edge2);
98        let det = edge1.dot(pvec);
99        if det.abs() <= EPSILON {
100            return None;
101        }
102
103        let inv_det = 1.0 / det;
104        let tvec = self.origin - a;
105        let u = tvec.dot(pvec) * inv_det;
106        if !(0.0..=1.0).contains(&u) {
107            return None;
108        }
109
110        let qvec = tvec.cross(edge1);
111        let v = self.direction.dot(qvec) * inv_det;
112        if v < 0.0 || u + v > 1.0 {
113            return None;
114        }
115
116        let t = edge2.dot(qvec) * inv_det;
117        if t >= 0.0 {
118            Some((t, Vec2::new(u, v)))
119        } else {
120            None
121        }
122    }
123}
124
125impl Default for Ray3 {
126    #[inline]
127    fn default() -> Self {
128        Self::new(Vec3::ZERO, Vec3::NEG_Z)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::assert_close;
136
137    #[test]
138    fn ray_intersects_aabb() {
139        let ray = Ray3::new(Vec3::new(0.0, 0.0, 5.0), Vec3::NEG_Z);
140        let aabb = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
141        assert_close(ray.intersect_aabb(aabb).unwrap(), 4.0);
142    }
143
144    #[test]
145    fn ray_intersects_sphere() {
146        let ray = Ray3::new(Vec3::new(0.0, 0.0, 5.0), Vec3::NEG_Z);
147        assert_close(ray.intersect_sphere(Vec3::ZERO, 1.0).unwrap(), 4.0);
148    }
149
150    #[test]
151    fn ray_intersects_triangle_with_barycentric_coordinates() {
152        let ray = Ray3::new(Vec3::new(0.25, 0.25, 1.0), Vec3::NEG_Z);
153        let (t, uv) = ray
154            .intersect_triangle(Vec3::ZERO, Vec3::X, Vec3::Y)
155            .unwrap();
156        assert_close(t, 1.0);
157        assert_close(uv.x, 0.25);
158        assert_close(uv.y, 0.25);
159    }
160}