1use egui::{Pos2, Rect};
2use glam::DVec3;
3
4use crate::camera::Eye;
5
6pub const NEAR: f64 = 1e-6;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Ray {
10 pub origin: DVec3,
11 pub direction: DVec3,
12}
13
14impl Ray {
15 pub fn at(&self, distance: f64) -> DVec3 {
16 self.origin + self.direction * distance
17 }
18}
19
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct Lens {
22 image: Rect,
23 position: DVec3,
24 right: DVec3,
25 up: DVec3,
26 forward: DVec3,
27 shift: [f64; 2],
28 half: [f64; 2],
29 ortho: bool,
30}
31
32impl Lens {
33 pub fn new(eye: &Eye, image: Rect) -> Lens {
34 let basis = eye.basis();
35 let aspect = f64::from(image.width().max(1.0)) / f64::from(image.height().max(1.0));
36 let (half_y, ortho) = match eye.ortho_height {
37 Some(height) => ((height * 0.5).max(1e-9), true),
38 None => ((eye.fov_y_deg.to_radians() * 0.5).tan().max(1e-9), false),
39 };
40 Lens {
41 image,
42 position: DVec3::from_array(eye.position),
43 right: basis.right,
44 up: basis.up,
45 forward: basis.forward,
46 shift: eye.shift,
47 half: [half_y * aspect, half_y],
48 ortho,
49 }
50 }
51
52 pub fn image(&self) -> Rect {
53 self.image
54 }
55
56 pub fn ortho(&self) -> bool {
57 self.ortho
58 }
59
60 pub fn depth(&self, point: DVec3) -> f64 {
61 (point - self.position).dot(self.forward)
62 }
63
64 pub fn ndc(&self, point: DVec3) -> Option<[f64; 2]> {
65 let offset = point - self.position;
66 let depth = offset.dot(self.forward);
67 let scale = if self.ortho {
68 1.0
69 } else if depth > NEAR {
70 depth
71 } else {
72 return None;
73 };
74 Some([
75 offset.dot(self.right) / (scale * self.half[0]) - self.shift[0],
76 offset.dot(self.up) / (scale * self.half[1]) - self.shift[1],
77 ])
78 }
79
80 pub fn project(&self, point: DVec3) -> Option<Pos2> {
81 let [x, y] = self.ndc(point)?;
82 let (left, top) = (f64::from(self.image.left()), f64::from(self.image.top()));
83 let (width, height) = (
84 f64::from(self.image.width()),
85 f64::from(self.image.height()),
86 );
87 Some(Pos2::new(
88 (left + (x + 1.0) * 0.5 * width) as f32,
89 (top + (1.0 - y) * 0.5 * height) as f32,
90 ))
91 }
92
93 pub fn segment(&self, a: DVec3, b: DVec3) -> Option<[Pos2; 2]> {
94 if self.ortho {
95 return Some([self.project(a)?, self.project(b)?]);
96 }
97 let near = NEAR * 1e3;
98 let (da, db) = (self.depth(a), self.depth(b));
99 if da < near && db < near {
100 return None;
101 }
102 let clip = |inside: DVec3, outside: DVec3, din: f64, dout: f64| {
103 inside + (outside - inside) * ((din - near) / (din - dout))
104 };
105 let (a, b) = match (da < near, db < near) {
106 (true, false) => (clip(b, a, db, da), b),
107 (false, true) => (a, clip(a, b, da, db)),
108 _ => (a, b),
109 };
110 Some([self.project(a)?, self.project(b)?])
111 }
112
113 pub fn ray(&self, at: Pos2) -> Ray {
114 let x = 2.0 * f64::from(at.x - self.image.left()) / f64::from(self.image.width().max(1.0))
115 - 1.0;
116 let y = 1.0
117 - 2.0 * f64::from(at.y - self.image.top()) / f64::from(self.image.height().max(1.0));
118 let across = (x + self.shift[0]) * self.half[0];
119 let rise = (y + self.shift[1]) * self.half[1];
120 if self.ortho {
121 Ray {
122 origin: self.position + self.right * across + self.up * rise,
123 direction: self.forward,
124 }
125 } else {
126 Ray {
127 origin: self.position,
128 direction: (self.forward + self.right * across + self.up * rise).normalize(),
129 }
130 }
131 }
132
133 pub fn metres_per_point(&self, point: DVec3) -> f64 {
134 let tall = if self.ortho {
135 2.0 * self.half[1]
136 } else {
137 2.0 * self.depth(point).max(NEAR) * self.half[1]
138 };
139 tall / f64::from(self.image.height().max(1.0))
140 }
141
142 pub fn toward_eye(&self, point: DVec3) -> DVec3 {
143 if self.ortho {
144 -self.forward
145 } else {
146 (self.position - point)
147 .try_normalize()
148 .unwrap_or(-self.forward)
149 }
150 }
151}
152
153pub fn pixel(image: Rect, at: Pos2, size: [u32; 2]) -> Option<[u32; 2]> {
154 if !image.contains(at) {
155 return None;
156 }
157 let side = |fraction: f32, length: u32| {
158 ((fraction * length as f32).floor().max(0.0) as u32).min(length.saturating_sub(1))
159 };
160 Some([
161 side((at.x - image.left()) / image.width().max(1.0), size[0]),
162 side((at.y - image.top()) / image.height().max(1.0), size[1]),
163 ])
164}
165
166pub fn triangle(ray: &Ray, corners: [DVec3; 3]) -> Option<f64> {
167 let [a, b, c] = corners;
168 let ab = b - a;
169 let ac = c - a;
170 let p = ray.direction.cross(ac);
171 let det = ab.dot(p);
172 if det.abs() < 1e-18 {
173 return None;
174 }
175 let inverse = 1.0 / det;
176 let t = ray.origin - a;
177 let u = t.dot(p) * inverse;
178 if !(0.0..=1.0).contains(&u) {
179 return None;
180 }
181 let q = t.cross(ab);
182 let v = ray.direction.dot(q) * inverse;
183 if v < 0.0 || u + v > 1.0 {
184 return None;
185 }
186 let distance = ac.dot(q) * inverse;
187 (distance > 0.0).then_some(distance)
188}
189
190pub fn slab(ray: &Ray, low: DVec3, high: DVec3) -> Option<f64> {
191 let mut near = f64::NEG_INFINITY;
192 let mut far = f64::INFINITY;
193 for axis in 0..3 {
194 let (origin, direction) = (ray.origin[axis], ray.direction[axis]);
195 if direction.abs() < 1e-300 {
196 if origin < low[axis] || origin > high[axis] {
197 return None;
198 }
199 continue;
200 }
201 let a = (low[axis] - origin) / direction;
202 let b = (high[axis] - origin) / direction;
203 near = near.max(a.min(b));
204 far = far.min(a.max(b));
205 }
206 (near <= far && far >= 0.0).then_some(near.max(0.0))
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use egui::Vec2;
213
214 fn eye(shift: [f64; 2]) -> Eye {
215 Eye {
216 position: [0.0, 0.0, 2.0],
217 target: [0.0, 0.0, 0.0],
218 up: [0.0, 1.0, 0.0],
219 fov_y_deg: 90.0,
220 shift,
221 ortho_height: None,
222 }
223 }
224
225 fn image() -> Rect {
226 Rect::from_min_size(Pos2::new(100.0, 50.0), Vec2::new(400.0, 200.0))
227 }
228
229 fn near(a: Pos2, b: Pos2) -> bool {
230 (a - b).length() < 1e-3
231 }
232
233 #[test]
234 fn projection_lands_on_hand_computed_points() {
235 let lens = Lens::new(&eye([0.0, 0.0]), image());
236 assert!(near(
237 lens.project(DVec3::ZERO).unwrap(),
238 Pos2::new(300.0, 150.0)
239 ));
240 assert!(near(
241 lens.project(DVec3::new(0.0, 2.0, 0.0)).unwrap(),
242 Pos2::new(300.0, 50.0)
243 ));
244 assert!(near(
245 lens.project(DVec3::new(2.0, 0.0, 0.0)).unwrap(),
246 Pos2::new(400.0, 150.0)
247 ));
248 assert!(near(
249 lens.project(DVec3::new(1.0, -1.0, 1.0)).unwrap(),
250 Pos2::new(400.0, 250.0)
251 ));
252 assert_eq!(lens.project(DVec3::new(0.0, 0.0, 3.0)), None);
253 }
254
255 #[test]
256 fn lens_shift_moves_the_picture_as_the_engine_does() {
257 let lens = Lens::new(&eye([0.1, -0.5]), image());
258 let at = lens.project(DVec3::ZERO).unwrap();
259 assert!(
260 near(at, Pos2::new(300.0 - 0.1 * 200.0, 150.0 - 0.5 * 100.0)),
261 "{at:?}"
262 );
263 }
264
265 #[test]
266 fn an_orthographic_lens_projects_flat() {
267 let mut flat = eye([0.25, 0.0]);
268 flat.ortho_height = Some(4.0);
269 let lens = Lens::new(&flat, image());
270 let centre = lens.project(DVec3::new(0.0, 0.0, -5.0)).unwrap();
271 assert!(near(centre, Pos2::new(300.0 - 0.25 * 200.0, 150.0)));
272 let corner = lens.project(DVec3::new(4.0, 2.0, 1.0)).unwrap();
273 assert!(near(corner, Pos2::new(450.0, 50.0)), "{corner:?}");
274 assert!((lens.metres_per_point(DVec3::ZERO) - 0.02).abs() < 1e-12);
275 }
276
277 #[test]
278 fn a_ray_through_a_projected_point_passes_through_it() {
279 for shift in [[0.0, 0.0], [0.1, -0.6395]] {
280 for ortho in [None, Some(3.0)] {
281 let mut view = eye(shift);
282 view.position = [0.4, 0.7, 1.9];
283 view.target = [0.1, 0.05, -0.2];
284 view.ortho_height = ortho;
285 let lens = Lens::new(&view, image());
286 let point = DVec3::new(0.2, -0.1, 0.3);
287 let ray = lens.ray(lens.project(point).unwrap());
288 let along = (point - ray.origin).dot(ray.direction);
289 assert!(
290 (ray.at(along) - point).length() < 1e-4,
291 "{shift:?} {ortho:?}"
292 );
293 }
294 }
295 }
296
297 #[test]
298 fn a_segment_behind_the_eye_is_cut_at_the_near_plane() {
299 let lens = Lens::new(&eye([0.0, 0.0]), image());
300 assert!(
301 lens.segment(DVec3::new(0.0, 0.0, 3.0), DVec3::new(1.0, 0.0, 4.0))
302 .is_none()
303 );
304 let [a, b] = lens
305 .segment(DVec3::new(-1.0, 0.0, 0.0), DVec3::new(-1.0, 0.0, 5.0))
306 .unwrap();
307 assert!(near(a, Pos2::new(250.0, 150.0)), "{a:?}");
308 assert!(b.x < a.x && b.x.is_finite(), "{b:?}");
309 }
310
311 #[test]
312 fn metres_per_point_is_the_view_height_over_the_image() {
313 let lens = Lens::new(&eye([0.0, 0.0]), image());
314 assert!((lens.metres_per_point(DVec3::ZERO) - 4.0 / 200.0).abs() < 1e-12);
315 }
316
317 #[test]
318 fn a_point_maps_to_the_frame_pixel() {
319 assert_eq!(
320 pixel(image(), Pos2::new(300.0, 150.0), [800, 400]),
321 Some([400, 200])
322 );
323 assert_eq!(pixel(image(), Pos2::new(90.0, 150.0), [800, 400]), None);
324 assert_eq!(
325 pixel(image(), Pos2::new(500.0, 250.0), [800, 400]),
326 Some([799, 399])
327 );
328 }
329
330 #[test]
331 fn a_slab_meets_the_box_in_front_only() {
332 let ray = Ray {
333 origin: DVec3::new(0.0, 0.0, 5.0),
334 direction: DVec3::NEG_Z,
335 };
336 let low = DVec3::splat(-1.0);
337 let high = DVec3::splat(1.0);
338 assert_eq!(slab(&ray, low, high), Some(4.0));
339 let away = Ray {
340 direction: DVec3::Z,
341 ..ray
342 };
343 assert_eq!(slab(&away, low, high), None);
344 let inside = Ray {
345 origin: DVec3::ZERO,
346 ..ray
347 };
348 assert_eq!(slab(&inside, low, high), Some(0.0));
349 }
350}