Skip to main content

pfx_editor_viewport/
lens.rs

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}