maplibre/render/
view_state.rs

1use std::{
2    f64,
3    ops::{Deref, DerefMut},
4};
5
6use cgmath::{prelude::*, *};
7
8use crate::{
9    coords::{ViewRegion, WorldCoords, Zoom, ZoomLevel},
10    render::camera::{
11        Camera, EdgeInsets, InvertedViewProjection, Perspective, ViewProjection, FLIP_Y,
12        OPENGL_TO_WGPU_MATRIX,
13    },
14    util::{
15        math::{bounds_from_points, Aabb2, Aabb3, Plane},
16        ChangeObserver,
17    },
18    window::{LogicalSize, PhysicalSize},
19};
20
21const VIEW_REGION_PADDING: i32 = 1;
22const MAX_N_TILES: usize = 512;
23
24pub enum ViewStatePadding {
25    // This is helpful for loading a set of tiles.
26    Loose,
27    // This is helpful for rendering a set of tiles.
28    Tight,
29}
30
31#[derive(Clone)] // TODO: Remove
32pub struct ViewState {
33    zoom: ChangeObserver<Zoom>,
34    camera: ChangeObserver<Camera>,
35    perspective: Perspective,
36
37    width: f64,
38    height: f64,
39    edge_insets: EdgeInsets,
40}
41
42impl ViewState {
43    pub fn new<F: Into<Rad<f64>>, P: Into<Deg<f64>>>(
44        window_size: PhysicalSize,
45        position: WorldCoords,
46        zoom: Zoom,
47        pitch: P,
48        fovy: F,
49    ) -> Self {
50        let camera = Camera::new((position.x, position.y), Deg(0.0), pitch.into());
51
52        let perspective = Perspective::new(fovy);
53
54        Self {
55            zoom: ChangeObserver::new(zoom),
56            camera: ChangeObserver::new(camera),
57            perspective,
58            width: window_size.width() as f64,
59            height: window_size.height() as f64,
60            edge_insets: EdgeInsets {
61                top: 0.0,
62                bottom: 0.0,
63                left: 0.0,
64                right: 0.0,
65            },
66        }
67    }
68    pub fn set_edge_insets(&mut self, edge_insets: EdgeInsets) {
69        self.edge_insets = edge_insets;
70    }
71
72    pub fn edge_insets(&self) -> &EdgeInsets {
73        &self.edge_insets
74    }
75
76    pub fn resize(&mut self, size: LogicalSize) {
77        self.width = size.width() as f64;
78        self.height = size.height() as f64;
79    }
80
81    pub fn create_view_region(
82        &self,
83        visible_level: ZoomLevel,
84        padding: ViewStatePadding,
85    ) -> Option<ViewRegion> {
86        self.view_region_bounding_box(&self.view_projection().invert())
87            .map(|bounding_box| {
88                ViewRegion::new(
89                    bounding_box,
90                    match padding {
91                        ViewStatePadding::Loose => VIEW_REGION_PADDING,
92                        ViewStatePadding::Tight => 0,
93                    },
94                    MAX_N_TILES,
95                    *self.zoom,
96                    visible_level,
97                )
98            })
99    }
100
101    fn get_intersection_time(
102        ray_origin: Vector3<f64>,
103        ray_direction: Vector3<f64>,
104        plane_origin: Vector3<f64>,
105        plane_normal: Vector3<f64>,
106    ) -> f64 {
107        let m = plane_origin - ray_origin;
108        let distance = (m).dot(plane_normal);
109
110        let approach_speed = ray_direction.dot(plane_normal);
111
112        // Returns an infinity if the ray is
113        // parallel to the plane and never intersects,
114        // or NaN if the ray is in the plane
115        // and intersects everywhere.
116        return distance / approach_speed;
117
118        // Otherwise returns t such that
119        // ray_origin + t * rayDirection
120        // is in the plane, to within rounding error.
121    }
122
123    fn furthest_distance(&self, camera_height: f64, center_offset: Point2<f64>) -> f64 {
124        let perspective = &self.perspective;
125        let width = self.width;
126        let height = self.height;
127        let camera = self.camera.position();
128
129        let y = perspective.y_tan();
130        let x = perspective.x_tan(width, height);
131        let offset_x = perspective.offset_x(center_offset, width);
132        let offset_y = perspective.offset_y(center_offset, height);
133
134        let rotation = Matrix4::from_angle_x(self.camera.get_pitch())
135            * Matrix4::from_angle_y(self.camera.get_yaw())
136            * Matrix4::from_angle_z(self.camera.get_roll());
137
138        let rays = [
139            Vector3::new(x * (1.0 - offset_x), y * (1.0 - offset_y), 1.0),
140            Vector3::new(x * (-1.0 - offset_x), y * (1.0 - offset_y), 1.0),
141            Vector3::new(x * (1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
142            Vector3::new(x * (-1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
143        ];
144        let ray_origin = Vector3::new(-camera.x, -camera.y, -camera_height);
145
146        let plane_origin = Vector3::new(-camera.x, -camera.y, 0.0);
147        let plane_normal = (rotation * Vector4::new(0.0, 0.0, 1.0, 1.0)).truncate();
148
149        rays.iter()
150            .map(|ray| Self::get_intersection_time(ray_origin, *ray, plane_origin, plane_normal))
151            .fold(0. / 0., f64::max)
152    }
153
154    pub fn camera_to_center_distance(&self) -> f64 {
155        let height = self.height;
156
157        let fovy = self.perspective.fovy();
158        let half_fovy = fovy / 2.0;
159
160        // Camera height, such that given a certain field-of-view, exactly height/2 are visible on ground.
161        let camera_to_center_distance = (height / 2.0) / (half_fovy.tan()); // We are using `height` here because this is the FOV in y direction (fovy).
162        camera_to_center_distance
163    }
164
165    /// This function matches how maplibre-gl-js implements perspective and cameras at the time
166    /// of the mapbox -> maplibre fork: [src/geo/transform.ts#L680](https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L680)
167    #[tracing::instrument(skip_all)]
168    pub fn view_projection(&self) -> ViewProjection {
169        let width = self.width;
170        let height = self.height;
171
172        let center = self.edge_insets.center(width, height);
173        // Offset between wanted center and usual/normal center
174        let center_offset = center - Vector2::new(width, height) / 2.0;
175
176        let camera_to_center_distance = self.camera_to_center_distance();
177
178        let camera_matrix = self.camera.calc_matrix(camera_to_center_distance);
179
180        // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthest_distance`
181        let furthest = self.furthest_distance(camera_to_center_distance, center_offset);
182        let far_z = furthest * 1.01;
183
184        let near_z = height / 50.0;
185
186        let perspective =
187            self.perspective
188                .calc_matrix_with_center(width, height, near_z, far_z, center_offset);
189
190        // Apply camera and move camera away from ground
191        let view_projection = perspective * camera_matrix;
192
193        ViewProjection(FLIP_Y * OPENGL_TO_WGPU_MATRIX * view_projection)
194    }
195
196    pub fn zoom(&self) -> Zoom {
197        *self.zoom
198    }
199
200    pub fn did_zoom_change(&self) -> bool {
201        self.zoom.did_change(0.05)
202    }
203
204    pub fn update_zoom(&mut self, new_zoom: Zoom) {
205        *self.zoom = new_zoom;
206        log::info!("zoom: {new_zoom}");
207    }
208
209    pub fn camera(&self) -> &Camera {
210        self.camera.deref()
211    }
212
213    pub fn camera_mut(&mut self) -> &mut Camera {
214        self.camera.deref_mut()
215    }
216
217    pub fn did_camera_change(&self) -> bool {
218        self.camera.did_change(0.05)
219    }
220
221    pub fn update_references(&mut self) {
222        self.camera.update_reference();
223        self.zoom.update_reference();
224    }
225
226    /// A transform which can be used to transform between clip and window space.
227    /// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/direct3d9/viewports-and-clipping#viewport-rectangle) (Direct3D).
228    pub(crate) fn clip_to_window_transform(&self) -> Matrix4<f64> {
229        let min_depth = 0.0;
230        let max_depth = 1.0;
231        let x = 0.0;
232        let y = 0.0;
233        let ox = x + self.width / 2.0;
234        let oy = y + self.height / 2.0;
235        let oz = min_depth;
236        let pz = max_depth - min_depth;
237        Matrix4::from_cols(
238            Vector4::new(self.width / 2.0, 0.0, 0.0, 0.0),
239            Vector4::new(0.0, -self.height / 2.0, 0.0, 0.0),
240            Vector4::new(0.0, 0.0, pz, 0.0),
241            Vector4::new(ox, oy, oz, 1.0),
242        )
243    }
244
245    /// Transforms coordinates in clip space to window coordinates.
246    ///
247    /// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline) (Direct3D).
248    pub(crate) fn clip_to_window(&self, clip: &Vector4<f64>) -> Vector4<f64> {
249        #[rustfmt::skip]
250        let ndc = Vector4::new(
251            clip.x / clip.w,
252            clip.y / clip.w,
253            clip.z / clip.w,
254            1.0
255        );
256
257        self.clip_to_window_transform() * ndc
258    }
259
260    /// The way how maplibre converts from clip to window space: https://github.com/maplibre/maplibre-native/blob/4add9ead08799577a37c465b8cb1266676b6c41e/src/mbgl/text/collision_index.cpp/#L437-L438
261    pub(crate) fn clip_to_window_maplibre(&self, clip: &Vector4<f64>) -> Vector4<f64> {
262        assert_eq!(clip.z, 0.0);
263        return Vector4::new(
264            ((clip.x / clip.w + 1.) / 2.) * self.width,
265            ((-clip.y / clip.w + 1.) / 2.) * self.height,
266            0.0,
267            1.0,
268        );
269    }
270
271    /// Alternative implementation to `clip_to_window`. Transforms coordinates in clip space to
272    /// window coordinates.
273    ///
274    /// Adopted from [here](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkViewport.html)
275    /// and [here](https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/) (Vulkan).
276    fn clip_to_window_vulkan(&self, clip: &Vector4<f64>) -> Vector3<f64> {
277        #[rustfmt::skip]
278            let ndc = Vector4::new(
279            clip.x / clip.w,
280            clip.y / clip.w,
281            clip.z / clip.w,
282            1.0
283        );
284
285        let min_depth = 0.0;
286        let max_depth = 1.0;
287
288        let x = 0.0;
289        let y = 0.0;
290        let ox = x + self.width / 2.0;
291        let oy = y + self.height / 2.0;
292        let oz = min_depth;
293        let px = self.width;
294        let py = self.height;
295        let pz = max_depth - min_depth;
296        let xd = ndc.x;
297        let yd = ndc.y;
298        let zd = ndc.z;
299        Vector3::new(px / 2.0 * xd + ox, py / 2.0 * yd + oy, pz * zd + oz)
300    }
301
302    /// Order of transformations reversed: https://computergraphics.stackexchange.com/questions/6087/screen-space-coordinates-to-eye-space-conversion/6093
303    /// `w` is lost.
304    ///
305    /// OpenGL explanation: https://www.khronos.org/opengl/wiki/Compute_eye_space_from_window_space#From_window_to_ndc
306    fn window_to_world(
307        &self,
308        window: &Vector3<f64>,
309        inverted_view_proj: &InvertedViewProjection,
310    ) -> Vector3<f64> {
311        #[rustfmt::skip]
312            let fixed_window = Vector4::new(
313            window.x,
314            window.y,
315            window.z,
316            1.0
317        );
318
319        let ndc = self.clip_to_window_transform().invert().unwrap() * fixed_window;
320        let unprojected = inverted_view_proj.project(ndc);
321
322        Vector3::new(
323            unprojected.x / unprojected.w,
324            unprojected.y / unprojected.w,
325            unprojected.z / unprojected.w,
326        )
327    }
328
329    /// Alternative implementation to `window_to_world`
330    ///
331    /// Adopted from [here](https://docs.rs/nalgebra-glm/latest/src/nalgebra_glm/ext/matrix_projection.rs.html#164-181).
332    fn window_to_world_nalgebra(
333        window: &Vector3<f64>,
334        inverted_view_proj: &InvertedViewProjection,
335        width: f64,
336        height: f64,
337    ) -> Vector3<f64> {
338        let pt = Vector4::new(
339            2.0 * (window.x - 0.0) / width - 1.0,
340            2.0 * (height - window.y - 0.0) / height - 1.0,
341            window.z,
342            1.0,
343        );
344        let unprojected = inverted_view_proj.project(pt);
345
346        Vector3::new(
347            unprojected.x / unprojected.w,
348            unprojected.y / unprojected.w,
349            unprojected.z / unprojected.w,
350        )
351    }
352
353    /// Gets the world coordinates for the specified `window` coordinates on the `z=0` plane.
354    pub fn window_to_world_at_ground(
355        &self,
356        window: &Vector2<f64>,
357        inverted_view_proj: &InvertedViewProjection,
358        bound: bool,
359    ) -> Option<Vector2<f64>> {
360        let near_world =
361            self.window_to_world(&Vector3::new(window.x, window.y, 0.0), inverted_view_proj);
362
363        let far_world =
364            self.window_to_world(&Vector3::new(window.x, window.y, 1.0), inverted_view_proj);
365
366        // for z = 0 in world coordinates
367        // Idea comes from: https://dondi.lmu.build/share/cg/unproject-explained.pdf
368        let u = -near_world.z / (far_world.z - near_world.z);
369        if !bound || (0.0..=1.01).contains(&u) {
370            let result = near_world + u * (far_world - near_world);
371            Some(Vector2::new(result.x, result.y))
372        } else {
373            None
374        }
375    }
376
377    /// Calculates an [`Aabb2`] bounding box which contains at least the visible area on the `z=0`
378    /// plane. One can think of it as being the bounding box of the geometry which forms the
379    /// intersection between the viewing frustum and the `z=0` plane.
380    ///
381    /// This implementation works in the world 3D space. It casts rays from the corners of the
382    /// window to calculate intersections points with the `z=0` plane. Then a bounding box is
383    /// calculated.
384    ///
385    /// *Note:* It is possible that no such bounding box exists. This is the case if the `z=0` plane
386    /// is not in view.
387    pub fn view_region_bounding_box(
388        &self,
389        inverted_view_proj: &InvertedViewProjection,
390    ) -> Option<Aabb2<f64>> {
391        let screen_bounding_box = [
392            Vector2::new(0.0, 0.0),
393            Vector2::new(self.width, 0.0),
394            Vector2::new(self.width, self.height),
395            Vector2::new(0.0, self.height),
396        ]
397        .map(|point| self.window_to_world_at_ground(&point, inverted_view_proj, false));
398
399        let (min, max) = bounds_from_points(
400            screen_bounding_box
401                .into_iter()
402                .flatten()
403                .map(|point| [point.x, point.y]),
404        )?;
405
406        Some(Aabb2::new(Point2::from(min), Point2::from(max)))
407    }
408    /// An alternative implementation for `view_region_bounding_box`.
409    ///
410    /// This implementation works in the NDC space. We are creating a plane in the world 3D space.
411    /// Then we are transforming it to the NDC space. In NDC space it is easy to calculate
412    /// the intersection points between an Aabb3 and a plane. The resulting Aabb2 is returned.
413    pub fn view_region_bounding_box_ndc(&self) -> Option<Aabb2<f64>> {
414        let view_proj = self.view_projection();
415        let a = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0));
416        let b = view_proj.project(Vector4::new(1.0, 0.0, 0.0, 1.0));
417        let c = view_proj.project(Vector4::new(1.0, 1.0, 0.0, 1.0));
418
419        let a_ndc = self.clip_to_window(&a).truncate();
420        let b_ndc = self.clip_to_window(&b).truncate();
421        let c_ndc = self.clip_to_window(&c).truncate();
422        let to_ndc = Vector3::new(1.0 / self.width, 1.0 / self.height, 1.0);
423        let plane: Plane<f64> = Plane::from_points(
424            Point3::from_vec(a_ndc.mul_element_wise(to_ndc)),
425            Point3::from_vec(b_ndc.mul_element_wise(to_ndc)),
426            Point3::from_vec(c_ndc.mul_element_wise(to_ndc)),
427        )?;
428
429        let points = plane.intersection_points_aabb3(&Aabb3::new(
430            Point3::new(0.0, 0.0, 0.0),
431            Point3::new(1.0, 1.0, 1.0),
432        ));
433
434        let inverted_view_proj = view_proj.invert();
435
436        let from_ndc = Vector3::new(self.width, self.height, 1.0);
437        let vec = points
438            .iter()
439            .map(|point| {
440                self.window_to_world(&point.mul_element_wise(from_ndc), &inverted_view_proj)
441            })
442            .collect::<Vec<_>>();
443
444        let min_x = vec
445            .iter()
446            .map(|point| point.x)
447            .min_by(|a, b| a.partial_cmp(b).unwrap())?;
448        let min_y = vec
449            .iter()
450            .map(|point| point.y)
451            .min_by(|a, b| a.partial_cmp(b).unwrap())?;
452        let max_x = vec
453            .iter()
454            .map(|point| point.x)
455            .max_by(|a, b| a.partial_cmp(b).unwrap())?;
456        let max_y = vec
457            .iter()
458            .map(|point| point.y)
459            .max_by(|a, b| a.partial_cmp(b).unwrap())?;
460        Some(Aabb2::new(
461            Point2::new(min_x, min_y),
462            Point2::new(max_x, max_y),
463        ))
464    }
465    pub fn height(&self) -> f64 {
466        self.height
467    }
468    pub fn width(&self) -> f64 {
469        self.width
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use cgmath::{Deg, Matrix4, Vector2, Vector4};
476
477    use crate::{
478        coords::{WorldCoords, Zoom},
479        render::view_state::ViewState,
480        window::PhysicalSize,
481    };
482
483    #[test]
484    fn conform_transformation() {
485        let fov = Deg(60.0);
486        let mut state = ViewState::new(
487            PhysicalSize::new(800, 600).unwrap(),
488            WorldCoords::at_ground(0.0, 0.0),
489            Zoom::new(10.0),
490            Deg(0.0),
491            fov,
492        );
493
494        //state.furthest_distance(state.camera_to_center_distance(), Point2::new(0.0, 0.0));
495
496        let projection = state.view_projection().invert();
497
498        let bottom_left = state
499            .window_to_world_at_ground(&Vector2::new(0.0, 0.0), &projection, true)
500            .unwrap();
501        println!("bottom left on ground {:?}", bottom_left);
502        let top_right = state
503            .window_to_world_at_ground(&Vector2::new(state.width, state.height), &projection, true)
504            .unwrap();
505        println!("top right on ground {:?}", top_right);
506
507        let mut rotated = Matrix4::from_angle_x(Deg(-30.0))
508            * Vector4::new(bottom_left.x, bottom_left.y, 0.0, 0.0);
509
510        println!("bottom left rotated around x axis {:?}", rotated);
511
512        rotated = Matrix4::from_angle_y(Deg(-30.0)) * rotated;
513
514        println!("bottom left rotated around x and y axis {:?}", rotated);
515
516        state.camera.set_pitch(Deg(30.0));
517        //state.camera.set_yaw(Deg(-30.0));
518
519        // TODO: verify far distance plane calculation
520    }
521}