use crate::math::{ComplexField, Real, Vector};
use crate::query::{Ray, RayCast, RayIntersection};
use crate::shape::{Capsule, FeatureId, Segment};
impl RayCast for Capsule {
#[inline]
fn cast_local_ray(&self, ray: &Ray, max_time_of_impact: Real, solid: bool) -> Option<Real> {
ray_toi_with_capsule(&self.segment, self.radius, ray, solid)
.1
.filter(|time_of_impact| *time_of_impact <= max_time_of_impact)
}
#[inline]
fn cast_local_ray_and_get_normal(
&self,
ray: &Ray,
max_time_of_impact: Real,
solid: bool,
) -> Option<RayIntersection> {
ray_toi_and_normal_with_capsule(&self.segment, self.radius, ray, solid)
.filter(|inter| inter.time_of_impact <= max_time_of_impact)
}
}
fn ray_toi_with_capsule(
segment: &Segment,
radius: Real,
ray: &Ray,
solid: bool,
) -> (bool, Option<Real>) {
let ab = segment.b - segment.a;
let ao = ray.origin - segment.a;
let ab_ab = ab.length_squared();
let dir_dir = ray.dir.length_squared();
let ab_dir = ab.dot(ray.dir);
let ab_ao = ab.dot(ao);
let radius_squared = radius * radius;
let dir_on_plane = cross(ray.dir, ab);
let origin_on_plane = cross(ao, ab);
let ray_step = dir_on_plane.length_squared();
let radius_on_plane_squared = radius_squared * ab_ab;
let inside = origin_on_plane.length_squared() <= radius_on_plane_squared
&& (0.0 < ab_ao || ao.length_squared() <= radius_squared)
&& (ab_ao < ab_ab || (ray.origin - segment.b).length_squared() <= radius_squared);
if inside && solid {
return (true, Some(0.0));
}
let check_sphere_a = if ray_step == 0.0 {
(ab_dir > 0.0) ^ inside
} else {
let Some(t) = closest_approach_root(
origin_on_plane,
dir_on_plane,
ray_step,
radius_on_plane_squared,
inside,
) else {
return (false, None);
};
let y = ab_ao + t * ab_dir;
if 0.0 < y && y < ab_ab && t >= 0.0 {
return (inside, Some(t));
}
y <= 0.0
};
if dir_dir == 0.0 {
return (inside, None);
}
let oc = if check_sphere_a {
ao
} else {
ray.origin - segment.b
};
let t = closest_approach_root(oc, ray.dir, dir_dir, radius_squared, inside);
(inside, t.filter(|t| *t >= 0.0))
}
#[inline]
fn closest_approach_root(
origin: Vector,
dir: Vector,
dir_dir: Real,
radius_squared: Real,
inside: bool,
) -> Option<Real> {
let t_closest = -dir.dot(origin) / dir_dir;
let closest = origin + dir * t_closest;
let h = radius_squared - closest.length_squared();
if h < 0.0 && !inside {
return None;
}
let half_chord = <Real as ComplexField>::sqrt(h.max(0.0) / dir_dir);
if inside {
Some((t_closest + half_chord).max(0.0))
} else {
Some(t_closest - half_chord)
}
}
#[cfg(feature = "dim3")]
#[inline]
fn cross(v: Vector, segment: Vector) -> Vector {
v.cross(segment)
}
#[cfg(feature = "dim2")]
#[inline]
fn cross(v: Vector, segment: Vector) -> Vector {
Vector::new(v.x * segment.y - v.y * segment.x, 0.0)
}
fn ray_toi_and_normal_with_capsule(
segment: &Segment,
radius: Real,
ray: &Ray,
solid: bool,
) -> Option<RayIntersection> {
let (inside, inter) = ray_toi_with_capsule(segment, radius, ray, solid);
inter.map(|t| {
let normal = if solid && inside {
Vector::ZERO
} else {
let p = ray.origin + ray.dir * t;
let a_to_p = p - segment.a;
let seg = segment.b - segment.a;
let seg_squared = seg.length_squared();
let proj_times_seg = a_to_p.dot(seg);
let n = if proj_times_seg <= 0.0 {
a_to_p.normalize_or_zero()
} else if proj_times_seg >= seg_squared {
(p - segment.b).normalize_or_zero()
} else {
(a_to_p - (proj_times_seg / seg_squared) * seg).normalize_or_zero()
};
if inside {
-n
} else {
n
}
};
RayIntersection::new(t, normal, FeatureId::Face(0))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Vector;
use crate::query::point::point_query::PointQuery;
use oorandom::Rand32;
#[test]
fn exact_cases() {
let c = Capsule::new(v2(0.0, 0.5), v2(0.0, 1.5), 0.5);
expect_hit(&c, v2(0.0, 5.0), v2(0.0, -0.2), true, 15.0, v2(0.0, 1.0));
expect_hit(&c, v2(5.0, 1.0), v2(-0.3, 0.0), true, 15.0, v2(1.0, 0.0));
expect_hit(&c, v2(5.0, 2.0), v2(-1.0, 0.0), true, 5.0, v2(0.0, 1.0));
expect_hit(
&c,
v2(0.1, -4.0),
v2(0.0, 0.2),
true,
20.0505,
v2(0.2, -0.9798),
);
assert!(c
.cast_local_ray(&Ray::new(v2(10.0, 5.0), v2(0.0, 0.1)), 50.0, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(0.4, 1.85), v2(1.0, 1.0)), 50.0, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(0.0, 2.1), v2(0.0, 1.0)), 50.0, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(0.4, 1.85), v2(1.0, 0.2)), 50.0, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(2.0, 1.0), v2(1.0, 0.0)), 50.0, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(2.0, 3.0), v2(1.0, 1.0)), 50.0, true)
.is_none());
expect_hit(&c, v2(0.1, 1.0), v2(0.0, 1.0), true, 0.0, v2(0.0, 0.0));
expect_hit(&c, v2(0.0, 1.0), v2(0.0, 1.0), false, 1.0, v2(0.0, -1.0));
expect_hit(&c, v2(0.0, 1.0), v2(0.0, -1.0), false, 1.0, v2(0.0, 1.0));
expect_hit(&c, v2(0.0, 1.0), v2(1.0, 0.0), false, 0.5, v2(-1.0, 0.0));
expect_hit(&c, v2(0.1, 0.8), v2(1.0, 0.5), false, 0.4, v2(-1.0, 0.0));
expect_hit(&c, v2(0.0, 1.6), v2(0.0, -1.0), false, 1.6, v2(0.0, 1.0));
expect_hit(&c, v2(0.1, 1.0), v2(0.0, 0.0), true, 0.0, v2(0.0, 0.0));
assert!(c
.cast_local_ray(&Ray::new(v2(0.1, 3.0), v2(0.0, 0.0)), 50.0, true)
.is_none());
let ball = Capsule::new(v2(0.0, 1.0), v2(0.0, 1.0), 1.0);
expect_hit(&ball, v2(0.0, 5.0), v2(0.0, -1.0), true, 3.0, v2(0.0, 1.0));
expect_hit(&ball, v2(0.5, 1.0), v2(1.0, 0.0), true, 0.0, v2(0.0, 0.0));
assert!(c
.cast_local_ray(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 14.9, true)
.is_none());
assert!(c
.cast_local_ray(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 15.1, true)
.is_some());
assert!(c
.cast_local_ray_and_get_normal(&Ray::new(v2(0.0, 5.0), v2(0.0, -0.2)), 14.9, true)
.is_none());
}
#[test]
fn distant_origin_precision() {
let c = Capsule::new(v2(1.0e4, 0.0), v2(1.0e4, 5.0), 1.0);
let i = c
.cast_local_ray_and_get_normal(&Ray::new(v2(-1.0e4, 2.5), v2(2.0e4, 0.0)), 2.0, true)
.unwrap();
let hit = -1.0e4 + i.time_of_impact * 2.0e4;
assert!((hit - (1.0e4 - 1.0)).abs() < 1.0e-2, "hit at x = {hit}");
let c = Capsule::new(v2(0.0, 0.0), v2(0.0, 5.0), 1.0);
let dir = v2(1.0e4, 3.0e3);
let i = c
.cast_local_ray_and_get_normal(&Ray::new(v2(-1.0, 2.5) - dir, dir), 2.0, true)
.unwrap();
assert!((i.time_of_impact - 1.0).abs() * dir.length() < 1.0e-2);
assert!((i.normal - v2(-1.0, 0.0)).length() < 1.0e-3);
}
#[test]
fn zero_radius_normal_is_finite() {
let c = Capsule::new(v2(0.0, -1.0), v2(0.0, 1.0), 0.0);
for dir in [v2(1.0, 0.0), v2(0.0, -1.0)] {
let i = c
.cast_local_ray_and_get_normal(&Ray::new(v2(0.0, 0.0) - dir * 2.0, dir), 10.0, true)
.unwrap();
assert!(i.normal.is_finite(), "normal: {:?}", i.normal);
}
}
fn v2(x: Real, y: Real) -> Vector {
Vector::new(
x,
y,
#[cfg(feature = "dim3")]
0.0,
)
}
#[track_caller]
fn expect_hit(c: &Capsule, o: Vector, d: Vector, solid: bool, et: Real, en: Vector) {
let i = c
.cast_local_ray_and_get_normal(&Ray::new(o, d), 50.0, solid)
.unwrap_or_else(|| panic!("expected hit (o={:?}, d={:?})", o, d));
assert!(
(i.time_of_impact - et).abs() < 1e-4,
"t: got {}, want {}",
i.time_of_impact,
et
);
assert!(
(i.normal - en).length() < 1e-3,
"n: got {:?}, want {:?}",
i.normal,
en
);
}
#[test]
fn fuzz_capsule_ray_casts() {
let epsilon = 0.002;
let mut rng = Rand32::new(42);
for _ in 0..100_000 {
let (a, b) = (rnd_vec(&mut rng, 10.0), rnd_vec(&mut rng, 10.0));
let r = 0.5 + 5.0 * rnd(&mut rng);
let capsule = Capsule::new(a, b, r);
let inside = {
let mut w = rnd_vec(&mut rng, r);
while w.length_squared() >= r * r {
w = rnd_vec(&mut rng, r);
}
a + (b - a) * rnd(&mut rng) + w
};
let far_enough = r + a.distance(b);
let mut offset = Vector::ZERO;
while offset.length_squared() < far_enough * far_enough {
offset = rnd_vec(&mut rng, far_enough * 2.0);
}
let o = (a + b) * 0.5 + offset;
let d = (inside - o) * (0.1 + 0.9 * rnd(&mut rng));
let i = capsule
.cast_local_ray_and_get_normal(&Ray::new(o, d), 1000.0, true)
.expect("a ray aimed at an interior point must hit");
let hit = o + d * i.time_of_impact;
assert!(
capsule.contains_local_point(hit - i.normal * epsilon),
"nudging inward along the normal should go inside the capsule"
);
assert!(
!capsule.contains_local_point(hit + i.normal * epsilon),
"nudging outward along the normal should go outside the capsule"
);
let i_in = capsule
.cast_local_ray_and_get_normal(&Ray::new(inside, o - inside), 1000.0, false)
.expect("a ray from inside toward the outside must exit");
let hit_in = inside + (o - inside) * i_in.time_of_impact;
assert!(
capsule.contains_local_point(hit_in + i_in.normal * epsilon),
"nudging along the inward normal should stay inside"
);
assert!(
!capsule.contains_local_point(hit_in - i_in.normal * epsilon),
"nudging against the inward normal should go outside"
);
assert!(
capsule
.cast_local_ray(&Ray::new(o, -d), 1000.0, true)
.is_none(),
"a retreating ray must miss"
);
#[cfg(feature = "dim2")]
let tangent = Vector::new(-i.normal.y, i.normal.x);
#[cfg(feature = "dim3")]
let tangent = {
let mut tangent = Vector::ZERO;
while tangent.length_squared() < 1e-8 {
tangent = rnd_vec(&mut rng, 1.0).cross(i.normal);
}
tangent
};
let origin = hit + i.normal * (epsilon + rnd(&mut rng)) - rnd(&mut rng) * tangent;
assert!(
capsule
.cast_local_ray(&Ray::new(origin, tangent), 1000.0, true)
.is_none(),
"tangent outside the capsule should miss"
);
}
}
fn rnd(rng: &mut Rand32) -> Real {
#[cfg(feature = "f32")]
{
rng.rand_float()
}
#[cfg(feature = "f64")]
{
rng.rand_float() as Real
}
}
fn rnd_vec(rng: &mut Rand32, scale: Real) -> Vector {
let mut component = || (rnd(rng) - 0.5) * 2.0 * scale;
#[cfg(feature = "dim2")]
{
Vector::new(component(), component())
}
#[cfg(feature = "dim3")]
{
Vector::new(component(), component(), component())
}
}
}