parry2d/bounding_volume/bounding_sphere.rs
1//! Bounding sphere.
2
3use crate::bounding_volume::BoundingVolume;
4use crate::math::{Pose, Real, Vector};
5
6/// A Bounding Sphere.
7///
8/// A bounding sphere is a spherical bounding volume defined by a center point and a radius.
9/// Unlike an AABB, a bounding sphere is rotation-invariant, meaning it doesn't need to be
10/// recomputed when an object rotates.
11///
12/// # Structure
13///
14/// - **center**: The center point of the sphere
15/// - **radius**: The distance from the center to any point on the sphere's surface
16///
17/// # Properties
18///
19/// - **Rotation-invariant**: Remains valid under rotation transformations
20/// - **Simple**: Only 4 values (3D: x, y, z, radius; 2D: x, y, radius)
21/// - **Conservative**: Often larger than the actual shape, especially for elongated objects
22/// - **Fast intersection tests**: Only requires distance comparison
23///
24/// # Use Cases
25///
26/// Bounding spheres are useful for:
27///
28/// - **Rotating objects**: No recomputation needed when objects rotate
29/// - **Broad-phase culling**: Quick rejection of distant object pairs
30/// - **View frustum culling**: Simple sphere-frustum tests
31/// - **Level of detail (LOD)**: Distance-based detail switching
32/// - **Physics simulations**: Fast bounds checking for moving/rotating bodies
33///
34/// # Performance
35///
36/// - **Intersection test**: O(1) - Single distance comparison
37/// - **Rotation**: O(1) - Only center needs transformation
38/// - **Contains test**: O(1) - Distance plus radius comparison
39///
40/// # Comparison to AABB
41///
42/// **When to use BoundingSphere:**
43/// - Objects rotate frequently
44/// - Objects are roughly spherical or evenly distributed
45/// - Memory is tight (fewer values to store)
46/// - Rotation-invariant bounds are required
47///
48/// **When to use AABB:**
49/// - Objects are axis-aligned or rarely rotate
50/// - Objects are elongated or box-like
51/// - Tighter bounds are critical
52/// - Building spatial hierarchies (BVH, octree)
53///
54/// # Example
55///
56/// ```rust
57/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
58/// use parry3d::bounding_volume::BoundingSphere;
59/// use parry3d::math::Vector;
60///
61/// // Create a bounding sphere with center at origin and radius 2.0
62/// let sphere = BoundingSphere::new(Vector::ZERO, 2.0);
63///
64/// // Check basic properties
65/// assert_eq!(sphere.center(), Vector::ZERO);
66/// assert_eq!(sphere.radius(), 2.0);
67///
68/// // Test if a point is within the sphere
69/// let point = Vector::new(1.0, 1.0, 0.0);
70/// let distance = (point - sphere.center()).length();
71/// assert!(distance <= sphere.radius());
72/// # }
73/// ```
74///
75/// ```rust
76/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
77/// use parry3d::bounding_volume::BoundingSphere;
78/// use parry3d::math::Vector;
79///
80/// // Create a sphere and translate it
81/// let sphere = BoundingSphere::new(Vector::new(1.0, 2.0, 3.0), 1.5);
82/// let translation = Vector::new(5.0, 0.0, 0.0);
83/// let moved = sphere.translated(translation);
84///
85/// assert_eq!(moved.center(), Vector::new(6.0, 2.0, 3.0));
86/// assert_eq!(moved.radius(), 1.5); // Radius unchanged by translation
87/// # }
88/// ```
89///
90/// ```rust
91/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
92/// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
93/// use parry3d::math::Vector;
94///
95/// // Merge two bounding spheres
96/// let sphere1 = BoundingSphere::new(Vector::ZERO, 1.0);
97/// let sphere2 = BoundingSphere::new(Vector::new(4.0, 0.0, 0.0), 1.0);
98///
99/// let merged = sphere1.merged(&sphere2);
100/// // The merged sphere contains both original spheres
101/// assert!(merged.contains(&sphere1));
102/// assert!(merged.contains(&sphere2));
103/// # }
104/// ```
105#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
106#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
107#[cfg_attr(
108 feature = "rkyv",
109 derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
110)]
111#[derive(Debug, PartialEq, Copy, Clone)]
112#[repr(C)]
113pub struct BoundingSphere {
114 /// The center point of the bounding sphere.
115 pub center: Vector,
116
117 /// The radius of the bounding sphere.
118 ///
119 /// This is the distance from the center to any point on the sphere's surface.
120 /// All points within the bounded object should satisfy: distance(point, center) <= radius
121 pub radius: Real,
122}
123
124impl BoundingSphere {
125 /// Creates a new bounding sphere from a center point and radius.
126 ///
127 /// # Arguments
128 ///
129 /// * `center` - The center point of the sphere
130 /// * `radius` - The radius of the sphere (must be non-negative)
131 ///
132 /// # Example
133 ///
134 /// ```
135 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
136 /// use parry3d::bounding_volume::BoundingSphere;
137 /// use parry3d::math::Vector;
138 ///
139 /// // Create a sphere centered at (1, 2, 3) with radius 5.0
140 /// let sphere = BoundingSphere::new(
141 /// Vector::new(1.0, 2.0, 3.0),
142 /// 5.0
143 /// );
144 ///
145 /// assert_eq!(sphere.center(), Vector::new(1.0, 2.0, 3.0));
146 /// assert_eq!(sphere.radius(), 5.0);
147 /// # }
148 /// ```
149 pub fn new(center: Vector, radius: Real) -> BoundingSphere {
150 BoundingSphere { center, radius }
151 }
152
153 /// Returns a reference to the center point of this bounding sphere.
154 ///
155 /// # Example
156 ///
157 /// ```
158 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
159 /// use parry3d::bounding_volume::BoundingSphere;
160 /// use parry3d::math::Vector;
161 ///
162 /// let sphere = BoundingSphere::new(Vector::new(1.0, 2.0, 3.0), 5.0);
163 /// let center = sphere.center();
164 ///
165 /// assert_eq!(center, Vector::new(1.0, 2.0, 3.0));
166 /// # }
167 /// ```
168 #[inline]
169 pub fn center(&self) -> Vector {
170 self.center
171 }
172
173 /// Returns the radius of this bounding sphere.
174 ///
175 /// The radius is the distance from the center to any point on the sphere's surface.
176 ///
177 /// # Example
178 ///
179 /// ```
180 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
181 /// use parry3d::bounding_volume::BoundingSphere;
182 /// use parry3d::math::Vector;
183 ///
184 /// let sphere = BoundingSphere::new(Vector::ZERO, 10.0);
185 ///
186 /// assert_eq!(sphere.radius(), 10.0);
187 /// # }
188 /// ```
189 #[inline]
190 pub fn radius(&self) -> Real {
191 self.radius
192 }
193
194 /// Transforms this bounding sphere by the given isometry.
195 ///
196 /// For a bounding sphere, only the center point is affected by the transformation.
197 /// The radius remains unchanged because spheres are rotation-invariant and isometries
198 /// preserve distances.
199 ///
200 /// # Arguments
201 ///
202 /// * `m` - The isometry (rigid transformation) to apply
203 ///
204 /// # Example
205 ///
206 /// ```
207 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
208 /// use parry3d::bounding_volume::BoundingSphere;
209 /// use parry3d::math::{Vector, Pose, Rotation};
210 ///
211 /// let sphere = BoundingSphere::new(Vector::new(1.0, 0.0, 0.0), 2.0);
212 ///
213 /// // Create a transformation: translate by (5, 0, 0) and rotate 90 degrees around Z
214 /// let translation = Vector::new(5.0, 0.0, 0.0);
215 /// let rotation = Rotation::from_rotation_z(std::f32::consts::FRAC_PI_2);
216 /// let transform = Pose::from_parts(translation, rotation);
217 ///
218 /// let transformed = sphere.transform_by(&transform);
219 ///
220 /// // The center is transformed
221 /// assert!((transformed.center() - Vector::new(5.0, 1.0, 0.0)).length() < 1e-5);
222 /// // The radius is unchanged
223 /// assert_eq!(transformed.radius(), 2.0);
224 /// # }
225 /// ```
226 #[inline]
227 pub fn transform_by(&self, m: &Pose) -> BoundingSphere {
228 BoundingSphere::new(m * self.center, self.radius)
229 }
230
231 /// Translates this bounding sphere by the given vector.
232 ///
233 /// This is equivalent to `transform_by` with a pure translation, but more efficient
234 /// as it doesn't involve rotation.
235 ///
236 /// # Arguments
237 ///
238 /// * `translation` - The translation vector to add to the center
239 ///
240 /// # Example
241 ///
242 /// ```
243 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
244 /// use parry3d::bounding_volume::BoundingSphere;
245 /// use parry3d::math::Vector;
246 ///
247 /// let sphere = BoundingSphere::new(Vector::ZERO, 1.0);
248 /// let translation = Vector::new(10.0, 5.0, -3.0);
249 ///
250 /// let moved = sphere.translated(translation);
251 ///
252 /// assert_eq!(moved.center(), Vector::new(10.0, 5.0, -3.0));
253 /// assert_eq!(moved.radius(), 1.0); // Radius unchanged
254 /// # }
255 /// ```
256 #[inline]
257 pub fn translated(&self, translation: Vector) -> BoundingSphere {
258 BoundingSphere::new(self.center + translation, self.radius)
259 }
260}
261
262impl BoundingVolume for BoundingSphere {
263 /// Returns the center point of this bounding sphere.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
269 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
270 /// use parry3d::math::Vector;
271 ///
272 /// let sphere = BoundingSphere::new(Vector::new(1.0, 2.0, 3.0), 5.0);
273 ///
274 /// // BoundingVolume::center() returns a Vector by value
275 /// assert_eq!(BoundingVolume::center(&sphere), Vector::new(1.0, 2.0, 3.0));
276 /// # }
277 /// ```
278 #[inline]
279 fn center(&self) -> Vector {
280 self.center()
281 }
282
283 /// Tests if this bounding sphere intersects another bounding sphere.
284 ///
285 /// Two spheres intersect if the distance between their centers is less than or equal
286 /// to the sum of their radii.
287 ///
288 /// # Arguments
289 ///
290 /// * `other` - The other bounding sphere to test against
291 ///
292 /// # Example
293 ///
294 /// ```
295 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
296 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
297 /// use parry3d::math::Vector;
298 ///
299 /// let sphere1 = BoundingSphere::new(Vector::ZERO, 2.0);
300 /// let sphere2 = BoundingSphere::new(Vector::new(3.0, 0.0, 0.0), 2.0);
301 /// let sphere3 = BoundingSphere::new(Vector::new(10.0, 0.0, 0.0), 1.0);
302 ///
303 /// assert!(sphere1.intersects(&sphere2)); // Distance 3.0 <= sum of radii 4.0
304 /// assert!(!sphere1.intersects(&sphere3)); // Distance 10.0 > sum of radii 3.0
305 /// # }
306 /// ```
307 #[inline]
308 fn intersects(&self, other: &BoundingSphere) -> bool {
309 // TODO: refactor that with the code from narrow_phase::ball_ball::collide(...) ?
310 let delta_pos = other.center - self.center;
311 let distance_squared = delta_pos.length_squared();
312 let sum_radius = self.radius + other.radius;
313
314 distance_squared <= sum_radius * sum_radius
315 }
316
317 /// Tests if this bounding sphere fully contains another bounding sphere.
318 ///
319 /// A sphere fully contains another sphere if the distance between their centers
320 /// plus the other's radius is less than or equal to this sphere's radius.
321 ///
322 /// # Arguments
323 ///
324 /// * `other` - The other bounding sphere to test
325 ///
326 /// # Example
327 ///
328 /// ```
329 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
330 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
331 /// use parry3d::math::Vector;
332 ///
333 /// let large = BoundingSphere::new(Vector::ZERO, 10.0);
334 /// let small = BoundingSphere::new(Vector::new(2.0, 0.0, 0.0), 1.0);
335 /// let outside = BoundingSphere::new(Vector::new(15.0, 0.0, 0.0), 2.0);
336 ///
337 /// assert!(large.contains(&small)); // Small sphere is inside large sphere
338 /// assert!(!large.contains(&outside)); // Outside sphere extends beyond large sphere
339 /// assert!(!small.contains(&large)); // Small cannot contain large
340 /// # }
341 /// ```
342 #[inline]
343 fn contains(&self, other: &BoundingSphere) -> bool {
344 let delta_pos = other.center - self.center;
345 let distance = delta_pos.length();
346
347 distance + other.radius <= self.radius
348 }
349
350 /// Merges this bounding sphere with another in-place.
351 ///
352 /// After this operation, this sphere will be the smallest sphere that contains
353 /// both the original sphere and the other sphere.
354 ///
355 /// # Arguments
356 ///
357 /// * `other` - The other bounding sphere to merge with
358 ///
359 /// # Example
360 ///
361 /// ```
362 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
363 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
364 /// use parry3d::math::Vector;
365 ///
366 /// let mut sphere1 = BoundingSphere::new(Vector::ZERO, 1.0);
367 /// let sphere2 = BoundingSphere::new(Vector::new(4.0, 0.0, 0.0), 1.0);
368 ///
369 /// sphere1.merge(&sphere2);
370 ///
371 /// // The merged sphere now contains both original spheres
372 /// assert!(sphere1.contains(&BoundingSphere::new(Vector::ZERO, 1.0)));
373 /// assert!(sphere1.contains(&sphere2));
374 /// # }
375 /// ```
376 #[inline]
377 fn merge(&mut self, other: &BoundingSphere) {
378 let dir = other.center() - self.center();
379 let (dir, length) = dir.normalize_and_length();
380
381 if length == 0.0 {
382 if other.radius > self.radius {
383 self.radius = other.radius
384 }
385 } else {
386 let s_center_dir = self.center.dot(dir);
387 let o_center_dir = other.center.dot(dir);
388
389 let right = if s_center_dir + self.radius > o_center_dir + other.radius {
390 self.center + dir * self.radius
391 } else {
392 other.center + dir * other.radius
393 };
394
395 let left = if -s_center_dir + self.radius > -o_center_dir + other.radius {
396 self.center - dir * self.radius
397 } else {
398 other.center - dir * other.radius
399 };
400
401 self.center = left.midpoint(right);
402 self.radius = right.distance(self.center);
403 }
404 }
405
406 /// Returns a new bounding sphere that is the merge of this sphere and another.
407 ///
408 /// The returned sphere is the smallest sphere that contains both input spheres.
409 /// This is the non-mutating version of `merge`.
410 ///
411 /// # Arguments
412 ///
413 /// * `other` - The other bounding sphere to merge with
414 ///
415 /// # Example
416 ///
417 /// ```
418 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
419 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
420 /// use parry3d::math::Vector;
421 ///
422 /// let sphere1 = BoundingSphere::new(Vector::ZERO, 1.0);
423 /// let sphere2 = BoundingSphere::new(Vector::new(4.0, 0.0, 0.0), 1.0);
424 ///
425 /// let merged = sphere1.merged(&sphere2);
426 ///
427 /// // Original spheres are unchanged
428 /// assert_eq!(sphere1.radius(), 1.0);
429 /// // Merged sphere contains both
430 /// assert!(merged.contains(&sphere1));
431 /// assert!(merged.contains(&sphere2));
432 /// # }
433 /// ```
434 #[inline]
435 fn merged(&self, other: &BoundingSphere) -> BoundingSphere {
436 let mut res = *self;
437
438 res.merge(other);
439
440 res
441 }
442
443 /// Increases the radius of this bounding sphere by the given amount in-place.
444 ///
445 /// This creates a larger sphere with the same center. Useful for adding safety margins
446 /// or creating conservative bounds.
447 ///
448 /// # Arguments
449 ///
450 /// * `amount` - The amount to increase the radius (must be non-negative)
451 ///
452 /// # Panics
453 ///
454 /// Panics if `amount` is negative.
455 ///
456 /// # Example
457 ///
458 /// ```
459 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
460 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
461 /// use parry3d::math::Vector;
462 ///
463 /// let mut sphere = BoundingSphere::new(Vector::ZERO, 5.0);
464 /// sphere.loosen(2.0);
465 ///
466 /// assert_eq!(sphere.radius(), 7.0);
467 /// assert_eq!(sphere.center(), Vector::ZERO); // Center unchanged
468 /// # }
469 /// ```
470 #[inline]
471 fn loosen(&mut self, amount: Real) {
472 assert!(amount >= 0.0, "The loosening margin must be positive.");
473 self.radius += amount
474 }
475
476 /// Returns a new bounding sphere with increased radius.
477 ///
478 /// This is the non-mutating version of `loosen`. The returned sphere has the same
479 /// center but a larger radius.
480 ///
481 /// # Arguments
482 ///
483 /// * `amount` - The amount to increase the radius (must be non-negative)
484 ///
485 /// # Panics
486 ///
487 /// Panics if `amount` is negative.
488 ///
489 /// # Example
490 ///
491 /// ```
492 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
493 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
494 /// use parry3d::math::Vector;
495 ///
496 /// let sphere = BoundingSphere::new(Vector::ZERO, 5.0);
497 /// let larger = sphere.loosened(3.0);
498 ///
499 /// assert_eq!(sphere.radius(), 5.0); // Original unchanged
500 /// assert_eq!(larger.radius(), 8.0);
501 /// assert_eq!(larger.center(), Vector::ZERO);
502 /// # }
503 /// ```
504 #[inline]
505 fn loosened(&self, amount: Real) -> BoundingSphere {
506 assert!(amount >= 0.0, "The loosening margin must be positive.");
507 BoundingSphere::new(self.center, self.radius + amount)
508 }
509
510 /// Decreases the radius of this bounding sphere by the given amount in-place.
511 ///
512 /// This creates a smaller sphere with the same center. Useful for conservative
513 /// collision detection or creating inner bounds.
514 ///
515 /// # Arguments
516 ///
517 /// * `amount` - The amount to decrease the radius (must be non-negative and ≤ radius)
518 ///
519 /// # Panics
520 ///
521 /// Panics if `amount` is negative or greater than the current radius.
522 ///
523 /// # Example
524 ///
525 /// ```
526 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
527 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
528 /// use parry3d::math::Vector;
529 ///
530 /// let mut sphere = BoundingSphere::new(Vector::ZERO, 10.0);
531 /// sphere.tighten(3.0);
532 ///
533 /// assert_eq!(sphere.radius(), 7.0);
534 /// assert_eq!(sphere.center(), Vector::ZERO); // Center unchanged
535 /// # }
536 /// ```
537 #[inline]
538 fn tighten(&mut self, amount: Real) {
539 assert!(amount >= 0.0, "The tightening margin must be positive.");
540 assert!(amount <= self.radius, "The tightening margin is to large.");
541 self.radius -= amount
542 }
543
544 /// Returns a new bounding sphere with decreased radius.
545 ///
546 /// This is the non-mutating version of `tighten`. The returned sphere has the same
547 /// center but a smaller radius.
548 ///
549 /// # Arguments
550 ///
551 /// * `amount` - The amount to decrease the radius (must be non-negative and ≤ radius)
552 ///
553 /// # Panics
554 ///
555 /// Panics if `amount` is negative or greater than the current radius.
556 ///
557 /// # Example
558 ///
559 /// ```
560 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
561 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
562 /// use parry3d::math::Vector;
563 ///
564 /// let sphere = BoundingSphere::new(Vector::ZERO, 10.0);
565 /// let smaller = sphere.tightened(4.0);
566 ///
567 /// assert_eq!(sphere.radius(), 10.0); // Original unchanged
568 /// assert_eq!(smaller.radius(), 6.0);
569 /// assert_eq!(smaller.center(), Vector::ZERO);
570 /// # }
571 /// ```
572 #[inline]
573 fn tightened(&self, amount: Real) -> BoundingSphere {
574 assert!(amount >= 0.0, "The tightening margin must be positive.");
575 assert!(amount <= self.radius, "The tightening margin is to large.");
576 BoundingSphere::new(self.center, self.radius - amount)
577 }
578}