Skip to main content

gizmo_math/
ray.rs

1use glam::{Quat, Vec3, Vec3A};
2
3#[derive(Debug, Clone, Copy)]
4pub struct Ray {
5    pub origin: Vec3A,
6    pub direction: Vec3A, // Normalize edilmiş olmalı
7}
8
9impl Ray {
10    #[inline]
11    pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Vec3A>) -> Self {
12        let dir = direction.into().normalize();
13        debug_assert!(dir.is_finite(), "Ray direction must be non-zero");
14        Self {
15            origin: origin.into(),
16            direction: dir,
17        }
18    }
19
20    /// Işının uzayda `t` uzaklığındaki ulaştığı (çarpıştığı) kesin noktayı hesaplar.
21    #[inline]
22    pub fn at(self, t: f32) -> Vec3A {
23        self.origin + self.direction * t
24    }
25
26    /// Bir eksen kısıtlı boundary kutusuyla kesişim testi yapar (Slab Algorithm).
27    /// Kesişiyorsa t_near mesafesini döner, kesişmiyorsa None döner.
28    #[inline]
29    pub fn intersect_bounds(self, min: Vec3A, max: Vec3A) -> Option<f32> {
30        let inv_dir = self.direction.recip();
31
32        let t0 = (min - self.origin) * inv_dir;
33        let t1 = (max - self.origin) * inv_dir;
34
35        let tmin_vec = t0.min(t1);
36        let tmax_vec = t0.max(t1);
37
38        let tmin = tmin_vec.x.max(tmin_vec.y).max(tmin_vec.z);
39        let tmax = tmax_vec.x.min(tmax_vec.y).min(tmax_vec.z);
40
41        if tmin <= tmax && tmax > 0.0 {
42            Some(if tmin > 0.0 { tmin } else { tmax })
43        } else {
44            None
45        }
46    }
47
48    /// Bir Aabb nesnesiyle (Axis-Aligned Bounding Box) doğrudan kesişim testi yapar.
49    #[inline]
50    pub fn intersect_aabb(self, aabb: crate::aabb::Aabb) -> Option<f32> {
51        self.intersect_bounds(aabb.min, aabb.max)
52    }
53
54    /// Möller–Trumbore algoritması kullanarak bir üçgenle hassas kesişim (Mesh Raycasting) testi yapar.
55    /// Kesişiyorsa t_near mesafesini döner, aksi halde None döner.
56    #[inline]
57    pub fn intersect_triangle(
58        self,
59        v0: impl Into<Vec3A>,
60        v1: impl Into<Vec3A>,
61        v2: impl Into<Vec3A>,
62    ) -> Option<f32> {
63        let v0 = v0.into();
64        let v1 = v1.into();
65        let v2 = v2.into();
66        let edge1 = v1 - v0;
67        let edge2 = v2 - v0;
68
69        let h = self.direction.cross(edge2);
70        let a = edge1.dot(h);
71
72        // Culling backfaces and parallel rays
73        if a.abs() < 1e-8 {
74            return None;
75        }
76
77        let f = 1.0 / a;
78        let s = self.origin - v0;
79        let u = f * s.dot(h);
80
81        if !(0.0..=1.0).contains(&u) {
82            return None;
83        }
84
85        let q = s.cross(edge1);
86        let v = f * self.direction.dot(q);
87
88        if v < 0.0 || u + v > 1.0 {
89            return None;
90        }
91
92        let t = f * edge2.dot(q);
93
94        if t > 1e-8 {
95            Some(t)
96        } else {
97            None
98        }
99    }
100
101    /// Bir OBB (Oriented Bounding Box) kutusuyla kesişim testi yapar.
102    /// Kesişiyorsa t_near mesafesini döner, kesişmiyorsa None döner.
103    #[inline]
104    pub fn intersect_obb(
105        self,
106        center: impl Into<Vec3A>,
107        half_extents: impl Into<Vec3A>,
108        rotation: Quat,
109    ) -> Option<f32> {
110        let c = center.into();
111        let he = half_extents.into();
112        let inv_rot = rotation.inverse();
113
114        // Işını OBB'nin yerel uzayına çeviriyoruz.
115        // Quat * Vec3A dönüşümü bulunmadığı için Vec3 üzerinden yapıp Vec3A'ya cast etmeliyiz.
116        let local_origin = Vec3A::from(inv_rot * Vec3::from(self.origin - c));
117        let local_direction = Vec3A::from(inv_rot * Vec3::from(self.direction));
118
119        // Quat dönüşümü uzunluğu koruduğu için direction zaten normalize edilmiştir.
120        // Performans için gereksiz normalize() ve debug_assert! çağrılarından kaçınarak doğrudan struct oluşturuyoruz.
121        let local_ray = Ray {
122            origin: local_origin,
123            direction: local_direction,
124        };
125
126        // Yerel koordinatlarda AABB testi
127        local_ray.intersect_bounds(-he, he)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_ray_intersect_aabb_hit() {
137        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
138        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
139
140        let t = ray.intersect_aabb(aabb);
141        assert!(t.is_some());
142        assert!((t.unwrap() - 4.0).abs() < 1e-5); // Hits the front face at z = -1, origin is -5, distance is 4
143    }
144
145    #[test]
146    fn test_ray_intersect_aabb_miss() {
147        let ray = Ray::new(Vec3::new(0.0, 5.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
148        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
149
150        let t = ray.intersect_aabb(aabb);
151        assert!(t.is_none());
152    }
153
154    #[test]
155    fn test_ray_intersect_aabb_inside() {
156        let ray = Ray::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
157        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
158
159        let t = ray.intersect_aabb(aabb);
160        assert!(t.is_some());
161        assert!((t.unwrap() - 1.0).abs() < 1e-5); // Inside the box, hits the back face at z = 1, distance is 1
162    }
163
164    #[test]
165    fn test_ray_intersect_aabb_behind() {
166        let ray = Ray::new(Vec3::new(0.0, 0.0, 5.0), Vec3::new(0.0, 0.0, 1.0));
167        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
168
169        let t = ray.intersect_aabb(aabb);
170        assert!(t.is_none()); // The box is strictly behind the ray origin
171    }
172
173    #[test]
174    fn test_ray_intersect_obb() {
175        let origin = Vec3::new(0.0, 0.0, -5.0);
176        let direction = Vec3::new(0.0, 0.0, 1.0);
177        let ray = Ray::new(origin, direction);
178
179        let obb_center = Vec3::new(0.0, 0.0, 0.0);
180        let obb_extents = Vec3::new(1.0, 1.0, 1.0);
181
182        // 45 degrees rotated around Y
183        let rot = Quat::from_rotation_y(std::f32::consts::FRAC_PI_4);
184
185        let t = ray.intersect_obb(obb_center, obb_extents, rot);
186        assert!(t.is_some());
187
188        // Since OBB is rotated by 45 degrees, ray hits the tilted face earlier.
189        // Unrotated distance is 4.0. With 45 degree tilt, the half-diagonal length is sqrt(2), so front face is at -sqrt(2).
190        // 5.0 - 1.414 = approx 3.585
191        assert!((t.unwrap() - (5.0 - std::f32::consts::SQRT_2)).abs() < 1e-4);
192    }
193
194    #[test]
195    fn test_ray_parallel_hit() {
196        // Parallel ray moving exactly along the Y-axis (Direction X and Z are exactly ZERO)
197        let ray = Ray::new(Vec3::new(0.0, -5.0, 0.0), Vec3::Y);
198        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
199
200        // This hit should register and perfectly calculate intersections without Div-By-Zero NaNs
201        let t = ray.intersect_aabb(aabb);
202        assert!(t.is_some());
203        assert!((t.unwrap() - 4.0).abs() < 1e-5);
204    }
205
206    #[test]
207    fn test_ray_parallel_miss() {
208        // Parallel ray moving exactly along the Y-axis but offset completely outside the AABB X-range
209        let ray_miss = Ray::new(Vec3::new(5.0, -5.0, 0.0), Vec3::Y);
210        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
211
212        let t_miss = ray_miss.intersect_aabb(aabb);
213        assert!(t_miss.is_none());
214    }
215}