1use crate::math::{sqrt, tan};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct PickRay {
10 pub origin: [f32; 3],
12 pub dir: [f32; 3],
14}
15
16pub fn screen_ray(
26 view: &[[f32; 4]; 4],
27 cam_pos: [f32; 3],
28 fov_y_radians: f32,
29 viewport: [f32; 2],
30 mouse: [f32; 2],
31) -> Option<PickRay> {
32 let (vw, vh) = (viewport[0], viewport[1]);
33 if vw <= 0.0 || vh <= 0.0 || !vw.is_finite() || !vh.is_finite() {
34 return None;
35 }
36 let ndc_x = 2.0 * mouse[0] / vw - 1.0;
37 let ndc_y = 1.0 - 2.0 * mouse[1] / vh;
38 let tan_half = tan(fov_y_radians * 0.5);
39 if tan_half <= 0.0 || !tan_half.is_finite() {
40 return None;
41 }
42 let aspect = vw / vh;
43
44 let d = [ndc_x * tan_half * aspect, ndc_y * tan_half, -1.0];
46 let world = [
49 view[0][0] * d[0] + view[0][1] * d[1] + view[0][2] * d[2],
50 view[1][0] * d[0] + view[1][1] * d[1] + view[1][2] * d[2],
51 view[2][0] * d[0] + view[2][1] * d[1] + view[2][2] * d[2],
52 ];
53 let len = sqrt(world[0] * world[0] + world[1] * world[1] + world[2] * world[2]);
54 if len <= 0.0 || !len.is_finite() {
55 return None;
56 }
57 Some(PickRay {
58 origin: cam_pos,
59 dir: [world[0] / len, world[1] / len, world[2] / len],
60 })
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct AabbFace {
70 pub t: f32,
72 pub axis: usize,
74 pub sign: f32,
76}
77
78pub fn ray_aabb_face(ray: &PickRay, bb_min: [f32; 3], bb_max: [f32; 3]) -> Option<AabbFace> {
83 if !crate::gfx::lod::bounds_finite(bb_min, bb_max) {
84 return None;
85 }
86 let mut t_enter = f32::NEG_INFINITY;
87 let mut t_exit = f32::INFINITY;
88 let mut enter_axis = None;
89 for i in 0..3 {
90 if ray.dir[i] == 0.0 {
91 if ray.origin[i] < bb_min[i] || ray.origin[i] > bb_max[i] {
94 return None;
95 }
96 continue;
97 }
98 let inv = 1.0 / ray.dir[i];
99 let (t1, t2) = (
100 (bb_min[i] - ray.origin[i]) * inv,
101 (bb_max[i] - ray.origin[i]) * inv,
102 );
103 let (near, far) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
104 if near > t_enter {
105 t_enter = near;
106 enter_axis = Some(i);
107 }
108 t_exit = t_exit.min(far);
109 if t_enter > t_exit {
110 return None;
111 }
112 }
113 if t_exit < 0.0 {
114 return None;
115 }
116 let (axis, sign) = match enter_axis {
117 Some(a) => (a, -ray.dir[a].signum()),
119 None => (0, 0.0),
120 };
121 Some(AabbFace {
122 t: t_enter.max(0.0),
123 axis,
124 sign,
125 })
126}
127
128pub fn ray_aabb(ray: &PickRay, bb_min: [f32; 3], bb_max: [f32; 3]) -> Option<f32> {
133 ray_aabb_face(ray, bb_min, bb_max).map(|f| f.t)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::gfx::camera::view_matrix;
140
141 const VP: [f32; 2] = [1280.0, 720.0];
142 const FOV: f32 = core::f32::consts::FRAC_PI_2;
143
144 fn assert_dir(ray: PickRay, expect: [f32; 3]) {
145 for i in 0..3 {
146 assert!(
147 (ray.dir[i] - expect[i]).abs() < 1e-5,
148 "dir {:?} != {expect:?}",
149 ray.dir
150 );
151 }
152 }
153
154 #[test]
155 fn center_pixel_rays_along_camera_forward() {
156 let view = view_matrix([1.0, 2.0, 3.0], 0.0, 0.0);
158 let ray = screen_ray(&view, [1.0, 2.0, 3.0], FOV, VP, [640.0, 360.0]).unwrap();
159 assert_eq!(ray.origin, [1.0, 2.0, 3.0]);
160 assert_dir(ray, [0.0, 0.0, -1.0]);
161
162 let view = view_matrix([0.0; 3], core::f32::consts::FRAC_PI_2, 0.0);
164 let ray = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 360.0]).unwrap();
165 assert_dir(ray, [-1.0, 0.0, 0.0]);
166
167 let view = view_matrix([0.0; 3], 0.0, core::f32::consts::FRAC_PI_2);
169 let ray = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 360.0]).unwrap();
170 assert_dir(ray, [0.0, 1.0, 0.0]);
171 }
172
173 #[test]
174 fn edge_pixels_span_the_field_of_view() {
175 let view = view_matrix([0.0; 3], 0.0, 0.0);
176 let top = screen_ray(&view, [0.0; 3], FOV, VP, [640.0, 0.0]).unwrap();
178 let vert_tan = top.dir[1] / -top.dir[2];
179 assert!((vert_tan - (FOV * 0.5).tan()).abs() < 1e-4, "{vert_tan}");
180 assert!(top.dir[0].abs() < 1e-6);
181
182 let right = screen_ray(&view, [0.0; 3], FOV, VP, [1280.0, 360.0]).unwrap();
184 let horiz_tan = right.dir[0] / -right.dir[2];
185 let expect = (FOV * 0.5).tan() * (VP[0] / VP[1]);
186 assert!((horiz_tan - expect).abs() < 1e-4, "{horiz_tan} vs {expect}");
187 assert!(right.dir[1].abs() < 1e-6);
188 }
189
190 #[test]
191 fn degenerate_viewport_or_fov_yields_no_ray() {
192 let view = view_matrix([0.0; 3], 0.0, 0.0);
193 assert_eq!(
194 screen_ray(&view, [0.0; 3], FOV, [0.0, 720.0], [0.0; 2]),
195 None
196 );
197 assert_eq!(
198 screen_ray(&view, [0.0; 3], FOV, [1280.0, 0.0], [0.0; 2]),
199 None
200 );
201 assert_eq!(screen_ray(&view, [0.0; 3], 0.0, VP, [0.0; 2]), None);
202 }
203
204 fn ray(origin: [f32; 3], dir: [f32; 3]) -> PickRay {
205 PickRay { origin, dir }
206 }
207
208 #[test]
209 fn ray_hits_a_box_at_the_entry_distance() {
210 let r = ray([0.0, 0.0, 5.0], [0.0, 0.0, -1.0]);
211 let t = ray_aabb(&r, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]).unwrap();
212 assert!((t - 4.0).abs() < 1e-6, "{t}");
213 }
214
215 #[test]
216 fn ray_misses_beside_behind_and_non_finite_boxes() {
217 let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
218 assert_eq!(
220 ray_aabb(&ray([0.0, 3.0, 5.0], [0.0, 0.0, -1.0]), mn, mx),
221 None
222 );
223 assert_eq!(
225 ray_aabb(&ray([0.0, 0.0, 5.0], [0.0, 0.0, 1.0]), mn, mx),
226 None
227 );
228 assert_eq!(
230 ray_aabb(
231 &ray([0.0; 3], [0.0, 0.0, -1.0]),
232 [f32::NAN; 3],
233 [f32::NAN; 3]
234 ),
235 None
236 );
237 }
238
239 #[test]
240 fn ray_inside_a_box_hits_at_zero() {
241 let t = ray_aabb(
242 &ray([0.0; 3], [0.0, 1.0, 0.0]),
243 [-1.0, -1.0, -1.0],
244 [1.0, 1.0, 1.0],
245 )
246 .unwrap();
247 assert_eq!(t, 0.0);
248 }
249
250 #[test]
251 fn entered_face_reports_axis_and_outward_sign() {
252 let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
253 let f = ray_aabb_face(&ray([0.0, 5.0, 0.0], [0.0, -1.0, 0.0]), mn, mx).unwrap();
255 assert_eq!((f.axis, f.sign), (1, 1.0));
256 assert!((f.t - 4.0).abs() < 1e-6);
257 let f = ray_aabb_face(&ray([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]), mn, mx).unwrap();
259 assert_eq!((f.axis, f.sign), (0, -1.0));
260 let f = ray_aabb_face(&ray([0.0, 0.5, 5.0], [0.0, -0.0995, -0.995]), mn, mx).unwrap();
262 assert_eq!((f.axis, f.sign), (2, 1.0));
263 let f = ray_aabb_face(&ray([0.0; 3], [0.0; 3]), mn, mx).unwrap();
265 assert_eq!((f.t, f.sign), (0.0, 0.0));
266 }
267
268 #[test]
269 fn axis_parallel_rays_respect_the_perpendicular_slabs() {
270 let (mn, mx) = ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
271 assert!(ray_aabb(&ray([0.5, -5.0, 0.5], [0.0, 1.0, 0.0]), mn, mx).is_some());
273 assert_eq!(
275 ray_aabb(&ray([2.0, -5.0, 0.5], [0.0, 1.0, 0.0]), mn, mx),
276 None
277 );
278 assert!(ray_aabb(&ray([1.0, -5.0, 0.0], [0.0, 1.0, 0.0]), mn, mx).is_some());
280 }
281}