Skip to content

Commit

Permalink
Support transforming bounding volumes (bevyengine#11681)
Browse files Browse the repository at this point in the history
# Objective

Make it straightforward to translate and rotate bounding volumes.

## Solution

Add `translate_by`/`translated_by`, `rotate_by`/`rotated_by`,
`transform_by`/`transformed_by` methods to the `BoundingVolume` trait.
This follows the naming used for mesh transformations (see bevyengine#11454 and
bevyengine#11675).

---

## Changelog

- Added `translate_by`/`translated_by`, `rotate_by`/`rotated_by`,
`transform_by`/`transformed_by` methods to the `BoundingVolume` trait
and implemented them for the bounding volumes
- Renamed `Position` associated type to `Translation`

---------

Co-authored-by: Mateusz Wachowiak <[email protected]>
  • Loading branch information
2 people authored and spectria-limina committed Mar 9, 2024
1 parent 577aeb7 commit d5feea4
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 15 deletions.
109 changes: 105 additions & 4 deletions crates/bevy_math/src/bounding/bounded2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down Expand Up @@ -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<Self> for Aabb2d {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<Self> for BoundingCircle {
Expand Down Expand Up @@ -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);
Expand Down
111 changes: 105 additions & 6 deletions crates/bevy_math/src/bounding/bounded3d/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod primitive_impls;

use glam::Mat3;

use super::{BoundingVolume, IntersectsVolume};
use crate::prelude::{Quat, Vec3};

Expand Down Expand Up @@ -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.
}

Expand Down Expand Up @@ -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<Self> for Aabb3d {
Expand Down Expand Up @@ -170,7 +221,7 @@ mod aabb3d_tests {
use super::Aabb3d;
use crate::{
bounding::{BoundingSphere, BoundingVolume, IntersectsVolume},
Vec3,
Quat, Vec3,
};

#[test]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<Self> for BoundingSphere {
Expand All @@ -471,10 +554,12 @@ impl IntersectsVolume<Aabb3d> 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]
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit d5feea4

Please sign in to comment.