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    /// NDC (Normalized Device Coordinates) uzayından 3B Dünya (World) uzayına bir Ray oluşturur.
21    /// `ndc`: [-1.0, 1.0] aralığında ekran koordinatları.
22    /// `view_proj_inv`: (Projection * View) matrisinin tersi.
23    #[inline]
24    pub fn from_ndc(ndc: glam::Vec2, view_proj_inv: glam::Mat4) -> Self {
25        // WGPU standardında NDC depth 0.0 (near) ile 1.0 (far) arasındadır.
26        let near_ndc = glam::Vec4::new(ndc.x, ndc.y, 0.0, 1.0);
27        let far_ndc = glam::Vec4::new(ndc.x, ndc.y, 1.0, 1.0);
28
29        let mut near_world = view_proj_inv * near_ndc;
30        debug_assert!(near_world.w.abs() > 1e-10, "Ray::from_ndc: degenerate near w (singular VP inverse?)");
31        near_world /= near_world.w;
32        
33        let mut far_world = view_proj_inv * far_ndc;
34        debug_assert!(far_world.w.abs() > 1e-10, "Ray::from_ndc: degenerate far w (singular VP inverse?)");
35        far_world /= far_world.w;
36
37        let origin = near_world.truncate();
38        let direction = (far_world.truncate() - origin).normalize();
39
40        Self::new(origin, direction)
41    }
42
43    /// Işının uzayda `t` uzaklığındaki ulaştığı (çarpıştığı) kesin noktayı hesaplar.
44    #[inline]
45    pub fn at(self, t: f32) -> Vec3A {
46        self.origin + self.direction * t
47    }
48
49    /// Bir eksen kısıtlı boundary kutusuyla kesişim testi yapar (Slab Algorithm).
50    /// Kesişiyorsa t_near mesafesini döner, kesişmiyorsa None döner.
51    #[inline]
52    pub fn intersect_bounds(self, min: Vec3A, max: Vec3A) -> Option<f32> {
53        let inv_dir = self.direction.recip();
54
55        let t0 = (min - self.origin) * inv_dir;
56        let t1 = (max - self.origin) * inv_dir;
57
58        let tmin_vec = t0.min(t1);
59        let tmax_vec = t0.max(t1);
60
61        let tmin = tmin_vec.x.max(tmin_vec.y).max(tmin_vec.z);
62        let tmax = tmax_vec.x.min(tmax_vec.y).min(tmax_vec.z);
63
64        if tmin <= tmax && tmax > 0.0 {
65            Some(if tmin > 0.0 { tmin } else { tmax })
66        } else {
67            None
68        }
69    }
70
71    /// Bir Aabb nesnesiyle (Axis-Aligned Bounding Box) doğrudan kesişim testi yapar.
72    #[inline]
73    pub fn intersect_aabb(self, aabb: crate::aabb::Aabb) -> Option<f32> {
74        self.intersect_bounds(aabb.min, aabb.max)
75    }
76
77    /// Möller–Trumbore algoritması kullanarak bir üçgenle hassas kesişim (Mesh Raycasting) testi yapar.
78    /// Kesişiyorsa t_near mesafesini döner, aksi halde None döner.
79    #[inline]
80    pub fn intersect_triangle(
81        self,
82        v0: impl Into<Vec3A>,
83        v1: impl Into<Vec3A>,
84        v2: impl Into<Vec3A>,
85    ) -> Option<f32> {
86        let v0 = v0.into();
87        let v1 = v1.into();
88        let v2 = v2.into();
89        let edge1 = v1 - v0;
90        let edge2 = v2 - v0;
91
92        let h = self.direction.cross(edge2);
93        let a = edge1.dot(h);
94
95        // Culling backfaces and parallel rays
96        if a.abs() < 1e-8 {
97            return None;
98        }
99
100        let f = 1.0 / a;
101        let s = self.origin - v0;
102        let u = f * s.dot(h);
103
104        if !(0.0..=1.0).contains(&u) {
105            return None;
106        }
107
108        let q = s.cross(edge1);
109        let v = f * self.direction.dot(q);
110
111        if v < 0.0 || u + v > 1.0 {
112            return None;
113        }
114
115        let t = f * edge2.dot(q);
116
117        if t > 1e-8 {
118            Some(t)
119        } else {
120            None
121        }
122    }
123
124    /// Bir OBB (Oriented Bounding Box) kutusuyla kesişim testi yapar.
125    /// Kesişiyorsa t_near mesafesini döner, kesişmiyorsa None döner.
126    #[inline]
127    pub fn intersect_obb(
128        self,
129        center: impl Into<Vec3A>,
130        half_extents: impl Into<Vec3A>,
131        rotation: Quat,
132    ) -> Option<f32> {
133        let c = center.into();
134        let he = half_extents.into();
135        let inv_rot = rotation.inverse();
136
137        // Işını OBB'nin yerel uzayına çeviriyoruz.
138        // Quat * Vec3A dönüşümü bulunmadığı için Vec3 üzerinden yapıp Vec3A'ya cast etmeliyiz.
139        let local_origin = Vec3A::from(inv_rot * Vec3::from(self.origin - c));
140        let local_direction = Vec3A::from(inv_rot * Vec3::from(self.direction));
141
142        // Quat dönüşümü uzunluğu koruduğu için direction zaten normalize edilmiştir.
143        // Performans için gereksiz normalize() ve debug_assert! çağrılarından kaçınarak doğrudan struct oluşturuyoruz.
144        let local_ray = Ray {
145            origin: local_origin,
146            direction: local_direction,
147        };
148
149        // Yerel koordinatlarda AABB testi
150        local_ray.intersect_bounds(-he, he)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_ray_intersect_aabb_hit() {
160        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
161        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
162
163        let t = ray.intersect_aabb(aabb);
164        assert!(t.is_some());
165        assert!((t.unwrap() - 4.0).abs() < 1e-5); // Hits the front face at z = -1, origin is -5, distance is 4
166    }
167
168    #[test]
169    fn test_ray_intersect_aabb_miss() {
170        let ray = Ray::new(Vec3::new(0.0, 5.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
171        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
172
173        let t = ray.intersect_aabb(aabb);
174        assert!(t.is_none());
175    }
176
177    #[test]
178    fn test_ray_intersect_aabb_inside() {
179        let ray = Ray::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
180        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
181
182        let t = ray.intersect_aabb(aabb);
183        assert!(t.is_some());
184        assert!((t.unwrap() - 1.0).abs() < 1e-5); // Inside the box, hits the back face at z = 1, distance is 1
185    }
186
187    #[test]
188    fn test_ray_intersect_aabb_behind() {
189        let ray = Ray::new(Vec3::new(0.0, 0.0, 5.0), Vec3::new(0.0, 0.0, 1.0));
190        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
191
192        let t = ray.intersect_aabb(aabb);
193        assert!(t.is_none()); // The box is strictly behind the ray origin
194    }
195
196    #[test]
197    fn test_ray_intersect_obb() {
198        let origin = Vec3::new(0.0, 0.0, -5.0);
199        let direction = Vec3::new(0.0, 0.0, 1.0);
200        let ray = Ray::new(origin, direction);
201
202        let obb_center = Vec3::new(0.0, 0.0, 0.0);
203        let obb_extents = Vec3::new(1.0, 1.0, 1.0);
204
205        // 45 degrees rotated around Y
206        let rot = Quat::from_rotation_y(std::f32::consts::FRAC_PI_4);
207
208        let t = ray.intersect_obb(obb_center, obb_extents, rot);
209        assert!(t.is_some());
210
211        // Since OBB is rotated by 45 degrees, ray hits the tilted face earlier.
212        // Unrotated distance is 4.0. With 45 degree tilt, the half-diagonal length is sqrt(2), so front face is at -sqrt(2).
213        // 5.0 - 1.414 = approx 3.585
214        assert!((t.unwrap() - (5.0 - std::f32::consts::SQRT_2)).abs() < 1e-4);
215    }
216
217    #[test]
218    fn test_ray_parallel_hit() {
219        // Parallel ray moving exactly along the Y-axis (Direction X and Z are exactly ZERO)
220        let ray = Ray::new(Vec3::new(0.0, -5.0, 0.0), Vec3::Y);
221        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
222
223        // This hit should register and perfectly calculate intersections without Div-By-Zero NaNs
224        let t = ray.intersect_aabb(aabb);
225        assert!(t.is_some());
226        assert!((t.unwrap() - 4.0).abs() < 1e-5);
227    }
228
229    #[test]
230    fn test_ray_parallel_miss() {
231        // Parallel ray moving exactly along the Y-axis but offset completely outside the AABB X-range
232        let ray_miss = Ray::new(Vec3::new(5.0, -5.0, 0.0), Vec3::Y);
233        let aabb = crate::aabb::Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
234
235        let t_miss = ray_miss.intersect_aabb(aabb);
236        assert!(t_miss.is_none());
237    }
238    #[test]
239    fn test_ray_from_ndc() {
240        let view = glam::Mat4::look_at_rh(
241            Vec3::new(0.0, 0.0, 10.0), // Camera at Z=10
242            Vec3::ZERO,                 // Looking at origin
243            Vec3::Y,                    // Up is Y
244        );
245        let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 100.0);
246        let view_proj_inv = (proj * view).inverse();
247
248        // Center pixel (NDC = 0,0)
249        let ray_center = Ray::from_ndc(glam::Vec2::new(0.0, 0.0), view_proj_inv);
250        
251        // Origins usually lie on the near plane.
252        // The camera is at Z=10, looking at Z=0. Direction should be -Z.
253        assert!((ray_center.direction.z - (-1.0)).abs() < 1e-5);
254        assert!(ray_center.direction.x.abs() < 1e-5);
255        assert!(ray_center.direction.y.abs() < 1e-5);
256    }
257}