mirror of
https://github.com/maplibre/maplibre-rs.git
synced 2025-12-08 19:05:57 +00:00
Transform improvements (#233)
This fundamentally reworks how the far distance plane is calculated. First I wanted to implement it like in maplibre-gl-js. Though that appraoch did not allow yaw AND pitch in both directions. It only allowed pitch in a single direction (only 90°, not the full 180°). The new approach is dicussed here: https://gamedev.stackexchange.com/questions/207328/calculation-of-far-distance-plane-based-on-yaw-and-pitch-for-a-map-renderer It works in all directions and should also be way faster to calculate. This PR also fixes the amount of tiles that are whown for the current zoom. If calculates the appropriate zoom level based on the current zoom and takes into account the resolution of tiles. * Introduce tile size based zoom level * Add debug handler for testing insets * Adjust handlers * Set fov to that of maplibre-gl-js * Move view projection calculation to view_state * Increase thickness of debug lines * Increase DEFAULT_TILE_VIEW_PATTERN_SIZE * Fix mdbook script * Enable debug plugin only in debug-like modes * Move view_state to render module * Remove 3D terrain specific code for transform * Remove overwriting z * Start to change camera movement * Change camera transformation to rotate around (x,y,0) instead of (x,y,camera_height) * Add all insets to debug handler * First completely working version which can yaw, pitch and roll * Update distance calculation * Set max/min pitch/yaw to 30 * Test stackoverflow approach * Fix far z with moved center * Use default instead of ::new
This commit is contained in:
parent
db2acb1fce
commit
fa2c436f74
2
.idea/runConfigurations/Serve_Book.xml
generated
2
.idea/runConfigurations/Serve_Book.xml
generated
@ -7,7 +7,7 @@
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/env" />
|
||||
<option name="INTERPRETER_PATH" value="/usr/bin/env" />
|
||||
<option name="INTERPRETER_OPTIONS" value="just --justfile" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="false" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
||||
|
||||
3
justfile
3
justfile
@ -187,6 +187,9 @@ xcodebuild-xcframework:
|
||||
echo "$framework_args" | xargs xcodebuild -create-xcframework -output "$XC_FRAMEWORK_PATH"
|
||||
cat "$XC_FRAMEWORK_PATH/Info.plist"
|
||||
|
||||
book-serve:
|
||||
cd docs && ./generate-summary.sh && mdbook serve
|
||||
|
||||
# language=bash
|
||||
extract-tiles:
|
||||
#!/usr/bin/env bash
|
||||
|
||||
107
maplibre-winit/src/input/camera_handler.rs
Normal file
107
maplibre-winit/src/input/camera_handler.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::{Deg, MetricSpace, Rad, Vector2};
|
||||
use maplibre::context::MapContext;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
pub struct CameraHandler {
|
||||
window_position: Option<Vector2<f64>>,
|
||||
start_window_position: Option<Vector2<f64>>,
|
||||
is_active: bool,
|
||||
is_middle: bool,
|
||||
|
||||
start_delta_pitch: Option<Rad<f64>>,
|
||||
start_delta_roll: Option<Rad<f64>>,
|
||||
start_delta_yaw: Option<Rad<f64>>,
|
||||
|
||||
sensitivity: f64,
|
||||
}
|
||||
|
||||
impl UpdateState for CameraHandler {
|
||||
fn update_state(&mut self, MapContext { view_state, .. }: &mut MapContext, dt: Duration) {
|
||||
if !self.is_active {
|
||||
return;
|
||||
}
|
||||
|
||||
if let (Some(window_position), Some(start_window_position)) =
|
||||
(self.window_position, self.start_window_position)
|
||||
{
|
||||
let camera = view_state.camera_mut();
|
||||
|
||||
if self.is_middle {
|
||||
let delta: Rad<_> = (Deg(0.001 * self.sensitivity)
|
||||
* start_window_position.distance(window_position))
|
||||
.into();
|
||||
|
||||
let previous = *self.start_delta_roll.get_or_insert(camera.get_roll());
|
||||
camera.set_roll(previous + delta);
|
||||
} else {
|
||||
let delta: Rad<_> = (Deg(0.001 * self.sensitivity)
|
||||
* (start_window_position.x - window_position.x))
|
||||
.into();
|
||||
let previous = *self.start_delta_yaw.get_or_insert(camera.get_yaw());
|
||||
camera.set_yaw(previous + delta);
|
||||
|
||||
let delta: Rad<_> = (Deg(0.001 * self.sensitivity)
|
||||
* (start_window_position.y - window_position.y))
|
||||
.into();
|
||||
let previous = *self.start_delta_pitch.get_or_insert(camera.get_pitch());
|
||||
camera.set_pitch(previous + delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraHandler {
|
||||
pub fn new(sensitivity: f64) -> Self {
|
||||
Self {
|
||||
window_position: None,
|
||||
start_window_position: None,
|
||||
is_active: false,
|
||||
is_middle: false,
|
||||
start_delta_pitch: None,
|
||||
start_delta_roll: None,
|
||||
start_delta_yaw: None,
|
||||
sensitivity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_window_position(&mut self, window_position: &Vector2<f64>, touch: bool) -> bool {
|
||||
if !self.is_active && !touch {
|
||||
self.start_window_position = Some(*window_position);
|
||||
self.window_position = Some(*window_position);
|
||||
} else {
|
||||
self.window_position = Some(*window_position);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn process_mouse_key_press(&mut self, key: &MouseButton, state: &ElementState) -> bool {
|
||||
if *state == ElementState::Pressed {
|
||||
// currently panning or starting to pan
|
||||
match *key {
|
||||
MouseButton::Right => {
|
||||
self.is_active = true;
|
||||
}
|
||||
MouseButton::Middle => {
|
||||
self.is_active = true;
|
||||
self.is_middle = true;
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
} else {
|
||||
// finished panning
|
||||
self.is_active = false;
|
||||
self.is_middle = false;
|
||||
self.start_window_position = None;
|
||||
self.window_position = None;
|
||||
self.start_delta_yaw = None;
|
||||
self.start_delta_pitch = None;
|
||||
self.start_delta_roll = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
68
maplibre-winit/src/input/debug_handler.rs
Normal file
68
maplibre-winit/src/input/debug_handler.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use maplibre::context::MapContext;
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DebugHandler {
|
||||
top_delta: f64,
|
||||
bottom_delta: f64,
|
||||
left_delta: f64,
|
||||
right_delta: f64,
|
||||
}
|
||||
|
||||
impl UpdateState for DebugHandler {
|
||||
fn update_state(&mut self, MapContext { view_state, .. }: &mut MapContext, dt: Duration) {
|
||||
let dt = dt.as_secs_f64() * 10.0;
|
||||
|
||||
let top_delta = self.top_delta * dt;
|
||||
let bottom_delta = self.bottom_delta * dt;
|
||||
let left_delta = self.left_delta * dt;
|
||||
let right_delta = self.right_delta * dt;
|
||||
|
||||
let mut edge_insets = *view_state.edge_insets();
|
||||
edge_insets.top += top_delta;
|
||||
edge_insets.bottom += bottom_delta;
|
||||
edge_insets.left += left_delta;
|
||||
edge_insets.right += right_delta;
|
||||
view_state.set_edge_insets(edge_insets);
|
||||
self.top_delta -= top_delta;
|
||||
self.bottom_delta -= bottom_delta;
|
||||
self.right_delta -= right_delta;
|
||||
self.left_delta -= left_delta;
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugHandler {
|
||||
pub fn process_key_press(
|
||||
&mut self,
|
||||
key: winit::event::VirtualKeyCode,
|
||||
state: winit::event::ElementState,
|
||||
) -> bool {
|
||||
let amount = if state == winit::event::ElementState::Pressed {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
match key {
|
||||
winit::event::VirtualKeyCode::V => {
|
||||
self.left_delta += amount;
|
||||
true
|
||||
}
|
||||
winit::event::VirtualKeyCode::B => {
|
||||
self.top_delta += amount;
|
||||
true
|
||||
}
|
||||
winit::event::VirtualKeyCode::N => {
|
||||
self.bottom_delta += amount;
|
||||
true
|
||||
}
|
||||
winit::event::VirtualKeyCode::M => {
|
||||
self.right_delta += amount;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,24 +7,27 @@ use maplibre::context::MapContext;
|
||||
use winit::event::{DeviceEvent, KeyboardInput, TouchPhase, WindowEvent};
|
||||
|
||||
use crate::input::{
|
||||
pan_handler::PanHandler, pinch_handler::PinchHandler, query_handler::QueryHandler,
|
||||
shift_handler::ShiftHandler, tilt_handler::TiltHandler, zoom_handler::ZoomHandler,
|
||||
camera_handler::CameraHandler, debug_handler::DebugHandler, pan_handler::PanHandler,
|
||||
pinch_handler::PinchHandler, query_handler::QueryHandler, shift_handler::ShiftHandler,
|
||||
zoom_handler::ZoomHandler,
|
||||
};
|
||||
|
||||
mod camera_handler;
|
||||
mod debug_handler;
|
||||
mod pan_handler;
|
||||
mod pinch_handler;
|
||||
mod query_handler;
|
||||
mod shift_handler;
|
||||
mod tilt_handler;
|
||||
mod zoom_handler;
|
||||
|
||||
pub struct InputController {
|
||||
pinch_handler: PinchHandler,
|
||||
pan_handler: PanHandler,
|
||||
zoom_handler: ZoomHandler,
|
||||
tilt_handler: TiltHandler,
|
||||
camera_handler: CameraHandler,
|
||||
shift_handler: ShiftHandler,
|
||||
query_handler: QueryHandler,
|
||||
debug_handler: DebugHandler,
|
||||
}
|
||||
|
||||
impl InputController {
|
||||
@ -40,12 +43,13 @@ impl InputController {
|
||||
///
|
||||
pub fn new(speed: f64, sensitivity: f64, zoom_sensitivity: f64) -> Self {
|
||||
Self {
|
||||
pinch_handler: PinchHandler::new(),
|
||||
pan_handler: PanHandler::new(),
|
||||
pinch_handler: PinchHandler::default(),
|
||||
pan_handler: PanHandler::default(),
|
||||
zoom_handler: ZoomHandler::new(zoom_sensitivity),
|
||||
tilt_handler: TiltHandler::new(speed, sensitivity),
|
||||
camera_handler: CameraHandler::new(sensitivity),
|
||||
shift_handler: ShiftHandler::new(speed, sensitivity),
|
||||
query_handler: QueryHandler::new(),
|
||||
debug_handler: DebugHandler::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,12 +63,12 @@ impl InputController {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let position: (f64, f64) = position.to_owned().into();
|
||||
self.pan_handler
|
||||
.process_window_position(&Vector2::from(position), false);
|
||||
self.query_handler
|
||||
.process_window_position(&Vector2::from(position), false);
|
||||
self.zoom_handler
|
||||
.process_window_position(&Vector2::from(position), false);
|
||||
let position = Vector2::from(position);
|
||||
self.pan_handler.process_window_position(&position, false);
|
||||
self.query_handler.process_window_position(&position, false);
|
||||
self.zoom_handler.process_window_position(&position, false);
|
||||
self.camera_handler
|
||||
.process_window_position(&position, false);
|
||||
true
|
||||
}
|
||||
WindowEvent::KeyboardInput {
|
||||
@ -76,15 +80,9 @@ impl InputController {
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if !self.shift_handler.process_key_press(*key, *state) {
|
||||
if !self.tilt_handler.process_key_press(*key, *state) {
|
||||
self.zoom_handler.process_key_press(*key, *state)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
self.shift_handler.process_key_press(*key, *state)
|
||||
|| self.debug_handler.process_key_press(*key, *state)
|
||||
|| self.zoom_handler.process_key_press(*key, *state)
|
||||
}
|
||||
WindowEvent::Touch(touch) => match touch.phase {
|
||||
TouchPhase::Started => {
|
||||
@ -107,6 +105,8 @@ impl InputController {
|
||||
.process_window_position(&Vector2::from(position), true);
|
||||
self.zoom_handler
|
||||
.process_window_position(&Vector2::from(position), true);
|
||||
self.camera_handler
|
||||
.process_window_position(&Vector2::from(position), true);
|
||||
true
|
||||
}
|
||||
TouchPhase::Cancelled => false,
|
||||
@ -118,7 +118,9 @@ impl InputController {
|
||||
}
|
||||
WindowEvent::MouseInput { button, state, .. } => {
|
||||
self.pan_handler.process_mouse_key_press(button, state);
|
||||
self.query_handler.process_mouse_key_press(button, state)
|
||||
self.query_handler.process_mouse_key_press(button, state);
|
||||
self.camera_handler.process_mouse_key_press(button, state);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
@ -134,8 +136,9 @@ impl UpdateState for InputController {
|
||||
self.pan_handler.update_state(map_context, dt);
|
||||
self.pinch_handler.update_state(map_context, dt);
|
||||
self.zoom_handler.update_state(map_context, dt);
|
||||
self.tilt_handler.update_state(map_context, dt);
|
||||
self.camera_handler.update_state(map_context, dt);
|
||||
self.shift_handler.update_state(map_context, dt);
|
||||
self.query_handler.update_state(map_context, dt);
|
||||
self.debug_handler.update_state(map_context, dt);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::{EuclideanSpace, Point3, Vector2, Vector3, Zero};
|
||||
use maplibre::{context::MapContext, render::camera::Camera};
|
||||
use cgmath::{EuclideanSpace, Point2, Vector2, Zero};
|
||||
use maplibre::context::MapContext;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PanHandler {
|
||||
window_position: Option<Vector2<f64>>,
|
||||
start_window_position: Option<Vector2<f64>>,
|
||||
start_camera_position: Option<Vector3<f64>>,
|
||||
reference_camera: Option<Camera>,
|
||||
start_camera_position: Option<Vector2<f64>>,
|
||||
is_panning: bool,
|
||||
}
|
||||
|
||||
@ -20,57 +20,39 @@ impl UpdateState for PanHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(reference_camera) = &self.reference_camera {
|
||||
if let (Some(window_position), Some(start_window_position)) =
|
||||
(self.window_position, self.start_window_position)
|
||||
{
|
||||
let view_proj = view_state.view_projection();
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
if let (Some(window_position), Some(start_window_position)) =
|
||||
(self.window_position, self.start_window_position)
|
||||
{
|
||||
let view_proj = view_state.view_projection();
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
|
||||
let delta = if let (Some(start), Some(current)) = (
|
||||
reference_camera.window_to_world_at_ground(
|
||||
&start_window_position,
|
||||
&inverted_view_proj,
|
||||
false,
|
||||
),
|
||||
reference_camera.window_to_world_at_ground(
|
||||
&window_position,
|
||||
&inverted_view_proj,
|
||||
false,
|
||||
),
|
||||
) {
|
||||
start - current
|
||||
} else {
|
||||
Vector3::zero()
|
||||
};
|
||||
let delta = if let (Some(start), Some(current)) = (
|
||||
view_state.window_to_world_at_ground(
|
||||
&start_window_position,
|
||||
&inverted_view_proj,
|
||||
false,
|
||||
),
|
||||
view_state.window_to_world_at_ground(&window_position, &inverted_view_proj, false),
|
||||
) {
|
||||
start - current
|
||||
} else {
|
||||
Vector2::zero()
|
||||
};
|
||||
|
||||
if self.start_camera_position.is_none() {
|
||||
self.start_camera_position = Some(view_state.camera().position().to_vec());
|
||||
}
|
||||
|
||||
if let Some(start_camera_position) = self.start_camera_position {
|
||||
view_state.camera_mut().move_to(Point3::from_vec(
|
||||
start_camera_position + Vector3::new(delta.x, delta.y, 0.0),
|
||||
));
|
||||
}
|
||||
if self.start_camera_position.is_none() {
|
||||
self.start_camera_position = Some(view_state.camera().position().to_vec());
|
||||
}
|
||||
|
||||
if let Some(start_camera_position) = self.start_camera_position {
|
||||
view_state.camera_mut().move_to(Point2::from_vec(
|
||||
start_camera_position + Vector2::new(delta.x, delta.y),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.reference_camera = Some(view_state.camera().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PanHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
window_position: None,
|
||||
start_window_position: None,
|
||||
start_camera_position: None,
|
||||
reference_camera: None,
|
||||
is_panning: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_touch_start(&mut self, window_position: &Vector2<f64>) -> bool {
|
||||
self.is_panning = true;
|
||||
self.start_window_position = Some(*window_position);
|
||||
@ -81,7 +63,6 @@ impl PanHandler {
|
||||
self.start_camera_position = None;
|
||||
self.start_window_position = None;
|
||||
self.window_position = None;
|
||||
self.reference_camera = None;
|
||||
self.is_panning = false;
|
||||
true
|
||||
}
|
||||
@ -110,7 +91,6 @@ impl PanHandler {
|
||||
self.start_camera_position = None;
|
||||
self.start_window_position = None;
|
||||
self.window_position = None;
|
||||
self.reference_camera = None;
|
||||
self.is_panning = false;
|
||||
}
|
||||
true
|
||||
|
||||
@ -4,6 +4,7 @@ use maplibre::context::MapContext;
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PinchHandler {}
|
||||
|
||||
impl UpdateState for PinchHandler {
|
||||
@ -11,9 +12,3 @@ impl UpdateState for PinchHandler {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
impl PinchHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::Vector2;
|
||||
use maplibre::{context::MapContext, coords::WorldCoords, io::geometry_index::IndexedGeometry};
|
||||
use maplibre::{
|
||||
context::MapContext, coords::WorldCoords, io::geometry_index::IndexedGeometry,
|
||||
render::tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
};
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
|
||||
use crate::input::UpdateState;
|
||||
@ -65,10 +68,10 @@ impl UpdateState for QueryHandler {
|
||||
let view_proj = view_state.view_projection();
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
|
||||
let z = view_state.visible_level(); // FIXME: can be wrong, if tiles of different z are visible
|
||||
let z = view_state.zoom().zoom_level(DEFAULT_TILE_SIZE); // FIXME: can be wrong, if tiles of different z are visible
|
||||
let zoom = view_state.zoom();
|
||||
|
||||
if let Some(coordinates) = view_state.camera().window_to_world_at_ground(
|
||||
if let Some(coordinates) = view_state.window_to_world_at_ground(
|
||||
&window_position,
|
||||
&inverted_view_proj,
|
||||
false,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::{Vector3, Zero};
|
||||
use cgmath::{Vector2, Zero};
|
||||
use maplibre::context::MapContext;
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
pub struct ShiftHandler {
|
||||
camera_translate: Vector3<f64>,
|
||||
camera_translate: Vector2<f64>,
|
||||
|
||||
speed: f64,
|
||||
sensitivity: f64,
|
||||
@ -25,7 +25,7 @@ impl UpdateState for ShiftHandler {
|
||||
impl ShiftHandler {
|
||||
pub fn new(speed: f64, sensitivity: f64) -> Self {
|
||||
Self {
|
||||
camera_translate: Vector3::zero(),
|
||||
camera_translate: Vector2::zero(),
|
||||
speed,
|
||||
sensitivity,
|
||||
}
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::{Deg, Zero};
|
||||
use maplibre::context::MapContext;
|
||||
|
||||
use super::UpdateState;
|
||||
|
||||
pub struct TiltHandler {
|
||||
delta_pitch: Deg<f64>,
|
||||
|
||||
speed: f64,
|
||||
sensitivity: f64,
|
||||
}
|
||||
|
||||
impl UpdateState for TiltHandler {
|
||||
fn update_state(&mut self, MapContext { view_state, .. }: &mut MapContext, dt: Duration) {
|
||||
let dt = dt.as_secs_f64() * (1.0 / self.speed);
|
||||
|
||||
let delta = self.delta_pitch * dt;
|
||||
view_state.camera_mut().tilt(delta);
|
||||
self.delta_pitch -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
impl TiltHandler {
|
||||
pub fn new(speed: f64, sensitivity: f64) -> Self {
|
||||
Self {
|
||||
delta_pitch: Deg::zero(),
|
||||
speed,
|
||||
sensitivity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_key_press(
|
||||
&mut self,
|
||||
key: winit::event::VirtualKeyCode,
|
||||
state: winit::event::ElementState,
|
||||
) -> bool {
|
||||
let amount = if state == winit::event::ElementState::Pressed {
|
||||
Deg(0.1 * self.sensitivity) // left, right is the same as panning 1 degree
|
||||
} else {
|
||||
Deg::zero()
|
||||
};
|
||||
match key {
|
||||
winit::event::VirtualKeyCode::R => {
|
||||
self.delta_pitch -= amount;
|
||||
true
|
||||
}
|
||||
winit::event::VirtualKeyCode::F => {
|
||||
self.delta_pitch += amount;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cgmath::{Vector2, Vector3};
|
||||
use cgmath::Vector2;
|
||||
use maplibre::{context::MapContext, coords::Zoom};
|
||||
|
||||
use super::UpdateState;
|
||||
@ -24,18 +24,15 @@ impl UpdateState for ZoomHandler {
|
||||
let view_proj = view_state.view_projection();
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
|
||||
if let Some(cursor_position) = view_state.camera().window_to_world_at_ground(
|
||||
if let Some(cursor_position) = view_state.window_to_world_at_ground(
|
||||
&window_position,
|
||||
&inverted_view_proj,
|
||||
false,
|
||||
) {
|
||||
let scale = current_zoom.scale_delta(&next_zoom);
|
||||
|
||||
let delta = Vector3::new(
|
||||
cursor_position.x * scale,
|
||||
cursor_position.y * scale,
|
||||
cursor_position.z,
|
||||
) - cursor_position;
|
||||
let delta = Vector2::new(cursor_position.x * scale, cursor_position.y * scale)
|
||||
- cursor_position;
|
||||
|
||||
view_state.camera_mut().move_relative(delta);
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use maplibre::{
|
||||
debug::DebugPlugin,
|
||||
event_loop::EventLoop,
|
||||
io::apc::SchedulerAsyncProcedureCall,
|
||||
kernel::{Kernel, KernelBuilder},
|
||||
@ -128,7 +127,8 @@ pub fn run_headed_map(
|
||||
Box::new(RenderPlugin::default()),
|
||||
//Box::new(VectorPlugin::<DefaultVectorTransferables>::default()),
|
||||
Box::new(RasterPlugin::<DefaultRasterTransferables>::default()),
|
||||
Box::new(DebugPlugin::default()),
|
||||
#[cfg(debug_assertions)]
|
||||
Box::new(maplibre::debug::DebugPlugin::default()),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
use crate::{render::Renderer, style::Style, tcs::world::World, view_state::ViewState};
|
||||
use crate::{
|
||||
render::{view_state::ViewState, Renderer},
|
||||
style::Style,
|
||||
tcs::world::World,
|
||||
};
|
||||
|
||||
/// Stores the context of the map.
|
||||
///
|
||||
|
||||
@ -146,6 +146,32 @@ impl LatLon {
|
||||
longitude,
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate radius of the earth in meters.
|
||||
/// Uses the WGS-84 approximation. The radius at the equator is ~6378137 and at the poles is ~6356752. https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84
|
||||
/// 6371008.8 is one published "average radius" see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius, or ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf p.4
|
||||
const EARTH_RADIUS: f64 = 6371008.8;
|
||||
|
||||
/// The average circumference of the world in meters.
|
||||
|
||||
const EARTH_CIRCUMFRENCE: f64 = 2.0 * PI * Self::EARTH_RADIUS; // meters
|
||||
|
||||
/// The circumference at a line of latitude in meters.
|
||||
fn circumference_at_latitude(&self) -> f64 {
|
||||
Self::EARTH_CIRCUMFRENCE * (self.latitude * PI / 180.0).cos()
|
||||
}
|
||||
|
||||
fn mercator_x_from_lng(&self) -> f64 {
|
||||
(180.0 + self.longitude) / 360.0
|
||||
}
|
||||
|
||||
fn mercator_y_from_lat(&self) -> f64 {
|
||||
(180.0 - (180.0 / PI * ((PI / 4.0 + self.latitude * PI / 360.0).tan()).ln())) / 360.0
|
||||
}
|
||||
|
||||
fn mercator_z_from_altitude(&self, altitude: f64) -> f64 {
|
||||
altitude / self.circumference_at_latitude()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LatLon {
|
||||
@ -221,8 +247,19 @@ impl Zoom {
|
||||
2.0_f64.powf(zoom.0 - self.0)
|
||||
}
|
||||
|
||||
pub fn level(&self) -> ZoomLevel {
|
||||
ZoomLevel::from(self.0.floor() as u8)
|
||||
/// Adopted from
|
||||
/// [Transform::coveringZoomLevel](https://github.com/maplibre/maplibre-gl-js/blob/80e232a64716779bfff841dbc18fddc1f51535ad/src/geo/transform.ts#L279-L288)
|
||||
///
|
||||
/// This function calculates which ZoomLevel to show at this zoom.
|
||||
///
|
||||
/// The `tile_size` is the size of the tile like specified in the source definition,
|
||||
/// For example raster tiles can be 512px or 256px. If it is 256px, then 2x as many tiles are
|
||||
/// displayed. If the raster tile is 512px then exactly as many raster tiles like vector
|
||||
/// tiles would be displayed.
|
||||
pub fn zoom_level(&self, tile_size: f64) -> ZoomLevel {
|
||||
// TODO: Also support round() instead of floor() here
|
||||
let z = (self.0 as f64 + (TILE_SIZE / tile_size).ln() / 2.0_f64.ln()).floor() as u8;
|
||||
return ZoomLevel(z.max(0));
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,6 +393,8 @@ impl WorldTileCoords {
|
||||
})
|
||||
}
|
||||
|
||||
/// Adopted from
|
||||
/// [Transform::calculatePosMatrix](https://github.com/maplibre/maplibre-gl-js/blob/80e232a64716779bfff841dbc18fddc1f51535ad/src/geo/transform.ts#L719-L731)
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn transform_for_zoom(&self, zoom: Zoom) -> Matrix4<f64> {
|
||||
/*
|
||||
@ -690,6 +729,7 @@ mod tests {
|
||||
coords::{
|
||||
Quadkey, TileCoords, ViewRegion, WorldCoords, WorldTileCoords, Zoom, ZoomLevel, EXTENT,
|
||||
},
|
||||
render::tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
style::source::TileAddressingScheme,
|
||||
util::math::Aabb2,
|
||||
};
|
||||
@ -704,7 +744,8 @@ mod tests {
|
||||
println!("{p1:?}\n{p2:?}");
|
||||
|
||||
assert_eq!(
|
||||
WorldCoords::from((p1.x, p1.y)).into_world_tile(zoom.level(), zoom),
|
||||
WorldCoords::from((p1.x, p1.y))
|
||||
.into_world_tile(zoom.zoom_level(DEFAULT_TILE_SIZE), zoom),
|
||||
tile
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ use crate::{
|
||||
kernel::Kernel,
|
||||
map::MapError,
|
||||
plugin::Plugin,
|
||||
render::{eventually::Eventually, Renderer},
|
||||
render::{eventually::Eventually, view_state::ViewState, Renderer},
|
||||
schedule::{Schedule, Stage},
|
||||
style::Style,
|
||||
tcs::world::World,
|
||||
@ -21,7 +21,6 @@ use crate::{
|
||||
LayerTessellated, ProcessVectorContext, VectorBufferPool, VectorLayerData,
|
||||
VectorLayersDataComponent, VectorTileRequest, VectorTransferables,
|
||||
},
|
||||
view_state::ViewState,
|
||||
};
|
||||
|
||||
pub struct HeadlessMap {
|
||||
@ -44,7 +43,7 @@ impl HeadlessMap {
|
||||
WorldCoords::from((TILE_SIZE / 2., TILE_SIZE / 2.)),
|
||||
Zoom::default(),
|
||||
cgmath::Deg(0.0),
|
||||
cgmath::Deg(110.0),
|
||||
cgmath::Rad(0.6435011087932844),
|
||||
);
|
||||
|
||||
let mut world = World::default();
|
||||
|
||||
@ -51,7 +51,6 @@ pub mod kernel;
|
||||
pub mod map;
|
||||
pub mod plugin;
|
||||
pub mod tcs;
|
||||
pub mod view_state;
|
||||
|
||||
// Plugins
|
||||
pub mod debug;
|
||||
|
||||
@ -14,11 +14,11 @@ use crate::{
|
||||
},
|
||||
error::RenderError,
|
||||
graph::RenderGraphError,
|
||||
view_state::ViewState,
|
||||
},
|
||||
schedule::{Schedule, Stage},
|
||||
style::Style,
|
||||
tcs::world::World,
|
||||
view_state::ViewState,
|
||||
window::{HeadedMapWindow, MapWindow, MapWindowConfig},
|
||||
};
|
||||
|
||||
@ -104,7 +104,7 @@ where
|
||||
WorldCoords::from_lat_lon(LatLon::new(center[0], center[1]), initial_zoom),
|
||||
initial_zoom,
|
||||
cgmath::Deg::<f64>(style.pitch.unwrap_or_default()),
|
||||
cgmath::Deg(110.0),
|
||||
cgmath::Rad(0.6435011087932844),
|
||||
);
|
||||
|
||||
let mut world = World::default();
|
||||
|
||||
@ -15,6 +15,7 @@ use crate::{
|
||||
transferables::{LayerRasterMissing, RasterTransferables},
|
||||
RasterLayersDataComponent,
|
||||
},
|
||||
render::tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
style::layer::LayerPaint,
|
||||
tcs::system::System,
|
||||
};
|
||||
@ -48,7 +49,8 @@ impl<E: Environment, T: RasterTransferables> System for RequestSystem<E, T> {
|
||||
}: &mut MapContext,
|
||||
) {
|
||||
let _tiles = &mut world.tiles;
|
||||
let view_region = view_state.create_view_region();
|
||||
let view_region =
|
||||
view_state.create_view_region(view_state.zoom().zoom_level(DEFAULT_TILE_SIZE));
|
||||
|
||||
if view_state.did_camera_change() || view_state.did_zoom_change() {
|
||||
if let Some(view_region) = &view_region {
|
||||
|
||||
@ -8,6 +8,7 @@ use crate::{
|
||||
},
|
||||
render::{
|
||||
eventually::{Eventually, Eventually::Initialized},
|
||||
tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
Renderer,
|
||||
},
|
||||
style::Style,
|
||||
@ -29,7 +30,8 @@ pub fn upload_system(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let view_region = view_state.create_view_region();
|
||||
let view_region =
|
||||
view_state.create_view_region(view_state.zoom().zoom_level(DEFAULT_TILE_SIZE));
|
||||
|
||||
if let Some(view_region) = &view_region {
|
||||
upload_raster_layer(
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
//! Main camera
|
||||
|
||||
use cgmath::{prelude::*, AbsDiffEq, Matrix4, Point2, Point3, Rad, Vector2, Vector3, Vector4};
|
||||
use std::convert::Into;
|
||||
|
||||
use crate::util::{
|
||||
math::{bounds_from_points, Aabb2, Aabb3, Plane},
|
||||
SignificantlyDifferent,
|
||||
};
|
||||
use cgmath::{num_traits::clamp, prelude::*, *};
|
||||
|
||||
use crate::util::SignificantlyDifferent;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub const OPENGL_TO_WGPU_MATRIX: Matrix4<f64> = Matrix4::new(
|
||||
@ -23,8 +22,8 @@ pub const FLIP_Y: Matrix4<f64> = Matrix4::new(
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ViewProjection(Matrix4<f64>);
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ViewProjection(pub Matrix4<f64>);
|
||||
|
||||
impl ViewProjection {
|
||||
#[tracing::instrument(skip_all)]
|
||||
@ -66,17 +65,18 @@ impl ModelViewProjection {
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_PITCH: Rad<f64> = Rad(-0.5);
|
||||
const MAX_PITCH: Rad<f64> = Rad(0.5);
|
||||
const MIN_PITCH: Deg<f64> = Deg(-30.0);
|
||||
const MAX_PITCH: Deg<f64> = Deg(30.0);
|
||||
|
||||
const MIN_YAW: Deg<f64> = Deg(-30.0);
|
||||
const MAX_YAW: Deg<f64> = Deg(30.0);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Camera {
|
||||
position: Point3<f64>, // The z axis never changes, the zoom is used instead
|
||||
position: Point2<f64>,
|
||||
yaw: Rad<f64>,
|
||||
pitch: Rad<f64>,
|
||||
|
||||
width: f64,
|
||||
height: f64,
|
||||
roll: Rad<f64>,
|
||||
}
|
||||
|
||||
impl SignificantlyDifferent for Camera {
|
||||
@ -86,441 +86,194 @@ impl SignificantlyDifferent for Camera {
|
||||
self.position.abs_diff_ne(&other.position, epsilon)
|
||||
|| self.yaw.abs_diff_ne(&other.yaw, epsilon)
|
||||
|| self.pitch.abs_diff_ne(&other.pitch, epsilon)
|
||||
|| self.roll.abs_diff_ne(&other.roll, epsilon)
|
||||
}
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn new<V: Into<Point3<f64>>, Y: Into<Rad<f64>>, P: Into<Rad<f64>>>(
|
||||
pub fn new<V: Into<Point2<f64>>, Y: Into<Rad<f64>>, P: Into<Rad<f64>>>(
|
||||
position: V,
|
||||
yaw: Y,
|
||||
pitch: P,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
position: position.into(),
|
||||
yaw: yaw.into(),
|
||||
pitch: pitch.into(),
|
||||
width: width as f64,
|
||||
height: height as f64,
|
||||
roll: Rad::zero(), // TODO: initialize
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.width = width as f64;
|
||||
self.height = height as f64;
|
||||
pub fn calc_matrix(&self, camera_height: f64) -> Matrix4<f64> {
|
||||
Matrix4::from_translation(Vector3::new(0.0, 0.0, -camera_height))
|
||||
* Matrix4::from_angle_x(self.pitch)
|
||||
* Matrix4::from_angle_y(self.yaw)
|
||||
* Matrix4::from_angle_z(self.roll)
|
||||
* Matrix4::from_translation(Vector3::new(-self.position.x, -self.position.y, 0.0))
|
||||
}
|
||||
|
||||
fn calc_matrix(&self) -> Matrix4<f64> {
|
||||
Matrix4::look_to_rh(
|
||||
self.position,
|
||||
Vector3::new(self.yaw.cos(), self.pitch.sin(), self.yaw.sin()).normalize(),
|
||||
Vector3::unit_y(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn calc_view_proj(&self, perspective: &Perspective) -> ViewProjection {
|
||||
ViewProjection(FLIP_Y * perspective.current_projection * self.calc_matrix())
|
||||
}
|
||||
|
||||
/// A transform which can be used to transform between clip and window space.
|
||||
/// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/direct3d9/viewports-and-clipping#viewport-rectangle) (Direct3D).
|
||||
fn clip_to_window_transform(&self) -> Matrix4<f64> {
|
||||
let min_depth = 0.0;
|
||||
let max_depth = 1.0;
|
||||
let x = 0.0;
|
||||
let y = 0.0;
|
||||
let ox = x + self.width / 2.0;
|
||||
let oy = y + self.height / 2.0;
|
||||
let oz = min_depth;
|
||||
let pz = max_depth - min_depth;
|
||||
Matrix4::from_cols(
|
||||
Vector4::new(self.width / 2.0, 0.0, 0.0, 0.0),
|
||||
Vector4::new(0.0, -self.height / 2.0, 0.0, 0.0),
|
||||
Vector4::new(0.0, 0.0, pz, 0.0),
|
||||
Vector4::new(ox, oy, oz, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Transforms coordinates in clip space to window coordinates.
|
||||
///
|
||||
/// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline) (Direct3D).
|
||||
fn clip_to_window(&self, clip: &Vector4<f64>) -> Vector4<f64> {
|
||||
#[rustfmt::skip]
|
||||
let ndc = Vector4::new(
|
||||
clip.x / clip.w,
|
||||
clip.y / clip.w,
|
||||
clip.z / clip.w,
|
||||
1.0
|
||||
);
|
||||
|
||||
self.clip_to_window_transform() * ndc
|
||||
}
|
||||
/// Alternative implementation to `clip_to_window`. Transforms coordinates in clip space to
|
||||
/// window coordinates.
|
||||
///
|
||||
/// Adopted from [here](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkViewport.html)
|
||||
/// and [here](https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/) (Vulkan).
|
||||
fn clip_to_window_vulkan(&self, clip: &Vector4<f64>) -> Vector3<f64> {
|
||||
#[rustfmt::skip]
|
||||
let ndc = Vector4::new(
|
||||
clip.x / clip.w,
|
||||
clip.y / clip.w,
|
||||
clip.z / clip.w,
|
||||
1.0
|
||||
);
|
||||
|
||||
let min_depth = 0.0;
|
||||
let max_depth = 1.0;
|
||||
|
||||
let x = 0.0;
|
||||
let y = 0.0;
|
||||
let ox = x + self.width / 2.0;
|
||||
let oy = y + self.height / 2.0;
|
||||
let oz = min_depth;
|
||||
let px = self.width;
|
||||
let py = self.height;
|
||||
let pz = max_depth - min_depth;
|
||||
let xd = ndc.x;
|
||||
let yd = ndc.y;
|
||||
let zd = ndc.z;
|
||||
Vector3::new(px / 2.0 * xd + ox, py / 2.0 * yd + oy, pz * zd + oz)
|
||||
}
|
||||
|
||||
/// Order of transformations reversed: https://computergraphics.stackexchange.com/questions/6087/screen-space-coordinates-to-eye-space-conversion/6093
|
||||
/// `w` is lost.
|
||||
///
|
||||
/// OpenGL explanation: https://www.khronos.org/opengl/wiki/Compute_eye_space_from_window_space#From_window_to_ndc
|
||||
fn window_to_world(
|
||||
&self,
|
||||
window: &Vector3<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
) -> Vector3<f64> {
|
||||
#[rustfmt::skip]
|
||||
let fixed_window = Vector4::new(
|
||||
window.x,
|
||||
window.y,
|
||||
window.z,
|
||||
1.0
|
||||
);
|
||||
|
||||
let ndc = self.clip_to_window_transform().invert().unwrap() * fixed_window;
|
||||
let unprojected = inverted_view_proj.project(ndc);
|
||||
|
||||
Vector3::new(
|
||||
unprojected.x / unprojected.w,
|
||||
unprojected.y / unprojected.w,
|
||||
unprojected.z / unprojected.w,
|
||||
)
|
||||
}
|
||||
|
||||
/// Alternative implementation to `window_to_world`
|
||||
///
|
||||
/// Adopted from [here](https://docs.rs/nalgebra-glm/latest/src/nalgebra_glm/ext/matrix_projection.rs.html#164-181).
|
||||
fn window_to_world_nalgebra(
|
||||
window: &Vector3<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) -> Vector3<f64> {
|
||||
let pt = Vector4::new(
|
||||
2.0 * (window.x - 0.0) / width - 1.0,
|
||||
2.0 * (height - window.y - 0.0) / height - 1.0,
|
||||
window.z,
|
||||
1.0,
|
||||
);
|
||||
let unprojected = inverted_view_proj.project(pt);
|
||||
|
||||
Vector3::new(
|
||||
unprojected.x / unprojected.w,
|
||||
unprojected.y / unprojected.w,
|
||||
unprojected.z / unprojected.w,
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the world coordinates for the specified `window` coordinates on the `z=0` plane.
|
||||
pub fn window_to_world_at_ground(
|
||||
&self,
|
||||
window: &Vector2<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
bound: bool,
|
||||
) -> Option<Vector3<f64>> {
|
||||
let near_world =
|
||||
self.window_to_world(&Vector3::new(window.x, window.y, 0.0), inverted_view_proj);
|
||||
|
||||
let far_world =
|
||||
self.window_to_world(&Vector3::new(window.x, window.y, 1.0), inverted_view_proj);
|
||||
|
||||
// for z = 0 in world coordinates
|
||||
// Idea comes from: https://dondi.lmu.build/share/cg/unproject-explained.pdf
|
||||
let u = -near_world.z / (far_world.z - near_world.z);
|
||||
if !bound || (0.0..=1.0).contains(&u) {
|
||||
Some(near_world + u * (far_world - near_world))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates an [`Aabb2`] bounding box which contains at least the visible area on the `z=0`
|
||||
/// plane. One can think of it as being the bounding box of the geometry which forms the
|
||||
/// intersection between the viewing frustum and the `z=0` plane.
|
||||
///
|
||||
/// This implementation works in the world 3D space. It casts rays from the corners of the
|
||||
/// window to calculate intersections points with the `z=0` plane. Then a bounding box is
|
||||
/// calculated.
|
||||
///
|
||||
/// *Note:* It is possible that no such bounding box exists. This is the case if the `z=0` plane
|
||||
/// is not in view.
|
||||
pub fn view_region_bounding_box(
|
||||
&self,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
) -> Option<Aabb2<f64>> {
|
||||
let screen_bounding_box = [
|
||||
Vector2::new(0.0, 0.0),
|
||||
Vector2::new(self.width, 0.0),
|
||||
Vector2::new(self.width, self.height),
|
||||
Vector2::new(0.0, self.height),
|
||||
]
|
||||
.map(|point| self.window_to_world_at_ground(&point, inverted_view_proj, false));
|
||||
|
||||
let (min, max) = bounds_from_points(
|
||||
screen_bounding_box
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|point| [point.x, point.y]),
|
||||
)?;
|
||||
|
||||
Some(Aabb2::new(Point2::from(min), Point2::from(max)))
|
||||
}
|
||||
/// An alternative implementation for `view_bounding_box`.
|
||||
///
|
||||
/// This implementation works in the NDC space. We are creating a plane in the world 3D space.
|
||||
/// Then we are transforming it to the NDC space. In NDC space it is easy to calculate
|
||||
/// the intersection points between an Aabb3 and a plane. The resulting Aabb2 is returned.
|
||||
pub fn view_region_bounding_box_ndc(&self, perspective: &Perspective) -> Option<Aabb2<f64>> {
|
||||
let view_proj = self.calc_view_proj(perspective);
|
||||
let a = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0));
|
||||
let b = view_proj.project(Vector4::new(1.0, 0.0, 0.0, 1.0));
|
||||
let c = view_proj.project(Vector4::new(1.0, 1.0, 0.0, 1.0));
|
||||
|
||||
let a_ndc = self.clip_to_window(&a).truncate();
|
||||
let b_ndc = self.clip_to_window(&b).truncate();
|
||||
let c_ndc = self.clip_to_window(&c).truncate();
|
||||
let to_ndc = Vector3::new(1.0 / self.width, 1.0 / self.height, 1.0);
|
||||
let plane: Plane<f64> = Plane::from_points(
|
||||
Point3::from_vec(a_ndc.mul_element_wise(to_ndc)),
|
||||
Point3::from_vec(b_ndc.mul_element_wise(to_ndc)),
|
||||
Point3::from_vec(c_ndc.mul_element_wise(to_ndc)),
|
||||
)?;
|
||||
|
||||
let points = plane.intersection_points_aabb3(&Aabb3::new(
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Point3::new(1.0, 1.0, 1.0),
|
||||
));
|
||||
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
|
||||
let from_ndc = Vector3::new(self.width, self.height, 1.0);
|
||||
let vec = points
|
||||
.iter()
|
||||
.map(|point| {
|
||||
self.window_to_world(&point.mul_element_wise(from_ndc), &inverted_view_proj)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let min_x = vec
|
||||
.iter()
|
||||
.map(|point| point.x)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let min_y = vec
|
||||
.iter()
|
||||
.map(|point| point.y)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let max_x = vec
|
||||
.iter()
|
||||
.map(|point| point.x)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let max_y = vec
|
||||
.iter()
|
||||
.map(|point| point.y)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
Some(Aabb2::new(
|
||||
Point2::new(min_x, min_y),
|
||||
Point2::new(max_x, max_y),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn position(&self) -> Point3<f64> {
|
||||
pub fn position(&self) -> Point2<f64> {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn yaw(&self) -> Rad<f64> {
|
||||
pub fn get_yaw(&self) -> Rad<f64> {
|
||||
self.yaw
|
||||
}
|
||||
|
||||
pub fn rotate<P: Into<Rad<f64>>>(&mut self, delta: P) {
|
||||
self.yaw += delta.into();
|
||||
pub fn yaw<P: Into<Rad<f64>>>(&mut self, delta: P) {
|
||||
let new_yaw = self.yaw + delta.into();
|
||||
|
||||
if new_yaw <= MAX_YAW.into() && new_yaw >= MIN_YAW.into() {
|
||||
self.yaw = new_yaw;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pitch(&self) -> Rad<f64> {
|
||||
pub fn get_roll(&self) -> Rad<f64> {
|
||||
self.roll
|
||||
}
|
||||
|
||||
pub fn roll<P: Into<Rad<f64>>>(&mut self, delta: P) {
|
||||
self.roll += delta.into();
|
||||
}
|
||||
|
||||
pub fn get_pitch(&self) -> Rad<f64> {
|
||||
self.pitch
|
||||
}
|
||||
|
||||
pub fn tilt<P: Into<Rad<f64>>>(&mut self, delta: P) {
|
||||
pub fn pitch<P: Into<Rad<f64>>>(&mut self, delta: P) {
|
||||
let new_pitch = self.pitch + delta.into();
|
||||
|
||||
if new_pitch <= MAX_PITCH && new_pitch >= MIN_PITCH {
|
||||
if new_pitch <= MAX_PITCH.into() && new_pitch >= MIN_PITCH.into() {
|
||||
self.pitch = new_pitch;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_relative(&mut self, delta: Vector3<f64>) {
|
||||
pub fn move_relative(&mut self, delta: Vector2<f64>) {
|
||||
self.position += delta;
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, new_position: Point3<f64>) {
|
||||
pub fn move_to(&mut self, new_position: Point2<f64>) {
|
||||
self.position = new_position;
|
||||
}
|
||||
|
||||
pub fn position_vector(&self) -> Vector3<f64> {
|
||||
pub fn position_vector(&self) -> Vector2<f64> {
|
||||
self.position.to_vec()
|
||||
}
|
||||
|
||||
pub fn homogenous_position(&self) -> Vector4<f64> {
|
||||
self.position.to_homogeneous()
|
||||
pub fn to_3d(&self, camera_height: f64) -> Point3<f64> {
|
||||
Point3::new(self.position.x, self.position.y, camera_height)
|
||||
}
|
||||
pub fn set_yaw<P: Into<Rad<f64>>>(&mut self, yaw: P) {
|
||||
let new_yaw = yaw.into();
|
||||
let max: Rad<_> = MAX_YAW.into();
|
||||
let min: Rad<_> = MIN_YAW.into();
|
||||
self.yaw = Rad(new_yaw.0.min(max.0).max(min.0))
|
||||
}
|
||||
pub fn set_pitch<P: Into<Rad<f64>>>(&mut self, pitch: P) {
|
||||
let new_pitch = pitch.into();
|
||||
let max: Rad<_> = MAX_PITCH.into();
|
||||
let min: Rad<_> = MIN_PITCH.into();
|
||||
self.pitch = Rad(new_pitch.0.min(max.0).max(min.0))
|
||||
}
|
||||
pub fn set_roll<P: Into<Rad<f64>>>(&mut self, roll: P) {
|
||||
self.roll = roll.into();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Default)]
|
||||
pub struct EdgeInsets {
|
||||
pub top: f64,
|
||||
pub bottom: f64,
|
||||
pub left: f64,
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
impl EdgeInsets {
|
||||
/**
|
||||
* Utility method that computes the new apprent center or vanishing point after applying insets.
|
||||
* This is in pixels and with the top left being (0.0) and +y being downwards.
|
||||
*
|
||||
* @param {number} width the width
|
||||
* @param {number} height the height
|
||||
* @returns {Point} the point
|
||||
* @memberof EdgeInsets
|
||||
*/
|
||||
pub fn center(&self, width: f64, height: f64) -> Point2<f64> {
|
||||
// Clamp insets so they never overflow width/height and always calculate a valid center
|
||||
let x = clamp((self.left + width - self.right) / 2.0, 0.0, width);
|
||||
let y = clamp((self.top + height - self.bottom) / 2.0, 0.0, height);
|
||||
|
||||
return Point2::new(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Perspective {
|
||||
fovy: Rad<f64>,
|
||||
znear: f64,
|
||||
zfar: f64,
|
||||
|
||||
current_projection: Matrix4<f64>,
|
||||
}
|
||||
|
||||
impl Perspective {
|
||||
pub fn new<F: Into<Rad<f64>>>(width: u32, height: u32, fovy: F, znear: f64, zfar: f64) -> Self {
|
||||
pub fn new<F: Into<Rad<f64>>>(fovy: F) -> Self {
|
||||
let rad = fovy.into();
|
||||
Self {
|
||||
current_projection: Self::calc_matrix(width as f64 / height as f64, rad, znear, zfar),
|
||||
fovy: rad,
|
||||
znear,
|
||||
zfar,
|
||||
}
|
||||
Self { fovy: rad }
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.current_projection = Self::calc_matrix(
|
||||
width as f64 / height as f64,
|
||||
self.fovy,
|
||||
self.znear,
|
||||
self.zfar,
|
||||
);
|
||||
pub fn fovy(&self) -> Rad<f64> {
|
||||
self.fovy
|
||||
}
|
||||
pub fn fovx(&self, width: f64, height: f64) -> Rad<f64> {
|
||||
let aspect = width / height;
|
||||
Rad(2.0 * ((self.fovy / 2.0).tan() * aspect).atan())
|
||||
}
|
||||
|
||||
fn calc_matrix(aspect: f64, fovy: Rad<f64>, znear: f64, zfar: f64) -> Matrix4<f64> {
|
||||
OPENGL_TO_WGPU_MATRIX * cgmath::perspective(fovy, aspect, znear, zfar)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cgmath::{AbsDiffEq, Vector2, Vector3, Vector4};
|
||||
|
||||
use super::{Camera, Perspective};
|
||||
use crate::render::camera::{InvertedViewProjection, ViewProjection};
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let width = 1920.0;
|
||||
let height = 1080.0;
|
||||
let camera = Camera::new(
|
||||
(0.0, 5.0, 5000.0),
|
||||
cgmath::Deg(-90.0),
|
||||
cgmath::Deg(45.0),
|
||||
width as u32,
|
||||
height as u32,
|
||||
);
|
||||
// 4732.561319582916
|
||||
let perspective = Perspective::new(
|
||||
width as u32,
|
||||
height as u32,
|
||||
cgmath::Deg(45.0),
|
||||
0.1,
|
||||
100000.0,
|
||||
);
|
||||
let view_proj: ViewProjection = camera.calc_view_proj(&perspective);
|
||||
let inverted_view_proj: InvertedViewProjection = view_proj.invert();
|
||||
|
||||
let world_pos: Vector4<f64> = Vector4::new(0.0, 0.0, 0.0, 1.0);
|
||||
let clip = view_proj.project(world_pos);
|
||||
|
||||
let origin_clip_space = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0));
|
||||
println!("origin w in clip space: {:?}", origin_clip_space.w);
|
||||
|
||||
println!("world_pos: {world_pos:?}");
|
||||
println!("clip: {clip:?}");
|
||||
println!("world_pos: {:?}", view_proj.invert().project(clip));
|
||||
|
||||
println!("window: {:?}", camera.clip_to_window_vulkan(&clip));
|
||||
let window = camera.clip_to_window(&clip);
|
||||
println!("window (matrix): {window:?}");
|
||||
|
||||
// --------- nalgebra
|
||||
|
||||
println!(
|
||||
"r world (nalgebra): {:?}",
|
||||
Camera::window_to_world_nalgebra(
|
||||
&window.truncate(),
|
||||
&inverted_view_proj,
|
||||
width,
|
||||
height
|
||||
)
|
||||
);
|
||||
|
||||
// -------- far vs. near plane trick
|
||||
|
||||
let near_world = Camera::window_to_world_nalgebra(
|
||||
&Vector3::new(window.x, window.y, 0.0),
|
||||
&inverted_view_proj,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
let far_world = Camera::window_to_world_nalgebra(
|
||||
&Vector3::new(window.x, window.y, 1.0),
|
||||
&inverted_view_proj,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
// for z = 0 in world coordinates
|
||||
let u = -near_world.z / (far_world.z - near_world.z);
|
||||
println!("u: {u:?}");
|
||||
let unprojected = near_world + u * (far_world - near_world);
|
||||
println!("unprojected: {unprojected:?}");
|
||||
assert!(Vector3::new(world_pos.x, world_pos.y, world_pos.z).abs_diff_eq(&unprojected, 0.05));
|
||||
|
||||
// ---- test for unproject
|
||||
|
||||
let window = Vector2::new(960.0, 631.0); // 0, 4096: passt nicht
|
||||
//let window = Vector2::new(962.0, 1.0); // 0, 300: passt nicht
|
||||
//let window = Vector2::new(960.0, 540.0); // 0, 0 passt
|
||||
let near_world =
|
||||
camera.window_to_world(&Vector3::new(window.x, window.y, 0.0), &inverted_view_proj);
|
||||
|
||||
let far_world =
|
||||
camera.window_to_world(&Vector3::new(window.x, window.y, 1.0), &inverted_view_proj);
|
||||
|
||||
// for z = 0 in world coordinates
|
||||
let u = -near_world.z / (far_world.z - near_world.z);
|
||||
println!("u: {u:?}");
|
||||
let unprojected = near_world + u * (far_world - near_world);
|
||||
println!("unprojected: {unprojected:?}");
|
||||
// ----
|
||||
|
||||
//assert!(reverse_world.abs_diff_eq(&world_pos, 0.05))
|
||||
pub fn y_tan(&self) -> f64 {
|
||||
let half_fovy = self.fovy / 2.0;
|
||||
half_fovy.tan()
|
||||
}
|
||||
pub fn x_tan(&self, width: f64, height: f64) -> f64 {
|
||||
let half_fovx = self.fovx(width, height) / 2.0;
|
||||
half_fovx.tan()
|
||||
}
|
||||
|
||||
pub fn offset_x(&self, center_offset: Point2<f64>, width: f64) -> f64 {
|
||||
center_offset.x * 2.0 / width
|
||||
}
|
||||
|
||||
pub fn offset_y(&self, center_offset: Point2<f64>, height: f64) -> f64 {
|
||||
center_offset.y * 2.0 / height
|
||||
}
|
||||
|
||||
pub fn calc_matrix(&self, aspect: f64, near_z: f64, far_z: f64) -> Matrix4<f64> {
|
||||
perspective(self.fovy, aspect, near_z, far_z)
|
||||
}
|
||||
|
||||
pub fn calc_matrix_with_center(
|
||||
&self,
|
||||
width: f64,
|
||||
height: f64,
|
||||
near_z: f64,
|
||||
far_z: f64,
|
||||
center_offset: Point2<f64>,
|
||||
) -> Matrix4<f64> {
|
||||
let ymax = near_z * self.y_tan();
|
||||
|
||||
//TODO maybe just: let xmax = ymax * aspect;
|
||||
let xmax = near_z * self.x_tan(width, height);
|
||||
|
||||
let offset_x = self.offset_x(center_offset, width);
|
||||
let offset_y = self.offset_y(center_offset, height);
|
||||
frustum(
|
||||
// https://webglfundamentals.org/webgl/lessons/webgl-qna-how-can-i-move-the-perspective-vanishing-point-from-the-center-of-the-canvas-.html
|
||||
xmax * (-1.0 + offset_x), /* = -xmax + (center_offset.x * screen_to_near_factor_x)
|
||||
where:
|
||||
screen_to_near_factor_x = near_width / width
|
||||
where:
|
||||
near_width = xmax * 2.0
|
||||
*/
|
||||
xmax * (1.0 + offset_x),
|
||||
ymax * (-1.0 + offset_y),
|
||||
ymax * (1.0 + offset_y),
|
||||
near_z,
|
||||
far_z,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ pub mod render_commands;
|
||||
pub mod render_phase;
|
||||
pub mod settings;
|
||||
pub mod tile_view_pattern;
|
||||
pub mod view_state;
|
||||
|
||||
pub use shaders::ShaderVertex;
|
||||
|
||||
|
||||
@ -36,8 +36,6 @@ fn main(
|
||||
//}
|
||||
|
||||
var final_position = mat4x4<f32>(translate1, translate2, translate3, translate4) * vec4<f32>(position + normal * width, z, 1.0);
|
||||
// FIXME: how to fix z-fighting?
|
||||
final_position.z = z_index;
|
||||
|
||||
return VertexOutput(color, final_position);
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ fn main(
|
||||
let target_width = 1.0;
|
||||
let target_height = 1.0;
|
||||
|
||||
let WIDTH = EXTENT * zoom_factor / 1024.0;
|
||||
let WIDTH = EXTENT / 256.0 * zoom_factor; // Width is 1/256 of a tile
|
||||
|
||||
var VERTICES: array<vec3<f32>, 24> = array<vec3<f32>, 24>(
|
||||
// Debug lines vertices
|
||||
|
||||
@ -42,7 +42,6 @@ fn main(
|
||||
let tex_coords = TEX_COORDS[vertex_idx];
|
||||
|
||||
var final_position = mat4x4<f32>(translate1, translate2, translate3, translate4) * vec4<f32>(vertex, 1.0);
|
||||
final_position.z = z_index;
|
||||
|
||||
return VertexOutput(tex_coords, final_position);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ use crate::{
|
||||
context::MapContext,
|
||||
render::{
|
||||
eventually::{Eventually, Eventually::Initialized},
|
||||
tile_view_pattern::{ViewTileSources, WgpuTileViewPattern},
|
||||
tile_view_pattern::{ViewTileSources, WgpuTileViewPattern, DEFAULT_TILE_SIZE},
|
||||
},
|
||||
};
|
||||
|
||||
@ -19,7 +19,8 @@ pub fn tile_view_pattern_system(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let view_region = view_state.create_view_region();
|
||||
let view_region =
|
||||
view_state.create_view_region(view_state.zoom().zoom_level(DEFAULT_TILE_SIZE));
|
||||
|
||||
if let Some(view_region) = &view_region {
|
||||
let zoom = view_state.zoom();
|
||||
|
||||
@ -15,6 +15,12 @@ use crate::{
|
||||
|
||||
pub type WgpuTileViewPattern = TileViewPattern<wgpu::Queue, wgpu::Buffer>;
|
||||
|
||||
/// If not otherwise specified, raster tiles usually are 512.0 by 512.0 pixel.
|
||||
/// In order to support 256.0 x 256.0 raster tiles 256.0 must be used.
|
||||
///
|
||||
/// Vector tiles always have a size of 512.0.
|
||||
pub const DEFAULT_TILE_SIZE: f64 = 512.0;
|
||||
|
||||
/// This defines the source tile shaped from which the content for the `target` is taken.
|
||||
/// For example if the target is `(0, 0, 1)` (of [`ViewTile`]) , we might use
|
||||
/// `SourceShapes::Parent((0, 0, 0))` as source.
|
||||
|
||||
@ -11,7 +11,11 @@ use crate::{
|
||||
tcs::world::World,
|
||||
};
|
||||
|
||||
pub const DEFAULT_TILE_VIEW_PATTERN_SIZE: wgpu::BufferAddress = 32 * 4;
|
||||
// FIXME: If network is very slow, this pattern size can
|
||||
// increase dramatically.
|
||||
// E.g. imagine if a pattern for zoom level 18 is drawn
|
||||
// when completely zoomed out.
|
||||
pub const DEFAULT_TILE_VIEW_PATTERN_SIZE: wgpu::BufferAddress = 512;
|
||||
pub const CHILDREN_SEARCH_DEPTH: usize = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -146,7 +150,7 @@ impl<Q: Queue<B>, B> TileViewPattern<Q, B> {
|
||||
let raw_buffer = bytemuck::cast_slice(buffer.as_slice());
|
||||
if raw_buffer.len() as wgpu::BufferAddress > self.view_tiles_buffer.inner_size {
|
||||
/* TODO: We need to avoid this case by either choosing a proper size
|
||||
TODO: (DEFAULT_TILE_VIEW_SIZE), or resizing the buffer */
|
||||
TODO: (DEFAULT_TILE_VIEW_PATTERN_SIZE), or resizing the buffer */
|
||||
panic!("Buffer is too small to store the tile pattern!");
|
||||
}
|
||||
queue.write_buffer(&self.view_tiles_buffer.inner, 0, raw_buffer);
|
||||
|
||||
513
maplibre/src/render/view_state.rs
Normal file
513
maplibre/src/render/view_state.rs
Normal file
@ -0,0 +1,513 @@
|
||||
use std::{
|
||||
f64,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use cgmath::{prelude::*, *};
|
||||
|
||||
use crate::{
|
||||
coords::{ViewRegion, WorldCoords, Zoom, ZoomLevel},
|
||||
render::camera::{
|
||||
Camera, EdgeInsets, InvertedViewProjection, Perspective, ViewProjection, FLIP_Y,
|
||||
OPENGL_TO_WGPU_MATRIX,
|
||||
},
|
||||
util::{
|
||||
math::{bounds_from_points, Aabb2, Aabb3, Plane},
|
||||
ChangeObserver,
|
||||
},
|
||||
window::WindowSize,
|
||||
};
|
||||
|
||||
const VIEW_REGION_PADDING: i32 = 1;
|
||||
const MAX_N_TILES: usize = 512;
|
||||
|
||||
pub struct ViewState {
|
||||
zoom: ChangeObserver<Zoom>,
|
||||
camera: ChangeObserver<Camera>,
|
||||
perspective: Perspective,
|
||||
|
||||
width: f64,
|
||||
height: f64,
|
||||
edge_insets: EdgeInsets,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
pub fn new<F: Into<Rad<f64>>, P: Into<Deg<f64>>>(
|
||||
window_size: WindowSize,
|
||||
position: WorldCoords,
|
||||
zoom: Zoom,
|
||||
pitch: P,
|
||||
fovy: F,
|
||||
) -> Self {
|
||||
let camera = Camera::new((position.x, position.y), Deg(0.0), pitch.into());
|
||||
|
||||
let perspective = Perspective::new(fovy);
|
||||
|
||||
Self {
|
||||
zoom: ChangeObserver::new(zoom),
|
||||
camera: ChangeObserver::new(camera),
|
||||
perspective,
|
||||
width: window_size.width() as f64,
|
||||
height: window_size.height() as f64,
|
||||
edge_insets: EdgeInsets {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn set_edge_insets(&mut self, edge_insets: EdgeInsets) {
|
||||
self.edge_insets = edge_insets;
|
||||
}
|
||||
|
||||
pub fn edge_insets(&self) -> &EdgeInsets {
|
||||
&self.edge_insets
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.width = width as f64;
|
||||
self.height = height as f64;
|
||||
}
|
||||
|
||||
pub fn create_view_region(&self, visible_level: ZoomLevel) -> Option<ViewRegion> {
|
||||
self.view_region_bounding_box(&self.view_projection().invert())
|
||||
.map(|bounding_box| {
|
||||
ViewRegion::new(
|
||||
bounding_box,
|
||||
VIEW_REGION_PADDING,
|
||||
MAX_N_TILES,
|
||||
*self.zoom,
|
||||
visible_level,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_intersection_time(
|
||||
ray_origin: Vector3<f64>,
|
||||
ray_direction: Vector3<f64>,
|
||||
plane_origin: Vector3<f64>,
|
||||
plane_normal: Vector3<f64>,
|
||||
) -> f64 {
|
||||
let m = plane_origin - ray_origin;
|
||||
let distance = (m).dot(plane_normal);
|
||||
|
||||
let approach_speed = ray_direction.dot(plane_normal);
|
||||
|
||||
// Returns an infinity if the ray is
|
||||
// parallel to the plane and never intersects,
|
||||
// or NaN if the ray is in the plane
|
||||
// and intersects everywhere.
|
||||
return distance / approach_speed;
|
||||
|
||||
// Otherwise returns t such that
|
||||
// ray_origin + t * rayDirection
|
||||
// is in the plane, to within rounding error.
|
||||
}
|
||||
|
||||
pub fn furthest_distance(&self, camera_height: f64, center_offset: Point2<f64>) -> f64 {
|
||||
let perspective = &self.perspective;
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
let camera = self.camera.position();
|
||||
|
||||
let y = perspective.y_tan();
|
||||
let x = perspective.x_tan(width, height);
|
||||
let offset_x = perspective.offset_x(center_offset, width);
|
||||
let offset_y = perspective.offset_y(center_offset, height);
|
||||
|
||||
let rotation = Matrix4::from_angle_x(self.camera.get_pitch())
|
||||
* Matrix4::from_angle_y(self.camera.get_yaw())
|
||||
* Matrix4::from_angle_z(self.camera.get_roll());
|
||||
|
||||
let rays = [
|
||||
Vector3::new(x * (1.0 - offset_x), y * (1.0 - offset_y), 1.0),
|
||||
Vector3::new(x * (-1.0 - offset_x), y * (1.0 - offset_y), 1.0),
|
||||
Vector3::new(x * (1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
|
||||
Vector3::new(x * (-1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
|
||||
];
|
||||
let ray_origin = Vector3::new(-camera.x, -camera.y, -camera_height);
|
||||
|
||||
let plane_origin = Vector3::new(-camera.x, -camera.y, 0.0);
|
||||
let plane_normal = (rotation * Vector4::new(0.0, 0.0, 1.0, 1.0)).truncate();
|
||||
|
||||
rays.iter()
|
||||
.map(|ray| Self::get_intersection_time(ray_origin, *ray, plane_origin, plane_normal))
|
||||
.fold(0. / 0., f64::max)
|
||||
}
|
||||
|
||||
pub fn camera_to_center_distance(&self) -> f64 {
|
||||
let height = self.height;
|
||||
|
||||
let fovy = self.perspective.fovy();
|
||||
let half_fovy = fovy / 2.0;
|
||||
|
||||
// Camera height, such that given a certain field-of-view, exactly height/2 are visible on ground.
|
||||
let camera_to_center_distance = (height / 2.0) / (half_fovy.tan()); // TODO: Not sure why it is height here and not width
|
||||
camera_to_center_distance
|
||||
}
|
||||
|
||||
/// This function matches how maplibre-gl-js implements perspective and cameras at the time
|
||||
/// of the mapbox -> maplibre fork: [src/geo/transform.ts#L680](https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L680)
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn view_projection(&self) -> ViewProjection {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
|
||||
let center = self.edge_insets.center(width, height);
|
||||
// Offset between wanted center and usual/normal center
|
||||
let center_offset = center - Vector2::new(width, height) / 2.0;
|
||||
|
||||
let camera_to_center_distance = self.camera_to_center_distance();
|
||||
|
||||
let camera_matrix = self.camera.calc_matrix(camera_to_center_distance);
|
||||
|
||||
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthest_distance`
|
||||
let far_z = self.furthest_distance(camera_to_center_distance, center_offset) * 1.01;
|
||||
|
||||
// The larger the value of near_z is
|
||||
// - the more depth precision is available for features (good)
|
||||
// - clipping starts appearing sooner when the camera is close to 3d features (bad)
|
||||
//
|
||||
// Smaller values worked well for mapbox-gl-js but deckgl was encountering precision issues
|
||||
// when rendering it's layers using custom layers. This value was experimentally chosen and
|
||||
// seems to solve z-fighting issues in deckgl while not clipping buildings too close to the camera.
|
||||
//
|
||||
// TODO remove: In tile.vertex.wgsl we are setting each layer's final `z` in ndc space to `z_index`.
|
||||
// This means that regardless of the `znear` value all layers will be rendered as part
|
||||
// of the near plane.
|
||||
// These values have been selected experimentally:
|
||||
// https://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
|
||||
let near_z = height / 50.0;
|
||||
|
||||
let mut perspective =
|
||||
self.perspective
|
||||
.calc_matrix_with_center(width, height, near_z, far_z, center_offset);
|
||||
|
||||
//let mut perspective = self.perspective.calc_matrix(width / height, near_z, far_z);
|
||||
// Apply center of perspective offset, in order to move the vanishing point
|
||||
//perspective.z[0] = -center_offset.x * 2.0 / width;
|
||||
//perspective.z[1] = center_offset.y * 2.0 / height;
|
||||
|
||||
// Apply camera and move camera away from ground
|
||||
let view_projection = perspective * camera_matrix;
|
||||
|
||||
// TODO for the below TODOs, check GitHub blame to get an idea of what these matrices are used for!
|
||||
// TODO mercatorMatrix https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L725-L727
|
||||
// TODO scale vertically to meters per pixel (inverse of ground resolution): https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L729-L730
|
||||
// TODO alignedProjMatrix https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L735-L747
|
||||
// TODO labelPlaneMatrix https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L749-L752C14
|
||||
// TODO glCoordMatrix https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L754-L758
|
||||
// TODO pixelMatrix, pixelMatrixInverse https://github.com/maplibre/maplibre-gl-js/blob/e78ad7944ef768e67416daa4af86b0464bd0f617/src/geo/transform.ts#L760-L761
|
||||
|
||||
ViewProjection(FLIP_Y * OPENGL_TO_WGPU_MATRIX * view_projection)
|
||||
}
|
||||
|
||||
pub fn zoom(&self) -> Zoom {
|
||||
*self.zoom
|
||||
}
|
||||
|
||||
pub fn did_zoom_change(&self) -> bool {
|
||||
self.zoom.did_change(0.05)
|
||||
}
|
||||
|
||||
pub fn update_zoom(&mut self, new_zoom: Zoom) {
|
||||
*self.zoom = new_zoom;
|
||||
log::info!("zoom: {new_zoom}");
|
||||
}
|
||||
|
||||
pub fn camera(&self) -> &Camera {
|
||||
self.camera.deref()
|
||||
}
|
||||
|
||||
pub fn camera_mut(&mut self) -> &mut Camera {
|
||||
self.camera.deref_mut()
|
||||
}
|
||||
|
||||
pub fn did_camera_change(&self) -> bool {
|
||||
self.camera.did_change(0.05)
|
||||
}
|
||||
|
||||
pub fn update_references(&mut self) {
|
||||
self.camera.update_reference();
|
||||
self.zoom.update_reference();
|
||||
}
|
||||
|
||||
/// A transform which can be used to transform between clip and window space.
|
||||
/// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/direct3d9/viewports-and-clipping#viewport-rectangle) (Direct3D).
|
||||
fn clip_to_window_transform(&self) -> Matrix4<f64> {
|
||||
let min_depth = 0.0;
|
||||
let max_depth = 1.0;
|
||||
let x = 0.0;
|
||||
let y = 0.0;
|
||||
let ox = x + self.width / 2.0;
|
||||
let oy = y + self.height / 2.0;
|
||||
let oz = min_depth;
|
||||
let pz = max_depth - min_depth;
|
||||
Matrix4::from_cols(
|
||||
Vector4::new(self.width / 2.0, 0.0, 0.0, 0.0),
|
||||
Vector4::new(0.0, -self.height / 2.0, 0.0, 0.0),
|
||||
Vector4::new(0.0, 0.0, pz, 0.0),
|
||||
Vector4::new(ox, oy, oz, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Transforms coordinates in clip space to window coordinates.
|
||||
///
|
||||
/// Adopted from [here](https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline) (Direct3D).
|
||||
fn clip_to_window(&self, clip: &Vector4<f64>) -> Vector4<f64> {
|
||||
#[rustfmt::skip]
|
||||
let ndc = Vector4::new(
|
||||
clip.x / clip.w,
|
||||
clip.y / clip.w,
|
||||
clip.z / clip.w,
|
||||
1.0
|
||||
);
|
||||
|
||||
self.clip_to_window_transform() * ndc
|
||||
}
|
||||
/// Alternative implementation to `clip_to_window`. Transforms coordinates in clip space to
|
||||
/// window coordinates.
|
||||
///
|
||||
/// Adopted from [here](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkViewport.html)
|
||||
/// and [here](https://matthewwellings.com/blog/the-new-vulkan-coordinate-system/) (Vulkan).
|
||||
fn clip_to_window_vulkan(&self, clip: &Vector4<f64>) -> Vector3<f64> {
|
||||
#[rustfmt::skip]
|
||||
let ndc = Vector4::new(
|
||||
clip.x / clip.w,
|
||||
clip.y / clip.w,
|
||||
clip.z / clip.w,
|
||||
1.0
|
||||
);
|
||||
|
||||
let min_depth = 0.0;
|
||||
let max_depth = 1.0;
|
||||
|
||||
let x = 0.0;
|
||||
let y = 0.0;
|
||||
let ox = x + self.width / 2.0;
|
||||
let oy = y + self.height / 2.0;
|
||||
let oz = min_depth;
|
||||
let px = self.width;
|
||||
let py = self.height;
|
||||
let pz = max_depth - min_depth;
|
||||
let xd = ndc.x;
|
||||
let yd = ndc.y;
|
||||
let zd = ndc.z;
|
||||
Vector3::new(px / 2.0 * xd + ox, py / 2.0 * yd + oy, pz * zd + oz)
|
||||
}
|
||||
|
||||
/// Order of transformations reversed: https://computergraphics.stackexchange.com/questions/6087/screen-space-coordinates-to-eye-space-conversion/6093
|
||||
/// `w` is lost.
|
||||
///
|
||||
/// OpenGL explanation: https://www.khronos.org/opengl/wiki/Compute_eye_space_from_window_space#From_window_to_ndc
|
||||
fn window_to_world(
|
||||
&self,
|
||||
window: &Vector3<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
) -> Vector3<f64> {
|
||||
#[rustfmt::skip]
|
||||
let fixed_window = Vector4::new(
|
||||
window.x,
|
||||
window.y,
|
||||
window.z,
|
||||
1.0
|
||||
);
|
||||
|
||||
let ndc = self.clip_to_window_transform().invert().unwrap() * fixed_window;
|
||||
let unprojected = inverted_view_proj.project(ndc);
|
||||
|
||||
Vector3::new(
|
||||
unprojected.x / unprojected.w,
|
||||
unprojected.y / unprojected.w,
|
||||
unprojected.z / unprojected.w,
|
||||
)
|
||||
}
|
||||
|
||||
/// Alternative implementation to `window_to_world`
|
||||
///
|
||||
/// Adopted from [here](https://docs.rs/nalgebra-glm/latest/src/nalgebra_glm/ext/matrix_projection.rs.html#164-181).
|
||||
fn window_to_world_nalgebra(
|
||||
window: &Vector3<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) -> Vector3<f64> {
|
||||
let pt = Vector4::new(
|
||||
2.0 * (window.x - 0.0) / width - 1.0,
|
||||
2.0 * (height - window.y - 0.0) / height - 1.0,
|
||||
window.z,
|
||||
1.0,
|
||||
);
|
||||
let unprojected = inverted_view_proj.project(pt);
|
||||
|
||||
Vector3::new(
|
||||
unprojected.x / unprojected.w,
|
||||
unprojected.y / unprojected.w,
|
||||
unprojected.z / unprojected.w,
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the world coordinates for the specified `window` coordinates on the `z=0` plane.
|
||||
pub fn window_to_world_at_ground(
|
||||
&self,
|
||||
window: &Vector2<f64>,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
bound: bool,
|
||||
) -> Option<Vector2<f64>> {
|
||||
let near_world =
|
||||
self.window_to_world(&Vector3::new(window.x, window.y, 0.0), inverted_view_proj);
|
||||
|
||||
let far_world =
|
||||
self.window_to_world(&Vector3::new(window.x, window.y, 1.0), inverted_view_proj);
|
||||
|
||||
// for z = 0 in world coordinates
|
||||
// Idea comes from: https://dondi.lmu.build/share/cg/unproject-explained.pdf
|
||||
let u = -near_world.z / (far_world.z - near_world.z);
|
||||
if !bound || (0.0..=1.01).contains(&u) {
|
||||
let result = near_world + u * (far_world - near_world);
|
||||
Some(Vector2::new(result.x, result.y))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates an [`Aabb2`] bounding box which contains at least the visible area on the `z=0`
|
||||
/// plane. One can think of it as being the bounding box of the geometry which forms the
|
||||
/// intersection between the viewing frustum and the `z=0` plane.
|
||||
///
|
||||
/// This implementation works in the world 3D space. It casts rays from the corners of the
|
||||
/// window to calculate intersections points with the `z=0` plane. Then a bounding box is
|
||||
/// calculated.
|
||||
///
|
||||
/// *Note:* It is possible that no such bounding box exists. This is the case if the `z=0` plane
|
||||
/// is not in view.
|
||||
pub fn view_region_bounding_box(
|
||||
&self,
|
||||
inverted_view_proj: &InvertedViewProjection,
|
||||
) -> Option<Aabb2<f64>> {
|
||||
let screen_bounding_box = [
|
||||
Vector2::new(0.0, 0.0),
|
||||
Vector2::new(self.width, 0.0),
|
||||
Vector2::new(self.width, self.height),
|
||||
Vector2::new(0.0, self.height),
|
||||
]
|
||||
.map(|point| self.window_to_world_at_ground(&point, inverted_view_proj, false));
|
||||
|
||||
let (min, max) = bounds_from_points(
|
||||
screen_bounding_box
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|point| [point.x, point.y]),
|
||||
)?;
|
||||
|
||||
Some(Aabb2::new(Point2::from(min), Point2::from(max)))
|
||||
}
|
||||
/// An alternative implementation for `view_bounding_box`.
|
||||
///
|
||||
/// This implementation works in the NDC space. We are creating a plane in the world 3D space.
|
||||
/// Then we are transforming it to the NDC space. In NDC space it is easy to calculate
|
||||
/// the intersection points between an Aabb3 and a plane. The resulting Aabb2 is returned.
|
||||
pub fn view_region_bounding_box_ndc(&self) -> Option<Aabb2<f64>> {
|
||||
let view_proj = self.view_projection();
|
||||
let a = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0));
|
||||
let b = view_proj.project(Vector4::new(1.0, 0.0, 0.0, 1.0));
|
||||
let c = view_proj.project(Vector4::new(1.0, 1.0, 0.0, 1.0));
|
||||
|
||||
let a_ndc = self.clip_to_window(&a).truncate();
|
||||
let b_ndc = self.clip_to_window(&b).truncate();
|
||||
let c_ndc = self.clip_to_window(&c).truncate();
|
||||
let to_ndc = Vector3::new(1.0 / self.width, 1.0 / self.height, 1.0);
|
||||
let plane: Plane<f64> = Plane::from_points(
|
||||
Point3::from_vec(a_ndc.mul_element_wise(to_ndc)),
|
||||
Point3::from_vec(b_ndc.mul_element_wise(to_ndc)),
|
||||
Point3::from_vec(c_ndc.mul_element_wise(to_ndc)),
|
||||
)?;
|
||||
|
||||
let points = plane.intersection_points_aabb3(&Aabb3::new(
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Point3::new(1.0, 1.0, 1.0),
|
||||
));
|
||||
|
||||
let inverted_view_proj = view_proj.invert();
|
||||
|
||||
let from_ndc = Vector3::new(self.width, self.height, 1.0);
|
||||
let vec = points
|
||||
.iter()
|
||||
.map(|point| {
|
||||
self.window_to_world(&point.mul_element_wise(from_ndc), &inverted_view_proj)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let min_x = vec
|
||||
.iter()
|
||||
.map(|point| point.x)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let min_y = vec
|
||||
.iter()
|
||||
.map(|point| point.y)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let max_x = vec
|
||||
.iter()
|
||||
.map(|point| point.x)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
let max_y = vec
|
||||
.iter()
|
||||
.map(|point| point.y)
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())?;
|
||||
Some(Aabb2::new(
|
||||
Point2::new(min_x, min_y),
|
||||
Point2::new(max_x, max_y),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cgmath::{Deg, Matrix4, Vector2, Vector4};
|
||||
|
||||
use crate::{
|
||||
coords::{WorldCoords, Zoom},
|
||||
render::view_state::ViewState,
|
||||
window::WindowSize,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn conform_transformation() {
|
||||
let fov = Deg(60.0);
|
||||
let mut state = ViewState::new(
|
||||
WindowSize::new(800, 600).unwrap(),
|
||||
WorldCoords::at_ground(0.0, 0.0),
|
||||
Zoom::new(10.0),
|
||||
Deg(0.0),
|
||||
fov,
|
||||
);
|
||||
|
||||
//state.furthest_distance(state.camera_to_center_distance(), Point2::new(0.0, 0.0));
|
||||
|
||||
let projection = state.view_projection().invert();
|
||||
|
||||
let bottom_left = state
|
||||
.window_to_world_at_ground(&Vector2::new(0.0, 0.0), &projection, true)
|
||||
.unwrap();
|
||||
println!("bottom left on ground {:?}", bottom_left);
|
||||
let top_right = state
|
||||
.window_to_world_at_ground(&Vector2::new(state.width, state.height), &projection, true)
|
||||
.unwrap();
|
||||
println!("top right on ground {:?}", top_right);
|
||||
|
||||
let mut rotated = Matrix4::from_angle_x(Deg(-30.0))
|
||||
* Vector4::new(bottom_left.x, bottom_left.y, 0.0, 0.0);
|
||||
|
||||
println!("bottom left rotated around x axis {:?}", rotated);
|
||||
|
||||
rotated = Matrix4::from_angle_y(Deg(-30.0)) * rotated;
|
||||
|
||||
println!("bottom left rotated around x and y axis {:?}", rotated);
|
||||
|
||||
state.camera.set_pitch(Deg(30.0));
|
||||
//state.camera.set_yaw(Deg(-30.0));
|
||||
|
||||
// TODO: verify far distance plane calculation
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ use crate::{
|
||||
source_type::{SourceType, TessellateSource},
|
||||
},
|
||||
kernel::Kernel,
|
||||
render::tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
style::layer::LayerPaint,
|
||||
tcs::system::System,
|
||||
vector::{
|
||||
@ -48,7 +49,8 @@ impl<E: Environment, T: VectorTransferables> System for RequestSystem<E, T> {
|
||||
}: &mut MapContext,
|
||||
) {
|
||||
let _tiles = &mut world.tiles;
|
||||
let view_region = view_state.create_view_region();
|
||||
let view_region =
|
||||
view_state.create_view_region(view_state.zoom().zoom_level(DEFAULT_TILE_SIZE));
|
||||
|
||||
if view_state.did_camera_change() || view_state.did_zoom_change() {
|
||||
if let Some(view_region) = &view_region {
|
||||
|
||||
@ -8,6 +8,7 @@ use crate::{
|
||||
render::{
|
||||
eventually::{Eventually, Eventually::Initialized},
|
||||
shaders::{ShaderFeatureStyle, ShaderLayerMetadata, Vec4f32},
|
||||
tile_view_pattern::DEFAULT_TILE_SIZE,
|
||||
Renderer,
|
||||
},
|
||||
style::Style,
|
||||
@ -33,7 +34,8 @@ pub fn upload_system(
|
||||
return;
|
||||
};
|
||||
|
||||
let view_region = view_state.create_view_region();
|
||||
let view_region =
|
||||
view_state.create_view_region(view_state.zoom().zoom_level(DEFAULT_TILE_SIZE));
|
||||
|
||||
if let Some(view_region) = &view_region {
|
||||
upload_tesselated_layer(
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use cgmath::Angle;
|
||||
|
||||
use crate::{
|
||||
coords::{ViewRegion, WorldCoords, Zoom, ZoomLevel, TILE_SIZE},
|
||||
render::camera::{Camera, Perspective, ViewProjection},
|
||||
util::ChangeObserver,
|
||||
window::WindowSize,
|
||||
};
|
||||
|
||||
const VIEW_REGION_PADDING: i32 = 1;
|
||||
|
||||
/// Stores the camera configuration.
|
||||
pub struct ViewState {
|
||||
zoom: ChangeObserver<Zoom>,
|
||||
camera: ChangeObserver<Camera>,
|
||||
perspective: Perspective,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
pub fn new<F: Into<cgmath::Rad<f64>>, P: Into<cgmath::Deg<f64>>>(
|
||||
window_size: WindowSize,
|
||||
position: WorldCoords,
|
||||
zoom: Zoom,
|
||||
pitch: P,
|
||||
fovy: F,
|
||||
) -> Self {
|
||||
let tile_center = TILE_SIZE / 2.0;
|
||||
let fovy = fovy.into();
|
||||
let height = tile_center / (fovy / 2.0).tan();
|
||||
|
||||
let camera = Camera::new(
|
||||
(position.x, position.y, height),
|
||||
cgmath::Deg(-90.0),
|
||||
pitch.into(),
|
||||
window_size.width(),
|
||||
window_size.height(),
|
||||
);
|
||||
|
||||
let perspective = Perspective::new(
|
||||
window_size.width(),
|
||||
window_size.height(),
|
||||
cgmath::Deg(110.0),
|
||||
// in tile.vertex.wgsl we are setting each layer's final `z` in ndc space to `z_index`.
|
||||
// This means that regardless of the `znear` value all layers will be rendered as part
|
||||
// of the near plane.
|
||||
// These values have been selected experimentally:
|
||||
// https://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
|
||||
1024.0,
|
||||
2048.0,
|
||||
);
|
||||
|
||||
Self {
|
||||
zoom: ChangeObserver::new(zoom),
|
||||
camera: ChangeObserver::new(camera),
|
||||
perspective,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.perspective.resize(width, height);
|
||||
self.camera.resize(width, height);
|
||||
}
|
||||
|
||||
pub fn create_view_region(&self) -> Option<ViewRegion> {
|
||||
self.camera
|
||||
.view_region_bounding_box(&self.view_projection().invert())
|
||||
.map(|bounding_box| {
|
||||
ViewRegion::new(
|
||||
bounding_box,
|
||||
VIEW_REGION_PADDING,
|
||||
32,
|
||||
*self.zoom,
|
||||
self.visible_level(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_projection(&self) -> ViewProjection {
|
||||
self.camera.calc_view_proj(&self.perspective)
|
||||
}
|
||||
|
||||
pub fn visible_level(&self) -> ZoomLevel {
|
||||
self.zoom.level()
|
||||
}
|
||||
|
||||
pub fn zoom(&self) -> Zoom {
|
||||
*self.zoom
|
||||
}
|
||||
|
||||
pub fn did_zoom_change(&self) -> bool {
|
||||
self.zoom.did_change(0.05)
|
||||
}
|
||||
|
||||
pub fn update_zoom(&mut self, new_zoom: Zoom) {
|
||||
*self.zoom = new_zoom;
|
||||
log::info!("zoom: {new_zoom}");
|
||||
}
|
||||
|
||||
pub fn camera(&self) -> &Camera {
|
||||
self.camera.deref()
|
||||
}
|
||||
|
||||
pub fn camera_mut(&mut self) -> &mut Camera {
|
||||
self.camera.deref_mut()
|
||||
}
|
||||
|
||||
pub fn did_camera_change(&self) -> bool {
|
||||
self.camera.did_change(0.05)
|
||||
}
|
||||
|
||||
pub fn update_references(&mut self) {
|
||||
self.camera.update_reference();
|
||||
self.zoom.update_reference();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user