Skip to main content

gizmo_physics_core/
raycast.rs

1use crate::components::{ColliderShape, Transform};
2use crate::BodyHandle;
3use gizmo_math::Aabb;
4use gizmo_math::Vec3;
5
6/// Ray for raycasting
7#[derive(Debug, Clone, Copy)]
8pub struct Ray {
9    pub origin: Vec3,
10    pub direction: Vec3, // Should be normalized
11}
12
13impl Ray {
14    /// Yeni bir ışın oluşturur. `direction` içeride normalize edilir.
15    ///
16    /// Sıfır-uzunlukta (veya non-finite) bir yön vektörü verilirse, glam'in
17    /// `normalize()` çağrısı sessizce NaN/Inf üreterek bozuk bir Ray yaratır ve
18    /// sonraki tüm raycast'ler sahte sonuç döndürür. Bunu önlemek için
19    /// `try_normalize()` kullanılır ve normalize edilemeyen yönlerde güvenli bir
20    /// varsayılan (`Vec3::Z`) seçilir; böylece Ray her zaman geçerli, sonlu bir
21    /// birim yöne sahip olur. Geçerli (sıfır olmayan) yönlerde davranış değişmez.
22    pub fn new(origin: Vec3, direction: Vec3) -> Self {
23        Self {
24            origin,
25            direction: direction.try_normalize().unwrap_or(Vec3::Z),
26        }
27    }
28
29    pub fn point_at(&self, t: f32) -> Vec3 {
30        self.origin + self.direction * t
31    }
32}
33
34/// Result of a raycast hit
35#[derive(Debug, Clone, Copy)]
36pub struct RaycastHit {
37    pub entity: BodyHandle,
38    pub point: Vec3,
39    pub normal: Vec3,
40    pub distance: f32,
41}
42
43/// Raycast query system
44pub struct Raycast;
45
46impl Raycast {
47    /// Test ray against AABB
48    pub fn ray_aabb(ray: &Ray, aabb: &Aabb) -> Option<f32> {
49        // tmin gerçek giriş (negatif olabilir), tmax çıkış. (Eskiden tmin=0'dan
50        // başlıyordu → ışın kutunun İÇİNDE başlarsa t=0 dönüp `origin` yüzey üstünde
51        // olmadığından çağıran sahte normal üretiyordu.)
52        let mut tmin: f32 = f32::NEG_INFINITY;
53        let mut tmax = f32::INFINITY;
54
55        for i in 0..3 {
56            let origin = match i {
57                0 => ray.origin.x,
58                1 => ray.origin.y,
59                _ => ray.origin.z,
60            };
61            let dir = match i {
62                0 => ray.direction.x,
63                1 => ray.direction.y,
64                _ => ray.direction.z,
65            };
66            let min = match i {
67                0 => aabb.min.x,
68                1 => aabb.min.y,
69                _ => aabb.min.z,
70            };
71            let max = match i {
72                0 => aabb.max.x,
73                1 => aabb.max.y,
74                _ => aabb.max.z,
75            };
76
77            if dir.abs() < 1e-8 {
78                // Ray is parallel to slab
79                if origin < min || origin > max {
80                    return None;
81                }
82            } else {
83                let inv_d = 1.0 / dir;
84                let mut t1 = (min - origin) * inv_d;
85                let mut t2 = (max - origin) * inv_d;
86
87                if t1 > t2 {
88                    std::mem::swap(&mut t1, &mut t2);
89                }
90
91                tmin = tmin.max(t1);
92                tmax = tmax.min(t2);
93
94                if tmin > tmax {
95                    return None;
96                }
97            }
98        }
99
100        if tmax < 0.0 {
101            return None; // tüm kutu ışının arkasında
102        }
103        // İçeriden başlama: tmin<0 ise çıkış yüzeyini (tmax) döndür → geçerli yüzey noktası/normal.
104        Some(if tmin < 0.0 { tmax } else { tmin })
105    }
106
107    /// Test ray against sphere
108    pub fn ray_sphere(ray: &Ray, center: Vec3, radius: f32) -> Option<(f32, Vec3)> {
109        let oc = ray.origin - center;
110        let b = oc.dot(ray.direction);
111        let c = oc.dot(oc) - radius * radius;
112        let discriminant = b * b - c;
113
114        if discriminant < 0.0 {
115            return None;
116        }
117
118        let sqrt_d = discriminant.sqrt();
119        let t1 = -b - sqrt_d;
120        let t2 = -b + sqrt_d;
121
122        let t = if t1 > 0.0 {
123            t1
124        } else if t2 > 0.0 {
125            t2
126        } else {
127            return None;
128        };
129
130        let hit_point = ray.point_at(t);
131        let normal = (hit_point - center).try_normalize().unwrap_or(Vec3::Y);
132
133        Some((t, normal))
134    }
135
136    /// Test ray against box (OBB)
137    pub fn ray_box(
138        ray: &Ray,
139        center: Vec3,
140        rotation: gizmo_math::Quat,
141        half_extents: Vec3,
142    ) -> Option<(f32, Vec3)> {
143        // Transform ray to box's local space
144        let inv_rot = rotation.inverse();
145        let local_origin = inv_rot * (ray.origin - center);
146        let local_dir = inv_rot * ray.direction;
147
148        let local_ray = Ray::new(local_origin, local_dir);
149
150        // Create AABB in local space
151        let local_aabb = Aabb::from_center_half_extents(Vec3::ZERO, half_extents);
152
153        if let Some(t) = Self::ray_aabb(&local_ray, &local_aabb) {
154            let local_hit = local_ray.point_at(t);
155
156            // Calculate normal in local space
157            let mut normal = Vec3::ZERO;
158
159            let epsilon = 1e-4;
160            for i in 0..3 {
161                if (local_hit[i] - half_extents[i]).abs() < epsilon {
162                    normal[i] = 1.0;
163                }
164                if (local_hit[i] + half_extents[i]).abs() < epsilon {
165                    normal[i] = -1.0;
166                }
167            }
168            normal = normal.try_normalize().unwrap_or(Vec3::Y);
169
170            // Transform normal back to world space
171            let world_normal = rotation * normal;
172
173            Some((t, world_normal))
174        } else {
175            None
176        }
177    }
178
179    /// Test ray against capsule
180    pub fn ray_capsule(
181        ray: &Ray,
182        center: Vec3,
183        rotation: gizmo_math::Quat,
184        radius: f32,
185        half_height: f32,
186    ) -> Option<(f32, Vec3)> {
187        // Transform to local space
188        let inv_rot = rotation.inverse();
189        let local_origin = inv_rot * (ray.origin - center);
190        let local_dir = inv_rot * ray.direction;
191
192        // Capsule is aligned along Y axis in local space
193        let p1 = Vec3::new(0.0, half_height, 0.0);
194        let p2 = Vec3::new(0.0, -half_height, 0.0);
195
196        // Ray-cylinder intersection
197        let ba = p2 - p1;
198        let oc = local_origin - p1;
199
200        let baba = ba.dot(ba);
201        let bard = ba.dot(local_dir);
202        let baoc = ba.dot(oc);
203
204        let k2 = baba - bard * bard;
205        let k1 = baba * oc.dot(local_dir) - baoc * bard;
206        let k0 = baba * oc.dot(oc) - baoc * baoc - radius * radius * baba;
207
208        if k2.abs() >= 1e-8 {
209            let h = k1 * k1 - k2 * k0;
210            if h >= 0.0 {
211                let t = (-k1 - h.sqrt()) / k2;
212                // Check if hit is within cylinder height AND in front of the ray.
213                // (`t > 0.0` eksikti: ışının ARKASINDAKİ kapsül negatif t ile sahte
214                // isabet döndürüyordu — küre-cap dalı zaten t>0 kontrol ediyor.)
215                let y = baoc + t * bard;
216                if t > 0.0 && y > 0.0 && y < baba {
217                    let hit_point = local_origin + local_dir * t;
218                    let normal = (hit_point - (p1 + ba * (y / baba)))
219                        .try_normalize()
220                        .unwrap_or(Vec3::Y);
221                    let world_normal = rotation * normal;
222                    return Some((t, world_normal));
223                }
224            }
225        }
226
227        // Check sphere caps
228        let mut best_t = f32::INFINITY;
229        let mut best_normal = Vec3::ZERO;
230
231        for &cap_center in &[p1, p2] {
232            let oc = local_origin - cap_center;
233            let a = local_dir.dot(local_dir);
234            let b = 2.0 * oc.dot(local_dir);
235            let c = oc.dot(oc) - radius * radius;
236            let discriminant = b * b - 4.0 * a * c;
237
238            if discriminant >= 0.0 {
239                let t = (-b - discriminant.sqrt()) / (2.0 * a);
240                if t > 0.0 && t < best_t {
241                    best_t = t;
242                    let hit = local_origin + local_dir * t;
243                    best_normal = (hit - cap_center).try_normalize().unwrap_or(Vec3::Y);
244                }
245            }
246        }
247
248        if best_t < f32::INFINITY {
249            let world_normal = rotation * best_normal;
250            Some((best_t, world_normal))
251        } else {
252            None
253        }
254    }
255
256    /// Test ray against collider shape
257    pub fn ray_shape(
258        ray: &Ray,
259        shape: &ColliderShape,
260        transform: &Transform,
261    ) -> Option<(f32, Vec3)> {
262        match shape {
263            ColliderShape::Sphere(s) => Self::ray_sphere(ray, transform.position, s.radius),
264            ColliderShape::Box(b) => {
265                Self::ray_box(ray, transform.position, transform.rotation, b.half_extents)
266            }
267            ColliderShape::Capsule(c) => Self::ray_capsule(
268                ray,
269                transform.position,
270                transform.rotation,
271                c.radius,
272                c.half_height,
273            ),
274            ColliderShape::Plane(p) => {
275                // Ray-plane intersection
276                let denom = ray.direction.dot(p.normal);
277                if denom.abs() > 1e-6 {
278                    let t = (p.distance - ray.origin.dot(p.normal)) / denom;
279                    if t >= 0.0 {
280                        let normal = if denom < 0.0 { p.normal } else { -p.normal };
281                        Some((t, normal))
282                    } else {
283                        None
284                    }
285                } else {
286                    None
287                }
288            }
289            ColliderShape::TriMesh(tm) => {
290                let mut best_t = f32::INFINITY;
291                let mut best_normal = Vec3::ZERO;
292                let inv_rot = transform.rotation.inverse();
293                let local_origin = inv_rot * (ray.origin - transform.position);
294                let local_dir = inv_rot * ray.direction;
295                let local_ray = Ray::new(local_origin, local_dir);
296
297                if !tm.bvh.nodes.is_empty() {
298                    let mut stack = Vec::with_capacity(64);
299                    stack.push(0); // root node
300
301                    while let Some(node_idx) = stack.pop() {
302                        let node = &tm.bvh.nodes[node_idx];
303
304                        // Check AABB
305                        if Self::ray_aabb(&local_ray, &node.aabb).is_none() {
306                            continue;
307                        }
308
309                        if node.is_leaf() {
310                            let start = (node.first_tri_index * 3) as usize;
311                            let end = start + (node.tri_count * 3) as usize;
312                            for i in (start..end).step_by(3) {
313                                let v0 = tm.vertices[tm.indices[i] as usize];
314                                let v1 = tm.vertices[tm.indices[i + 1] as usize];
315                                let v2 = tm.vertices[tm.indices[i + 2] as usize];
316
317                                let e1 = v1 - v0;
318                                let e2 = v2 - v0;
319                                let h = local_dir.cross(e2);
320                                let a = e1.dot(h);
321                                if a.abs() < 1e-6 {
322                                    continue;
323                                }
324                                let f = 1.0 / a;
325                                let s = local_origin - v0;
326                                let u = f * s.dot(h);
327                                if !(0.0..=1.0).contains(&u) {
328                                    continue;
329                                }
330                                let q = s.cross(e1);
331                                let v = f * local_dir.dot(q);
332                                if v < 0.0 || u + v > 1.0 {
333                                    continue;
334                                }
335                                let t = f * e2.dot(q);
336                                if t > 0.0 && t < best_t {
337                                    best_t = t;
338                                    best_normal = e1.cross(e2).try_normalize().unwrap_or(Vec3::Y);
339                                    if best_normal.dot(local_dir) > 0.0 {
340                                        best_normal = -best_normal;
341                                    }
342                                }
343                            }
344                        } else {
345                            if node.left_child >= 0 {
346                                stack.push(node.left_child as usize);
347                            }
348                            if node.right_child >= 0 {
349                                stack.push(node.right_child as usize);
350                            }
351                        }
352                    }
353                } else {
354                    // Fallback to naive loop if BVH is missing
355                    for chunk in tm.indices.chunks_exact(3) {
356                        let v0 = tm.vertices[chunk[0] as usize];
357                        let v1 = tm.vertices[chunk[1] as usize];
358                        let v2 = tm.vertices[chunk[2] as usize];
359                        let e1 = v1 - v0;
360                        let e2 = v2 - v0;
361                        let h = local_dir.cross(e2);
362                        let a = e1.dot(h);
363                        if a.abs() < 1e-6 {
364                            continue;
365                        }
366                        let f = 1.0 / a;
367                        let s = local_origin - v0;
368                        let u = f * s.dot(h);
369                        if !(0.0..=1.0).contains(&u) {
370                            continue;
371                        }
372                        let q = s.cross(e1);
373                        let v = f * local_dir.dot(q);
374                        if v < 0.0 || u + v > 1.0 {
375                            continue;
376                        }
377                        let t = f * e2.dot(q);
378                        if t > 0.0 && t < best_t {
379                            best_t = t;
380                            best_normal = e1.cross(e2).try_normalize().unwrap_or(Vec3::Y);
381                            if best_normal.dot(local_dir) > 0.0 {
382                                best_normal = -best_normal;
383                            }
384                        }
385                    }
386                }
387
388                if best_t < f32::INFINITY {
389                    Some((best_t, transform.rotation * best_normal))
390                } else {
391                    None
392                }
393            }
394            ColliderShape::ConvexHull(ch) => {
395                // Yüz yoksa AABB yaklaşımına düş (nadiren; hull genelde yüzleriyle gelir).
396                if ch.faces.is_empty() {
397                    let mut min = Vec3::splat(f32::MAX);
398                    let mut max = Vec3::splat(f32::MIN);
399                    for v in ch.vertices.iter() {
400                        min = min.min(*v);
401                        max = max.max(*v);
402                    }
403                    let center = (min + max) * 0.5;
404                    let half_extents = (max - min) * 0.5;
405                    let world_center = transform.position + transform.rotation * center;
406                    return Self::ray_box(ray, world_center, transform.rotation, half_extents);
407                }
408
409                // Tam ray-hull testi: hull üçgenlerine Möller-Trumbore (eskiden yalnız AABB
410                // yaklaşımı vardı → kutu köşelerinde gerçek hull'ı ıskalayan sahte isabet).
411                let inv_rot = transform.rotation.inverse();
412                let local_origin = inv_rot * (ray.origin - transform.position);
413                let local_dir = inv_rot * ray.direction;
414                let mut best_t = f32::INFINITY;
415                let mut best_normal = Vec3::ZERO;
416                for tri in ch.faces.iter() {
417                    let v0 = ch.vertices[tri[0] as usize];
418                    let v1 = ch.vertices[tri[1] as usize];
419                    let v2 = ch.vertices[tri[2] as usize];
420                    let e1 = v1 - v0;
421                    let e2 = v2 - v0;
422                    let h = local_dir.cross(e2);
423                    let a = e1.dot(h);
424                    if a.abs() < 1e-6 {
425                        continue;
426                    }
427                    let f = 1.0 / a;
428                    let s = local_origin - v0;
429                    let u = f * s.dot(h);
430                    if !(0.0..=1.0).contains(&u) {
431                        continue;
432                    }
433                    let q = s.cross(e1);
434                    let v = f * local_dir.dot(q);
435                    if v < 0.0 || u + v > 1.0 {
436                        continue;
437                    }
438                    let t = f * e2.dot(q);
439                    if t > 0.0 && t < best_t {
440                        best_t = t;
441                        best_normal = e1.cross(e2).try_normalize().unwrap_or(Vec3::Y);
442                        if best_normal.dot(local_dir) > 0.0 {
443                            best_normal = -best_normal;
444                        }
445                    }
446                }
447                if best_t < f32::INFINITY {
448                    Some((best_t, transform.rotation * best_normal))
449                } else {
450                    None
451                }
452            }
453            ColliderShape::Compound(shapes) => {
454                let mut closest_dist = f32::MAX;
455                let mut closest_normal = Vec3::ZERO;
456                for (local_t, sub_shape) in shapes {
457                    let world_pos =
458                        transform.position + transform.rotation.mul_vec3(local_t.position);
459                    let world_rot = transform.rotation * local_t.rotation;
460                    let world_t =
461                        crate::components::Transform::new(world_pos).with_rotation(world_rot);
462                    if let Some((d, n)) = Self::ray_shape(ray, sub_shape, &world_t) {
463                        if d < closest_dist {
464                            closest_dist = d;
465                            closest_normal = n;
466                        }
467                    }
468                }
469                if closest_dist < f32::MAX {
470                    Some((closest_dist, closest_normal))
471                } else {
472                    None
473                }
474            }
475        }
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_ray_sphere() {
485        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
486        let center = Vec3::ZERO;
487        let radius = 1.0;
488
489        let result = Raycast::ray_sphere(&ray, center, radius);
490        assert!(result.is_some());
491
492        let (t, _normal) = result.unwrap();
493        assert!((t - 4.0).abs() < 0.01);
494    }
495
496    #[test]
497    fn test_ray_aabb() {
498        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
499        let aabb = Aabb::from_center_half_extents(Vec3::ZERO, Vec3::splat(1.0));
500
501        let result = Raycast::ray_aabb(&ray, &aabb);
502        assert!(result.is_some());
503
504        let t = result.unwrap();
505        assert!((t - 4.0).abs() < 0.01);
506    }
507
508    #[test]
509    fn test_ray_miss() {
510        let ray = Ray::new(Vec3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
511        let center = Vec3::ZERO;
512        let radius = 1.0;
513
514        let result = Raycast::ray_sphere(&ray, center, radius);
515        assert!(result.is_none());
516    }
517
518    #[test]
519    fn test_ray_box() {
520        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
521        let center = Vec3::ZERO;
522        let result = Raycast::ray_box(&ray, center, gizmo_math::Quat::IDENTITY, Vec3::splat(1.0));
523        assert!(result.is_some());
524        let (t, normal) = result.unwrap();
525        assert!((t - 4.0).abs() < 0.01);
526        assert!((normal.z - -1.0).abs() < 0.01);
527    }
528
529    #[test]
530    fn test_ray_capsule() {
531        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
532        let center = Vec3::ZERO;
533        let result = Raycast::ray_capsule(&ray, center, gizmo_math::Quat::IDENTITY, 1.0, 1.0);
534        assert!(result.is_some());
535        let (t, normal) = result.unwrap();
536        assert!((t - 4.0).abs() < 0.01);
537        assert!((normal.z - -1.0).abs() < 0.01);
538    }
539
540    #[test]
541    fn test_ray_capsule_parallel() {
542        let ray = Ray::new(Vec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
543        let center = Vec3::ZERO;
544        // The ray is parallel to the Y axis (the capsule's internal axis).
545        // It hits the top sphere cap. The height is half_height = 1.0.
546        // The top sphere cap is centered at Y=1.0 with radius 1.0. Hit should be at Y=2.0.
547        let result = Raycast::ray_capsule(&ray, center, gizmo_math::Quat::IDENTITY, 1.0, 1.0);
548        assert!(result.is_some());
549        let (t, normal) = result.unwrap();
550        assert!((t - 8.0).abs() < 0.01); // 10.0 - 2.0 = 8.0
551        assert!((normal.y - 1.0).abs() < 0.01);
552    }
553
554    #[test]
555    fn test_ray_plane_backface() {
556        // Plane is at Z=0, pointing towards +Z.
557        let plane = crate::components::PlaneShape {
558            normal: Vec3::Z,
559            distance: 0.0,
560        };
561        let shape = ColliderShape::Plane(plane);
562
563        // Ray from -5 looking towards +Z
564        let ray = Ray::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 1.0));
565        let result = Raycast::ray_shape(&ray, &shape, &Transform::new(Vec3::ZERO));
566        assert!(result.is_some());
567        assert_eq!(result.unwrap().1, -Vec3::Z); // Should be flipped since ray hits the backface
568    }
569
570    /// İçeriden başlayan ışın geçerli bir çıkış-yüzeyi normali vermeli (eskiden t=0'da
571    /// `origin` yüzey üstünde olmadığından sahte +Y dönüyordu).
572    #[test]
573    fn ray_box_from_inside_returns_valid_exit_normal() {
574        use gizmo_math::Quat;
575        let ray = Ray::new(Vec3::ZERO, Vec3::X); // kutu merkezinden +X
576        let (t, normal) =
577            Raycast::ray_box(&ray, Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0)).unwrap();
578        assert!(t > 0.0, "çıkış mesafesi pozitif olmalı");
579        assert!(
580            (normal - Vec3::X).length() < 1e-3,
581            "çıkış normali +X olmalı (sahte +Y değil), oldu: {normal:?}"
582        );
583    }
584
585    /// ConvexHull raycast'i AABB değil GERÇEK hull'a karşı olmalı: AABB köşesinden geçip
586    /// hull'ı ıskalayan ışın None dönmeli.
587    #[test]
588    fn convex_hull_raycast_is_exact_not_aabb() {
589        use crate::components::collider::ConvexHullShape;
590        use crate::quickhull::compute_convex_hull;
591        use std::sync::Arc;
592        // Tetrahedron (x+y+z ≤ 1 bölgesi); AABB ise [0,1]³.
593        let hull = compute_convex_hull(&[Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::Z]);
594        let shape = ColliderShape::ConvexHull(ConvexHullShape {
595            vertices: Arc::new(hull.vertices),
596            faces: Arc::new(hull.faces),
597        });
598        let tr = Transform::new(Vec3::ZERO);
599
600        // (0.9,0.9): AABB içinde ama tetrahedron dışında (x+y=1.8>1) → ıskala.
601        let miss = Ray::new(Vec3::new(0.9, 0.9, 5.0), Vec3::new(0.0, 0.0, -1.0));
602        assert!(
603            Raycast::ray_shape(&miss, &shape, &tr).is_none(),
604            "AABB köşesinden geçip hull'ı ıskalayan ışın None dönmeli (tam test)"
605        );
606        // Tetrahedronun içinden geçen ışın isabet etmeli.
607        let hit = Ray::new(Vec3::new(0.2, 0.2, 5.0), Vec3::new(0.0, 0.0, -1.0));
608        assert!(
609            Raycast::ray_shape(&hit, &shape, &tr).is_some(),
610            "hull'dan geçen ışın isabet etmeli"
611        );
612    }
613}