Skip to content

Commit e2c95c3

Browse files
committed
Aabb: add intersect_ray() + lots of extra tests
1 parent 54bf053 commit e2c95c3

File tree

1 file changed

+257
-8
lines changed

1 file changed

+257
-8
lines changed

godot-core/src/builtin/aabb.rs

+257-8
Original file line numberDiff line numberDiff line change
@@ -344,22 +344,56 @@ impl Aabb {
344344

345345
/// Returns `true` if the given ray intersects with this AABB. Ray length is infinite.
346346
///
347-
/// # Panics
347+
/// Semantically equivalent to `self.intersects_ray(ray_from, ray_dir).is_some()`; might be microscopically faster.
348+
///
349+
/// # Panics (Debug)
348350
/// If `self.size` is negative.
349351
#[inline]
350-
pub fn intersects_ray(self, from: Vector3, dir: Vector3) -> bool {
352+
pub fn intersects_ray(self, ray_from: Vector3, ray_dir: Vector3) -> bool {
353+
let (tnear, tfar) = self.compute_ray_tnear_tfar(ray_from, ray_dir);
354+
355+
tnear <= tfar
356+
}
357+
358+
/// Returns the point where the given (infinite) ray intersects with this AABB, or `None` if there is no intersection.
359+
///
360+
/// # Panics (Debug)
361+
/// If `self.size` is negative, or if `ray_dir` is zero. Note that this differs from Godot, which treats rays that degenerate to points as
362+
/// intersecting if inside, and not if outside the AABB.
363+
#[inline]
364+
pub fn intersect_ray(self, ray_from: Vector3, ray_dir: Vector3) -> Option<Vector3> {
365+
let (tnear, tfar) = self.compute_ray_tnear_tfar(ray_from, ray_dir);
366+
367+
if tnear <= tfar {
368+
// if tnear < 0: the ray starts inside the box -> take other intersection point.
369+
let t = if tnear < 0.0 { tfar } else { tnear };
370+
Some(ray_from + ray_dir * t)
371+
} else {
372+
None
373+
}
374+
}
375+
376+
// Credits: https://tavianator.com/2011/ray_box.html
377+
fn compute_ray_tnear_tfar(self, ray_from: Vector3, ray_dir: Vector3) -> (real, real) {
351378
self.assert_nonnegative();
379+
debug_assert!(
380+
ray_dir != Vector3::ZERO,
381+
"ray direction must not be zero; use contains_point() for point checks"
382+
);
352383

353-
let tmin = (self.position - from) / dir;
354-
let tmax = (self.end() - from) / dir;
384+
// Note: leads to -inf/inf for each component that is 0. This should generally balance out, unless all are zero.
385+
let recip_dir = ray_dir.recip();
386+
387+
let tmin = (self.position - ray_from) * recip_dir;
388+
let tmax = (self.end() - ray_from) * recip_dir;
355389

356390
let t1 = tmin.coord_min(tmax);
357391
let t2 = tmin.coord_max(tmax);
358392

359393
let tnear = t1.x.max(t1.y).max(t1.z);
360-
let tfar = t2.y.min(t2.x).min(t2.z);
394+
let tfar = t2.x.min(t2.y).min(t2.z);
361395

362-
tnear <= tfar
396+
(tnear, tfar)
363397
}
364398

365399
/// Returns `true` if the given ray intersects with this AABB. Segment length is finite.
@@ -571,6 +605,7 @@ mod test {
571605
};
572606
let from1 = Vector3::new(1.0, 1.0, -1.0);
573607
let dir1 = Vector3::new(0.0, 0.0, 1.0);
608+
574609
assert!(aabb1.intersects_ray(from1, dir1));
575610

576611
// Test case 2: Ray misses the AABB
@@ -619,15 +654,159 @@ mod test {
619654
assert!(aabb6.intersects_ray(from6, dir6));
620655
}
621656

657+
#[test] // Ported from Godot tests.
658+
fn test_intersect_ray_2() {
659+
let aabb = Aabb {
660+
position: Vector3::new(-1.5, 2.0, -2.5),
661+
size: Vector3::new(4.0, 5.0, 6.0),
662+
};
663+
664+
assert_eq!(
665+
aabb.intersect_ray(Vector3::new(-100.0, 3.0, 0.0), Vector3::new(1.0, 0.0, 0.0)),
666+
Some(Vector3::new(-1.5, 3.0, 0.0)),
667+
"intersect_ray(), ray points directly at AABB -> Some"
668+
);
669+
670+
assert_eq!(
671+
aabb.intersect_ray(Vector3::new(10.0, 10.0, 0.0), Vector3::new(0.0, 1.0, 0.0)),
672+
None,
673+
"intersect_ray(), ray parallel and outside the AABB -> None"
674+
);
675+
676+
assert_eq!(
677+
aabb.intersect_ray(Vector3::ONE, Vector3::new(0.0, 1.0, 0.0)),
678+
Some(Vector3::new(1.0, 2.0, 1.0)),
679+
"intersect_ray(), ray originating inside the AABB -> Some"
680+
);
681+
682+
assert_eq!(
683+
aabb.intersect_ray(Vector3::new(-10.0, 0.0, 0.0), Vector3::new(-1.0, 0.0, 0.0)),
684+
None,
685+
"intersect_ray(), ray points away from AABB -> None"
686+
);
687+
688+
assert_eq!(
689+
aabb.intersect_ray(Vector3::new(0.0, 0.0, 0.0), Vector3::ONE),
690+
Some(Vector3::new(2.0, 2.0, 2.0)),
691+
"intersect_ray(), ray along the AABB diagonal -> Some"
692+
);
693+
694+
assert_eq!(
695+
aabb.intersect_ray(
696+
aabb.position + Vector3::splat(0.0001),
697+
Vector3::new(-1.0, 0.0, 0.0)
698+
),
699+
Some(Vector3::new(-1.5, 2.0001, -2.4999)),
700+
"intersect_ray(), ray starting on the AABB's edge -> Some"
701+
);
702+
703+
assert_eq!(
704+
aabb.intersect_ray(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0)),
705+
Some(Vector3::new(0.0, 2.0, 0.0)),
706+
"intersect_ray(): ray has 2 axes parallel to AABB -> Some"
707+
);
708+
}
709+
710+
#[test] // Ported from Godot tests.
711+
fn test_intersect_aabb() {
712+
let aabb_big = Aabb {
713+
position: Vector3::new(-1.5, 2.0, -2.5),
714+
size: Vector3::new(4.0, 5.0, 6.0),
715+
};
716+
717+
let aabb_small = Aabb {
718+
position: Vector3::new(-1.5, 2.0, -2.5),
719+
size: Vector3::ONE,
720+
};
721+
assert!(
722+
aabb_big.intersects(aabb_small),
723+
"intersects() with fully contained AABB (touching the edge) should return true."
724+
);
725+
726+
let aabb_small = Aabb {
727+
position: Vector3::new(0.5, 1.5, -2.0),
728+
size: Vector3::ONE,
729+
};
730+
assert!(
731+
aabb_big.intersects(aabb_small),
732+
"intersects() with partially contained AABB (overflowing on Y axis) should return true."
733+
);
734+
735+
let aabb_small = Aabb {
736+
position: Vector3::new(10.0, -10.0, -10.0),
737+
size: Vector3::ONE,
738+
};
739+
assert!(
740+
!aabb_big.intersects(aabb_small),
741+
"intersects() with non-contained AABB should return false."
742+
);
743+
744+
let aabb_small = Aabb {
745+
position: Vector3::new(-1.5, 2.0, -2.5),
746+
size: Vector3::ONE,
747+
};
748+
let inter = aabb_big.intersection(aabb_small);
749+
assert!(
750+
inter.unwrap().approx_eq(&aabb_small),
751+
"intersection() with fully contained AABB should return the smaller AABB."
752+
);
753+
754+
let aabb_small = Aabb {
755+
position: Vector3::new(0.5, 1.5, -2.0),
756+
size: Vector3::ONE,
757+
};
758+
let expected = Aabb {
759+
position: Vector3::new(0.5, 2.0, -2.0),
760+
size: Vector3::new(1.0, 0.5, 1.0),
761+
};
762+
let inter = aabb_big.intersection(aabb_small);
763+
assert!(
764+
inter.unwrap().approx_eq(&expected),
765+
"intersect() with partially contained AABB (overflowing on Y axis) should match expected."
766+
);
767+
768+
let aabb_small = Aabb {
769+
position: Vector3::new(10.0, -10.0, -10.0),
770+
size: Vector3::ONE,
771+
};
772+
let inter = aabb_big.intersection(aabb_small);
773+
assert!(
774+
inter.is_none(),
775+
"intersect() with non-contained AABB should return None."
776+
);
777+
}
778+
779+
#[test]
780+
#[should_panic]
781+
#[cfg(debug_assertions)]
782+
fn test_intersect_ray_zero_dir_inside() {
783+
let aabb = Aabb {
784+
position: Vector3::new(-1.5, 2.0, -2.5),
785+
size: Vector3::new(4.0, 5.0, 6.0),
786+
};
787+
788+
aabb.intersect_ray(Vector3::new(-1.0, 3.0, -2.0), Vector3::ZERO);
789+
}
790+
791+
#[test]
792+
#[should_panic]
793+
#[cfg(debug_assertions)]
794+
fn test_intersect_ray_zero_dir_outside() {
795+
let aabb = Aabb {
796+
position: Vector3::new(-1.5, 2.0, -2.5),
797+
size: Vector3::new(4.0, 5.0, 6.0),
798+
};
799+
800+
aabb.intersect_ray(Vector3::new(-1000.0, 3.0, -2.0), Vector3::ZERO);
801+
}
802+
622803
#[test]
623804
fn test_intersects_plane() {
624-
// Create an AABB
625805
let aabb = Aabb {
626806
position: Vector3::new(-1.0, -1.0, -1.0),
627807
size: Vector3::new(2.0, 2.0, 2.0),
628808
};
629809

630-
// Define planes for testing
631810
let plane_inside = Plane {
632811
normal: Vector3::new(1.0, 0.0, 0.0),
633812
d: 0.0,
@@ -655,6 +834,32 @@ mod test {
655834
assert!(!aabb.intersects_plane(plane_parallel));
656835
}
657836

837+
#[test] // Ported from Godot tests.
838+
fn test_intersects_plane_2() {
839+
let aabb_big = Aabb {
840+
position: Vector3::new(-1.5, 2.0, -2.5),
841+
size: Vector3::new(4.0, 5.0, 6.0),
842+
};
843+
844+
let plane1 = Plane::new(Vector3::new(0.0, 1.0, 0.0), 4.0);
845+
assert!(
846+
aabb_big.intersects_plane(plane1),
847+
"intersects_plane() should return true (plane near top)."
848+
);
849+
850+
let plane2 = Plane::new(Vector3::new(0.0, -1.0, 0.0), -4.0);
851+
assert!(
852+
aabb_big.intersects_plane(plane2),
853+
"intersects_plane() should return true (plane near bottom)."
854+
);
855+
856+
let plane3 = Plane::new(Vector3::new(0.0, 1.0, 0.0), 200.0);
857+
assert!(
858+
!aabb_big.intersects_plane(plane3),
859+
"intersects_plane() should return false (plane far away)."
860+
);
861+
}
862+
658863
#[test]
659864
fn test_aabb_intersects_segment() {
660865
let aabb = Aabb {
@@ -672,4 +877,48 @@ mod test {
672877
let to = Vector3::new(-1.0, 1.0, 1.0);
673878
assert!(!aabb.intersects_segment(from, to));
674879
}
880+
881+
#[test] // Ported from Godot tests.
882+
fn test_intersects_segment_2() {
883+
let aabb = Aabb {
884+
position: Vector3::new(-1.5, 2.0, -2.5),
885+
size: Vector3::new(4.0, 5.0, 6.0),
886+
};
887+
888+
// True cases.
889+
assert!(
890+
aabb.intersects_segment(Vector3::new(1.0, 3.0, 0.0), Vector3::new(0.0, 3.0, 0.0)),
891+
"intersects_segment(), segment fully inside -> true"
892+
);
893+
assert!(
894+
aabb.intersects_segment(Vector3::new(0.0, 3.0, 0.0), Vector3::new(0.0, -300.0, 0.0)),
895+
"intersects_segment(), segment crossing the box -> true"
896+
);
897+
assert!(
898+
aabb.intersects_segment(
899+
Vector3::new(-50.0, 3.0, -50.0),
900+
Vector3::new(50.0, 3.0, 50.0)
901+
),
902+
"intersects_segment(), diagonal crossing the box -> true"
903+
);
904+
905+
// False case.
906+
assert!(
907+
!aabb.intersects_segment(
908+
Vector3::new(-50.0, 25.0, -50.0),
909+
Vector3::new(50.0, 25.0, 50.0)
910+
),
911+
"intersects_segment(), segment above the box -> false"
912+
);
913+
914+
// Degenerate segments (points).
915+
assert!(
916+
aabb.intersects_segment(Vector3::new(0.0, 3.0, 0.0), Vector3::new(0.0, 3.0, 0.0)),
917+
"intersects_segment(), segment of length 0 *inside* the box -> true"
918+
);
919+
assert!(
920+
!aabb.intersects_segment(Vector3::new(0.0, 300.0, 0.0), Vector3::new(0.0, 300.0, 0.0)),
921+
"intersects_segment(), segment of length 0 *outside* the box -> false"
922+
);
923+
}
675924
}

0 commit comments

Comments
 (0)