Skip to main content

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}