# Objective Closes #10570. #10946 added bounding volume types and traits, but didn't use them for anything yet. This PR implements `Bounded2d` and `Bounded3d` for Bevy's primitive shapes. ## Solution Implement `Bounded2d` and `Bounded3d` for primitive shapes. This allows computing AABBs and bounding circles/spheres for them. For most shapes, there are several ways of implementing bounding volumes. I took inspiration from [Parry's bounding volumes](https://github.com/dimforge/parry/tree/master/src/bounding_volume), [Inigo Quilez](http://iquilezles.org/articles/diskbbox/), and figured out the rest myself using geometry. I tried to comment all slightly non-trivial or unclear math to make it understandable. Parry uses support mapping (finding the farthest point in some direction for convex shapes) for some AABBs like cones, cylinders, and line segments. This involves several quat operations and normalizations, so I opted for the simpler and more efficient geometric approaches shown in [Quilez's article](http://iquilezles.org/articles/diskbbox/). Below you can see some of the bounding volumes working in 2D and 3D. Note that I can't conveniently add these examples yet because they use primitive shape meshing, which is still WIP. https://github.com/bevyengine/bevy/assets/57632562/4465cbc6-285b-4c71-b62d-a2b3ee16f8b4 https://github.com/bevyengine/bevy/assets/57632562/94b4ac84-a092-46d7-b438-ce2e971496a4 --- ## Changelog - Implemented `Bounded2d`/`Bounded3d` for primitive shapes - Added `from_point_cloud` method for bounding volumes (used by many bounding implementations) - Added `point_cloud_2d/3d_center` and `rotate_vec2` utility functions - Added `RegularPolygon::vertices` method (used in regular polygon AABB construction) - Added `Triangle::circumcenter` method (used in triangle bounding circle construction) - Added bounding circle/sphere creation from AABBs and vice versa ## Extra Do we want to implement `Bounded2d` for some "3D-ish" shapes too? For example, capsules are sort of dimension-agnostic and useful for 2D, so I think that would be good to implement. But a cylinder in 2D is just a rectangle, and a cone is a triangle, so they wouldn't make as much sense to me. A conical frustum would be an isosceles trapezoid, which could be useful, but I'm not sure if computing the 2D AABB of a 3D frustum makes semantic sense.
467 lines
16 KiB
Rust
467 lines
16 KiB
Rust
//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).
|
|
|
|
use glam::{Mat2, Vec2};
|
|
|
|
use crate::primitives::{
|
|
BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d,
|
|
Rectangle, RegularPolygon, Segment2d, Triangle2d,
|
|
};
|
|
|
|
use super::{Aabb2d, Bounded2d, BoundingCircle};
|
|
|
|
impl Bounded2d for Circle {
|
|
fn aabb_2d(&self, translation: Vec2, _rotation: f32) -> Aabb2d {
|
|
Aabb2d {
|
|
min: translation - Vec2::splat(self.radius),
|
|
max: translation + Vec2::splat(self.radius),
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, self.radius)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Ellipse {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
// V = (hh * cos(beta), hh * sin(beta))
|
|
// #####*#####
|
|
// ### | ###
|
|
// # hh | #
|
|
// # *---------* U = (hw * cos(alpha), hw * sin(alpha))
|
|
// # hw #
|
|
// ### ###
|
|
// ###########
|
|
|
|
let (hw, hh) = (self.half_width, self.half_height);
|
|
|
|
// Sine and cosine of rotation angle alpha.
|
|
let (alpha_sin, alpha_cos) = rotation.sin_cos();
|
|
|
|
// Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions:
|
|
// sin(beta) = sin(alpha + pi/2) = cos(alpha)
|
|
// cos(beta) = cos(alpha + pi/2) = -sin(alpha)
|
|
let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);
|
|
|
|
// Compute points U and V, the extremes of the ellipse
|
|
let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);
|
|
let (vx, vy) = (hh * beta_cos, hh * beta_sin);
|
|
|
|
let half_extents = Vec2::new(ux.hypot(vx), uy.hypot(vy));
|
|
|
|
Aabb2d {
|
|
min: translation - half_extents,
|
|
max: translation + half_extents,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, self.half_width.max(self.half_height))
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Plane2d {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
let normal = Mat2::from_angle(rotation) * *self.normal;
|
|
let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;
|
|
let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;
|
|
|
|
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
|
|
// like growing or shrinking the AABB without breaking things.
|
|
let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
|
|
let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
|
|
let half_size = Vec2::new(half_width, half_height);
|
|
|
|
Aabb2d {
|
|
min: translation - half_size,
|
|
max: translation + half_size,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, f32::MAX / 2.0)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Line2d {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
let direction = Mat2::from_angle(rotation) * *self.direction;
|
|
|
|
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
|
|
// like growing or shrinking the AABB without breaking things.
|
|
let max = f32::MAX / 2.0;
|
|
let half_width = if direction.x == 0.0 { 0.0 } else { max };
|
|
let half_height = if direction.y == 0.0 { 0.0 } else { max };
|
|
let half_size = Vec2::new(half_width, half_height);
|
|
|
|
Aabb2d {
|
|
min: translation - half_size,
|
|
max: translation + half_size,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, f32::MAX / 2.0)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Segment2d {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
// Rotate the segment by `rotation`
|
|
let direction = Mat2::from_angle(rotation) * *self.direction;
|
|
let half_extent = (self.half_length * direction).abs();
|
|
|
|
Aabb2d {
|
|
min: translation - half_extent,
|
|
max: translation + half_extent,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, self.half_length)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Bounded2d for Polyline2d<N> {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for BoxedPolyline2d {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Triangle2d {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
let rotation_mat = Mat2::from_angle(rotation);
|
|
let [a, b, c] = self.vertices.map(|vtx| rotation_mat * vtx);
|
|
|
|
let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));
|
|
let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));
|
|
|
|
Aabb2d {
|
|
min: min + translation,
|
|
max: max + translation,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
|
|
let rotation_mat = Mat2::from_angle(rotation);
|
|
let [a, b, c] = self.vertices;
|
|
|
|
// The points of the segment opposite to the obtuse or right angle if one exists
|
|
let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {
|
|
Some((b, c))
|
|
} else if (c - b).dot(a - b) <= 0.0 {
|
|
Some((c, a))
|
|
} else if (a - c).dot(b - c) <= 0.0 {
|
|
Some((a, b))
|
|
} else {
|
|
// The triangle is acute.
|
|
None
|
|
};
|
|
|
|
// Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices.
|
|
// Otherwise, it's the circumcircle and passes through all three.
|
|
if let Some((point1, point2)) = side_opposite_to_non_acute {
|
|
// The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side.
|
|
// We can compute the minimum bounding circle from the line segment of the longest side.
|
|
let (segment, center) = Segment2d::from_points(point1, point2);
|
|
segment.bounding_circle(rotation_mat * center + translation, rotation)
|
|
} else {
|
|
// The triangle is acute, so the smallest bounding circle is the circumcircle.
|
|
let (Circle { radius }, circumcenter) = self.circumcircle();
|
|
BoundingCircle::new(rotation_mat * circumcenter + translation, radius)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for Rectangle {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
let half_size = Vec2::new(self.half_width, self.half_height);
|
|
|
|
// Compute the AABB of the rotated rectangle by transforming the half-extents
|
|
// by an absolute rotation matrix.
|
|
let (sin, cos) = rotation.sin_cos();
|
|
let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]);
|
|
let half_extents = abs_rot_mat * half_size;
|
|
|
|
Aabb2d {
|
|
min: translation - half_extents,
|
|
max: translation + half_extents,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
let radius = self.half_width.hypot(self.half_height);
|
|
BoundingCircle::new(translation, radius)
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> Bounded2d for Polygon<N> {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for BoxedPolygon {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
|
|
}
|
|
}
|
|
|
|
impl Bounded2d for RegularPolygon {
|
|
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
|
|
let mut min = Vec2::ZERO;
|
|
let mut max = Vec2::ZERO;
|
|
|
|
for vertex in self.vertices(rotation) {
|
|
min = min.min(vertex);
|
|
max = max.max(vertex);
|
|
}
|
|
|
|
Aabb2d {
|
|
min: min + translation,
|
|
max: max + translation,
|
|
}
|
|
}
|
|
|
|
fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle {
|
|
BoundingCircle::new(translation, self.circumcircle.radius)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use glam::Vec2;
|
|
|
|
use crate::{
|
|
bounding::Bounded2d,
|
|
primitives::{
|
|
Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle,
|
|
RegularPolygon, Segment2d, Triangle2d,
|
|
},
|
|
};
|
|
|
|
#[test]
|
|
fn circle() {
|
|
let circle = Circle { radius: 1.0 };
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = circle.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
|
|
|
|
let bounding_circle = circle.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn ellipse() {
|
|
let ellipse = Ellipse {
|
|
half_width: 1.0,
|
|
half_height: 0.5,
|
|
};
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = ellipse.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
|
|
|
|
let bounding_circle = ellipse.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn plane() {
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb1 = Plane2d::new(Vec2::X).aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
|
|
assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
|
|
|
|
let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
|
|
assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
|
|
|
|
let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
|
|
assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
|
|
|
|
let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn line() {
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb1 = Line2d {
|
|
direction: Direction2d::Y,
|
|
}
|
|
.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
|
|
assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
|
|
|
|
let aabb2 = Line2d {
|
|
direction: Direction2d::X,
|
|
}
|
|
.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
|
|
assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
|
|
|
|
let aabb3 = Line2d {
|
|
direction: Direction2d::from_xy(1.0, 1.0).unwrap(),
|
|
}
|
|
.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
|
|
assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
|
|
|
|
let bounding_circle = Line2d {
|
|
direction: Direction2d::Y,
|
|
}
|
|
.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn segment() {
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0;
|
|
|
|
let aabb = segment.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
|
|
|
|
let bounding_circle = segment.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5));
|
|
}
|
|
|
|
#[test]
|
|
fn polyline() {
|
|
let polyline = Polyline2d::<4>::new([
|
|
Vec2::ONE,
|
|
Vec2::new(-1.0, 1.0),
|
|
Vec2::NEG_ONE,
|
|
Vec2::new(1.0, -1.0),
|
|
]);
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = polyline.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
|
|
|
|
let bounding_circle = polyline.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2);
|
|
}
|
|
|
|
#[test]
|
|
fn acute_triangle() {
|
|
let acute_triangle =
|
|
Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = acute_triangle.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
|
|
|
|
// For acute triangles, the center is the circumcenter
|
|
let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
|
|
let bounding_circle = acute_triangle.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, circumcenter + translation);
|
|
assert_eq!(bounding_circle.radius(), radius);
|
|
}
|
|
|
|
#[test]
|
|
fn obtuse_triangle() {
|
|
let obtuse_triangle = Triangle2d::new(
|
|
Vec2::new(0.0, 1.0),
|
|
Vec2::new(-10.0, -1.0),
|
|
Vec2::new(10.0, -1.0),
|
|
);
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = obtuse_triangle.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
|
|
assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
|
|
|
|
// For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle)
|
|
let bounding_circle = obtuse_triangle.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation - Vec2::Y);
|
|
assert_eq!(bounding_circle.radius(), 10.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rectangle() {
|
|
let rectangle = Rectangle::new(2.0, 1.0);
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = rectangle.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
|
|
let expected_half_size = Vec2::splat(1.0606601);
|
|
assert_eq!(aabb.min, translation - expected_half_size);
|
|
assert_eq!(aabb.max, translation + expected_half_size);
|
|
|
|
let bounding_circle = rectangle.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5));
|
|
}
|
|
|
|
#[test]
|
|
fn polygon() {
|
|
let polygon = Polygon::<4>::new([
|
|
Vec2::ONE,
|
|
Vec2::new(-1.0, 1.0),
|
|
Vec2::NEG_ONE,
|
|
Vec2::new(1.0, -1.0),
|
|
]);
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = polygon.aabb_2d(translation, 0.0);
|
|
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
|
|
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
|
|
|
|
let bounding_circle = polygon.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2);
|
|
}
|
|
|
|
#[test]
|
|
fn regular_polygon() {
|
|
let regular_polygon = RegularPolygon::new(1.0, 5);
|
|
let translation = Vec2::new(2.0, 1.0);
|
|
|
|
let aabb = regular_polygon.aabb_2d(translation, 0.0);
|
|
assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
|
|
assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
|
|
|
|
let bounding_circle = regular_polygon.bounding_circle(translation, 0.0);
|
|
assert_eq!(bounding_circle.center, translation);
|
|
assert_eq!(bounding_circle.radius(), 1.0);
|
|
}
|
|
}
|