use super::camera::Camera;
use super::mat4::Vec3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TriangleHit {
pub triangle: usize,
pub t: f32,
pub barycentric: [f32; 3],
}
impl TriangleHit {
pub fn position(&self, s0: Vec3, s1: Vec3) -> Vec3 {
s0 + (s1 - s0) * self.t
}
}
pub fn picking_segment(camera: &Camera, ndc: (f32, f32)) -> Option<(Vec3, Vec3)> {
let inv = camera.matrix().inverse()?;
let near = inv.transform_point(Vec3::new(ndc.0, ndc.1, -1.0), true);
let far = inv.transform_point(Vec3::new(ndc.0, ndc.1, 1.0), true);
Some((near, far))
}
pub fn segment_triangles_intersection(
segment: (Vec3, Vec3),
triangles: &[[Vec3; 3]],
) -> Vec<TriangleHit> {
let (s0, s1) = segment;
let d = s1 - s0;
let mut hits: Vec<TriangleHit> = Vec::new();
for (index, tri) in triangles.iter().enumerate() {
let t0s0 = s0 - tri[0];
let edge01 = tri[1] - tri[0];
let edge02 = tri[2] - tri[0];
let d_cross_edge02 = d.cross(edge02);
let t0s0_cross_edge01 = t0s0.cross(edge01);
let volume = d_cross_edge02.dot(edge01);
if volume == 0.0 {
continue;
}
let sub1 = d_cross_edge02.dot(t0s0);
let sub2 = t0s0_cross_edge01.dot(d);
let sub0 = volume - sub1 - sub2;
let all_pos = sub0 >= 0.0 && sub1 >= 0.0 && sub2 >= 0.0;
let all_neg = sub0 <= 0.0 && sub1 <= 0.0 && sub2 <= 0.0;
if !(all_pos || all_neg) {
continue;
}
let t = t0s0_cross_edge01.dot(edge02) / volume;
if !(0.0..=1.0).contains(&t) {
continue;
}
hits.push(TriangleHit {
triangle: index,
t,
barycentric: [sub0 / volume, sub1 / volume, sub2 / volume],
});
}
hits.sort_by(|a, b| a.t.total_cmp(&b.t));
hits
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-5, "{a} != {b}");
}
fn approx_vec(a: Vec3, b: Vec3) {
approx(a.x, b.x);
approx(a.y, b.y);
approx(a.z, b.z);
}
#[test]
fn segment_through_triangle_centre_hits_at_midpoint() {
let tri = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let centroid = Vec3::new(1.0 / 3.0, 1.0 / 3.0, 0.0);
let s0 = centroid + Vec3::new(0.0, 0.0, 1.0);
let s1 = centroid + Vec3::new(0.0, 0.0, -1.0);
let hits = segment_triangles_intersection((s0, s1), &[tri]);
assert_eq!(hits.len(), 1);
approx(hits[0].t, 0.5);
approx_vec(hits[0].position(s0, s1), centroid);
for w in hits[0].barycentric {
approx(w, 1.0 / 3.0);
}
}
#[test]
fn segment_missing_triangle_returns_no_hit() {
let tri = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let s0 = Vec3::new(5.0, 5.0, 1.0);
let s1 = Vec3::new(5.0, 5.0, -1.0);
assert!(segment_triangles_intersection((s0, s1), &[tri]).is_empty());
}
#[test]
fn segment_short_of_triangle_returns_no_hit() {
let tri = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let c = Vec3::new(0.25, 0.25, 0.0);
let s0 = c + Vec3::new(0.0, 0.0, 2.0);
let s1 = c + Vec3::new(0.0, 0.0, 1.0); assert!(segment_triangles_intersection((s0, s1), &[tri]).is_empty());
}
#[test]
fn parallel_segment_is_skipped() {
let tri = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let s0 = Vec3::new(0.0, 0.0, 1.0);
let s1 = Vec3::new(1.0, 1.0, 1.0);
assert!(segment_triangles_intersection((s0, s1), &[tri]).is_empty());
}
#[test]
fn picking_segment_round_trips_to_screen_centre_and_depth_ends() {
let mut cam = Camera::new(
30.0,
0.1,
100.0,
(4.0, 3.0),
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
cam.reset_camera((Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0)));
let (near, far) = picking_segment(&cam, (0.0, 0.0)).expect("camera is non-singular");
let m = cam.matrix();
let pn = m.transform_point(near, true);
let pf = m.transform_point(far, true);
approx(pn.x, 0.0);
approx(pn.y, 0.0);
approx(pn.z, -1.0);
approx(pf.x, 0.0);
approx(pf.y, 0.0);
approx(pf.z, 1.0);
}
#[test]
fn hits_are_sorted_near_to_far() {
let tri_lo = [
Vec3::new(-1.0, -1.0, 0.0),
Vec3::new(1.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let tri_hi = [
Vec3::new(-1.0, -1.0, 1.0),
Vec3::new(1.0, -1.0, 1.0),
Vec3::new(0.0, 1.0, 1.0),
];
let s0 = Vec3::new(0.0, 0.0, 2.0);
let s1 = Vec3::new(0.0, 0.0, -1.0);
let hits = segment_triangles_intersection((s0, s1), &[tri_lo, tri_hi]);
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].triangle, 1);
assert_eq!(hits[1].triangle, 0);
assert!(hits[0].t < hits[1].t);
}
}