1#[must_use]
36pub fn clip_ray_to_aabb(
37 origin: [f32; 3],
38 dir: [f32; 3],
39 aabb_min: [f32; 3],
40 aabb_max: [f32; 3],
41) -> Option<(f32, f32)> {
42 let mut t_enter: f32 = f32::NEG_INFINITY;
43 let mut t_exit: f32 = f32::INFINITY;
44 for axis in 0..3 {
45 let o = origin[axis];
46 let d = dir[axis];
47 let lo = aabb_min[axis];
48 let hi = aabb_max[axis];
49 if d == 0.0 {
50 if o < lo || o > hi {
51 return None;
52 }
53 continue;
54 }
55 let inv = 1.0 / d;
56 let (t1, t2) = {
57 let a = (lo - o) * inv;
58 let b = (hi - o) * inv;
59 if a <= b {
60 (a, b)
61 } else {
62 (b, a)
63 }
64 };
65 if t1 > t_enter {
66 t_enter = t1;
67 }
68 if t2 < t_exit {
69 t_exit = t2;
70 }
71 if t_enter > t_exit {
72 return None;
73 }
74 }
75 if t_exit < 0.0 {
76 return None;
77 }
78 Some((t_enter, t_exit))
79}
80
81#[must_use]
87pub fn clip_ray_to_xy_aabb(
88 origin_xy: [f32; 2],
89 dir_xy: [f32; 2],
90 aabb_min_xy: [f32; 2],
91 aabb_max_xy: [f32; 2],
92) -> Option<(f32, f32)> {
93 clip_ray_to_aabb(
94 [origin_xy[0], origin_xy[1], 0.0],
95 [dir_xy[0], dir_xy[1], 0.0],
96 [aabb_min_xy[0], aabb_min_xy[1], -1.0],
97 [aabb_max_xy[0], aabb_max_xy[1], 1.0],
98 )
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 const UNIT_BOX_MIN: [f32; 3] = [0.0, 0.0, 0.0];
106 const UNIT_BOX_MAX: [f32; 3] = [1.0, 1.0, 1.0];
107
108 fn approx_eq(a: f32, b: f32) -> bool {
109 (a - b).abs() < 1e-5
110 }
111
112 #[test]
113 fn direct_hit_through_x_face() {
114 let r = clip_ray_to_aabb(
115 [-5.0, 0.5, 0.5],
116 [1.0, 0.0, 0.0],
117 UNIT_BOX_MIN,
118 UNIT_BOX_MAX,
119 )
120 .expect("expected hit");
121 assert!(approx_eq(r.0, 5.0), "t_enter={}", r.0);
122 assert!(approx_eq(r.1, 6.0), "t_exit={}", r.1);
123 }
124
125 #[test]
126 fn parallel_outside_misses() {
127 let r = clip_ray_to_aabb(
128 [-5.0, 5.0, 0.5],
129 [1.0, 0.0, 0.0],
130 UNIT_BOX_MIN,
131 UNIT_BOX_MAX,
132 );
133 assert!(r.is_none());
134 }
135
136 #[test]
137 fn origin_inside_yields_negative_t_enter() {
138 let r = clip_ray_to_aabb([0.5, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
139 .expect("expected hit");
140 assert!(approx_eq(r.0, -0.5), "t_enter={}", r.0);
141 assert!(approx_eq(r.1, 0.5), "t_exit={}", r.1);
142 assert!(r.0 <= 0.0);
143 }
144
145 #[test]
146 fn parallel_inside_does_not_constrain() {
147 let r = clip_ray_to_aabb([0.5, 0.5, 0.5], [0.0, 0.0, 1.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
148 .expect("expected hit");
149 assert!(approx_eq(r.0, -0.5));
150 assert!(approx_eq(r.1, 0.5));
151 }
152
153 #[test]
154 fn ray_pointed_away_from_box_misses() {
155 let r = clip_ray_to_aabb(
156 [-1.0, 0.5, 0.5],
157 [-1.0, 0.0, 0.0],
158 UNIT_BOX_MIN,
159 UNIT_BOX_MAX,
160 );
161 assert!(r.is_none(), "got {r:?}");
162 }
163
164 #[test]
165 fn corner_grazing_hit_is_a_hit() {
166 let r = clip_ray_to_aabb(
167 [-1.0, -1.0, 0.5],
168 [1.0, 1.0, 0.0],
169 UNIT_BOX_MIN,
170 UNIT_BOX_MAX,
171 )
172 .expect("corner graze should still be a hit");
173 assert!(approx_eq(r.0, 1.0));
174 assert!(approx_eq(r.1, 2.0));
175 }
176
177 #[test]
178 fn corner_grazing_miss_by_epsilon() {
179 let r = clip_ray_to_aabb(
180 [-1.0, -1.001, 0.5],
181 [1.0, 1.0, 0.0],
182 UNIT_BOX_MIN,
183 UNIT_BOX_MAX,
184 );
185 assert!(r.is_some());
189 }
190
191 #[test]
192 fn entry_face_origin_returns_zero_t_enter() {
193 let r = clip_ray_to_aabb([0.0, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
194 .expect("expected hit");
195 assert!(approx_eq(r.0, 0.0));
196 assert!(approx_eq(r.1, 1.0));
197 }
198
199 #[test]
200 fn exit_face_origin_zero_length_interval() {
201 let r = clip_ray_to_aabb([1.0, 0.5, 0.5], [1.0, 0.0, 0.0], UNIT_BOX_MIN, UNIT_BOX_MAX)
202 .expect("graze of exit face is technically a hit");
203 assert!(approx_eq(r.0, -1.0));
204 assert!(approx_eq(r.1, 0.0));
205 }
206
207 #[test]
208 fn xy_helper_matches_3d_for_xy_ray() {
209 let three_d = clip_ray_to_aabb(
210 [-3.0, 0.5, 0.5],
211 [1.0, 0.5, 0.0],
212 [0.0, 0.0, -1.0],
213 [10.0, 10.0, 1.0],
214 )
215 .expect("3D hit");
216 let two_d =
217 clip_ray_to_xy_aabb([-3.0, 0.5], [1.0, 0.5], [0.0, 0.0], [10.0, 10.0]).expect("2D hit");
218 assert!(approx_eq(three_d.0, two_d.0));
219 assert!(approx_eq(three_d.1, two_d.1));
220 }
221
222 #[test]
223 fn outside_orbit_camera_into_world() {
224 let vsid = 2048.0;
227 let origin_xy = [vsid + 256.0, vsid * 0.5];
228 let dir_xy = [-1.0, 0.0];
229 let r = clip_ray_to_xy_aabb(origin_xy, dir_xy, [0.0, 0.0], [vsid, vsid])
230 .expect("outside_orbit center ray must hit world AABB");
231 assert!(approx_eq(r.0, 256.0), "t_enter={}", r.0);
232 assert!(approx_eq(r.1, 256.0 + vsid), "t_exit={}", r.1);
233 }
234
235 #[test]
236 fn nan_dir_zero_origin_outside_misses() {
237 let r = clip_ray_to_aabb(
240 [10.0, 0.5, 0.5],
241 [0.0, 1.0, 0.0],
242 UNIT_BOX_MIN,
243 UNIT_BOX_MAX,
244 );
245 assert!(r.is_none());
246 }
247}