use crate::math::Vec3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ray {
origin: Vec3,
direction: Vec3,
}
impl Ray {
pub fn new(origin: Vec3, direction: Vec3) -> Self {
Self {
origin,
direction: direction.normalize_or_zero(),
}
}
pub const fn origin(&self) -> Vec3 {
self.origin
}
pub const fn direction(&self) -> Vec3 {
self.direction
}
pub fn at(&self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
pub fn hit_plane(&self, plane: Plane) -> Option<f32> {
reached((plane.point - self.origin).dot(plane.normal) / self.direction.dot(plane.normal))
}
pub fn hit_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
let to_center = center - self.origin;
let length_squared = self.direction.length_squared();
let along = to_center.dot(self.direction);
let half_chord = (along * along
- length_squared * (to_center.length_squared() - radius * radius))
.sqrt();
reached((along - half_chord) / length_squared)
.or_else(|| reached((along + half_chord) / length_squared))
}
pub fn hit_aabb(&self, min: Vec3, max: Vec3) -> Option<f32> {
let (to_min, to_max) = (
(min - self.origin) / self.direction,
(max - self.origin) / self.direction,
);
let (entry, exit) = (
to_min.min(to_max).max_element(),
to_min.max(to_max).min_element(),
);
if entry > exit {
return None;
}
reached(entry).or_else(|| reached(exit))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Plane {
pub point: Vec3,
pub normal: Vec3,
}
fn reached(t: f32) -> Option<f32> {
(t >= 0.0 && t.is_finite()).then_some(t)
}
#[cfg(test)]
mod tests {
use super::*;
fn along_x() -> Ray {
Ray::new(Vec3::ZERO, Vec3::X)
}
#[test]
fn a_ray_is_measured_in_meters_whatever_it_was_built_from() {
let ray = Ray::new(Vec3::Y, Vec3::X * 8.0);
assert_eq!(ray.origin(), Vec3::Y);
assert_eq!(ray.direction(), Vec3::X);
assert_eq!(ray.at(3.0), Vec3::new(3.0, 1.0, 0.0));
}
#[test]
fn a_plane_is_hit_from_either_side_and_never_behind() {
let down = Ray::new(Vec3::Y * 5.0, Vec3::NEG_Y);
assert_eq!(
down.hit_plane(Plane {
point: Vec3::ZERO,
normal: Vec3::Y
}),
Some(5.0)
);
assert_eq!(
down.hit_plane(Plane {
point: Vec3::ZERO,
normal: Vec3::NEG_Y
}),
Some(5.0),
"a plane faces both ways"
);
assert_eq!(
down.hit_plane(Plane {
point: Vec3::Y * 5.0,
normal: Vec3::Y
}),
Some(0.0),
"a ray that starts on one hits it at once"
);
assert_eq!(
down.hit_plane(Plane {
point: Vec3::Y * 9.0,
normal: Vec3::Y
}),
None,
"the plane behind it is missed"
);
}
#[test]
fn a_plane_the_ray_cannot_reach_is_never_hit() {
let across = along_x();
assert_eq!(
across.hit_plane(Plane {
point: Vec3::ZERO,
normal: Vec3::Y
}),
None,
"parallel"
);
assert_eq!(
across.hit_plane(Plane {
point: Vec3::ZERO,
normal: Vec3::ZERO
}),
None,
"a plane with no normal is no plane"
);
assert_eq!(
Ray::new(Vec3::ZERO, Vec3::ZERO).hit_plane(Plane {
point: Vec3::X,
normal: Vec3::X
}),
None,
"a ray with no direction reaches nothing"
);
}
#[test]
fn a_sphere_is_hit_at_its_near_side_and_left_from_within() {
let ray = along_x();
assert_eq!(ray.hit_sphere(Vec3::X * 10.0, 2.0), Some(8.0));
assert_eq!(
ray.hit_sphere(Vec3::ZERO, 2.0),
Some(2.0),
"from within, where it leaves"
);
assert_eq!(ray.hit_sphere(Vec3::NEG_X * 10.0, 2.0), None, "behind");
assert_eq!(
ray.hit_sphere(Vec3::new(10.0, 3.0, 0.0), 2.0),
None,
"past it"
);
}
#[test]
fn a_sphere_the_ray_grazes_or_that_has_no_size_still_answers_a_distance() {
let ray = along_x();
assert_eq!(ray.hit_sphere(Vec3::new(5.0, 2.0, 0.0), 2.0), Some(5.0));
assert!(
ray.hit_sphere(Vec3::X * 5.0, 0.0)
.is_none_or(|t| t.is_finite()),
"a sphere of no size is a point on the ray or nothing at all"
);
assert_eq!(
ray.hit_sphere(Vec3::X * 5.0, -2.0),
Some(3.0),
"one turned inside out is the same sphere"
);
}
#[test]
fn bounds_are_hit_where_the_ray_enters_and_left_where_it_leaves() {
let ray = along_x();
let (min, max) = (Vec3::new(4.0, -1.0, -1.0), Vec3::new(6.0, 1.0, 1.0));
assert_eq!(ray.hit_aabb(min, max), Some(4.0));
assert_eq!(
ray.hit_aabb(Vec3::splat(-1.0), Vec3::ONE),
Some(1.0),
"from within, where it leaves"
);
assert_eq!(ray.hit_aabb(max, min), Some(4.0), "however they are given");
assert_eq!(ray.hit_aabb(-max, -min), None, "behind");
assert_eq!(
ray.hit_aabb(min + Vec3::Y * 5.0, max + Vec3::Y * 5.0),
None,
"above"
);
}
#[test]
fn bounds_with_no_thickness_are_hit_head_on() {
let down = Ray::new(Vec3::Y, Vec3::NEG_Y);
let (min, max) = (Vec3::new(-1.0, 0.0, -1.0), Vec3::new(1.0, 0.0, 1.0));
assert_eq!(down.hit_aabb(min, max), Some(1.0));
assert!(
down.hit_aabb(Vec3::ZERO, Vec3::ZERO)
.is_none_or(|t| t.is_finite()),
"bounds of no size at all are a point, or nothing"
);
}
#[test]
fn a_ray_with_no_direction_reaches_nothing() {
let still = Ray::new(Vec3::ZERO, Vec3::ZERO);
let hits = [
still.hit_plane(Plane {
point: Vec3::X,
normal: Vec3::X,
}),
still.hit_sphere(Vec3::ZERO, 2.0),
still.hit_sphere(Vec3::X * 5.0, 2.0),
still.hit_aabb(Vec3::splat(-1.0), Vec3::ONE),
];
assert!(hits.iter().all(Option::is_none), "not even what it sits in");
}
}