use crate::math::{sqrt, tan};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PickRay {
pub origin: [f32; 3],
pub dir: [f32; 3],
}
pub fn screen_ray(
view: &[[f32; 4]; 4],
cam_pos: [f32; 3],
fov_y_radians: f32,
viewport: [f32; 2],
mouse: [f32; 2],
) -> Option<PickRay> {
let (vw, vh) = (viewport[0], viewport[1]);
if vw <= 0.0 || vh <= 0.0 || !vw.is_finite() || !vh.is_finite() {
return None;
}
let ndc_x = 2.0 * mouse[0] / vw - 1.0;
let ndc_y = 1.0 - 2.0 * mouse[1] / vh;
let tan_half = tan(fov_y_radians * 0.5);
if tan_half <= 0.0 || !tan_half.is_finite() {
return None;
}
let aspect = vw / vh;
let d = [ndc_x * tan_half * aspect, ndc_y * tan_half, -1.0];
let world = [
view[0][0] * d[0] + view[0][1] * d[1] + view[0][2] * d[2],
view[1][0] * d[0] + view[1][1] * d[1] + view[1][2] * d[2],
view[2][0] * d[0] + view[2][1] * d[1] + view[2][2] * d[2],
];
let len = sqrt(world[0] * world[0] + world[1] * world[1] + world[2] * world[2]);
if len <= 0.0 || !len.is_finite() {
return None;
}
Some(PickRay {
origin: cam_pos,
dir: [world[0] / len, world[1] / len, world[2] / len],
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AabbFace {
pub t: f32,
pub axis: usize,
pub sign: f32,
}
pub fn ray_aabb_face(ray: &PickRay, bb_min: [f32; 3], bb_max: [f32; 3]) -> Option<AabbFace> {
if !crate::gfx::lod::bounds_finite(bb_min, bb_max) {
return None;
}
let mut t_enter = f32::NEG_INFINITY;
let mut t_exit = f32::INFINITY;
let mut enter_axis = None;
for i in 0..3 {
if ray.dir[i] == 0.0 {
if ray.origin[i] < bb_min[i] || ray.origin[i] > bb_max[i] {
return None;
}
continue;
}
let inv = 1.0 / ray.dir[i];
let (t1, t2) = (
(bb_min[i] - ray.origin[i]) * inv,
(bb_max[i] - ray.origin[i]) * inv,
);
let (near, far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
if near > t_enter {
t_enter = near;
enter_axis = Some(i);
}
t_exit = t_exit.min(far);
if t_enter > t_exit {
return None;
}
}
if t_exit < 0.0 {
return None;
}
let (axis, sign) = match enter_axis {
Some(a) => (a, -ray.dir[a].signum()),
None => (0, 0.0),
};
Some(AabbFace {
t: t_enter.max(0.0),
axis,
sign,
})
}
pub fn ray_aabb(ray: &PickRay, bb_min: [f32; 3], bb_max: [f32; 3]) -> Option<f32> {
ray_aabb_face(ray, bb_min, bb_max).map(|f| f.t)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::camera::view_matrix;
const VP: [f32; 2] = [1280.0, 720.0];
const FOV: f32 = core::f32::consts::FRAC_PI_2;
fn assert_dir(ray: PickRay, expect: [f32; 3]) {
for i in 0..3 {
assert!(
(ray.dir[i] - expect[i]).abs() < 1e-5,
"dir {:?} != {expect:?}",
ray.dir
);
}
}
#[test]
fn center_pixel_rays_along_camera_forward() {
let view = view_matrix([1.0, 2.0, 3.0], 0.0, 0.0);
let ray = screen_ray(&view, [1.0, 2.0, 3.0], FOV, VP, [640.0, 360.0]).unwrap();
assert_eq!(ray.origin, [1.0, 2.0, 3.0]);
assert_dir(ray, [0.0, 0.0, -1.0]);
let view = view_matrix([0.0; 3], core::f32::consts::FRAC_PI_2, 0.0);
let ray = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 360.0]).unwrap();
assert_dir(ray, [-1.0, 0.0, 0.0]);
let view = view_matrix([0.0; 3], 0.0, core::f32::consts::FRAC_PI_2);
let ray = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 360.0]).unwrap();
assert_dir(ray, [0.0, 1.0, 0.0]);
}
#[test]
fn edge_pixels_span_the_field_of_view() {
let view = view_matrix([0.0; 3], 0.0, 0.0);
let top = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 0.0]).unwrap();
let vert_tan = top.dir[1] / -top.dir[2];
assert!((vert_tan - (FOV * 0.5).tan()).abs() < 1e-4, "{vert_tan}");
assert!(top.dir[0].abs() < 1e-6);
let right = screen_ray(&view, [0.0; 3], FOV, VP, [1280.0, 360.0]).unwrap();
let horiz_tan = right.dir[0] / -right.dir[2];
let expect = (FOV * 0.5).tan() * (VP[0] / VP[1]);
assert!((horiz_tan - expect).abs() < 1e-4, "{horiz_tan} vs {expect}");
assert!(right.dir[1].abs() < 1e-6);
}
#[test]
fn degenerate_viewport_or_fov_yields_no_ray() {
let view = view_matrix([0.0; 3], 0.0, 0.0);
assert_eq!(
screen_ray(&view, [0.0; 3], FOV, [0.0, 720.0], [0.0; 2]),
None
);
assert_eq!(
screen_ray(&view, [0.0; 3], FOV, [1280.0, 0.0], [0.0; 2]),
None
);
assert_eq!(screen_ray(&view, [0.0; 3], 0.0, VP, [0.0; 2]), None);
}
fn ray(origin: [f32; 3], dir: [f32; 3]) -> PickRay {
PickRay { origin, dir }
}
#[test]
fn ray_hits_a_box_at_the_entry_distance() {
let r = ray([0.0, 0.0, 5.0], [0.0, 0.0, -1.0]);
let t = ray_aabb(&r, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]).unwrap();
assert!((t - 4.0).abs() < 1e-6, "{t}");
}
#[test]
fn ray_misses_beside_behind_and_non_finite_boxes() {
let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
assert_eq!(
ray_aabb(&ray([0.0, 3.0, 5.0], [0.0, 0.0, -1.0]), mn, mx),
None
);
assert_eq!(
ray_aabb(&ray([0.0, 0.0, 5.0], [0.0, 0.0, 1.0]), mn, mx),
None
);
assert_eq!(
ray_aabb(
&ray([0.0; 3], [0.0, 0.0, -1.0]),
[f32::NAN; 3],
[f32::NAN; 3]
),
None
);
}
#[test]
fn ray_inside_a_box_hits_at_zero() {
let t = ray_aabb(
&ray([0.0; 3], [0.0, 1.0, 0.0]),
[-1.0, -1.0, -1.0],
[1.0, 1.0, 1.0],
)
.unwrap();
assert_eq!(t, 0.0);
}
#[test]
fn entered_face_reports_axis_and_outward_sign() {
let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
let f = ray_aabb_face(&ray([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]), mn, mx).unwrap();
assert_eq!((f.axis, f.sign), (1, 1.0));
assert!((f.t - 4.0).abs() < 1e-6);
let f = ray_aabb_face(&ray([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]), mn, mx).unwrap();
assert_eq!((f.axis, f.sign), (0, -1.0));
let f = ray_aabb_face(&ray([0.0, 0.5, 5.0], [0.0, -0.0995, -0.995]), mn, mx).unwrap();
assert_eq!((f.axis, f.sign), (2, 1.0));
let f = ray_aabb_face(&ray([0.0; 3], [0.0; 3]), mn, mx).unwrap();
assert_eq!((f.t, f.sign), (0.0, 0.0));
}
#[test]
fn axis_parallel_rays_respect_the_perpendicular_slabs() {
let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
assert!(ray_aabb(&ray([0.5, -5.0, 0.5], [0.0, 1.0, 0.0]), mn, mx).is_some());
assert_eq!(
ray_aabb(&ray([2.0, -5.0, 0.5], [0.0, 1.0, 0.0]), mn, mx),
None
);
assert!(ray_aabb(&ray([1.0, -5.0, 0.0], [0.0, 1.0, 0.0]), mn, mx).is_some());
}
}