Skip to main content

box3d_rust/geometry/
capsule.rs

1//! Capsule geometry queries. Port of box3d-cpp-reference/src/capsule.c.
2//!
3//! SPDX-FileCopyrightText: 2025 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use super::sphere::ray_cast_sphere;
7use super::types::{Capsule, MassData, PlaneResult, RayCastInput, ShapeCastInput, Sphere};
8use crate::constants::{linear_slop, overlap_slop};
9use crate::distance::{
10    make_proxy, shape_cast, shape_distance, CastOutput, DistanceInput, ShapeCastPairInput,
11    ShapeProxy, SimplexCache,
12};
13use crate::math_functions::{
14    add, add_mm, clamp_float, compute_quat_between_unit_vectors, cylinder_inertia, distance, dot,
15    get_length_and_normalize, inv_mul_transforms, length_squared, make_matrix_from_quat, max, min,
16    mul_add, mul_mm, mul_sub, mul_sv, normalize, perp, segment_distance, sphere_inertia, sub,
17    transform_point, transpose, Aabb, Plane, Transform, Vec3, MAT3_IDENTITY, TRANSFORM_IDENTITY,
18    VEC3_AXIS_Y,
19};
20
21/// Compute mass properties of a capsule. (b3ComputeCapsuleMass)
22pub fn compute_capsule_mass(shape: &Capsule, density: f32) -> MassData {
23    let c1 = shape.center1;
24    let c2 = shape.center2;
25    let r = shape.radius;
26
27    // Cylinder
28    let cylinder_height = distance(c1, c2);
29    let cylinder_volume = crate::math_functions::PI * r * r * cylinder_height;
30    let cylinder_mass = cylinder_volume * density;
31
32    // Sphere
33    let sphere_volume = (4.0 / 3.0) * crate::math_functions::PI * r * r * r;
34    let sphere_mass = sphere_volume * density;
35
36    // Local accumulated inertia
37    let mut inertia = add_mm(
38        cylinder_inertia(cylinder_mass, r, cylinder_height),
39        sphere_inertia(sphere_mass, r),
40    );
41
42    let steiner = 0.125 * sphere_mass * (3.0 * r + 2.0 * cylinder_height) * cylinder_height;
43    inertia.cx.x += steiner;
44    inertia.cz.z += steiner;
45
46    // Align capsule axis with chosen up-axis
47    let mut rotation = MAT3_IDENTITY;
48    if cylinder_height * cylinder_height > 1000.0 * f32::MIN_POSITIVE {
49        let direction = normalize(sub(c2, c1));
50        let q = compute_quat_between_unit_vectors(VEC3_AXIS_Y, direction);
51        rotation = make_matrix_from_quat(q);
52    }
53
54    let mass = sphere_mass + cylinder_mass;
55    let center = mul_sv(0.5, add(c1, c2));
56
57    MassData {
58        mass,
59        center,
60        // Rotate the central inertia into the shape frame
61        inertia: mul_mm(rotation, mul_mm(inertia, transpose(rotation))),
62    }
63}
64
65/// Compute the AABB of a capsule. (b3ComputeCapsuleAABB)
66pub fn compute_capsule_aabb(shape: &Capsule, transform: Transform) -> Aabb {
67    let r = shape.radius;
68
69    let center1 = transform_point(transform, shape.center1);
70    let center2 = transform_point(transform, shape.center2);
71    let extent = Vec3 { x: r, y: r, z: r };
72
73    Aabb {
74        lower_bound: sub(min(center1, center2), extent),
75        upper_bound: add(max(center1, center2), extent),
76    }
77}
78
79/// Compute the swept AABB of a capsule between two transforms.
80/// (b3ComputeSweptCapsuleAABB)
81pub fn compute_swept_capsule_aabb(shape: &Capsule, xf1: Transform, xf2: Transform) -> Aabb {
82    let r = Vec3 {
83        x: shape.radius,
84        y: shape.radius,
85        z: shape.radius,
86    };
87    let a = transform_point(xf1, shape.center1);
88    let b = transform_point(xf1, shape.center2);
89    let c = transform_point(xf2, shape.center1);
90    let d = transform_point(xf2, shape.center2);
91
92    Aabb {
93        lower_bound: sub(min(min(a, b), min(c, d)), r),
94        upper_bound: add(max(max(a, b), max(c, d)), r),
95    }
96}
97
98/// Test overlap between a capsule and a shape proxy. (b3OverlapCapsule)
99pub fn overlap_capsule(shape: &Capsule, shape_transform: Transform, proxy: &ShapeProxy) -> bool {
100    let input = DistanceInput {
101        proxy_a: make_proxy(&[shape.center1, shape.center2], shape.radius),
102        proxy_b: *proxy,
103        transform: inv_mul_transforms(shape_transform, TRANSFORM_IDENTITY),
104        use_radii: true,
105    };
106
107    let mut cache = SimplexCache::default();
108    let output = shape_distance(&input, &mut cache, None);
109    output.distance < overlap_slop()
110}
111
112/// Ray cast versus capsule shape in local space. (b3RayCastCapsule)
113///
114/// Precision Improvements for Ray / Sphere Intersection - Ray Tracing Gems 2019
115/// <http://www.codercorner.com/blog/?p=321>
116pub fn ray_cast_capsule(shape: &Capsule, input: &RayCastInput) -> CastOutput {
117    debug_assert!(super::is_valid_ray(input));
118
119    let c1 = shape.center1;
120    let c2 = shape.center2;
121    let r = shape.radius;
122
123    // Initialize result structure
124    let mut output = CastOutput::default();
125
126    let d = sub(c2, c1);
127
128    // Fall back to sphere if the capsule is short
129    let tol = 0.01 * linear_slop();
130    let length_squared_d = length_squared(d);
131    if length_squared_d < tol * tol {
132        let sphere_center = mul_sv(0.5, add(shape.center1, shape.center2));
133        let sphere = Sphere {
134            center: sphere_center,
135            radius: shape.radius,
136        };
137        return ray_cast_sphere(&sphere, input);
138    }
139
140    // Vector from first center to ray origin.
141    let s = sub(input.origin, c1);
142
143    // Capsule axis
144    let length = length_squared_d.sqrt();
145    let axis = mul_sv(1.0 / length, d);
146
147    // Project ray origin onto capsule axis.
148    let u = dot(s, axis);
149
150    // Closest point on infinite capsule axis, relative to c1.
151    let c = mul_sv(u, axis);
152
153    // Vector from closest point to ray origin
154    let sc = sub(s, c);
155
156    // Squared distance from ray origin to capsule axis
157    let sc2 = length_squared(sc);
158
159    // Is the ray origin within the infinite cylinder along the capsule axis?
160    if sc2 < r * r {
161        // Clamped barycentric coordinate of ray origin projected onto capsule axis.
162        let u_clamped = clamp_float(u, 0.0, length);
163
164        // The closest point on the bounded capsule segment, relative to c1.
165        let cp = mul_sv(u_clamped, axis);
166
167        // Vector from ray origin to closest point on segment.
168        let scp = sub(s, cp);
169
170        // Squared distance of ray origin from capsule segment.
171        let scp2 = length_squared(scp);
172
173        // Is the ray origin within the capsule?
174        if scp2 < r * r {
175            output.hit = true;
176            output.point = input.origin;
177            return output;
178        }
179
180        // The ray can hit an endcap.
181        let sphere = Sphere {
182            center: add(c1, cp),
183            radius: r,
184        };
185
186        return ray_cast_sphere(&sphere, input);
187    }
188
189    // Ray axis. A zero length ray reaching here starts outside the capsule, so it misses.
190    // Same zero length convention as b3RayCastSphere.
191    let dr = input.translation;
192    let mut ray_length = 0.0;
193    let ray_axis = get_length_and_normalize(&mut ray_length, dr);
194    if ray_length == 0.0 {
195        return output;
196    }
197
198    // Barycentric coordinate of ray end point.
199    let v = u + input.max_fraction * dot(dr, axis);
200
201    // Early out: does the projected ray fall outside the capsule?
202    if (u < -r && v < -r) || (length + r < u && length + r < v) {
203        return output;
204    }
205
206    // Compute the closest point between the ray segment and the capsule segment.
207    // See Real-Time Collision Detection, section 5.1.9
208
209    let a1 = axis;
210    let a2 = ray_axis;
211    let a12 = dot(a1, a2);
212
213    // Ray distance to the near intersection with the infinite cylinder. Length units.
214    let tr;
215
216    let det = 1.0 - a12 * a12;
217    if det < f32::EPSILON {
218        // Solve the 2D problem of ray versus circle starting at the ray origin, where the circle is
219        // the axial view of the infinite capsule cylinder. This works well when the ray origin is
220        // not too far from the capsule axis.
221
222        // Instead of a cross product, subtract the parallel part to get a perpendicular vector. Non-dimensional.
223        let perp = mul_sub(a2, a12, a1);
224        let perp2 = length_squared(perp);
225
226        // Project to origin to infinite capsule axis vector onto the perpendicular vector. beta has length units.
227        let beta = dot(sc, perp);
228
229        // Setup quadratic root finder.
230        let gamma = sc2 - r * r;
231
232        // Discriminant
233        let disc = beta * beta - perp2 * gamma;
234
235        // Casting away from the axis, or the perpendicular gap never closes to the radius.
236        if beta >= 0.0 || disc < 0.0 {
237            return output;
238        }
239
240        // Quadratic near root. Expressed in an alternate form to avoid the (-beta - sqrt) cancellation as
241        // the ray nears parallel.
242        tr = gamma / (-beta + disc.sqrt());
243    } else {
244        // Ray and capsules axes are not parallel.
245
246        // Closest points between the infinite ray and the infinite capsule axis.
247        let inv_det = 1.0 / det;
248        let sa1 = u;
249        let sa2 = dot(s, a2);
250
251        let t1 = (sa1 - a12 * sa2) * inv_det;
252        let t2 = (a12 * sa1 - sa2) * inv_det;
253
254        // Closest points
255        let p1 = mul_sv(t1, a1);
256        let p2 = mul_add(s, t2, a2);
257
258        // Vector from closest point on infinite capsule to infinite ray.
259        let g = sub(p2, p1);
260
261        let g2 = length_squared(g);
262        if g2 > r * r {
263            // Early out: closest point on infinite ray is outside infinite cylinder.
264            return output;
265        }
266
267        // Intersect the infinite ray with the infinite cylinder. Like ray versus sphere this is done
268        // relative to the closest point to avoid round-off errors. Length units, not a fraction.
269        // https://en.wikipedia.org/wiki/Line-cylinder_intersection
270        let h = ((r * r - g2) * inv_det).sqrt();
271
272        tr = t2 - h;
273    }
274
275    // Outside ray?
276    if tr < 0.0 || input.max_fraction * ray_length < tr {
277        return output;
278    }
279
280    // The corresponding distance on the capsule axis. Length units.
281    let tc = u + tr * a12;
282
283    // Outside c1 end?
284    if tc < 0.0 {
285        // Ray cast sphere 1.
286        let sphere = Sphere {
287            center: c1,
288            radius: r,
289        };
290
291        return ray_cast_sphere(&sphere, input);
292    }
293
294    // Outside c2 end?
295    if length < tc {
296        // Ray cast sphere 2.
297        let sphere = Sphere {
298            center: c2,
299            radius: r,
300        };
301
302        return ray_cast_sphere(&sphere, input);
303    }
304
305    // Hit point on capsule side, relative to c1.
306    let p = mul_add(s, tr, ray_axis);
307
308    // Hit normal.
309    let mut normal = mul_sub(p, tc, axis);
310    normal = normalize(normal);
311
312    output.point = add(c1, p);
313    output.normal = normal;
314    output.fraction = clamp_float(tr / ray_length, 0.0, input.max_fraction);
315    output.hit = true;
316    output
317}
318
319/// Shape cast versus a capsule. (b3ShapeCastCapsule)
320pub fn shape_cast_capsule(capsule: &Capsule, input: &ShapeCastInput) -> CastOutput {
321    let pair_input = ShapeCastPairInput {
322        proxy_a: make_proxy(&[capsule.center1, capsule.center2], capsule.radius),
323        proxy_b: input.proxy,
324        transform: TRANSFORM_IDENTITY,
325        translation_b: input.translation,
326        max_fraction: input.max_fraction,
327        can_encroach: input.can_encroach,
328    };
329
330    shape_cast(&pair_input)
331}
332
333/// Collide a capsule mover against a capsule. (b3CollideMoverAndCapsule)
334pub fn collide_mover_and_capsule(
335    result: &mut PlaneResult,
336    shape: &Capsule,
337    mover: &Capsule,
338) -> i32 {
339    let total_radius = mover.radius + shape.radius;
340
341    let approach = segment_distance(shape.center1, shape.center2, mover.center1, mover.center2);
342
343    // The normal points from the shape toward the mover.
344    let mut distance = 0.0;
345    let mut normal = get_length_and_normalize(&mut distance, sub(approach.point2, approach.point1));
346
347    if distance > total_radius {
348        return 0;
349    }
350
351    let linear_slop = linear_slop();
352    if distance < linear_slop {
353        // Deep overlap: the core segments intersect. Pick an arbitrary direction
354        // perpendicular to the capsule axis.
355        let mut mover_length = 0.0;
356        let mover_axis =
357            get_length_and_normalize(&mut mover_length, sub(mover.center2, mover.center1));
358        normal = if mover_length > linear_slop {
359            perp(mover_axis)
360        } else {
361            VEC3_AXIS_Y
362        };
363        distance = 0.0;
364    }
365
366    result.plane = Plane {
367        normal,
368        offset: total_radius - distance,
369    };
370    result.point = approach.point1;
371    1
372}