Skip to main content

box3d_rust/geometry/
sphere.rs

1//! Sphere geometry queries. Port of box3d-cpp-reference/src/sphere.c.
2//!
3//! SPDX-FileCopyrightText: 2026 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use super::types::{Capsule, MassData, PlaneResult, RayCastInput, ShapeCastInput, Sphere};
7use crate::constants::{linear_slop, overlap_slop};
8use crate::distance::{
9    make_proxy, shape_cast, shape_distance, CastOutput, DistanceInput, ShapeCastPairInput,
10    ShapeProxy, SimplexCache,
11};
12use crate::math_functions::{
13    add, get_length_and_normalize, inv_mul_transforms, length_squared, make_diagonal_matrix, max,
14    min, mul_add, normalize, perp, point_to_segment_distance, sub, transform_point, Aabb, Plane,
15    Transform, Vec3, TRANSFORM_IDENTITY, VEC3_AXIS_Y,
16};
17
18/// Compute mass properties of a sphere. (b3ComputeSphereMass)
19pub fn compute_sphere_mass(shape: &Sphere, density: f32) -> MassData {
20    let center = shape.center;
21    let radius = shape.radius;
22
23    let volume = 4.0 / 3.0 * crate::math_functions::PI * radius * radius * radius;
24    let mass = volume * density;
25    let ixx = 0.4 * mass * radius * radius;
26
27    MassData {
28        mass,
29        center,
30        // Inertia about the center of mass
31        inertia: make_diagonal_matrix(ixx, ixx, ixx),
32    }
33}
34
35/// Compute the AABB of a sphere. (b3ComputeSphereAABB)
36pub fn compute_sphere_aabb(shape: &Sphere, transform: Transform) -> Aabb {
37    let center = transform_point(transform, shape.center);
38    let radius = shape.radius;
39    let extent = Vec3 {
40        x: radius,
41        y: radius,
42        z: radius,
43    };
44    Aabb {
45        lower_bound: sub(center, extent),
46        upper_bound: add(center, extent),
47    }
48}
49
50/// Compute the swept AABB of a sphere between two transforms.
51/// (b3ComputeSweptSphereAABB)
52pub fn compute_swept_sphere_aabb(shape: &Sphere, xf1: Transform, xf2: Transform) -> Aabb {
53    let r = Vec3 {
54        x: shape.radius,
55        y: shape.radius,
56        z: shape.radius,
57    };
58    let center1 = transform_point(xf1, shape.center);
59    let center2 = transform_point(xf2, shape.center);
60    Aabb {
61        lower_bound: sub(min(center1, center2), r),
62        upper_bound: add(max(center1, center2), r),
63    }
64}
65
66/// Test overlap between a sphere and a shape proxy. (b3OverlapSphere)
67pub fn overlap_sphere(shape: &Sphere, shape_transform: Transform, proxy: &ShapeProxy) -> bool {
68    let input = DistanceInput {
69        proxy_a: make_proxy(&[shape.center], shape.radius),
70        proxy_b: *proxy,
71        transform: inv_mul_transforms(shape_transform, TRANSFORM_IDENTITY),
72        use_radii: true,
73    };
74
75    let mut cache = SimplexCache::default();
76    let output = shape_distance(&input, &mut cache, None);
77    output.distance < overlap_slop()
78}
79
80/// Ray cast versus sphere shape in local space. (b3RayCastSphere)
81///
82/// Precision Improvements for Ray / Sphere Intersection - Ray Tracing Gems 2019
83/// <http://www.codercorner.com/blog/?p=321>
84pub fn ray_cast_sphere(shape: &Sphere, input: &RayCastInput) -> CastOutput {
85    debug_assert!(super::is_valid_ray(input));
86    let mut output = CastOutput::default();
87
88    let p = shape.center;
89
90    // Shift ray so sphere center is the origin
91    let s = sub(input.origin, p);
92
93    let r = shape.radius;
94    let rr = r * r;
95
96    let mut length = 0.0;
97    let d = get_length_and_normalize(&mut length, input.translation);
98    if length == 0.0 {
99        // zero length ray
100
101        if length_squared(s) < rr {
102            // initial overlap
103            output.point = input.origin;
104            output.hit = true;
105        }
106
107        return output;
108    }
109
110    // Find closest point on ray to origin
111
112    // solve: dot(s + t * d, d) = 0
113    let t = -crate::math_functions::dot(s, d);
114
115    // c is the closest point on the line to the origin
116    let c = mul_add(s, t, d);
117
118    let cc = crate::math_functions::dot(c, c);
119
120    if cc > rr {
121        // closest point is outside the sphere
122        return output;
123    }
124
125    // Pythagoras
126    let h = (rr - cc).sqrt();
127
128    let fraction = t - h;
129
130    if fraction < 0.0 || input.max_fraction * length < fraction {
131        // intersection is point outside the range of the ray segment
132
133        if length_squared(s) < rr {
134            // initial overlap
135            output.point = input.origin;
136            output.hit = true;
137        }
138
139        return output;
140    }
141
142    let hit_point = mul_add(s, fraction, d);
143
144    output.fraction = fraction / length;
145
146    if output.fraction > input.max_fraction {
147        // C calls b3Log here for bad input data; skip logs, keep clamp.
148        output.fraction = input.max_fraction;
149    }
150
151    output.normal = normalize(hit_point);
152    output.point = mul_add(p, shape.radius, output.normal);
153    output.hit = true;
154
155    output
156}
157
158/// Ray cast versus a hollow sphere (interior hits allowed).
159/// (b3RayCastHollowSphere)
160///
161/// Precision Improvements for Ray / Sphere Intersection - Ray Tracing Gems 2019
162/// <http://www.codercorner.com/blog/?p=321>
163pub fn ray_cast_hollow_sphere(sphere: &Sphere, input: &RayCastInput) -> CastOutput {
164    let p = sphere.center;
165
166    let mut output = CastOutput::default();
167
168    // Shift ray so sphere center is the origin
169    let s = sub(input.origin, p);
170    let d = normalize(input.translation);
171
172    // Find closest point on ray to origin
173
174    // solve: dot(s + t * d, d) = 0
175    let t = -crate::math_functions::dot(s, d);
176
177    // c is the closest point on the line to the origin
178    let c = mul_add(s, t, d);
179
180    let cc = crate::math_functions::dot(c, c);
181    let r = sphere.radius;
182    let rr = r * r;
183
184    if cc > rr {
185        // closest point is outside the sphere
186        return output;
187    }
188
189    // Pythagoras
190    let h = (rr - cc).sqrt();
191
192    let mut fraction = t - h;
193
194    if fraction < 0.0 {
195        fraction = t + h;
196    }
197
198    if fraction < 0.0 {
199        // behind the ray
200        return output;
201    }
202
203    if fraction > input.max_fraction {
204        return output;
205    }
206
207    let hit_point = mul_add(s, fraction, d);
208
209    output.fraction = fraction;
210    output.normal = normalize(hit_point);
211    output.point = mul_add(p, sphere.radius, output.normal);
212    output.hit = true;
213
214    output
215}
216
217/// Shape cast versus a sphere. (b3ShapeCastSphere)
218pub fn shape_cast_sphere(sphere: &Sphere, input: &ShapeCastInput) -> CastOutput {
219    let pair_input = ShapeCastPairInput {
220        proxy_a: make_proxy(&[sphere.center], sphere.radius),
221        proxy_b: input.proxy,
222        transform: TRANSFORM_IDENTITY,
223        translation_b: input.translation,
224        max_fraction: input.max_fraction,
225        can_encroach: input.can_encroach,
226    };
227
228    shape_cast(&pair_input)
229}
230
231/// Collide a capsule mover against a sphere. (b3CollideMoverAndSphere)
232pub fn collide_mover_and_sphere(result: &mut PlaneResult, shape: &Sphere, mover: &Capsule) -> i32 {
233    let total_radius = mover.radius + shape.radius;
234    let closest = point_to_segment_distance(mover.center1, mover.center2, shape.center);
235
236    // The normal points from the sphere toward the mover.
237    let mut distance = 0.0;
238    let mut normal = get_length_and_normalize(&mut distance, sub(closest, shape.center));
239
240    if distance > total_radius {
241        return 0;
242    }
243
244    let linear_slop = linear_slop();
245    if distance < linear_slop {
246        // Deep overlap: the mover axis passes through the sphere center, so no
247        // direction is preferred. Push perpendicular to the mover axis.
248        let mut length = 0.0;
249        let axis = get_length_and_normalize(&mut length, sub(mover.center2, mover.center1));
250        normal = if length > linear_slop {
251            perp(axis)
252        } else {
253            VEC3_AXIS_Y
254        };
255        distance = 0.0;
256    }
257
258    result.plane = Plane {
259        normal,
260        offset: total_radius - distance,
261    };
262    result.point = shape.center;
263    1
264}