use phys_geom::math::*;
use phys_geom::shape::Cylinder;
use crate::{Raycast, RaycastHitResult};
impl Raycast for Cylinder {
#[allow(clippy::many_single_char_names)]
fn raycast(
&self,
local_ray: phys_geom::Ray,
max_distance: Real,
discard_inside_hit: bool,
) -> Option<RaycastHitResult> {
let half_height = self.half_height();
let radius = self.radius();
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 circle_center_y: Real;
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 discr_a = direction_xz.norm_squared();
if discr_a > 1e-8 {
let delta = discr_half_b * discr_half_b - discr_a * discr_c;
if delta < 0.0 {
return None;
}
let mut t = (-discr_half_b - delta.sqrt()) / discr_a;
let inside = if t < -offset {
t = -offset;
true
} else {
false
};
let hit_position = translated_origin + local_ray.direction.into_inner() * t;
if hit_position.y > half_height {
circle_center_y = half_height;
} else if hit_position.y < -half_height {
circle_center_y = -half_height;
} else {
if inside {
if discard_inside_hit {
return None;
}
return Some(RaycastHitResult {
distance: 0.0,
normal: -local_ray.direction,
});
}
let normal =
UnitVec3::new_normalize(Vec3::new(hit_position.x, 0.0, hit_position.z));
let distance = t + offset;
if distance <= max_distance {
return Some(RaycastHitResult { distance, normal });
}
return None;
}
} else {
circle_center_y = if local_ray.direction.y > 0.0 {
-half_height
} else {
half_height
}
}
if translated_origin.y.abs() > half_height
&& translated_origin.y * local_ray.direction.y >= 0.0
{
return None;
}
let t = (circle_center_y - translated_origin.y) / local_ray.direction.y;
let hit_position = translated_origin + local_ray.direction.into_inner() * t;
if hit_position.x * hit_position.x + hit_position.z * hit_position.z > radius * radius {
return None;
}
let distance = t + offset;
if distance <= max_distance {
Some(RaycastHitResult {
distance,
normal: UnitVec3::new_normalize(Vec3::new(
0.0,
if local_ray.direction.y < 0.0 {
1.0
} else {
-1.0
},
0.0,
)),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
use super::*;
#[test]
fn test_raycast_cylinder_side() {
let cylinder = Cylinder::new(2.0, 1.0);
let ray = phys_geom::Ray::new(
Point3::new(-3.0, 0.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
let hit = cylinder.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 2.0);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0))
);
}
#[test]
fn test_raycast_cylinder_top() {
let cylinder = Cylinder::new(2.0, 1.0);
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 = cylinder.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 2.0);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0))
);
}
#[test]
fn test_raycast_cylinder_bottom() {
let cylinder = Cylinder::new(2.0, 1.0);
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 = cylinder.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 2.0);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0))
);
}
#[test]
fn test_raycast_cylinder_inside() {
let cylinder = Cylinder::new(2.0, 1.0);
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 = cylinder.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_cylinder_inside_discarded() {
let cylinder = Cylinder::new(2.0, 1.0);
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!(cylinder.raycast(ray, 10.0, true), None);
}
#[test]
fn test_raycast_cylinder_miss() {
let cylinder = Cylinder::new(2.0, 1.0);
let ray = phys_geom::Ray::new(
Point3::new(-3.0, 5.0, 0.0),
UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
);
assert_eq!(cylinder.raycast(ray, 10.0, false), None);
}
#[test]
fn test_raycast_cylinder_parallel() {
let cylinder = Cylinder::new(2.0, 1.0);
let ray = phys_geom::Ray::new(
Point3::new(0.5, 4.0, 0.0),
UnitVec3::new_normalize(Vec3::new(0.0, -1.0, 0.0)),
);
let hit = cylinder.raycast(ray, 10.0, false).unwrap();
assert_relative_eq!(hit.distance, 2.0);
assert_relative_eq!(
hit.normal,
UnitVec3::new_normalize(Vec3::new(0.0, 1.0, 0.0))
);
}
#[test]
fn test_raycast_cylinder_max_distance() {
let cylinder = Cylinder::new(2.0, 1.0);
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!(cylinder.raycast(ray, 1.0, false), None);
let hit = cylinder.raycast(ray, 5.0, false).unwrap();
assert_relative_eq!(hit.distance, 4.0);
}
}