parry3d/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/// // Note: due to floating-point rounding, containment may fail by a tiny margin
102/// // for some inputs; apply `BoundingVolume::loosened` if strict containment matters.
103/// assert!(merged.contains(&sphere1));
104/// assert!(merged.contains(&sphere2));
105/// # }
106/// ```
107#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
108#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
109#[cfg_attr(
110 feature = "rkyv",
111 derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
112)]
113#[derive(Debug, PartialEq, Copy, Clone)]
114#[repr(C)]
115pub struct BoundingSphere {
116 /// The center point of the bounding sphere.
117 pub center: Vector,
118
119 /// The radius of the bounding sphere.
120 ///
121 /// This is the distance from the center to any point on the sphere's surface.
122 /// All points within the bounded object should satisfy: distance(point, center) <= radius
123 pub radius: Real,
124}
125
126impl BoundingSphere {
127 /// Creates a new bounding sphere from a center point and radius.
128 ///
129 /// # Arguments
130 ///
131 /// * `center` - The center point of the sphere
132 /// * `radius` - The radius of the sphere (must be non-negative)
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
138 /// use parry3d::bounding_volume::BoundingSphere;
139 /// use parry3d::math::Vector;
140 ///
141 /// // Create a sphere centered at (1, 2, 3) with radius 5.0
142 /// let sphere = BoundingSphere::new(
143 /// Vector::new(1.0, 2.0, 3.0),
144 /// 5.0
145 /// );
146 ///
147 /// assert_eq!(sphere.center(), Vector::new(1.0, 2.0, 3.0));
148 /// assert_eq!(sphere.radius(), 5.0);
149 /// # }
150 /// ```
151 pub fn new(center: Vector, radius: Real) -> BoundingSphere {
152 BoundingSphere { center, radius }
153 }
154
155 /// Returns a reference to the center point of this bounding sphere.
156 ///
157 /// # Example
158 ///
159 /// ```
160 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
161 /// use parry3d::bounding_volume::BoundingSphere;
162 /// use parry3d::math::Vector;
163 ///
164 /// let sphere = BoundingSphere::new(Vector::new(1.0, 2.0, 3.0), 5.0);
165 /// let center = sphere.center();
166 ///
167 /// assert_eq!(center, Vector::new(1.0, 2.0, 3.0));
168 /// # }
169 /// ```
170 #[inline]
171 pub fn center(&self) -> Vector {
172 self.center
173 }
174
175 /// Returns the radius of this bounding sphere.
176 ///
177 /// The radius is the distance from the center to any point on the sphere's surface.
178 ///
179 /// # Example
180 ///
181 /// ```
182 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
183 /// use parry3d::bounding_volume::BoundingSphere;
184 /// use parry3d::math::Vector;
185 ///
186 /// let sphere = BoundingSphere::new(Vector::ZERO, 10.0);
187 ///
188 /// assert_eq!(sphere.radius(), 10.0);
189 /// # }
190 /// ```
191 #[inline]
192 pub fn radius(&self) -> Real {
193 self.radius
194 }
195
196 /// Transforms this bounding sphere by the given isometry.
197 ///
198 /// For a bounding sphere, only the center point is affected by the transformation.
199 /// The radius remains unchanged because spheres are rotation-invariant and isometries
200 /// preserve distances.
201 ///
202 /// # Arguments
203 ///
204 /// * `m` - The isometry (rigid transformation) to apply
205 ///
206 /// # Example
207 ///
208 /// ```
209 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
210 /// use parry3d::bounding_volume::BoundingSphere;
211 /// use parry3d::math::{Vector, Pose, Rotation};
212 ///
213 /// let sphere = BoundingSphere::new(Vector::new(1.0, 0.0, 0.0), 2.0);
214 ///
215 /// // Create a transformation: translate by (5, 0, 0) and rotate 90 degrees around Z
216 /// let translation = Vector::new(5.0, 0.0, 0.0);
217 /// let rotation = Rotation::from_rotation_z(std::f32::consts::FRAC_PI_2);
218 /// let transform = Pose::from_parts(translation, rotation);
219 ///
220 /// let transformed = sphere.transform_by(&transform);
221 ///
222 /// // The center is transformed
223 /// assert!((transformed.center() - Vector::new(5.0, 1.0, 0.0)).length() < 1e-5);
224 /// // The radius is unchanged
225 /// assert_eq!(transformed.radius(), 2.0);
226 /// # }
227 /// ```
228 #[inline]
229 pub fn transform_by(&self, m: &Pose) -> BoundingSphere {
230 BoundingSphere::new(m * self.center, self.radius)
231 }
232
233 /// Translates this bounding sphere by the given vector.
234 ///
235 /// This is equivalent to `transform_by` with a pure translation, but more efficient
236 /// as it doesn't involve rotation.
237 ///
238 /// # Arguments
239 ///
240 /// * `translation` - The translation vector to add to the center
241 ///
242 /// # Example
243 ///
244 /// ```
245 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
246 /// use parry3d::bounding_volume::BoundingSphere;
247 /// use parry3d::math::Vector;
248 ///
249 /// let sphere = BoundingSphere::new(Vector::ZERO, 1.0);
250 /// let translation = Vector::new(10.0, 5.0, -3.0);
251 ///
252 /// let moved = sphere.translated(translation);
253 ///
254 /// assert_eq!(moved.center(), Vector::new(10.0, 5.0, -3.0));
255 /// assert_eq!(moved.radius(), 1.0); // Radius unchanged
256 /// # }
257 /// ```
258 #[inline]
259 pub fn translated(&self, translation: Vector) -> BoundingSphere {
260 BoundingSphere::new(self.center + translation, self.radius)
261 }
262}
263
264impl BoundingVolume for BoundingSphere {
265 /// Returns the center point of this bounding sphere.
266 ///
267 /// # Example
268 ///
269 /// ```
270 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
271 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
272 /// use parry3d::math::Vector;
273 ///
274 /// let sphere = BoundingSphere::new(Vector::new(1.0, 2.0, 3.0), 5.0);
275 ///
276 /// // BoundingVolume::center() returns a Vector by value
277 /// assert_eq!(BoundingVolume::center(&sphere), Vector::new(1.0, 2.0, 3.0));
278 /// # }
279 /// ```
280 #[inline]
281 fn center(&self) -> Vector {
282 self.center()
283 }
284
285 /// Tests if this bounding sphere intersects another bounding sphere.
286 ///
287 /// Two spheres intersect if the distance between their centers is less than or equal
288 /// to the sum of their radii.
289 ///
290 /// # Arguments
291 ///
292 /// * `other` - The other bounding sphere to test against
293 ///
294 /// # Example
295 ///
296 /// ```
297 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
298 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
299 /// use parry3d::math::Vector;
300 ///
301 /// let sphere1 = BoundingSphere::new(Vector::ZERO, 2.0);
302 /// let sphere2 = BoundingSphere::new(Vector::new(3.0, 0.0, 0.0), 2.0);
303 /// let sphere3 = BoundingSphere::new(Vector::new(10.0, 0.0, 0.0), 1.0);
304 ///
305 /// assert!(sphere1.intersects(&sphere2)); // Distance 3.0 <= sum of radii 4.0
306 /// assert!(!sphere1.intersects(&sphere3)); // Distance 10.0 > sum of radii 3.0
307 /// # }
308 /// ```
309 #[inline]
310 fn intersects(&self, other: &BoundingSphere) -> bool {
311 // TODO: refactor that with the code from narrow_phase::ball_ball::collide(...) ?
312 let delta_pos = other.center - self.center;
313 let distance_squared = delta_pos.length_squared();
314 let sum_radius = self.radius + other.radius;
315
316 distance_squared <= sum_radius * sum_radius
317 }
318
319 /// Tests if this bounding sphere fully contains another bounding sphere.
320 ///
321 /// A sphere fully contains another sphere if the distance between their centers
322 /// plus the other's radius is less than or equal to this sphere's radius.
323 ///
324 /// # Arguments
325 ///
326 /// * `other` - The other bounding sphere to test
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
332 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
333 /// use parry3d::math::Vector;
334 ///
335 /// let large = BoundingSphere::new(Vector::ZERO, 10.0);
336 /// let small = BoundingSphere::new(Vector::new(2.0, 0.0, 0.0), 1.0);
337 /// let outside = BoundingSphere::new(Vector::new(15.0, 0.0, 0.0), 2.0);
338 ///
339 /// assert!(large.contains(&small)); // Small sphere is inside large sphere
340 /// assert!(!large.contains(&outside)); // Outside sphere extends beyond large sphere
341 /// assert!(!small.contains(&large)); // Small cannot contain large
342 /// # }
343 /// ```
344 #[inline]
345 fn contains(&self, other: &BoundingSphere) -> bool {
346 let delta_pos = other.center - self.center;
347 let distance = delta_pos.length();
348
349 distance + other.radius <= self.radius
350 }
351
352 /// Merges this bounding sphere with another in-place.
353 ///
354 /// After this operation, this sphere will be the smallest sphere that contains
355 /// both the original sphere and the other sphere.
356 ///
357 /// Note that due to floating-point rounding, [`contains`](BoundingVolume::contains)
358 /// is not guaranteed to return `true` for the two input spheres afterwards: the
359 /// result can be off by a tiny margin. Use [`loosened`](BoundingVolume::loosened)
360 /// with a small margin if strict containment is required.
361 ///
362 /// # Arguments
363 ///
364 /// * `other` - The other bounding sphere to merge with
365 ///
366 /// # Example
367 ///
368 /// ```
369 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
370 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
371 /// use parry3d::math::Vector;
372 ///
373 /// let mut sphere1 = BoundingSphere::new(Vector::ZERO, 1.0);
374 /// let sphere2 = BoundingSphere::new(Vector::new(4.0, 0.0, 0.0), 1.0);
375 ///
376 /// sphere1.merge(&sphere2);
377 ///
378 /// // The merged sphere now contains both original spheres
379 /// assert!(sphere1.contains(&BoundingSphere::new(Vector::ZERO, 1.0)));
380 /// assert!(sphere1.contains(&sphere2));
381 /// # }
382 /// ```
383 #[inline]
384 fn merge(&mut self, other: &BoundingSphere) {
385 let dir = other.center() - self.center();
386 let (dir, length) = dir.normalize_and_length();
387
388 if length == 0.0 {
389 if other.radius > self.radius {
390 self.radius = other.radius
391 }
392 } else {
393 let s_center_dir = self.center.dot(dir);
394 let o_center_dir = other.center.dot(dir);
395
396 let right = if s_center_dir + self.radius > o_center_dir + other.radius {
397 self.center + dir * self.radius
398 } else {
399 other.center + dir * other.radius
400 };
401
402 let left = if -s_center_dir + self.radius > -o_center_dir + other.radius {
403 self.center - dir * self.radius
404 } else {
405 other.center - dir * other.radius
406 };
407
408 self.center = left.midpoint(right);
409 self.radius = right.distance(self.center);
410 }
411 }
412
413 /// Returns a new bounding sphere that is the merge of this sphere and another.
414 ///
415 /// The returned sphere is the smallest sphere that contains both input spheres.
416 /// This is the non-mutating version of `merge`.
417 ///
418 /// Note that due to floating-point rounding, [`contains`](BoundingVolume::contains)
419 /// is not guaranteed to return `true` for the two input spheres: the result can be
420 /// off by a tiny margin. Use [`loosened`](BoundingVolume::loosened) with a small
421 /// margin if strict containment is required.
422 ///
423 /// # Arguments
424 ///
425 /// * `other` - The other bounding sphere to merge with
426 ///
427 /// # Example
428 ///
429 /// ```
430 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
431 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
432 /// use parry3d::math::Vector;
433 ///
434 /// let sphere1 = BoundingSphere::new(Vector::ZERO, 1.0);
435 /// let sphere2 = BoundingSphere::new(Vector::new(4.0, 0.0, 0.0), 1.0);
436 ///
437 /// let merged = sphere1.merged(&sphere2);
438 ///
439 /// // Original spheres are unchanged
440 /// assert_eq!(sphere1.radius(), 1.0);
441 /// // Merged sphere contains both
442 /// assert!(merged.contains(&sphere1));
443 /// assert!(merged.contains(&sphere2));
444 /// # }
445 /// ```
446 #[inline]
447 fn merged(&self, other: &BoundingSphere) -> BoundingSphere {
448 let mut res = *self;
449
450 res.merge(other);
451
452 res
453 }
454
455 /// Increases the radius of this bounding sphere by the given amount in-place.
456 ///
457 /// This creates a larger sphere with the same center. Useful for adding safety margins
458 /// or creating conservative bounds.
459 ///
460 /// # Arguments
461 ///
462 /// * `amount` - The amount to increase the radius (must be non-negative)
463 ///
464 /// # Panics
465 ///
466 /// Panics if `amount` is negative.
467 ///
468 /// # Example
469 ///
470 /// ```
471 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
472 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
473 /// use parry3d::math::Vector;
474 ///
475 /// let mut sphere = BoundingSphere::new(Vector::ZERO, 5.0);
476 /// sphere.loosen(2.0);
477 ///
478 /// assert_eq!(sphere.radius(), 7.0);
479 /// assert_eq!(sphere.center(), Vector::ZERO); // Center unchanged
480 /// # }
481 /// ```
482 #[inline]
483 fn loosen(&mut self, amount: Real) {
484 assert!(amount >= 0.0, "The loosening margin must be non-negative.");
485 self.radius += amount
486 }
487
488 /// Returns a new bounding sphere with increased radius.
489 ///
490 /// This is the non-mutating version of `loosen`. The returned sphere has the same
491 /// center but a larger radius.
492 ///
493 /// # Arguments
494 ///
495 /// * `amount` - The amount to increase the radius (must be non-negative)
496 ///
497 /// # Panics
498 ///
499 /// Panics if `amount` is negative.
500 ///
501 /// # Example
502 ///
503 /// ```
504 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
505 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
506 /// use parry3d::math::Vector;
507 ///
508 /// let sphere = BoundingSphere::new(Vector::ZERO, 5.0);
509 /// let larger = sphere.loosened(3.0);
510 ///
511 /// assert_eq!(sphere.radius(), 5.0); // Original unchanged
512 /// assert_eq!(larger.radius(), 8.0);
513 /// assert_eq!(larger.center(), Vector::ZERO);
514 /// # }
515 /// ```
516 #[inline]
517 fn loosened(&self, amount: Real) -> BoundingSphere {
518 assert!(amount >= 0.0, "The loosening margin must be non-negative.");
519 BoundingSphere::new(self.center, self.radius + amount)
520 }
521
522 /// Decreases the radius of this bounding sphere by the given amount in-place.
523 ///
524 /// This creates a smaller sphere with the same center. Useful for conservative
525 /// collision detection or creating inner bounds.
526 ///
527 /// # Arguments
528 ///
529 /// * `amount` - The amount to decrease the radius (must be non-negative and ≤ radius)
530 ///
531 /// # Panics
532 ///
533 /// Panics if `amount` is negative or greater than the current radius.
534 ///
535 /// # Example
536 ///
537 /// ```
538 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
539 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
540 /// use parry3d::math::Vector;
541 ///
542 /// let mut sphere = BoundingSphere::new(Vector::ZERO, 10.0);
543 /// sphere.tighten(3.0);
544 ///
545 /// assert_eq!(sphere.radius(), 7.0);
546 /// assert_eq!(sphere.center(), Vector::ZERO); // Center unchanged
547 /// # }
548 /// ```
549 #[inline]
550 fn tighten(&mut self, amount: Real) {
551 assert!(amount >= 0.0, "The tightening margin must be non-negative.");
552 assert!(amount <= self.radius, "The tightening margin is to large.");
553 self.radius -= amount
554 }
555
556 /// Returns a new bounding sphere with decreased radius.
557 ///
558 /// This is the non-mutating version of `tighten`. The returned sphere has the same
559 /// center but a smaller radius.
560 ///
561 /// # Arguments
562 ///
563 /// * `amount` - The amount to decrease the radius (must be non-negative and ≤ radius)
564 ///
565 /// # Panics
566 ///
567 /// Panics if `amount` is negative or greater than the current radius.
568 ///
569 /// # Example
570 ///
571 /// ```
572 /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
573 /// use parry3d::bounding_volume::{BoundingSphere, BoundingVolume};
574 /// use parry3d::math::Vector;
575 ///
576 /// let sphere = BoundingSphere::new(Vector::ZERO, 10.0);
577 /// let smaller = sphere.tightened(4.0);
578 ///
579 /// assert_eq!(sphere.radius(), 10.0); // Original unchanged
580 /// assert_eq!(smaller.radius(), 6.0);
581 /// assert_eq!(smaller.center(), Vector::ZERO);
582 /// # }
583 /// ```
584 #[inline]
585 fn tightened(&self, amount: Real) -> BoundingSphere {
586 assert!(amount >= 0.0, "The tightening margin must be non-negative.");
587 assert!(amount <= self.radius, "The tightening margin is to large.");
588 BoundingSphere::new(self.center, self.radius - amount)
589 }
590}