use godot_ffi as sys;
use sys::{ExtVariantType, GodotFfi, ffi_methods};
use crate::builtin::math::{ApproxEq, FloatExt};
use crate::builtin::{Plane, Vector3, Vector3Axis, real};
#[derive(Default, Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Aabb {
pub position: Vector3,
pub size: Vector3,
}
impl Aabb {
#[inline]
pub const fn new(position: Vector3, size: Vector3) -> Self {
Self { position, size }
}
#[inline]
pub fn from_position_end(position: Vector3, end: Vector3) -> Self {
Self::new(position, end - position)
}
#[inline]
#[deprecated = "Renamed to `from_position_end`."]
pub fn from_corners(position: Vector3, end: Vector3) -> Self {
Self::from_position_end(position, end)
}
#[inline]
pub fn abs(self) -> Self {
Aabb {
position: self.position + self.size.coord_min(Vector3::ZERO),
size: self.size.abs(),
}
}
#[inline]
pub fn encloses(self, b: Aabb) -> bool {
let end = self.end();
let b_end = b.end();
b.position.x >= self.position.x
&& b.position.y >= self.position.y
&& b.position.z >= self.position.z
&& b_end.x <= end.x
&& b_end.y <= end.y
&& b_end.z <= end.z
}
#[inline]
pub fn expand(self, to: Vector3) -> Self {
self.merge(Aabb::new(to, Vector3::ZERO))
}
#[inline]
pub fn merge(self, b: Aabb) -> Self {
self.assert_nonnegative();
b.assert_nonnegative();
let position = Vector3::coord_min(self.position, b.position);
let end = Vector3::coord_max(self.end(), b.end());
Self::from_position_end(position, end)
}
#[inline]
pub fn volume(self) -> real {
self.assert_nonnegative();
self.size.x * self.size.y * self.size.z
}
#[inline]
pub fn center(self) -> Vector3 {
self.position + (self.size / 2.0)
}
#[inline]
#[must_use]
pub fn grow(self, amount: real) -> Self {
let position = self.position - Vector3::new(amount, amount, amount);
let size = self.size + Vector3::new(amount, amount, amount) * 2.0;
Self { position, size }
}
#[inline]
#[doc(alias = "has_point")]
pub fn contains_point(self, point: Vector3) -> bool {
self.assert_nonnegative();
let point = point - self.position;
point.abs() == point
&& point.x < self.size.x
&& point.y < self.size.y
&& point.z < self.size.z
}
#[inline]
pub const fn has_surface(self) -> bool {
(self.size.x > 0.0) || (self.size.y > 0.0) || (self.size.z > 0.0)
}
#[inline]
pub const fn has_volume(self) -> bool {
self.size.x > 0.0 && self.size.y > 0.0 && self.size.z > 0.0
}
#[inline]
pub fn intersect(self, b: Aabb) -> Option<Self> {
self.assert_nonnegative();
if !self.intersects(b) {
return None;
}
let mut rect = b;
rect.position = rect.position.coord_max(self.position);
let end = self.end();
let end_b = b.end();
rect.size = end.coord_min(end_b) - rect.position;
Some(rect)
}
#[inline]
pub fn is_finite(self) -> bool {
self.position.is_finite() && self.size.is_finite()
}
#[inline]
pub fn end(self) -> Vector3 {
self.position + self.size
}
#[inline]
#[doc(alias = "get_endpoint")]
pub fn get_corner(self, idx: usize) -> Vector3 {
*self
.corners()
.get(idx)
.expect("Tried to retrieve vertex no. {idx} from Aabb which has only 8 vertices")
}
#[inline]
pub fn set_end(&mut self, end: Vector3) {
self.size = end - self.position;
}
#[inline]
pub fn longest_axis(self) -> Option<Vector3> {
self.longest_axis_index().map(|axis| match axis {
Vector3Axis::X => Vector3::RIGHT,
Vector3Axis::Y => Vector3::UP,
Vector3Axis::Z => Vector3::BACK,
})
}
#[inline]
pub fn longest_axis_index(self) -> Option<Vector3Axis> {
self.size.max_axis()
}
#[inline]
pub const fn longest_axis_size(self) -> real {
let size = self.size;
real::max(size.x, size.y).max(size.z)
}
#[inline]
pub fn shortest_axis(self) -> Option<Vector3> {
self.shortest_axis_index().map(|axis| match axis {
Vector3Axis::X => Vector3::RIGHT,
Vector3Axis::Y => Vector3::UP,
Vector3Axis::Z => Vector3::BACK,
})
}
#[inline]
pub fn shortest_axis_index(self) -> Option<Vector3Axis> {
self.size.min_axis()
}
#[inline]
pub const fn shortest_axis_size(self) -> real {
let size = self.size;
real::min(size.x, size.y).min(size.z)
}
#[inline]
#[deprecated = "Renamed to `get_support`."]
pub fn support(self, dir: Vector3) -> Vector3 {
self.get_support(dir)
}
#[inline]
pub fn get_support(self, dir: Vector3) -> Vector3 {
let mut support = self.position;
if dir.x > 0. {
support.x += self.size.x;
}
if dir.y > 0. {
support.y += self.size.y;
}
if dir.z > 0. {
support.z += self.size.z;
}
support
}
#[inline]
pub fn intersects(self, b: Aabb) -> bool {
let end = self.end();
let end_b = b.end();
self.position.x <= end_b.x
&& end.x >= b.position.x
&& self.position.y <= end_b.y
&& end.y >= b.position.y
&& self.position.z <= end_b.z
}
#[inline]
pub fn intersects_exclude_borders(self, b: Aabb) -> bool {
let end = self.end();
let end_b = b.end();
self.position.x < end_b.x
&& end.x > b.position.x
&& self.position.y < end_b.y
&& end.y > b.position.y
&& self.position.z < end_b.z
&& end.z > b.position.z
}
#[inline]
pub fn corners(self) -> [Vector3; 8] {
[
self.position,
self.position + Vector3::new(0.0, 0.0, self.size.z),
self.position + Vector3::new(0.0, self.size.y, 0.0),
self.position + Vector3::new(0.0, self.size.y, self.size.z),
self.position + Vector3::new(self.size.x, 0.0, 0.0),
self.position + Vector3::new(self.size.x, 0.0, self.size.z),
self.position + Vector3::new(self.size.x, self.size.y, 0.0),
self.position + self.size,
]
}
#[inline]
pub fn intersects_plane(self, plane: Plane) -> bool {
let points = self.corners();
let mut over = false;
let mut under = false;
for point in points {
let dist_to = plane.distance_to(point);
if dist_to > 0.0 {
over = true
} else {
under = true
}
}
over && under
}
#[inline]
pub fn intersects_ray(self, ray_from: Vector3, ray_dir: Vector3) -> bool {
let (tnear, tfar) = self.compute_ray_tnear_tfar(ray_from, ray_dir);
tnear <= tfar
}
#[inline]
pub fn intersect_ray(self, ray_from: Vector3, ray_dir: Vector3) -> Option<Vector3> {
let (tnear, tfar) = self.compute_ray_tnear_tfar(ray_from, ray_dir);
if tnear <= tfar {
let t = if tnear < 0.0 { tfar } else { tnear };
Some(ray_from + ray_dir * t)
} else {
None
}
}
fn compute_ray_tnear_tfar(self, ray_from: Vector3, ray_dir: Vector3) -> (real, real) {
self.assert_nonnegative();
sys::balanced_assert!(
ray_dir != Vector3::ZERO,
"ray direction must not be zero; use contains_point() for point checks"
);
let recip_dir = ray_dir.recip();
let tmin = (self.position - ray_from) * recip_dir;
let tmax = (self.end() - ray_from) * recip_dir;
let t1 = tmin.coord_min(tmax);
let t2 = tmin.coord_max(tmax);
let tnear = real::max(t1.x, t1.y).max(t1.z);
let tfar = real::min(t2.x, t2.y).min(t2.z);
(tnear, tfar)
}
fn intersect_segment_inner(self, from: Vector3, to: Vector3) -> Option<real> {
self.assert_nonnegative();
let segment_dir = to - from;
let mut t_min: real = 0.0;
let mut t_max: real = 1.0;
for axis in [Vector3Axis::X, Vector3Axis::Y, Vector3Axis::Z] {
if segment_dir[axis].is_zero_approx() {
if from[axis] < self.position[axis] || from[axis] > self.end()[axis] {
return None;
}
continue;
}
let inv_dir = 1.0 / segment_dir[axis];
let t1 = (self.position[axis] - from[axis]) * inv_dir;
let t2 = (self.end()[axis] - from[axis]) * inv_dir;
let (t_near, t_far) = if t1 < t2 { (t1, t2) } else { (t2, t1) };
t_min = real::max(t_min, t_near);
t_max = real::min(t_max, t_far);
if t_min > t_max {
return None;
}
}
Some(t_min)
}
#[inline]
pub fn intersects_segment(self, from: Vector3, to: Vector3) -> bool {
self.intersect_segment_inner(from, to).is_some()
}
#[inline]
pub fn intersect_segment(self, from: Vector3, to: Vector3) -> Option<Vector3> {
let t_min = self.intersect_segment_inner(from, to)?;
Some(from + (to - from) * t_min)
}
#[inline]
fn assert_nonnegative(self) {
assert!(
self.size.x >= 0.0 && self.size.y >= 0.0 && self.size.z >= 0.0,
"size {:?} is negative",
self.size
);
}
}
impl std::fmt::Display for Aabb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[P: {}, S: {}]", self.position, self.size)
}
}
unsafe impl GodotFfi for Aabb {
const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::AABB);
ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
}
crate::meta::impl_godot_as_self!(Aabb: ByValue);
impl ApproxEq for Aabb {
#[inline]
fn approx_eq(&self, other: &Self) -> bool {
Vector3::approx_eq(&self.position, &other.position)
&& Vector3::approx_eq(&self.size, &other.size)
}
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod test {
use super::*;
#[cfg(feature = "serde")] #[cfg_attr(published_docs, doc(cfg(feature = "serde")))]
#[test]
fn serde_roundtrip() {
let aabb = super::Aabb::default();
let expected_json = "{\"position\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"size\":{\"x\":0.0,\"y\":0.0,\"z\":0.0}}";
crate::builtin::test_utils::roundtrip(&aabb, expected_json);
}
#[test]
fn test_axes_functions() {
let aabb = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(4.0, 6.0, 8.0),
};
assert_eq!(aabb.shortest_axis(), Some(Vector3::RIGHT));
assert_eq!(aabb.longest_axis(), Some(Vector3::BACK));
assert_eq!(aabb.shortest_axis_size(), 4.0);
assert_eq!(aabb.longest_axis_size(), 8.0);
assert_eq!(aabb.shortest_axis_index(), Some(Vector3Axis::X));
assert_eq!(aabb.longest_axis_index(), Some(Vector3Axis::Z));
}
#[test]
fn test_intersects() {
let aabb1 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(4.0, 4.0, 4.0),
};
let aabb2 = Aabb {
position: Vector3::new(3.0, 3.0, 3.0),
size: Vector3::new(3.0, 3.0, 3.0),
};
let aabb3 = Aabb {
position: Vector3::new(5.0, 5.0, 5.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let aabb4 = Aabb {
position: Vector3::new(6.0, 6.0, 6.0),
size: Vector3::new(1.0, 1.0, 1.0),
};
assert!(aabb1.intersects(aabb2));
assert!(aabb2.intersects(aabb1));
assert!(!aabb1.intersects(aabb3));
assert!(!aabb3.intersects(aabb1));
assert!(aabb1.intersects_exclude_borders(aabb2));
assert!(aabb2.intersects_exclude_borders(aabb1));
assert!(!aabb1.intersects_exclude_borders(aabb3));
assert!(!aabb3.intersects_exclude_borders(aabb1));
assert!(!aabb1.intersects_exclude_borders(aabb4));
assert!(!aabb4.intersects_exclude_borders(aabb1));
assert!(aabb1.intersects(aabb1));
}
#[test]
fn test_intersection() {
let aabb1 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let aabb2 = Aabb {
position: Vector3::new(1.0, 1.0, 1.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let aabb3 = Aabb {
position: Vector3::new(3.0, 3.0, 3.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let aabb4 = Aabb {
position: Vector3::new(-1.0, -1.0, -1.0),
size: Vector3::new(1.0, 1.0, 1.0),
};
assert_eq!(
aabb1.intersect(aabb2),
Some(Aabb {
position: Vector3::new(1.0, 1.0, 1.0),
size: Vector3::new(1.0, 1.0, 1.0),
})
);
assert_eq!(aabb1.intersect(aabb3), None);
assert_eq!(
aabb1.intersect(aabb4),
Some(Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(0.0, 0.0, 0.0),
})
);
}
#[test]
fn test_intersects_ray() {
let aabb1 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from1 = Vector3::new(1.0, 1.0, -1.0);
let dir1 = Vector3::new(0.0, 0.0, 1.0);
assert!(aabb1.intersects_ray(from1, dir1));
let aabb2 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from2 = Vector3::new(4.0, 4.0, 4.0);
let dir2 = Vector3::new(0.0, 0.0, 1.0);
assert!(!aabb2.intersects_ray(from2, dir2));
let aabb3 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from3 = Vector3::new(1.0, 1.0, 1.0);
let dir3 = Vector3::new(0.0, 0.0, 1.0);
assert!(aabb3.intersects_ray(from3, dir3));
let aabb4 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from4 = Vector3::new(1.0, 1.0, 1.0);
let dir4 = Vector3::new(1.0, 0.0, 0.0);
assert!(aabb4.intersects_ray(from4, dir4));
let aabb5 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from5 = Vector3::new(0.5, 0.5, 0.5);
let dir5 = Vector3::new(1.0, 1.0, 1.0);
assert!(aabb5.intersects_ray(from5, dir5));
let aabb6 = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let from6 = Vector3::new(1.0, 2.0, 1.0);
let dir6 = Vector3::new(0.0, -1.0, 0.0);
assert!(aabb6.intersects_ray(from6, dir6));
}
#[test] fn test_intersect_ray_2() {
let aabb = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
assert_eq!(
aabb.intersect_ray(Vector3::new(-100.0, 3.0, 0.0), Vector3::new(1.0, 0.0, 0.0)),
Some(Vector3::new(-1.5, 3.0, 0.0)),
"intersect_ray(), ray points directly at AABB -> Some"
);
assert_eq!(
aabb.intersect_ray(Vector3::new(10.0, 10.0, 0.0), Vector3::new(0.0, 1.0, 0.0)),
None,
"intersect_ray(), ray parallel and outside the AABB -> None"
);
assert_eq!(
aabb.intersect_ray(Vector3::ONE, Vector3::new(0.0, 1.0, 0.0)),
Some(Vector3::new(1.0, 2.0, 1.0)),
"intersect_ray(), ray originating inside the AABB -> Some"
);
assert_eq!(
aabb.intersect_ray(Vector3::new(-10.0, 0.0, 0.0), Vector3::new(-1.0, 0.0, 0.0)),
None,
"intersect_ray(), ray points away from AABB -> None"
);
assert_eq!(
aabb.intersect_ray(Vector3::new(0.0, 0.0, 0.0), Vector3::ONE),
Some(Vector3::new(2.0, 2.0, 2.0)),
"intersect_ray(), ray along the AABB diagonal -> Some"
);
assert_eq!(
aabb.intersect_ray(
aabb.position + Vector3::splat(0.0001),
Vector3::new(-1.0, 0.0, 0.0)
),
Some(Vector3::new(-1.5, 2.0001, -2.4999)),
"intersect_ray(), ray starting on the AABB's edge -> Some"
);
assert_eq!(
aabb.intersect_ray(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0)),
Some(Vector3::new(0.0, 2.0, 0.0)),
"intersect_ray(): ray has 2 axes parallel to AABB -> Some"
);
}
#[test] fn test_intersect_aabb() {
let aabb_big = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
let aabb_small = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::ONE,
};
assert!(
aabb_big.intersects(aabb_small),
"intersects() with fully contained AABB (touching the edge) should return true."
);
let aabb_small = Aabb {
position: Vector3::new(0.5, 1.5, -2.0),
size: Vector3::ONE,
};
assert!(
aabb_big.intersects(aabb_small),
"intersects() with partially contained AABB (overflowing on Y axis) should return true."
);
let aabb_small = Aabb {
position: Vector3::new(10.0, -10.0, -10.0),
size: Vector3::ONE,
};
assert!(
!aabb_big.intersects(aabb_small),
"intersects() with non-contained AABB should return false."
);
let aabb_small = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::ONE,
};
let inter = aabb_big.intersect(aabb_small);
assert!(
inter.unwrap().approx_eq(&aabb_small),
"intersect() with fully contained AABB should return the smaller AABB."
);
let aabb_small = Aabb {
position: Vector3::new(0.5, 1.5, -2.0),
size: Vector3::ONE,
};
let expected = Aabb {
position: Vector3::new(0.5, 2.0, -2.0),
size: Vector3::new(1.0, 0.5, 1.0),
};
let inter = aabb_big.intersect(aabb_small);
assert!(
inter.unwrap().approx_eq(&expected),
"intersect() with partially contained AABB (overflowing on Y axis) should match expected."
);
let aabb_small = Aabb {
position: Vector3::new(10.0, -10.0, -10.0),
size: Vector3::ONE,
};
let inter = aabb_big.intersect(aabb_small);
assert!(
inter.is_none(),
"intersect() with non-contained AABB should return None."
);
}
#[test] #[cfg_attr(safeguards_balanced, should_panic)]
fn test_intersect_ray_zero_dir_inside() {
let aabb = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
aabb.intersect_ray(Vector3::new(-1.0, 3.0, -2.0), Vector3::ZERO);
}
#[test]
#[cfg_attr(safeguards_balanced, should_panic)]
fn test_intersect_ray_zero_dir_outside() {
let aabb = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
aabb.intersect_ray(Vector3::new(-1000.0, 3.0, -2.0), Vector3::ZERO);
}
#[test]
fn test_intersects_plane() {
let aabb = Aabb {
position: Vector3::new(-1.0, -1.0, -1.0),
size: Vector3::new(2.0, 2.0, 2.0),
};
let plane_inside = Plane {
normal: Vector3::new(1.0, 0.0, 0.0),
d: 0.0,
};
let plane_outside = Plane {
normal: Vector3::new(1.0, 0.0, 0.0),
d: 2.0,
};
let plane_intersect = Plane {
normal: Vector3::new(0.0, 1.0, 0.0),
d: 0.5,
};
let plane_parallel = Plane {
normal: Vector3::new(0.0, 1.0, 0.0),
d: 2.0,
};
assert!(aabb.intersects_plane(plane_inside));
assert!(!aabb.intersects_plane(plane_outside));
assert!(aabb.intersects_plane(plane_intersect));
assert!(!aabb.intersects_plane(plane_parallel));
}
#[test] fn test_intersects_plane_2() {
let aabb_big = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
let plane1 = Plane::new(Vector3::new(0.0, 1.0, 0.0), 4.0);
assert!(
aabb_big.intersects_plane(plane1),
"intersects_plane() should return true (plane near top)."
);
let plane2 = Plane::new(Vector3::new(0.0, -1.0, 0.0), -4.0);
assert!(
aabb_big.intersects_plane(plane2),
"intersects_plane() should return true (plane near bottom)."
);
let plane3 = Plane::new(Vector3::new(0.0, 1.0, 0.0), 200.0);
assert!(
!aabb_big.intersects_plane(plane3),
"intersects_plane() should return false (plane far away)."
);
}
#[test]
fn test_aabb_intersects_segment() {
let aabb = Aabb {
position: Vector3::new(0.0, 0.0, 0.0),
size: Vector3::new(4.0, 4.0, 4.0),
};
let from = Vector3::new(1.0, 1.0, 1.0);
let to = Vector3::new(3.0, 3.0, 3.0);
assert!(aabb.intersects_segment(from, to));
let from = Vector3::new(-2.0, 2.0, 2.0);
let to = Vector3::new(-1.0, 1.0, 1.0);
assert!(!aabb.intersects_segment(from, to));
}
#[test] fn test_intersects_segment_2() {
let aabb = Aabb {
position: Vector3::new(-1.5, 2.0, -2.5),
size: Vector3::new(4.0, 5.0, 6.0),
};
assert!(
aabb.intersects_segment(Vector3::new(1.0, 3.0, 0.0), Vector3::new(0.0, 3.0, 0.0)),
"intersects_segment(), segment fully inside -> true"
);
assert!(
aabb.intersects_segment(Vector3::new(0.0, 3.0, 0.0), Vector3::new(0.0, -300.0, 0.0)),
"intersects_segment(), segment crossing the box -> true"
);
assert!(
aabb.intersects_segment(
Vector3::new(-50.0, 3.0, -50.0),
Vector3::new(50.0, 3.0, 50.0)
),
"intersects_segment(), diagonal crossing the box -> true"
);
assert!(
!aabb.intersects_segment(
Vector3::new(-50.0, 25.0, -50.0),
Vector3::new(50.0, 25.0, 50.0)
),
"intersects_segment(), segment above the box -> false"
);
assert!(
aabb.intersects_segment(Vector3::new(0.0, 3.0, 0.0), Vector3::new(0.0, 3.0, 0.0)),
"intersects_segment(), segment of length 0 *inside* the box -> true"
);
assert!(
!aabb.intersects_segment(Vector3::new(0.0, 300.0, 0.0), Vector3::new(0.0, 300.0, 0.0)),
"intersects_segment(), segment of length 0 *outside* the box -> false"
);
}
}