diff --git a/Cargo.lock b/Cargo.lock index 3de6c0b..cdeede6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1282,7 +1282,7 @@ dependencies = [ [[package]] name = "bevy_web_asset" version = "0.7.1" -source = "git+https://github.com/oli-obk/bevy_web_asset.git?branch=android#dfe8f5b6e7bf1dbbde2c1a8a577f5a381c6a6e77" +source = "git+https://github.com/oli-obk/bevy_web_asset.git?branch=user-agent#bf804cc5a87b7d0a7aa90810d77c4b2279034390" dependencies = [ "bevy", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index eecf4a2..d71622e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ bevy_screen_diagnostics = { git = "https://github.com/oli-obk/bevy_screen_diagno globe-rs = "0.1.8" directories = "5.0.1" async-fs = "2.1.0" -bevy_web_asset = { git = "https://github.com/oli-obk/bevy_web_asset.git", branch = "android" } +bevy_web_asset = { git = "https://github.com/oli-obk/bevy_web_asset.git", branch = "user-agent" } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.22", default-features = false, features = [ diff --git a/src/geopos.rs b/src/geopos.rs index 1b6b16a..ae25d46 100644 --- a/src/geopos.rs +++ b/src/geopos.rs @@ -36,8 +36,7 @@ impl GeoPos { x: ((self.lon + 180.) / 360. * pow_zoom), // y: Latitude, (Breitengrad) Nort/South "index" - y: ((1. - - ((self.lat * PI / 180.).tan() + 1. / (self.lat * PI / 180.).cos()).ln() / PI) + y: ((1. - (self.lat.to_radians().tan() + 1. / self.lat.to_radians().cos()).ln() / PI) / 2. * pow_zoom), // The Nort/South y tile name part is not linear, the tiles gets stretched to the poles @@ -47,8 +46,8 @@ impl GeoPos { pub fn to_cartesian(self) -> Vec3 { let geo = GeographicPoint::new( - self.lon as f64 / 180.0 * std::f64::consts::PI, - self.lat as f64 / 180.0 * std::f64::consts::PI, + (self.lon as f64).to_radians(), + (self.lat as f64).to_radians(), EARTH_RADIUS as f64, ); let cart = CartesianPoint::from_geographic(&geo); @@ -60,19 +59,25 @@ impl GeoPos { let cart = CartesianPoint::new(pos.x, pos.y, pos.z); let geo = GeographicPoint::from_cartesian(&cart); GeoPos { - lat: geo.latitude() as f32 / PI * 180.0, - lon: geo.longitude() as f32 / PI * 180.0, + lat: (geo.latitude() as f32).to_degrees(), + lon: (geo.longitude() as f32).to_degrees(), } } /// Tile width and height in meters pub fn tile_size(self, zoom: u8) -> Vec2 { - let pow_zoom = 2_u32.pow(zoom.into()) as f32; - let tile_width = EQUATOR_METERS / pow_zoom; - let tile_height = tile_width * self.lat.cos(); - Vec2::new(tile_width, tile_height.abs()) + let coord = self.to_tile_coordinates(zoom); + let pos = self.to_cartesian(); + let x = TileCoord(coord.0 + Vec2::X) + .to_geo_pos(zoom) + .to_cartesian() + .distance(pos); + let y = TileCoord(coord.0 + Vec2::Y) + .to_geo_pos(zoom) + .to_cartesian() + .distance(pos); + Vec2 { x, y } } } pub const EARTH_RADIUS: f32 = 6378000.; -pub const EQUATOR_METERS: f32 = 40_075_016.686; diff --git a/src/lib.rs b/src/lib.rs index f7b9d86..e57e457 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,9 @@ pub fn main() { app.add_plugins(HttpAssetReaderPlugin { base_url: "gltiles.osm2world.org/glb/".into(), }); + app.add_plugins(bevy_web_asset::WebAssetPlugin { + user_agent: Some("osmeta 0.1.0".into()), + }); if xr { #[cfg(all(feature = "xr", not(any(target_os = "macos", target_arch = "wasm32"))))] app.add_plugins(xr::Plugin); @@ -94,16 +97,11 @@ pub fn main() { .run(); } -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut diags: ResMut, - pos: Res, -) { +fn setup(mut commands: Commands, mut diags: ResMut, pos: Res) { diags.modify("fps").aggregate(Aggregate::Average); commands.spawn(( - TileMap::new(&mut meshes), + TileMap::default(), SpatialBundle { transform: Transform::from_translation(-pos.0), ..default() @@ -152,6 +150,7 @@ fn load_next_tile( Without, ), >, + mut meshes: ResMut>, diagnostics: Res, mut fog: Query<&mut FogSettings>, ) { @@ -171,7 +170,7 @@ fn load_next_tile( } else if fps > 59.5 { sky.scale = Vec3::splat(sky.scale.x * 1.01) } - sky.scale = Vec3::splat(sky.scale.x.clamp(1000.0, 3000.0)); + sky.scale = Vec3::splat(sky.scale.x.clamp(1000.0, 10000.0)); fog.single_mut().falloff = FogFalloff::from_visibility_colors( sky.scale.x, // distance in world units up to which objects retain visibility (>= 5% contrast) Color::rgb(0.35, 0.5, 0.66), // atmospheric extinction color (after light is lost due to absorption by atmospheric particles) @@ -189,6 +188,7 @@ fn load_next_tile( id, &mut commands, &server, + &mut meshes, // FIXME: Maybe use https://crates.io/crates/big_space in order to be able to remove // the translation from the tilemap and instead just use its real coordinates. origin, diff --git a/src/tilemap.rs b/src/tilemap.rs index 2bae089..26a65c9 100644 --- a/src/tilemap.rs +++ b/src/tilemap.rs @@ -1,6 +1,10 @@ use std::{collections::BTreeMap, f32::consts::PI}; -use bevy::{gltf::Gltf, prelude::*}; +use bevy::{ + gltf::Gltf, + prelude::*, + render::{mesh::Indices, render_resource::PrimitiveTopology}, +}; use crate::geopos::GeoPos; @@ -10,8 +14,6 @@ pub struct TileMap { tiles: BTreeMap>, /// The tile currently being loaded. loading: Option<(UVec2, Handle)>, - /// Dummy square to show while a scene is loading - dummy: Handle, } pub const TILE_ZOOM: u8 = 15; @@ -25,6 +27,7 @@ impl TileMap { tilemap_id: Entity, commands: &mut Commands, server: &AssetServer, + meshes: &mut Assets, origin: TileCoord, radius: Vec2, ) { @@ -62,7 +65,7 @@ impl TileMap { } } if let Some(best_pos) = best_pos { - self.load(tilemap_id, commands, server, best_pos); + self.load(tilemap_id, commands, server, meshes, best_pos); } } @@ -88,6 +91,7 @@ impl TileMap { tilemap_id: Entity, commands: &mut Commands, server: &AssetServer, + meshes: &mut Assets, pos: UVec2, ) { if self.loading.is_some() { @@ -103,15 +107,9 @@ impl TileMap { .or_default() .entry(pos.y) .or_insert_with(|| { - let transform = Self::test_transform(pos); - - let id = commands - .spawn(PbrBundle { - mesh: self.dummy.clone(), - transform, - ..default() - }) - .id(); + let mesh = meshes.add(flat_tile(pos).1); + + let id = commands.spawn(PbrBundle { mesh, ..default() }).id(); commands.entity(tilemap_id).add_child(id); id }); @@ -121,6 +119,8 @@ impl TileMap { mut commands: Commands, server: Res, scenes: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, mut tilemap: Query<(Entity, &mut Self)>, ) { for (id, mut tilemap) in &mut tilemap { @@ -131,7 +131,7 @@ impl TileMap { NotLoaded | Loading => { tilemap.loading = Some((pos, scene)); } - Loaded => { + state @ (Loaded | Failed) => { // FIXME: implement caching of downloaded assets by implementing something like // https://github.com/bevyengine/bevy/blob/main/examples/asset/processing/asset_processing.rs @@ -141,53 +141,58 @@ impl TileMap { continue; }; - let transform = Self::test_transform(pos); - let scene = scenes.get(scene).unwrap().scenes[0].clone(); - let tile = commands - .spawn(( - SceneBundle { - scene, // "models/17430_11371.glb#Scene0" - transform, + let tile = match state { + NotLoaded | Loading => unreachable!(), + Loaded => { + let transform = Self::test_transform(pos); + let scene = scenes.get(scene).unwrap().scenes[0].clone(); + commands + .spawn(( + SceneBundle { + scene, // "models/17430_11371.glb#Scene0" + transform, + ..default() + }, + Tile, + )) + .id() + } + Failed => { + warn!("failed to load tile {pos} from network, switching to flat tile"); + + let (coord, mesh) = flat_tile(pos); + let mesh = meshes.add(mesh); + let image: Handle = server.load(format!( + "https://a.tile.openstreetmap.org/{TILE_ZOOM}/{}/{}.png", + coord.0.x, coord.0.y + )); + let material = materials.add(StandardMaterial { + base_color_texture: Some(image), + perceptual_roughness: 1.0, ..default() - }, - Tile, - )) - .id(); + }); + commands + .spawn(PbrBundle { + mesh, + material, + ..default() + }) + .id() + } + }; commands.entity(id).add_child(tile); let dummy = std::mem::replace(entity, tile); if let Some(mut entity) = commands.get_entity(dummy) { entity.despawn(); } } - Failed => { - error!("failed to load tile {pos} from network"); - } } } } } - pub fn new(meshes: &mut Assets) -> Self { - // FIXME: compute dummy tile size on the fly - let half = 814.5 / 2.0; - Self { - dummy: meshes.add( - shape::Box { - min_x: -half, - max_x: half, - min_y: 0.0, - max_y: 1.0, - min_z: -half, - max_z: half, - } - .into(), - ), - ..default() - } - } - fn test_transform(pos: UVec2) -> Transform { - let coord = TileCoord(pos.as_vec2()); + let coord = TileCoord(pos.as_vec2() + 0.5); let pos = coord.to_geo_pos(TILE_ZOOM).to_cartesian(); let next = TileCoord(Vec2 { x: coord.0.x, @@ -199,6 +204,40 @@ impl TileMap { } } +// Compute a square mesh at the position for the given tile. +fn flat_tile(pos: UVec2) -> (TileCoord, Mesh) { + let coord = TileCoord(pos.as_vec2()); + + // Four corners of the tile in cartesian coordinates relative to the + // planet's center. + let a = coord.to_geo_pos(TILE_ZOOM).to_cartesian(); + let b = TileCoord(pos.as_vec2() + Vec2::X) + .to_geo_pos(TILE_ZOOM) + .to_cartesian(); + let c = TileCoord(pos.as_vec2() + 1.) + .to_geo_pos(TILE_ZOOM) + .to_cartesian(); + let d = TileCoord(pos.as_vec2() + Vec2::Y) + .to_geo_pos(TILE_ZOOM) + .to_cartesian(); + + // Normals on a sphere are just the position on the sphere normalized. + let normal = a.normalize(); + + let positions = vec![a.to_array(), b.to_array(), c.to_array(), d.to_array()]; + let normals = vec![normal; 4]; + let uvs = vec![Vec2::ZERO, Vec2::X, Vec2::splat(1.0), Vec2::Y]; + + let indices = Indices::U32(vec![0, 3, 2, 2, 1, 0]); + + let mesh = Mesh::new(PrimitiveTopology::TriangleList) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_indices(Some(indices)); + (coord, mesh) +} + /// A coordinate in the OWM tile coordinate system. Allows for positions within a tile. ??? #[derive(Debug, Copy, Clone)] pub struct TileCoord(pub Vec2); @@ -209,7 +248,7 @@ impl TileCoord { let lon = self.0.x / pow_zoom * 360.0 - 180.0; let lat_rad = (PI * (1. - 2. * self.0.y / pow_zoom)).sinh().atan(); - let lat = lat_rad * 180.0 / PI; + let lat = lat_rad.to_degrees(); GeoPos { lat, lon } } }