1use crate::components::{ColliderShape, Transform};
2use crate::BodyHandle;
3use gizmo_math::Aabb;
4use gizmo_math::Vec3;
5
6#[derive(Debug, Clone, Copy)]
8pub struct Ray {
9 pub origin: Vec3,
10 pub direction: Vec3, }
12
13impl Ray {
14 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#[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
43pub struct Raycast;
45
46impl Raycast {
47 pub fn ray_aabb(ray: &Ray, aabb: &Aabb) -> Option<f32> {
49 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 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; }
103 Some(if tmin < 0.0 { tmax } else { tmin })
105 }
106
107 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 pub fn ray_box(
138 ray: &Ray,
139 center: Vec3,
140 rotation: gizmo_math::Quat,
141 half_extents: Vec3,
142 ) -> Option<(f32, Vec3)> {
143 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 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 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 let world_normal = rotation * normal;
172
173 Some((t, world_normal))
174 } else {
175 None
176 }
177 }
178
179 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 let inv_rot = rotation.inverse();
189 let local_origin = inv_rot * (ray.origin - center);
190 let local_dir = inv_rot * ray.direction;
191
192 let p1 = Vec3::new(0.0, half_height, 0.0);
194 let p2 = Vec3::new(0.0, -half_height, 0.0);
195
196 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 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 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 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 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); while let Some(node_idx) = stack.pop() {
302 let node = &tm.bvh.nodes[node_idx];
303
304 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 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 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 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 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); assert!((normal.y - 1.0).abs() < 0.01);
552 }
553
554 #[test]
555 fn test_ray_plane_backface() {
556 let plane = crate::components::PlaneShape {
558 normal: Vec3::Z,
559 distance: 0.0,
560 };
561 let shape = ColliderShape::Plane(plane);
562
563 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); }
569
570 #[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); 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 #[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 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 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 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}