diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 237dda02c2..26ccd39b94 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -93,11 +93,12 @@ impl Aabb2d { } impl BoundingVolume for Aabb2d { - type Position = Vec2; + type Translation = Vec2; + type Rotation = f32; type HalfSize = Vec2; #[inline(always)] - fn center(&self) -> Self::Position { + fn center(&self) -> Self::Translation { (self.min + self.max) / 2. } @@ -147,6 +148,66 @@ impl BoundingVolume for Aabb2d { debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y); b } + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + /// + /// Note that the result may not be as tightly fitting as the original, and repeated rotations + /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, + /// and consider storing the original AABB and rotating that every time instead. + #[inline(always)] + fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + self.transform_by(translation, rotation); + self + } + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + /// + /// Note that the result may not be as tightly fitting as the original, and repeated rotations + /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, + /// and consider storing the original AABB and rotating that every time instead. + #[inline(always)] + fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + self.rotate_by(rotation); + self.translate_by(translation); + } + + #[inline(always)] + fn translate_by(&mut self, translation: Self::Translation) { + self.min += translation; + self.max += translation; + } + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + /// + /// Note that the result may not be as tightly fitting as the original, and repeated rotations + /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, + /// and consider storing the original AABB and rotating that every time instead. + #[inline(always)] + fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + self.rotate_by(rotation); + self + } + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + /// + /// Note that the result may not be as tightly fitting as the original, and repeated rotations + /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, + /// and consider storing the original AABB and rotating that every time instead. + #[inline(always)] + fn rotate_by(&mut self, rotation: Self::Rotation) { + let rot_mat = Mat2::from_angle(rotation); + let abs_rot_mat = Mat2::from_cols(rot_mat.x_axis.abs(), rot_mat.y_axis.abs()); + let half_size = abs_rot_mat * self.half_size(); + *self = Self::new(rot_mat * self.center(), half_size); + } } impl IntersectsVolume for Aabb2d { @@ -277,6 +338,24 @@ mod aabb2d_tests { assert!(!shrunk.contains(&a)); } + #[test] + fn transform() { + let a = Aabb2d { + min: Vec2::new(-2.0, -2.0), + max: Vec2::new(2.0, 2.0), + }; + let transformed = a.transformed_by(Vec2::new(2.0, -2.0), std::f32::consts::FRAC_PI_4); + let half_length = 2_f32.hypot(2.0); + assert_eq!( + transformed.min, + Vec2::new(2.0 - half_length, -half_length - 2.0) + ); + assert_eq!( + transformed.max, + Vec2::new(2.0 + half_length, half_length - 2.0) + ); + } + #[test] fn closest_point() { let aabb = Aabb2d { @@ -396,11 +475,12 @@ impl BoundingCircle { } impl BoundingVolume for BoundingCircle { - type Position = Vec2; + type Translation = Vec2; + type Rotation = f32; type HalfSize = f32; #[inline(always)] - fn center(&self) -> Self::Position { + fn center(&self) -> Self::Translation { self.center } @@ -449,6 +529,16 @@ impl BoundingVolume for BoundingCircle { debug_assert!(self.radius() >= amount); Self::new(self.center, self.radius() - amount) } + + #[inline(always)] + fn translate_by(&mut self, translation: Vec2) { + self.center += translation; + } + + #[inline(always)] + fn rotate_by(&mut self, rotation: f32) { + self.center = Mat2::from_angle(rotation) * self.center; + } } impl IntersectsVolume for BoundingCircle { @@ -551,6 +641,17 @@ mod bounding_circle_tests { assert!(!shrunk.contains(&a)); } + #[test] + fn transform() { + let a = BoundingCircle::new(Vec2::ONE, 5.0); + let transformed = a.transformed_by(Vec2::new(2.0, -2.0), std::f32::consts::FRAC_PI_4); + assert_eq!( + transformed.center, + Vec2::new(2.0, std::f32::consts::SQRT_2 - 2.0) + ); + assert_eq!(transformed.radius(), 5.0); + } + #[test] fn closest_point() { let circle = BoundingCircle::new(Vec2::ZERO, 1.0); diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index fff2b900de..3856a5541b 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -1,5 +1,7 @@ mod primitive_impls; +use glam::Mat3; + use super::{BoundingVolume, IntersectsVolume}; use crate::prelude::{Quat, Vec3}; @@ -87,11 +89,12 @@ impl Aabb3d { } impl BoundingVolume for Aabb3d { - type Position = Vec3; + type Translation = Vec3; + type Rotation = Quat; type HalfSize = Vec3; #[inline(always)] - fn center(&self) -> Self::Position { + fn center(&self) -> Self::Translation { (self.min + self.max) / 2. } @@ -143,6 +146,54 @@ impl BoundingVolume for Aabb3d { debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y && b.min.z <= b.max.z); b } + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + #[inline(always)] + fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + self.transform_by(translation, rotation); + self + } + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + #[inline(always)] + fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + self.rotate_by(rotation); + self.translate_by(translation); + } + + #[inline(always)] + fn translate_by(&mut self, translation: Self::Translation) { + self.min += translation; + self.max += translation; + } + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + #[inline(always)] + fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + self.rotate_by(rotation); + self + } + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape. + #[inline(always)] + fn rotate_by(&mut self, rotation: Self::Rotation) { + let rot_mat = Mat3::from_quat(rotation); + let abs_rot_mat = Mat3::from_cols( + rot_mat.x_axis.abs(), + rot_mat.y_axis.abs(), + rot_mat.z_axis.abs(), + ); + let half_size = abs_rot_mat * self.half_size(); + *self = Self::new(rot_mat * self.center(), half_size); + } } impl IntersectsVolume for Aabb3d { @@ -170,7 +221,7 @@ mod aabb3d_tests { use super::Aabb3d; use crate::{ bounding::{BoundingSphere, BoundingVolume, IntersectsVolume}, - Vec3, + Quat, Vec3, }; #[test] @@ -273,6 +324,27 @@ mod aabb3d_tests { assert!(!shrunk.contains(&a)); } + #[test] + fn transform() { + let a = Aabb3d { + min: Vec3::new(-2.0, -2.0, -2.0), + max: Vec3::new(2.0, 2.0, 2.0), + }; + let transformed = a.transformed_by( + Vec3::new(2.0, -2.0, 4.0), + Quat::from_rotation_z(std::f32::consts::FRAC_PI_4), + ); + let half_length = 2_f32.hypot(2.0); + assert_eq!( + transformed.min, + Vec3::new(2.0 - half_length, -half_length - 2.0, 2.0) + ); + assert_eq!( + transformed.max, + Vec3::new(2.0 + half_length, half_length - 2.0, 6.0) + ); + } + #[test] fn closest_point() { let aabb = Aabb3d { @@ -388,11 +460,12 @@ impl BoundingSphere { } impl BoundingVolume for BoundingSphere { - type Position = Vec3; + type Translation = Vec3; + type Rotation = Quat; type HalfSize = f32; #[inline(always)] - fn center(&self) -> Self::Position { + fn center(&self) -> Self::Translation { self.center } @@ -451,6 +524,16 @@ impl BoundingVolume for BoundingSphere { }, } } + + #[inline(always)] + fn translate_by(&mut self, translation: Vec3) { + self.center += translation; + } + + #[inline(always)] + fn rotate_by(&mut self, rotation: Quat) { + self.center = rotation * self.center; + } } impl IntersectsVolume for BoundingSphere { @@ -471,10 +554,12 @@ impl IntersectsVolume for BoundingSphere { #[cfg(test)] mod bounding_sphere_tests { + use approx::assert_relative_eq; + use super::BoundingSphere; use crate::{ bounding::{BoundingVolume, IntersectsVolume}, - Vec3, + Quat, Vec3, }; #[test] @@ -553,6 +638,20 @@ mod bounding_sphere_tests { assert!(!shrunk.contains(&a)); } + #[test] + fn transform() { + let a = BoundingSphere::new(Vec3::ONE, 5.0); + let transformed = a.transformed_by( + Vec3::new(2.0, -2.0, 4.0), + Quat::from_rotation_z(std::f32::consts::FRAC_PI_4), + ); + assert_relative_eq!( + transformed.center, + Vec3::new(2.0, std::f32::consts::SQRT_2 - 2.0, 5.0) + ); + assert_eq!(transformed.radius(), 5.0); + } + #[test] fn closest_point() { let sphere = BoundingSphere::new(Vec3::ZERO, 1.0); diff --git a/crates/bevy_math/src/bounding/mod.rs b/crates/bevy_math/src/bounding/mod.rs index caadf7eb52..3fcda1d706 100644 --- a/crates/bevy_math/src/bounding/mod.rs +++ b/crates/bevy_math/src/bounding/mod.rs @@ -10,16 +10,20 @@ /// overlapping elements or finding intersections. /// /// This trait supports both 2D and 3D bounding shapes. -pub trait BoundingVolume { +pub trait BoundingVolume: Sized { /// The position type used for the volume. This should be `Vec2` for 2D and `Vec3` for 3D. - type Position: Clone + Copy + PartialEq; + type Translation: Clone + Copy + PartialEq; + + /// The rotation type used for the volume. This should be `f32` for 2D and `Quat` for 3D. + type Rotation: Clone + Copy + PartialEq; + /// The type used for the size of the bounding volume. Usually a half size. For example an /// `f32` radius for a circle, or a `Vec3` with half sizes for x, y and z for a 3D axis-aligned /// bounding box type HalfSize; /// Returns the center of the bounding volume. - fn center(&self) -> Self::Position; + fn center(&self) -> Self::Translation; /// Returns the half size of the bounding volume. fn half_size(&self) -> Self::HalfSize; @@ -38,11 +42,47 @@ pub trait BoundingVolume { /// Computes the smallest bounding volume that contains both `self` and `other`. fn merge(&self, other: &Self) -> Self; - /// Increase the size of the bounding volume in each direction by the given amount + /// Increases the size of the bounding volume in each direction by the given amount. fn grow(&self, amount: Self::HalfSize) -> Self; - /// Decrease the size of the bounding volume in each direction by the given amount + /// Decreases the size of the bounding volume in each direction by the given amount. fn shrink(&self, amount: Self::HalfSize) -> Self; + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + self.transform_by(translation, rotation); + self + } + + /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. + fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + self.rotate_by(rotation); + self.translate_by(translation); + } + + /// Translates the bounding volume by the given translation. + fn translated_by(mut self, translation: Self::Translation) -> Self { + self.translate_by(translation); + self + } + + /// Translates the bounding volume by the given translation. + fn translate_by(&mut self, translation: Self::Translation); + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is a combination of the original volume and the rotated volume, + /// so it is guaranteed to be either the same size or larger than the original. + fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + self.rotate_by(rotation); + self + } + + /// Rotates the bounding volume around the origin by the given rotation. + /// + /// The result is a combination of the original volume and the rotated volume, + /// so it is guaranteed to be either the same size or larger than the original. + fn rotate_by(&mut self, rotation: Self::Rotation); } /// A trait that generalizes intersection tests against a volume.