use phys_geom::math::{Real, *};
use phys_geom::shape::Capsule;
use crate::{Raycast, RaycastHitResult};
impl Raycast for Capsule {
fn raycast(
&self,
local_ray: phys_geom::Ray,
max_distance: Real,
discard_inside_hit: bool,
) -> Option<RaycastHitResult> {
let radius = self.radius();
let half_height = self.half_height();
let offset = (-local_ray
.origin
.coords
.dot(&local_ray.direction.into_inner())
- (half_height + radius))
.max(0.0);
let translated_origin = local_ray.origin + local_ray.direction.into_inner() * offset;
let origin_xz = Vec3::new(translated_origin.x, 0.0, translated_origin.z);
let direction_xz = Vec3::new(local_ray.direction.x, 0.0, local_ray.direction.z);
let discr_half_b = origin_xz.dot(&direction_xz);
let discr_c = origin_xz.norm_squared() - radius * radius;
if discr_half_b > 0.0 && discr_c > 0.0 {
return None;
}
let sphere_center_y: Real;
let discr_a = direction_xz.norm_squared();
if discr_a > 1e-8 {
let discr = discr_half_b * discr_half_b - discr_a * discr_c;
if discr < 0.0 {
return None;
}
let mut distance = (-discr_half_b - discr.sqrt()) / discr_a;
let inside = if distance < -offset {
distance = -offset;
true
} else {
false
};
let hit_position = translated_origin + local_ray.direction.into_inner() * distance;
if hit_position.y > half_height {
sphere_center_y = half_height;
} else if hit_position.y < -half_height {
sphere_center_y = -half_height;
} else {
if inside {
if discard_inside_hit {
return None;
}
return Some(RaycastHitResult {
distance: 0.0,
normal: -local_ray.direction,
});
}
distance += offset;
if distance <= max_distance {
return Some(RaycastHitResult {
distance,
normal: UnitVec3::new_normalize(Vec3::new(
hit_position.x,
0.0,
hit_position.z,
)),
});
}
return None;
}
} else {
if discr_c < 0.0
&& translated_origin.y < half_height
&& translated_origin.y > -half_height
&& half_height > 0.0
{
if discard_inside_hit {
return None;
}
return Some(RaycastHitResult {
distance: 0.0,
normal: -local_ray.direction,
});
}
sphere_center_y = if translated_origin.y > 0.0 {
half_height
} else {
-half_height
};
}
let origin_relative_to_sphere = translated_origin - Point3::new(0.0, sphere_center_y, 0.0);
let discr_half_b = origin_relative_to_sphere.dot(&local_ray.direction.into_inner());
let discr_c = origin_relative_to_sphere.norm_squared() - radius * radius;
if discr_half_b > 0.0 && discr_c > 0.0 {
return None;
}
let discr = discr_half_b * discr_half_b - discr_c;
if discr < 0.0 {
return None;
}
let mut distance = -discr_half_b - discr.sqrt();
if distance < -offset {
if discard_inside_hit {
return None;
}
return Some(RaycastHitResult {
distance: 0.0,
normal: -local_ray.direction,
});
}
let hit_position_relative_to_sphere =
origin_relative_to_sphere + local_ray.direction.into_inner() * distance;
distance += offset;
if distance <= max_distance {
Some(RaycastHitResult {
distance,
normal: UnitVec3::new_normalize(hit_position_relative_to_sphere),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use geom::Ray;
use super::*;
#[test]
fn test_raycast_capsule_cylinder() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(-2.0, 0.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
let hit = capsule.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 1.5);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0))
);
}
#[test]
fn test_raycast_capsule_top_sphere() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(0.0, 4.0, 0.0),
UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
);
let hit = capsule.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 1.5);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0))
);
}
#[test]
fn test_raycast_capsule_inside() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(0.0, 0.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
let hit = capsule.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 0.0);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0))
);
}
#[test]
fn test_raycast_capsule_inside_discarded() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(0.0, 0.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
assert_eq!(capsule.raycast(ray, 10.0, true), None);
}
#[test]
fn test_raycast_capsule_miss() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(-2.0, 5.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
assert_eq!(capsule.raycast(ray, 10.0, false), None);
}
#[test]
fn test_raycast_capsule_max_distance() {
let capsule = Capsule::new(2.0, 0.5);
let ray = phys_geom::Ray::new(
Point3::new(-5.0, 0.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
assert_eq!(capsule.raycast(ray, 1.0, false), None);
let hit = capsule.raycast(ray, 5.0, false).unwrap();
assert_relative_eq!(hit.distance, 4.5);
}
#[test]
fn test_small_segment() {
let capsule = Capsule::new(0.0, 1.0);
let ray = Ray::new_with_vec3(Point3::new(0.0, 0.0, 2.0), Vec3::new(0.0, 0.0, -1.0));
assert_eq!(
capsule.raycast(ray, 5.0, false),
Some(RaycastHitResult {
distance: 1.0,
normal: Vec3::z_axis(),
})
);
}
#[test]
fn test_inner() {
let capsule = Capsule::new(1.0, 1.0);
let ray_in_cylinder =
Ray::new_with_vec3(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, -1.0));
assert_eq!(
capsule.raycast(ray_in_cylinder, 5.0, false),
Some(RaycastHitResult {
distance: 0.0,
normal: -ray_in_cylinder.direction,
})
);
let ray_in_sphere =
Ray::new_with_vec3(Point3::new(0.0, 1.1, 0.0), Vec3::new(0.0, 0.0, -1.0));
assert_eq!(
capsule.raycast(ray_in_sphere, 5.0, false),
Some(RaycastHitResult {
distance: 0.0,
normal: -ray_in_sphere.direction,
})
);
assert_eq!(capsule.raycast(ray_in_cylinder, 5.0, true), None);
assert_eq!(capsule.raycast(ray_in_sphere, 5.0, true), None);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.0, 0.5, 0.0), Vec3::new(0.0, 1.0, 0.0)),
5.0,
false
),
Some(RaycastHitResult {
distance: 0.0,
normal: UnitVec3::new_unchecked(Vec3::new(0.0, -1.0, 0.0)),
})
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.0, 0.5, 0.0), Vec3::new(0.0, 1.0, 0.0)),
5.0,
true
),
None
);
}
#[test]
fn test_outer() {
let capsule = Capsule::new(1.0, 1.0);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(1.1, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
5.0,
false
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.75, 1.75, 0.0), Vec3::new(1.0, 0.0, 0.0)),
5.0,
false
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.75, -1.75, 0.0), Vec3::new(1.0, 0.0, 0.0)),
5.0,
false
),
None
);
}
#[test]
fn test_sphere() {
let capsule = Capsule::new(1.0, 1.0);
let up_ray = Ray::new_with_vec3(Point3::new(0.0, -4.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
assert_eq!(
capsule.raycast(up_ray, 20.0, false),
Some(RaycastHitResult {
distance: 2.0,
normal: -Vec3::y_axis(),
})
);
assert_eq!(capsule.raycast(up_ray, 1.0, false), None);
let down_ray = Ray::new_with_vec3(Point3::new(0.0, 4.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
assert_eq!(
capsule.raycast(down_ray, 20.0, false),
Some(RaycastHitResult {
distance: 2.0,
normal: Vec3::y_axis(),
})
);
assert_eq!(capsule.raycast(down_ray, 1.0, false), None);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(1.75, 1.8, 0.0), Vec3::new(-14.0, 0.0, 6.0)),
10.0,
false
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(1.75, -1.8, 0.0), Vec3::new(-14.0, 0.0, 6.0)),
10.0,
false
),
None
);
assert!(capsule
.raycast(
Ray::new_with_vec3(Point3::new(2.0, -1.8, 0.0), Vec3::new(-1.0, 0.0, 0.0)),
10.0,
false
)
.is_some());
}
#[test]
fn test_cylinder() {
let capsule = Capsule::new(1.0, 1.0);
let ray = Ray::new_with_vec3(Point3::new(2.0, 0.0, 2.0), Vec3::new(-2.0, 0.0, -1.0));
if let Some(result) = capsule.raycast(ray, 20.0, false) {
const EPSILON: f32 = 1e-6;
assert!((result.distance - Real::sqrt(5.0)).abs() < EPSILON);
assert!((result.normal.x - 0.0).abs() < EPSILON);
assert!((result.normal.y - 0.0).abs() < EPSILON);
assert!((result.normal.z - 1.0).abs() < EPSILON);
}
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(2.0, 0.0, 0.0), Vec3::new(-2.0, 0.0, 2.0)),
10.0,
false
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(2.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)),
0.5,
false
),
None
);
}
#[test]
fn test_from_surface_and_outward() {
let capsule = Capsule::new(1.0, 1.0);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
10.0,
true
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.0, 2.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
10.0,
true
),
None
);
assert_eq!(
capsule.raycast(
Ray::new_with_vec3(Point3::new(0.0, -2.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
10.0,
true
),
None
);
assert!(capsule
.raycast(
Ray::new_with_vec3(Point3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
10.0,
false
)
.is_some());
assert!(capsule
.raycast(
Ray::new_with_vec3(Point3::new(0.0, 2.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
10.0,
false
)
.is_some());
assert!(capsule
.raycast(
Ray::new_with_vec3(Point3::new(0.0, -2.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
10.0,
false
)
.is_some());
}
}