#![allow(clippy::many_single_char_names)]
use crate::algebra::{Matrix4, Point3, Vector3};
use crate::math::aabb::AxisAlignedBoundingBox;
use crate::math::{is_point_inside_triangle, plane::Plane, solve_quadratic};
#[derive(Copy, Clone, Debug)]
pub struct Ray {
pub origin: Vector3<f32>,
pub dir: Vector3<f32>,
}
impl Default for Ray {
fn default() -> Self {
Ray {
origin: Vector3::new(0.0, 0.0, 0.0),
dir: Vector3::new(0.0, 0.0, 1.0),
}
}
}
#[derive(Clone, Debug)]
pub struct IntersectionResult {
pub min: f32,
pub max: f32,
}
impl IntersectionResult {
pub fn from_slice(roots: &[f32]) -> Self {
let mut min = std::f32::MAX;
let mut max = -std::f32::MAX;
for n in roots {
min = min.min(*n);
max = max.max(*n);
}
Self { min, max }
}
pub fn from_set(results: &[Option<IntersectionResult>]) -> Option<Self> {
let mut result = None;
for v in results {
match result {
None => result = v.clone(),
Some(ref mut result) => {
if let Some(v) = v {
result.merge(v.min);
result.merge(v.max);
}
}
}
}
result
}
pub fn merge(&mut self, param: f32) {
if param < self.min {
self.min = param;
}
if param > self.max {
self.max = param;
}
}
pub fn merge_slice(&mut self, params: &[f32]) {
for param in params {
self.merge(*param)
}
}
}
pub enum CylinderKind {
Infinite,
Finite,
Capped,
}
impl Ray {
#[inline]
pub fn from_two_points(begin: &Vector3<f32>, end: &Vector3<f32>) -> Option<Ray> {
let dir = end - begin;
if dir.norm() >= std::f32::EPSILON {
Some(Ray {
origin: *begin,
dir,
})
} else {
None
}
}
#[inline]
pub fn sphere_intersection_points(
&self,
position: &Vector3<f32>,
radius: f32,
) -> Option<[Vector3<f32>; 2]> {
self.try_eval_points(self.sphere_intersection(position, radius))
}
pub fn sphere_intersection(
&self,
position: &Vector3<f32>,
radius: f32,
) -> Option<IntersectionResult> {
let d = self.origin - *position;
let a = self.dir.dot(&self.dir);
let b = 2.0 * self.dir.dot(&d);
let c = d.dot(&d) - radius * radius;
if let Some(roots) = solve_quadratic(a, b, c) {
Some(IntersectionResult::from_slice(&roots))
} else {
None
}
}
#[inline]
pub fn is_intersect_sphere(&self, position: &Vector3<f32>, radius: f32) -> bool {
let d = self.origin - position;
let a = self.dir.dot(&self.dir);
let b = 2.0 * self.dir.dot(&d);
let c = d.dot(&d) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
discriminant >= 0.0
}
#[inline]
pub fn project_point(&self, point: &Vector3<f32>) -> f32 {
(point - self.origin).dot(&self.dir) / self.dir.norm_squared()
}
#[inline]
pub fn get_point(&self, t: f32) -> Vector3<f32> {
self.origin + self.dir.scale(t)
}
pub fn box_intersection(
&self,
min: &Vector3<f32>,
max: &Vector3<f32>,
) -> Option<IntersectionResult> {
let (mut tmin, mut tmax) = if self.dir.x >= 0.0 {
(
(min.x - self.origin.x) / self.dir.x,
(max.x - self.origin.x) / self.dir.x,
)
} else {
(
(max.x - self.origin.x) / self.dir.x,
(min.x - self.origin.x) / self.dir.x,
)
};
let (tymin, tymax) = if self.dir.y >= 0.0 {
(
(min.y - self.origin.y) / self.dir.y,
(max.y - self.origin.y) / self.dir.y,
)
} else {
(
(max.y - self.origin.y) / self.dir.y,
(min.y - self.origin.y) / self.dir.y,
)
};
if tmin > tymax || (tymin > tmax) {
return None;
}
if tymin > tmin {
tmin = tymin;
}
if tymax < tmax {
tmax = tymax;
}
let (tzmin, tzmax) = if self.dir.z >= 0.0 {
(
(min.z - self.origin.z) / self.dir.z,
(max.z - self.origin.z) / self.dir.z,
)
} else {
(
(max.z - self.origin.z) / self.dir.z,
(min.z - self.origin.z) / self.dir.z,
)
};
if (tmin > tzmax) || (tzmin > tmax) {
return None;
}
if tzmin > tmin {
tmin = tzmin;
}
if tzmax < tmax {
tmax = tzmax;
}
if tmin < 1.0 && tmax > 0.0 {
Some(IntersectionResult {
min: tmin,
max: tmax,
})
} else {
None
}
}
pub fn box_intersection_points(
&self,
min: &Vector3<f32>,
max: &Vector3<f32>,
) -> Option<[Vector3<f32>; 2]> {
self.try_eval_points(self.box_intersection(min, max))
}
pub fn aabb_intersection(&self, aabb: &AxisAlignedBoundingBox) -> Option<IntersectionResult> {
self.box_intersection(&aabb.min, &aabb.max)
}
pub fn aabb_intersection_points(
&self,
aabb: &AxisAlignedBoundingBox,
) -> Option<[Vector3<f32>; 2]> {
self.box_intersection_points(&aabb.min, &aabb.max)
}
pub fn plane_intersection(&self, plane: &Plane) -> f32 {
let u = -(self.origin.dot(&plane.normal) + plane.d);
let v = self.dir.dot(&plane.normal);
u / v
}
pub fn plane_intersection_point(&self, plane: &Plane) -> Option<Vector3<f32>> {
let t = self.plane_intersection(plane);
if t < 0.0 || t > 1.0 {
None
} else {
Some(self.get_point(t))
}
}
pub fn triangle_intersection(&self, vertices: &[Vector3<f32>; 3]) -> Option<Vector3<f32>> {
let ba = vertices[1] - vertices[0];
let ca = vertices[2] - vertices[0];
let plane = Plane::from_normal_and_point(&ba.cross(&ca), &vertices[0]).ok()?;
if let Some(point) = self.plane_intersection_point(&plane) {
if is_point_inside_triangle(&point, vertices) {
return Some(point);
}
}
None
}
pub fn cylinder_intersection(
&self,
pa: &Vector3<f32>,
pb: &Vector3<f32>,
r: f32,
kind: CylinderKind,
) -> Option<IntersectionResult> {
let va = (*pb - *pa)
.try_normalize(std::f32::EPSILON)
.unwrap_or_else(|| Vector3::new(0.0, 1.0, 0.0));
let vl = self.dir - va.scale(self.dir.dot(&va));
let dp = self.origin - *pa;
let dpva = dp - va.scale(dp.dot(&va));
let a = vl.norm_squared();
let b = 2.0 * vl.dot(&dpva);
let c = dpva.norm_squared() - r * r;
if let Some(cylinder_roots) = solve_quadratic(a, b, c) {
match kind {
CylinderKind::Infinite => Some(IntersectionResult::from_slice(&cylinder_roots)),
CylinderKind::Capped => {
let mut result = IntersectionResult::from_slice(&cylinder_roots);
for (cap_center, cap_normal) in [(pa, -va), (pb, va)].iter() {
let cap_plane =
Plane::from_normal_and_point(cap_normal, cap_center).unwrap();
let t = self.plane_intersection(&cap_plane);
if t > 0.0 {
let intersection = self.get_point(t);
if (*cap_center - intersection).norm_squared() <= r * r {
result.merge(t);
}
}
}
result.merge_slice(&cylinder_roots);
Some(result)
}
CylinderKind::Finite => {
let mut result = None;
for root in cylinder_roots.iter() {
let int_point = self.get_point(*root);
if (int_point - *pa).dot(&va) >= 0.0 && (*pb - int_point).dot(&va) >= 0.0 {
match &mut result {
None => {
result = Some(IntersectionResult {
min: *root,
max: *root,
})
}
Some(result) => result.merge(*root),
}
}
}
result
}
}
} else {
None
}
}
pub fn try_eval_points(&self, result: Option<IntersectionResult>) -> Option<[Vector3<f32>; 2]> {
match result {
None => None,
Some(result) => {
let a = if result.min >= 0.0 && result.min <= 1.0 {
Some(self.get_point(result.min))
} else {
None
};
let b = if result.max >= 0.0 && result.max <= 1.0 {
Some(self.get_point(result.max))
} else {
None
};
match a {
None => match b {
None => None,
Some(b) => Some([b, b]),
},
Some(a) => match b {
None => Some([a, a]),
Some(b) => Some([a, b]),
},
}
}
}
}
pub fn capsule_intersection(
&self,
pa: &Vector3<f32>,
pb: &Vector3<f32>,
radius: f32,
) -> Option<[Vector3<f32>; 2]> {
let cylinder = self.cylinder_intersection(pa, pb, radius, CylinderKind::Finite);
let cap_a = self.sphere_intersection(pa, radius);
let cap_b = self.sphere_intersection(pb, radius);
self.try_eval_points(IntersectionResult::from_set(&[cylinder, cap_a, cap_b]))
}
#[must_use = "Method does not modify ray, instead it returns transformed copy"]
pub fn transform(&self, mat: Matrix4<f32>) -> Self {
Self {
origin: mat.transform_point(&Point3::from(self.origin)).coords,
dir: mat.transform_point(&Point3::from(self.dir)).coords,
}
}
}
#[cfg(test)]
mod test {
use crate::math::ray::Ray;
use crate::math::Vector3;
#[test]
fn intersection() {
let triangle = [
Vector3::new(0.0, 0.5, 0.0),
Vector3::new(-0.5, -0.5, 0.0),
Vector3::new(0.5, -0.5, 0.0),
];
let ray =
Ray::from_two_points(&Vector3::new(0.0, 0.0, -2.0), &Vector3::new(0.0, 0.0, -1.0))
.unwrap();
assert!(ray.triangle_intersection(&triangle).is_none());
}
}