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}