use crate::vec::{Point3, Vec3};
#[derive(Debug, Clone, Copy)]
pub struct RayTriangleHit {
pub t: f64,
pub u: f64,
pub v: f64,
}
fn edge_tiebreak(px: f64, py: f64, qx: f64, qy: f64) -> f64 {
let a = px * qy;
let b = py * qx;
if a > b {
f64::MIN_POSITIVE
} else if a < b {
-f64::MIN_POSITIVE
} else {
if px > qx {
f64::MIN_POSITIVE
} else if px < qx {
-f64::MIN_POSITIVE
} else if py > qy {
f64::MIN_POSITIVE
} else {
-f64::MIN_POSITIVE
}
}
}
#[must_use]
#[allow(clippy::many_single_char_names)]
pub fn watertight_ray_triangle_intersect(
origin: Point3,
dir: Vec3,
v0: Point3,
v1: Point3,
v2: Point3,
) -> Option<RayTriangleHit> {
let d = dir.0;
let abs_x = d[0].abs();
let abs_y = d[1].abs();
let abs_z = d[2].abs();
let kz = if abs_x > abs_y && abs_x > abs_z {
0
} else if abs_y > abs_z {
1
} else {
2
};
let mut kx = (kz + 1) % 3;
let mut ky = (kx + 1) % 3;
if d[kz] < 0.0 {
std::mem::swap(&mut kx, &mut ky);
}
let sz = 1.0 / d[kz];
let sx = d[kx] * sz;
let sy = d[ky] * sz;
let a = (v0 - origin).0;
let b = (v1 - origin).0;
let c = (v2 - origin).0;
let ax = a[kx] - sx * a[kz];
let ay = a[ky] - sy * a[kz];
let bx = b[kx] - sx * b[kz];
let by = b[ky] - sy * b[kz];
let cx = c[kx] - sx * c[kz];
let cy = c[ky] - sy * c[kz];
let mut u = cx.mul_add(by, -(cy * bx));
let mut v = ax.mul_add(cy, -(ay * cx));
let mut w = bx.mul_add(ay, -(by * ax));
if u == 0.0 {
u = edge_tiebreak(cx, cy, bx, by);
}
if v == 0.0 {
v = edge_tiebreak(ax, ay, cx, cy);
}
if w == 0.0 {
w = edge_tiebreak(bx, by, ax, ay);
}
if (u < 0.0 || v < 0.0 || w < 0.0) && (u > 0.0 || v > 0.0 || w > 0.0) {
return None;
}
let det = u + v + w;
if det == 0.0 {
return None;
}
let az = sz * a[kz];
let bz = sz * b[kz];
let cz = sz * c[kz];
let t_scaled = u.mul_add(az, v.mul_add(bz, w * cz));
if (det > 0.0 && t_scaled <= 0.0) || (det < 0.0 && t_scaled >= 0.0) {
return None;
}
let inv_det = 1.0 / det;
Some(RayTriangleHit {
t: t_scaled * inv_det,
u: v * inv_det,
v: w * inv_det,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_lossless,
clippy::suboptimal_flops
)]
mod tests {
use super::*;
const EPS: f64 = 1e-12;
fn tri() -> (Point3, Point3, Point3) {
(
Point3::new(-1.0, -1.0, 0.0),
Point3::new(1.0, -1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
)
}
#[test]
fn ray_hits_triangle() {
let (v0, v1, v2) = tri();
let origin = Point3::new(0.0, 0.0, -1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
let hit = watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).expect("should hit");
assert!((hit.t - 1.0).abs() < EPS);
}
#[test]
fn ray_misses_triangle() {
let (v0, v1, v2) = tri();
let origin = Point3::new(10.0, 10.0, -1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
}
#[test]
fn ray_parallel_to_triangle() {
let (v0, v1, v2) = tri();
let origin = Point3::new(0.0, 0.0, 0.0);
let dir = Vec3::new(1.0, 0.0, 0.0);
assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
}
#[test]
fn shared_edge_exactly_one_hit() {
let shared_a = Point3::new(0.0, 0.0, 0.0);
let shared_b = Point3::new(1.0, 0.0, 0.0);
let tri1_c = Point3::new(0.5, 1.0, 0.0);
let tri2_c = Point3::new(0.5, -1.0, 0.0);
let origin = Point3::new(0.5, 0.0, -1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
let hit1 = watertight_ray_triangle_intersect(origin, dir, shared_a, shared_b, tri1_c);
let hit2 = watertight_ray_triangle_intersect(origin, dir, shared_a, shared_b, tri2_c);
let count = hit1.is_some() as u32 + hit2.is_some() as u32;
assert_eq!(count, 1, "shared edge must report exactly one hit");
}
#[test]
fn ray_hits_vertex() {
let shared = Point3::new(0.0, 0.0, 0.0);
let tris = [
(
shared,
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
),
(
shared,
Point3::new(0.0, 1.0, 0.0),
Point3::new(-1.0, 0.0, 0.0),
),
(
shared,
Point3::new(-1.0, 0.0, 0.0),
Point3::new(0.0, -1.0, 0.0),
),
(
shared,
Point3::new(0.0, -1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
),
];
let origin = Point3::new(0.0, 0.0, -1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
let count: u32 = tris
.iter()
.map(|&(a, b, c)| {
watertight_ray_triangle_intersect(origin, dir, a, b, c).is_some() as u32
})
.sum();
assert_eq!(
count, 1,
"vertex shared by 4 triangles must report exactly one hit"
);
}
#[test]
fn backface_not_hit() {
let (v0, v1, v2) = tri();
let origin = Point3::new(0.0, 0.0, -1.0);
let dir = Vec3::new(0.0, 0.0, -1.0);
assert!(watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).is_none());
}
#[test]
fn barycentric_coordinates_valid() {
let (v0, v1, v2) = tri();
let origin = Point3::new(0.0, 0.0, -1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
let hit = watertight_ray_triangle_intersect(origin, dir, v0, v1, v2).expect("should hit");
assert!(hit.u >= -EPS, "u = {} should be >= 0", hit.u);
assert!(hit.v >= -EPS, "v = {} should be >= 0", hit.v);
assert!(
hit.u + hit.v <= 1.0 + EPS,
"u + v = {} should be <= 1",
hit.u + hit.v,
);
let w = 1.0 - hit.u - hit.v;
let px = w * v0.x() + hit.u * v1.x() + hit.v * v2.x();
let py = w * v0.y() + hit.u * v1.y() + hit.v * v2.y();
let pz = w * v0.z() + hit.u * v1.z() + hit.v * v2.z();
let expected = origin.0[2] + hit.t * dir.0[2];
assert!(
(pz - expected).abs() < EPS,
"pz = {pz}, expected = {expected}"
);
assert!((px - origin.x()).abs() < EPS);
assert!((py - origin.y()).abs() < EPS);
}
}