Skip to content

Commit c4e479a

Browse files
Implement bounding volume types (#10946)
# Objective Implement bounding volume trait and the 4 types from #10570. I will add intersection tests in a future PR. ## Solution Implement mostly everything as written in the issue, except: - Intersection is no longer a method on the bounding volumes, but a separate trait. - I implemented a `visible_area` since it's the most common usecase to care about the surface that could collide with cast rays. - Maybe we want both? --- ## Changelog - Added bounding volume types to bevy_math --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent d4ffd4f commit c4e479a

File tree

4 files changed

+775
-0
lines changed

4 files changed

+775
-0
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
use super::BoundingVolume;
2+
use crate::prelude::Vec2;
3+
4+
/// A trait with methods that return 2D bounded volumes for a shape
5+
pub trait Bounded2d {
6+
/// Get an axis-aligned bounding box for the shape with the given translation and rotation.
7+
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
8+
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d;
9+
/// Get a bounding circle for the shape
10+
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
11+
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle;
12+
}
13+
14+
/// A 2D axis-aligned bounding box, or bounding rectangle
15+
#[doc(alias = "BoundingRectangle")]
16+
#[derive(Clone, Debug)]
17+
pub struct Aabb2d {
18+
/// The minimum, conventionally bottom-left, point of the box
19+
pub min: Vec2,
20+
/// The maximum, conventionally top-right, point of the box
21+
pub max: Vec2,
22+
}
23+
24+
impl BoundingVolume for Aabb2d {
25+
type Position = Vec2;
26+
type HalfSize = Vec2;
27+
28+
#[inline(always)]
29+
fn center(&self) -> Self::Position {
30+
(self.min + self.max) / 2.
31+
}
32+
33+
#[inline(always)]
34+
fn half_size(&self) -> Self::HalfSize {
35+
(self.max - self.min) / 2.
36+
}
37+
38+
#[inline(always)]
39+
fn visible_area(&self) -> f32 {
40+
let b = self.max - self.min;
41+
b.x * b.y
42+
}
43+
44+
#[inline(always)]
45+
fn contains(&self, other: &Self) -> bool {
46+
other.min.x >= self.min.x
47+
&& other.min.y >= self.min.y
48+
&& other.max.x <= self.max.x
49+
&& other.max.y <= self.max.y
50+
}
51+
52+
#[inline(always)]
53+
fn merge(&self, other: &Self) -> Self {
54+
Self {
55+
min: self.min.min(other.min),
56+
max: self.max.max(other.max),
57+
}
58+
}
59+
60+
#[inline(always)]
61+
fn grow(&self, amount: Self::HalfSize) -> Self {
62+
let b = Self {
63+
min: self.min - amount,
64+
max: self.max + amount,
65+
};
66+
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
67+
b
68+
}
69+
70+
#[inline(always)]
71+
fn shrink(&self, amount: Self::HalfSize) -> Self {
72+
let b = Self {
73+
min: self.min + amount,
74+
max: self.max - amount,
75+
};
76+
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
77+
b
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod aabb2d_tests {
83+
use super::Aabb2d;
84+
use crate::{bounding::BoundingVolume, Vec2};
85+
86+
#[test]
87+
fn center() {
88+
let aabb = Aabb2d {
89+
min: Vec2::new(-0.5, -1.),
90+
max: Vec2::new(1., 1.),
91+
};
92+
assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < std::f32::EPSILON);
93+
let aabb = Aabb2d {
94+
min: Vec2::new(5., -10.),
95+
max: Vec2::new(10., -5.),
96+
};
97+
assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < std::f32::EPSILON);
98+
}
99+
100+
#[test]
101+
fn half_size() {
102+
let aabb = Aabb2d {
103+
min: Vec2::new(-0.5, -1.),
104+
max: Vec2::new(1., 1.),
105+
};
106+
let half_size = aabb.half_size();
107+
assert!((half_size - Vec2::new(0.75, 1.)).length() < std::f32::EPSILON);
108+
}
109+
110+
#[test]
111+
fn area() {
112+
let aabb = Aabb2d {
113+
min: Vec2::new(-1., -1.),
114+
max: Vec2::new(1., 1.),
115+
};
116+
assert!((aabb.visible_area() - 4.).abs() < std::f32::EPSILON);
117+
let aabb = Aabb2d {
118+
min: Vec2::new(0., 0.),
119+
max: Vec2::new(1., 0.5),
120+
};
121+
assert!((aabb.visible_area() - 0.5).abs() < std::f32::EPSILON);
122+
}
123+
124+
#[test]
125+
fn contains() {
126+
let a = Aabb2d {
127+
min: Vec2::new(-1., -1.),
128+
max: Vec2::new(1., 1.),
129+
};
130+
let b = Aabb2d {
131+
min: Vec2::new(-2., -1.),
132+
max: Vec2::new(1., 1.),
133+
};
134+
assert!(!a.contains(&b));
135+
let b = Aabb2d {
136+
min: Vec2::new(-0.25, -0.8),
137+
max: Vec2::new(1., 1.),
138+
};
139+
assert!(a.contains(&b));
140+
}
141+
142+
#[test]
143+
fn merge() {
144+
let a = Aabb2d {
145+
min: Vec2::new(-1., -1.),
146+
max: Vec2::new(1., 0.5),
147+
};
148+
let b = Aabb2d {
149+
min: Vec2::new(-2., -0.5),
150+
max: Vec2::new(0.75, 1.),
151+
};
152+
let merged = a.merge(&b);
153+
assert!((merged.min - Vec2::new(-2., -1.)).length() < std::f32::EPSILON);
154+
assert!((merged.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON);
155+
assert!(merged.contains(&a));
156+
assert!(merged.contains(&b));
157+
assert!(!a.contains(&merged));
158+
assert!(!b.contains(&merged));
159+
}
160+
161+
#[test]
162+
fn grow() {
163+
let a = Aabb2d {
164+
min: Vec2::new(-1., -1.),
165+
max: Vec2::new(1., 1.),
166+
};
167+
let padded = a.grow(Vec2::ONE);
168+
assert!((padded.min - Vec2::new(-2., -2.)).length() < std::f32::EPSILON);
169+
assert!((padded.max - Vec2::new(2., 2.)).length() < std::f32::EPSILON);
170+
assert!(padded.contains(&a));
171+
assert!(!a.contains(&padded));
172+
}
173+
174+
#[test]
175+
fn shrink() {
176+
let a = Aabb2d {
177+
min: Vec2::new(-2., -2.),
178+
max: Vec2::new(2., 2.),
179+
};
180+
let shrunk = a.shrink(Vec2::ONE);
181+
assert!((shrunk.min - Vec2::new(-1., -1.)).length() < std::f32::EPSILON);
182+
assert!((shrunk.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON);
183+
assert!(a.contains(&shrunk));
184+
assert!(!shrunk.contains(&a));
185+
}
186+
}
187+
188+
use crate::primitives::Circle;
189+
190+
/// A bounding circle
191+
#[derive(Clone, Debug)]
192+
pub struct BoundingCircle {
193+
/// The center of the bounding circle
194+
pub center: Vec2,
195+
/// The circle
196+
pub circle: Circle,
197+
}
198+
199+
impl BoundingCircle {
200+
/// Construct a bounding circle from its center and radius
201+
#[inline(always)]
202+
pub fn new(center: Vec2, radius: f32) -> Self {
203+
debug_assert!(radius >= 0.);
204+
Self {
205+
center,
206+
circle: Circle { radius },
207+
}
208+
}
209+
210+
/// Get the radius of the bounding circle
211+
#[inline(always)]
212+
pub fn radius(&self) -> f32 {
213+
self.circle.radius
214+
}
215+
}
216+
217+
impl BoundingVolume for BoundingCircle {
218+
type Position = Vec2;
219+
type HalfSize = f32;
220+
221+
#[inline(always)]
222+
fn center(&self) -> Self::Position {
223+
self.center
224+
}
225+
226+
#[inline(always)]
227+
fn half_size(&self) -> Self::HalfSize {
228+
self.radius()
229+
}
230+
231+
#[inline(always)]
232+
fn visible_area(&self) -> f32 {
233+
std::f32::consts::PI * self.radius() * self.radius()
234+
}
235+
236+
#[inline(always)]
237+
fn contains(&self, other: &Self) -> bool {
238+
let diff = self.radius() - other.radius();
239+
self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff)
240+
}
241+
242+
#[inline(always)]
243+
fn merge(&self, other: &Self) -> Self {
244+
let diff = other.center - self.center;
245+
let length = diff.length();
246+
if self.radius() >= length + other.radius() {
247+
return self.clone();
248+
}
249+
if other.radius() >= length + self.radius() {
250+
return other.clone();
251+
}
252+
let dir = diff / length;
253+
Self::new(
254+
(self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
255+
(length + self.radius() + other.radius()) / 2.,
256+
)
257+
}
258+
259+
#[inline(always)]
260+
fn grow(&self, amount: Self::HalfSize) -> Self {
261+
debug_assert!(amount >= 0.);
262+
Self::new(self.center, self.radius() + amount)
263+
}
264+
265+
#[inline(always)]
266+
fn shrink(&self, amount: Self::HalfSize) -> Self {
267+
debug_assert!(amount >= 0.);
268+
debug_assert!(self.radius() >= amount);
269+
Self::new(self.center, self.radius() - amount)
270+
}
271+
}
272+
273+
#[cfg(test)]
274+
mod bounding_circle_tests {
275+
use super::BoundingCircle;
276+
use crate::{bounding::BoundingVolume, Vec2};
277+
278+
#[test]
279+
fn area() {
280+
let circle = BoundingCircle::new(Vec2::ONE, 5.);
281+
// Since this number is messy we check it with a higher threshold
282+
assert!((circle.visible_area() - 78.5398).abs() < 0.001);
283+
}
284+
285+
#[test]
286+
fn contains() {
287+
let a = BoundingCircle::new(Vec2::ONE, 5.);
288+
let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.);
289+
assert!(!a.contains(&b));
290+
let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5);
291+
assert!(a.contains(&b));
292+
}
293+
294+
#[test]
295+
fn contains_identical() {
296+
let a = BoundingCircle::new(Vec2::ONE, 5.);
297+
assert!(a.contains(&a));
298+
}
299+
300+
#[test]
301+
fn merge() {
302+
// When merging two circles that don't contain each other, we find a center position that
303+
// contains both
304+
let a = BoundingCircle::new(Vec2::ONE, 5.);
305+
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
306+
let merged = a.merge(&b);
307+
assert!((merged.center - Vec2::new(1., 0.5)).length() < std::f32::EPSILON);
308+
assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON);
309+
assert!(merged.contains(&a));
310+
assert!(merged.contains(&b));
311+
assert!(!a.contains(&merged));
312+
assert!(!b.contains(&merged));
313+
314+
// When one circle contains the other circle, we use the bigger circle
315+
let b = BoundingCircle::new(Vec2::ZERO, 3.);
316+
assert!(a.contains(&b));
317+
let merged = a.merge(&b);
318+
assert_eq!(merged.center, a.center);
319+
assert_eq!(merged.radius(), a.radius());
320+
321+
// When two circles are at the same point, we use the bigger radius
322+
let b = BoundingCircle::new(Vec2::ONE, 6.);
323+
let merged = a.merge(&b);
324+
assert_eq!(merged.center, a.center);
325+
assert_eq!(merged.radius(), b.radius());
326+
}
327+
328+
#[test]
329+
fn merge_identical() {
330+
let a = BoundingCircle::new(Vec2::ONE, 5.);
331+
let merged = a.merge(&a);
332+
assert_eq!(merged.center, a.center);
333+
assert_eq!(merged.radius(), a.radius());
334+
}
335+
336+
#[test]
337+
fn grow() {
338+
let a = BoundingCircle::new(Vec2::ONE, 5.);
339+
let padded = a.grow(1.25);
340+
assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON);
341+
assert!(padded.contains(&a));
342+
assert!(!a.contains(&padded));
343+
}
344+
345+
#[test]
346+
fn shrink() {
347+
let a = BoundingCircle::new(Vec2::ONE, 5.);
348+
let shrunk = a.shrink(0.5);
349+
assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON);
350+
assert!(a.contains(&shrunk));
351+
assert!(!shrunk.contains(&a));
352+
}
353+
}

0 commit comments

Comments
 (0)