#[must_use]
pub fn clip_ray_to_aabb(
origin: [f32; 3],
dir: [f32; 3],
aabb_min: [f32; 3],
aabb_max: [f32; 3],
) -> Option<(f32, f32)> {
let mut t_enter: f32 = f32::NEG_INFINITY;
let mut t_exit: f32 = f32::INFINITY;
for axis in 0..3 {
let o = origin[axis];
let d = dir[axis];
let lo = aabb_min[axis];
let hi = aabb_max[axis];
if d == 0.0 {
if o < lo || o > hi {
return None;
}
continue;
}
let inv = 1.0 / d;
let (t1, t2) = {
let a = (lo - o) * inv;
let b = (hi - o) * inv;
if a <= b {
(a, b)
} else {
(b, a)
}
};
if t1 > t_enter {
t_enter = t1;
}
if t2 < t_exit {
t_exit = t2;
}
if t_enter > t_exit {
return None;
}
}
if t_exit < 0.0 {
return None;
}
Some((t_enter, t_exit))
}
#[must_use]
pub fn clip_ray_to_xy_aabb(
origin_xy: [f32; 2],
dir_xy: [f32; 2],
aabb_min_xy: [f32; 2],
aabb_max_xy: [f32; 2],
) -> Option<(f32, f32)> {
clip_ray_to_aabb(
[origin_xy[0], origin_xy[1], 0.0],
[dir_xy[0], dir_xy[1], 0.0],
[aabb_min_xy[0], aabb_min_xy[1], -1.0],
[aabb_max_xy[0], aabb_max_xy[1], 1.0],
)
}
#[cfg(test)]
mod tests {
use super::*;
const UNIT_BOX_MIN: [f32; 3] = [0.0, 0.0, 0.0];
const UNIT_BOX_MAX: [f32; 3] = [1.0, 1.0, 1.0];
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-5
}
#[test]
fn direct_hit_through_x_face() {
let r = clip_ray_to_aabb(
[-5.0, 0.5, 0.5],
[1.0, 0.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
)
.expect("expected hit");
assert!(approx_eq(r.0, 5.0), "t_enter={}", r.0);
assert!(approx_eq(r.1, 6.0), "t_exit={}", r.1);
}
#[test]
fn parallel_outside_misses() {
let r = clip_ray_to_aabb(
[-5.0, 5.0, 0.5],
[1.0, 0.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
);
assert!(r.is_none());
}
#[test]
fn origin_inside_yields_negative_t_enter() {
let r = clip_ray_to_aabb([0.5, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
.expect("expected hit");
assert!(approx_eq(r.0, -0.5), "t_enter={}", r.0);
assert!(approx_eq(r.1, 0.5), "t_exit={}", r.1);
assert!(r.0 <= 0.0);
}
#[test]
fn parallel_inside_does_not_constrain() {
let r = clip_ray_to_aabb([0.5, 0.5, 0.5], [0.0, 0.0, 1.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
.expect("expected hit");
assert!(approx_eq(r.0, -0.5));
assert!(approx_eq(r.1, 0.5));
}
#[test]
fn ray_pointed_away_from_box_misses() {
let r = clip_ray_to_aabb(
[-1.0, 0.5, 0.5],
[-1.0, 0.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
);
assert!(r.is_none(), "got {r:?}");
}
#[test]
fn corner_grazing_hit_is_a_hit() {
let r = clip_ray_to_aabb(
[-1.0, -1.0, 0.5],
[1.0, 1.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
)
.expect("corner graze should still be a hit");
assert!(approx_eq(r.0, 1.0));
assert!(approx_eq(r.1, 2.0));
}
#[test]
fn corner_grazing_miss_by_epsilon() {
let r = clip_ray_to_aabb(
[-1.0, -1.001, 0.5],
[1.0, 1.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
);
assert!(r.is_some());
}
#[test]
fn entry_face_origin_returns_zero_t_enter() {
let r = clip_ray_to_aabb([0.0, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
.expect("expected hit");
assert!(approx_eq(r.0, 0.0));
assert!(approx_eq(r.1, 1.0));
}
#[test]
fn exit_face_origin_zero_length_interval() {
let r = clip_ray_to_aabb([1.0, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
.expect("graze of exit face is technically a hit");
assert!(approx_eq(r.0, -1.0));
assert!(approx_eq(r.1, 0.0));
}
#[test]
fn xy_helper_matches_3d_for_xy_ray() {
let three_d = clip_ray_to_aabb(
[-3.0, 0.5, 0.5],
[1.0, 0.5, 0.0],
[0.0, 0.0, -1.0],
[10.0, 10.0, 1.0],
)
.expect("3D hit");
let two_d =
clip_ray_to_xy_aabb([-3.0, 0.5], [1.0, 0.5], [0.0, 0.0], [10.0, 10.0]).expect("2D hit");
assert!(approx_eq(three_d.0, two_d.0));
assert!(approx_eq(three_d.1, two_d.1));
}
#[test]
fn outside_orbit_camera_into_world() {
let vsid = 2048.0;
let origin_xy = [vsid + 256.0, vsid * 0.5];
let dir_xy = [-1.0, 0.0];
let r = clip_ray_to_xy_aabb(origin_xy, dir_xy, [0.0, 0.0], [vsid, vsid])
.expect("outside_orbit center ray must hit world AABB");
assert!(approx_eq(r.0, 256.0), "t_enter={}", r.0);
assert!(approx_eq(r.1, 256.0 + vsid), "t_exit={}", r.1);
}
#[test]
fn nan_dir_zero_origin_outside_misses() {
let r = clip_ray_to_aabb(
[10.0, 0.5, 0.5],
[0.0, 1.0, 0.0],
UNIT_BOX_MIN,
UNIT_BOX_MAX,
);
assert!(r.is_none());
}
}