1use crate::math::{ComplexField, Real, Vector};
2use crate::query::{Ray, RayCast, RayIntersection};
3use crate::shape::{Capsule, FeatureId, Segment};
4
5impl RayCast for Capsule {
6 #[inline]
7 fn cast_local_ray(&self, ray: &Ray, max_time_of_impact: Real, solid: bool) -> Option<Real> {
8 ray_toi_with_capsule(&self.segment, self.radius, ray, solid)
9 .1
10 .filter(|time_of_impact| *time_of_impact <= max_time_of_impact)
11 }
12
13 #[inline]
14 fn cast_local_ray_and_get_normal(
15 &self,
16 ray: &Ray,
17 max_time_of_impact: Real,
18 solid: bool,
19 ) -> Option<RayIntersection> {
20 ray_toi_and_normal_with_capsule(&self.segment, self.radius, ray, solid)
21 .filter(|inter| inter.time_of_impact <= max_time_of_impact)
22 }
23}
24
25fn ray_toi_with_capsule(
36 segment: &Segment,
37 radius: Real,
38 ray: &Ray,
39 solid: bool,
40) -> (bool, Option<Real>) {
41 let ab = segment.b - segment.a;
42 let ao = ray.origin - segment.a;
43
44 let ab_ab = ab.length_squared();
45 let dir_dir = ray.dir.length_squared();
46 let ab_dir = ab.dot(ray.dir);
47 let ab_ao = ab.dot(ao);
48 let radius_squared = radius * radius;
49
50 let dir_on_plane = cross(ray.dir, ab);
53 let origin_on_plane = cross(ao, ab);
54 let ray_step = dir_on_plane.length_squared();
55 let radius_on_plane_squared = radius_squared * ab_ab;
56
57 let inside = origin_on_plane.length_squared() <= radius_on_plane_squared
58 && (0.0 < ab_ao || ao.length_squared() <= radius_squared)
59 && (ab_ao < ab_ab || (ray.origin - segment.b).length_squared() <= radius_squared);
60
61 if inside && solid {
62 return (true, Some(0.0));
63 }
64
65 let check_sphere_a = if ray_step == 0.0 {
66 (ab_dir > 0.0) ^ inside
69 } else {
70 let Some(t) = closest_approach_root(
72 origin_on_plane,
73 dir_on_plane,
74 ray_step,
75 radius_on_plane_squared,
76 inside,
77 ) else {
78 return (false, None);
79 };
80 let y = ab_ao + t * ab_dir;
81 if 0.0 < y && y < ab_ab && t >= 0.0 {
82 return (inside, Some(t));
83 }
84 y <= 0.0
85 };
86
87 if dir_dir == 0.0 {
88 return (inside, None);
89 }
90
91 let oc = if check_sphere_a {
93 ao
94 } else {
95 ray.origin - segment.b
96 };
97 let t = closest_approach_root(oc, ray.dir, dir_dir, radius_squared, inside);
98 (inside, t.filter(|t| *t >= 0.0))
99}
100
101#[inline]
106fn closest_approach_root(
107 origin: Vector,
108 dir: Vector,
109 dir_dir: Real,
110 radius_squared: Real,
111 inside: bool,
112) -> Option<Real> {
113 let t_closest = -dir.dot(origin) / dir_dir;
114 let closest = origin + dir * t_closest;
115 let h = radius_squared - closest.length_squared();
116 if h < 0.0 && !inside {
117 return None;
118 }
119 let half_chord = <Real as ComplexField>::sqrt(h.max(0.0) / dir_dir);
120 if inside {
121 Some((t_closest + half_chord).max(0.0))
122 } else {
123 Some(t_closest - half_chord)
124 }
125}
126
127#[cfg(feature = "dim3")]
128#[inline]
129fn cross(v: Vector, segment: Vector) -> Vector {
130 v.cross(segment)
131}
132
133#[cfg(feature = "dim2")]
136#[inline]
137fn cross(v: Vector, segment: Vector) -> Vector {
138 Vector::new(v.x * segment.y - v.y * segment.x, 0.0)
139}
140
141fn ray_toi_and_normal_with_capsule(
143 segment: &Segment,
144 radius: Real,
145 ray: &Ray,
146 solid: bool,
147) -> Option<RayIntersection> {
148 let (inside, inter) = ray_toi_with_capsule(segment, radius, ray, solid);
149
150 inter.map(|t| {
151 let normal = if solid && inside {
152 Vector::ZERO
153 } else {
154 let p = ray.origin + ray.dir * t;
155 let a_to_p = p - segment.a;
156 let seg = segment.b - segment.a;
157 let seg_squared = seg.length_squared();
158
159 let proj_times_seg = a_to_p.dot(seg);
161
162 let n = if proj_times_seg <= 0.0 {
164 a_to_p.normalize_or_zero()
165 } else if proj_times_seg >= seg_squared {
166 (p - segment.b).normalize_or_zero()
167 } else {
168 (a_to_p - (proj_times_seg / seg_squared) * seg).normalize_or_zero()
169 };
170 if inside {
171 -n
172 } else {
173 n
174 }
175 };
176 RayIntersection::new(t, normal, FeatureId::Face(0))
177 })
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::math::Vector;
184 use crate::query::point::point_query::PointQuery;
185 use oorandom::Rand32;
186
187 #[test]
188 fn exact_cases() {
189 let c = Capsule::new(v2(0.0, 0.5), v2(0.0, 1.5), 0.5);
190 expect_hit(&c, v2(0.0, 5.0), v2(0.0, -0.2), true, 15.0, v2(0.0, 1.0));
192 expect_hit(&c, v2(5.0, 1.0), v2(-0.3, 0.0), true, 15.0, v2(1.0, 0.0));
194 expect_hit(&c, v2(5.0, 2.0), v2(-1.0, 0.0), true, 5.0, v2(0.0, 1.0));
196 expect_hit(
199 &c,
200 v2(0.1, -4.0),
201 v2(0.0, 0.2),
202 true,
203 20.0505,
204 v2(0.2, -0.9798),
205 );
206 assert!(c
208 .cast_local_ray(&Ray::new(v2(10.0, 5.0), v2(0.0, 0.1)), 50.0, true)
209 .is_none());
210
211 assert!(c
218 .cast_local_ray(&Ray::new(v2(0.4, 1.85), v2(1.0, 1.0)), 50.0, true)
219 .is_none());
220 assert!(c
222 .cast_local_ray(&Ray::new(v2(0.0, 2.1), v2(0.0, 1.0)), 50.0, true)
223 .is_none());
224 assert!(c
227 .cast_local_ray(&Ray::new(v2(0.4, 1.85), v2(1.0, 0.2)), 50.0, true)
228 .is_none());
229 assert!(c
232 .cast_local_ray(&Ray::new(v2(2.0, 1.0), v2(1.0, 0.0)), 50.0, true)
233 .is_none());
234 assert!(c
236 .cast_local_ray(&Ray::new(v2(2.0, 3.0), v2(1.0, 1.0)), 50.0, true)
237 .is_none());
238 expect_hit(&c, v2(0.1, 1.0), v2(0.0, 1.0), true, 0.0, v2(0.0, 0.0));
240 expect_hit(&c, v2(0.0, 1.0), v2(0.0, 1.0), false, 1.0, v2(0.0, -1.0));
242 expect_hit(&c, v2(0.0, 1.0), v2(0.0, -1.0), false, 1.0, v2(0.0, 1.0));
244 expect_hit(&c, v2(0.0, 1.0), v2(1.0, 0.0), false, 0.5, v2(-1.0, 0.0));
247 expect_hit(&c, v2(0.1, 0.8), v2(1.0, 0.5), false, 0.4, v2(-1.0, 0.0));
249 expect_hit(&c, v2(0.0, 1.6), v2(0.0, -1.0), false, 1.6, v2(0.0, 1.0));
253 expect_hit(&c, v2(0.1, 1.0), v2(0.0, 0.0), true, 0.0, v2(0.0, 0.0));
255 assert!(c
256 .cast_local_ray(&Ray::new(v2(0.1, 3.0), v2(0.0, 0.0)), 50.0, true)
257 .is_none());
258 let ball = Capsule::new(v2(0.0, 1.0), v2(0.0, 1.0), 1.0);
260 expect_hit(&ball, v2(0.0, 5.0), v2(0.0, -1.0), true, 3.0, v2(0.0, 1.0));
261 expect_hit(&ball, v2(0.5, 1.0), v2(1.0, 0.0), true, 0.0, v2(0.0, 0.0));
262 assert!(c
264 .cast_local_ray(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 14.9, true)
265 .is_none());
266 assert!(c
267 .cast_local_ray(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 15.1, true)
268 .is_some());
269 assert!(c
270 .cast_local_ray_and_get_normal(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 14.9, true)
271 .is_none());
272 }
273
274 #[test]
275 fn distant_origin_precision() {
276 let c = Capsule::new(v2(1.0e4, 0.0), v2(1.0e4, 5.0), 1.0);
279 let i = c
280 .cast_local_ray_and_get_normal(&Ray::new(v2(-1.0e4, 2.5), v2(2.0e4, 0.0)), 2.0, true)
281 .unwrap();
282 let hit = -1.0e4 + i.time_of_impact * 2.0e4;
283 assert!((hit - (1.0e4 - 1.0)).abs() < 1.0e-2, "hit at x = {hit}");
284
285 let c = Capsule::new(v2(0.0, 0.0), v2(0.0, 5.0), 1.0);
287 let dir = v2(1.0e4, 3.0e3);
288 let i = c
289 .cast_local_ray_and_get_normal(&Ray::new(v2(-1.0, 2.5) - dir, dir), 2.0, true)
290 .unwrap();
291 assert!((i.time_of_impact - 1.0).abs() * dir.length() < 1.0e-2);
292 assert!((i.normal - v2(-1.0, 0.0)).length() < 1.0e-3);
293 }
294
295 #[test]
296 fn zero_radius_normal_is_finite() {
297 let c = Capsule::new(v2(0.0, -1.0), v2(0.0, 1.0), 0.0);
298 for dir in [v2(1.0, 0.0), v2(0.0, -1.0)] {
299 let i = c
300 .cast_local_ray_and_get_normal(&Ray::new(v2(0.0, 0.0) - dir * 2.0, dir), 10.0, true)
301 .unwrap();
302 assert!(i.normal.is_finite(), "normal: {:?}", i.normal);
303 }
304 }
305
306 fn v2(x: Real, y: Real) -> Vector {
307 Vector::new(
308 x,
309 y,
310 #[cfg(feature = "dim3")]
311 0.0,
312 )
313 }
314
315 #[track_caller]
316 fn expect_hit(c: &Capsule, o: Vector, d: Vector, solid: bool, et: Real, en: Vector) {
317 let i = c
318 .cast_local_ray_and_get_normal(&Ray::new(o, d), 50.0, solid)
319 .unwrap_or_else(|| panic!("expected hit (o={:?}, d={:?})", o, d));
320 assert!(
321 (i.time_of_impact - et).abs() < 1e-4,
322 "t: got {}, want {}",
323 i.time_of_impact,
324 et
325 );
326 assert!(
327 (i.normal - en).length() < 1e-3,
328 "n: got {:?}, want {:?}",
329 i.normal,
330 en
331 );
332 }
333
334 #[test]
335 fn fuzz_capsule_ray_casts() {
336 let epsilon = 0.002;
337 let mut rng = Rand32::new(42);
338
339 for _ in 0..100_000 {
340 let (a, b) = (rnd_vec(&mut rng, 10.0), rnd_vec(&mut rng, 10.0));
341 let r = 0.5 + 5.0 * rnd(&mut rng);
342 let capsule = Capsule::new(a, b, r);
343
344 let inside = {
346 let mut w = rnd_vec(&mut rng, r);
347 while w.length_squared() >= r * r {
348 w = rnd_vec(&mut rng, r);
349 }
350 a + (b - a) * rnd(&mut rng) + w
351 };
352
353 let far_enough = r + a.distance(b);
355 let mut offset = Vector::ZERO;
356 while offset.length_squared() < far_enough * far_enough {
357 offset = rnd_vec(&mut rng, far_enough * 2.0);
358 }
359
360 let o = (a + b) * 0.5 + offset;
361 let d = (inside - o) * (0.1 + 0.9 * rnd(&mut rng));
362 let i = capsule
363 .cast_local_ray_and_get_normal(&Ray::new(o, d), 1000.0, true)
364 .expect("a ray aimed at an interior point must hit");
365
366 let hit = o + d * i.time_of_impact;
367 assert!(
368 capsule.contains_local_point(hit - i.normal * epsilon),
369 "nudging inward along the normal should go inside the capsule"
370 );
371 assert!(
372 !capsule.contains_local_point(hit + i.normal * epsilon),
373 "nudging outward along the normal should go outside the capsule"
374 );
375
376 let i_in = capsule
379 .cast_local_ray_and_get_normal(&Ray::new(inside, o - inside), 1000.0, false)
380 .expect("a ray from inside toward the outside must exit");
381 let hit_in = inside + (o - inside) * i_in.time_of_impact;
382 assert!(
383 capsule.contains_local_point(hit_in + i_in.normal * epsilon),
384 "nudging along the inward normal should stay inside"
385 );
386 assert!(
387 !capsule.contains_local_point(hit_in - i_in.normal * epsilon),
388 "nudging against the inward normal should go outside"
389 );
390
391 assert!(
392 capsule
393 .cast_local_ray(&Ray::new(o, -d), 1000.0, true)
394 .is_none(),
395 "a retreating ray must miss"
396 );
397
398 #[cfg(feature = "dim2")]
399 let tangent = Vector::new(-i.normal.y, i.normal.x);
400 #[cfg(feature = "dim3")]
401 let tangent = {
402 let mut tangent = Vector::ZERO;
403 while tangent.length_squared() < 1e-8 {
404 tangent = rnd_vec(&mut rng, 1.0).cross(i.normal);
405 }
406 tangent
407 };
408 let origin = hit + i.normal * (epsilon + rnd(&mut rng)) - rnd(&mut rng) * tangent;
409 assert!(
410 capsule
411 .cast_local_ray(&Ray::new(origin, tangent), 1000.0, true)
412 .is_none(),
413 "tangent outside the capsule should miss"
414 );
415 }
416 }
417
418 fn rnd(rng: &mut Rand32) -> Real {
419 #[cfg(feature = "f32")]
420 {
421 rng.rand_float()
422 }
423 #[cfg(feature = "f64")]
424 {
425 rng.rand_float() as Real
426 }
427 }
428
429 fn rnd_vec(rng: &mut Rand32, scale: Real) -> Vector {
430 let mut component = || (rnd(rng) - 0.5) * 2.0 * scale;
431 #[cfg(feature = "dim2")]
432 {
433 Vector::new(component(), component())
434 }
435 #[cfg(feature = "dim3")]
436 {
437 Vector::new(component(), component(), component())
438 }
439 }
440}