diff --git a/benchmarks/benches/data.rs b/benchmarks/benches/data.rs index a58677ea6..981e0f009 100644 --- a/benchmarks/benches/data.rs +++ b/benchmarks/benches/data.rs @@ -32,6 +32,7 @@ fn parse_tile(c: &mut Criterion) { .into_world_tile(TileAddressingScheme::XYZ) .unwrap(), layers: HashSet::from(["boundary".to_owned(), "water".to_owned()]), + style: Default::default(), }; let data = fetcher .sync_fetch_tile(&MUNICH_COORDS) @@ -54,6 +55,7 @@ fn tessellate_tile(c: &mut Criterion) { .into_world_tile(TileAddressingScheme::XYZ) .unwrap(), layers: HashSet::from(["boundary".to_owned(), "water".to_owned()]), + style: Default::default(), }; let data = fetcher .sync_fetch_tile(&MUNICH_COORDS) diff --git a/maplibre/src/coords.rs b/maplibre/src/coords.rs index e0426462d..de2c27ced 100644 --- a/maplibre/src/coords.rs +++ b/maplibre/src/coords.rs @@ -377,8 +377,11 @@ impl WorldTileCoords { // Divide by EXTENT to normalize tile // Scale tiles where zoom level = self.z to 512x512 - let normalize_and_scale = - Matrix4::from_nonuniform_scale(tile_scale / EXTENT, tile_scale / EXTENT, 1.0); + let normalize_and_scale = Matrix4::from_nonuniform_scale( + tile_scale / EXTENT, + tile_scale / EXTENT, + tile_scale / EXTENT, + ); translate * normalize_and_scale } diff --git a/maplibre/src/headless/map.rs b/maplibre/src/headless/map.rs index 37916e558..76ab0c83f 100644 --- a/maplibre/src/headless/map.rs +++ b/maplibre/src/headless/map.rs @@ -114,6 +114,7 @@ impl HeadlessMap { .iter() .map(|layer| layer.to_string()) .collect::>(), + style: Default::default(), }, tile_data, ), diff --git a/maplibre/src/io/mod.rs b/maplibre/src/io/mod.rs index f8c784a2d..cbc6b4709 100644 --- a/maplibre/src/io/mod.rs +++ b/maplibre/src/io/mod.rs @@ -19,11 +19,14 @@ pub mod transferables; pub use geozero::mvt::tile::Layer as RawLayer; +use crate::style::Style; + /// A request for a tile at the given coordinates and in the given layers. #[derive(Clone, Serialize, Deserialize)] pub struct TileRequest { pub coords: WorldTileCoords, pub layers: HashSet, + pub style: Style, } impl fmt::Debug for TileRequest { diff --git a/maplibre/src/io/tile_pipelines.rs b/maplibre/src/io/tile_pipelines.rs index 2029bf5ab..391d5f526 100644 --- a/maplibre/src/io/tile_pipelines.rs +++ b/maplibre/src/io/tile_pipelines.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use cgmath::InnerSpace; use geozero::GeozeroDatasource; use prost::Message; @@ -9,6 +10,7 @@ use crate::{ pipeline::{DataPipeline, PipelineContext, PipelineEnd, PipelineError, Processable}, TileRequest, }, + render::ShaderVertex, tessellation::{zero_tessellator::ZeroTessellator, IndexDataType}, }; @@ -97,6 +99,106 @@ impl Processable for TessellateLayer { e ); } else { + let layer_style = tile_request + .style + .layers + .iter() + .find(|layer_style| { + layer.name == *layer_style.source_layer.as_ref().unwrap_or(&"".to_string()) + }) + .unwrap(); + + // Extrude all the buildings on the z axis if osm_3d_extrusion is enabled on the layer + if layer_style.extrusion { + // We create a list of all the outer/contour edges. Meaning that these + // edges are not inside the 2d mesh, and a "wall" should be instantiated for them. + // In order to do that, we create a `HashSet` of every edge that appears only + // once in the entire layer. + let mut contour_edges: HashSet<(u32, u32)> = + HashSet::with_capacity(tessellator.buffer.indices.len()); + for i in 0..tessellator.buffer.indices.len() { + let a = tessellator.buffer.indices[i]; + let b = tessellator.buffer.indices + [if (i + 1) % 3 == 0 { i - 2 } else { i + 1 }]; + + // If the contour edge already exist, it is an inner edge and not a contour edge so we remove it + if contour_edges.contains(&(b, a)) { + contour_edges.remove(&(b, a)); + } else { + contour_edges.insert((a, b)); + } + } + + // For each "wall" of the buildings, we create 2 triangles in the clockwise + // direction so that their normals are facing outward. + let mut extruded_vertices = vec![]; + let mut side_faces_indices = vec![]; + for mut edge in contour_edges { + let edge_vector = [ + tessellator.buffer.vertices[edge.1 as usize].position[0] + - tessellator.buffer.vertices[edge.0 as usize].position[0], + tessellator.buffer.vertices[edge.1 as usize].position[1] + - tessellator.buffer.vertices[edge.0 as usize].position[1], + 0.0, + ]; + let normal_vector = + cgmath::Vector3::from([-edge_vector[1], edge_vector[0], 0.0]) + .normalize() + .into(); + let a_position = tessellator.buffer.vertices[edge.0 as usize].position; + let b_position = tessellator.buffer.vertices[edge.1 as usize].position; + extruded_vertices.push(ShaderVertex::new( + [a_position[0], a_position[1], 0.0], + normal_vector, + )); + let a = (extruded_vertices.len() + tessellator.buffer.vertices.len() - 1) + as u32; + extruded_vertices.push(ShaderVertex::new( + [b_position[0], b_position[1], 0.0], + normal_vector, + )); + let b = (extruded_vertices.len() + tessellator.buffer.vertices.len() - 1) + as u32; + extruded_vertices.push(ShaderVertex::new( + [a_position[0], a_position[1], 40.0], + normal_vector, + )); + let a_extruded = (extruded_vertices.len() + + tessellator.buffer.vertices.len() + - 1) as u32; + extruded_vertices.push(ShaderVertex::new( + [b_position[0], b_position[1], 40.0], + normal_vector, + )); + let b_extruded = (extruded_vertices.len() + + tessellator.buffer.vertices.len() + - 1) as u32; + side_faces_indices.push(a); + side_faces_indices.push(b_extruded); + side_faces_indices.push(a_extruded); + side_faces_indices.push(b); + side_faces_indices.push(b_extruded); + side_faces_indices.push(a); + } + + // We move the vertices to the top, because the bottom will not be visible anyway. + for i in 0..tessellator.buffer.vertices.len() { + tessellator.buffer.vertices[i] = ShaderVertex::new( + [ + tessellator.buffer.vertices[i].position[0], + tessellator.buffer.vertices[i].position[1], + 40.0, + ], + tessellator.buffer.vertices[i].normal, + ); + } + + // We insert the new walls to the buffer. + tessellator.buffer.vertices.extend(extruded_vertices.iter()); + tessellator.buffer.indices.extend(side_faces_indices.iter()); + } + + // We send the tessellated layer to the pipeline. context.processor_mut().layer_tesselation_finished( coords, tessellator.buffer.into(), @@ -169,6 +271,7 @@ mod tests { TileRequest { coords: (0, 0, ZoomLevel::default()).into(), layers: Default::default(), + style: Default::default(), }, Box::new([0]), // TODO: Add proper tile byte array ), diff --git a/maplibre/src/render/shaders/light.fragment.wgsl b/maplibre/src/render/shaders/light.fragment.wgsl new file mode 100644 index 000000000..23eb23e3a --- /dev/null +++ b/maplibre/src/render/shaders/light.fragment.wgsl @@ -0,0 +1,35 @@ +struct ShaderCamera { + view_proj: mat4x4, + view_position: vec4, +}; + +struct ShaderLight { + direction: vec4, + color: vec4, +} + +struct ShaderGlobals { + camera: ShaderCamera, + light: ShaderLight, +}; + +@group(0) @binding(0) var globals: ShaderGlobals; + +struct Output { + @location(0) out_color: vec4, +}; + +@fragment +fn main(@builtin(position) position: vec4, @location(0) v_color: vec4, @location(1) normal: vec3) -> Output { + + // We don't need (or want) much ambient light, so 0.1 is fine + let ambient_strength = 0.1; + let ambient_color = globals.light.color.xyz * ambient_strength; + + let diffuse_strength = max(dot(normal, globals.light.direction.xyz), 0.0); + let diffuse_color = globals.light.color.xyz * diffuse_strength; + + let result = (ambient_color + diffuse_color) * v_color.xyz; + + return Output(vec4(result, v_color.a)); +} diff --git a/maplibre/src/render/shaders/mod.rs b/maplibre/src/render/shaders/mod.rs index 0bdfc679a..2703b0748 100644 --- a/maplibre/src/render/shaders/mod.rs +++ b/maplibre/src/render/shaders/mod.rs @@ -104,13 +104,13 @@ impl Shader for TileShader { // position wgpu::VertexAttribute { offset: 0, - format: wgpu::VertexFormat::Float32x2, + format: wgpu::VertexFormat::Float32x3, shader_location: 0, }, // normal wgpu::VertexAttribute { - offset: wgpu::VertexFormat::Float32x2.size(), - format: wgpu::VertexFormat::Float32x2, + offset: wgpu::VertexFormat::Float32x3.size(), + format: wgpu::VertexFormat::Float32x3, shader_location: 1, }, ], @@ -181,7 +181,7 @@ impl Shader for TileShader { fn describe_fragment(&self) -> FragmentState { FragmentState { - source: include_str!("basic.fragment.wgsl"), + source: include_str!("light.fragment.wgsl"), entry_point: "main", targets: vec![Some(wgpu::ColorTargetState { format: self.format, @@ -217,16 +217,40 @@ impl Default for ShaderCamera { } } +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct ShaderLight { + direction: Vec4f32, + color: Vec4f32, +} + +impl ShaderLight { + pub fn new(direction: Vec4f32, color: Vec4f32) -> Self { + Self { direction, color } + } +} + +impl Default for ShaderLight { + fn default() -> Self { + Self { + direction: [-0.3, -0.4, 1.0, 0.0], // Sun orientation + color: [1.0, 1.0, 1.0, 1.0], // Sun color and intensity + } + } +} + #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct ShaderGlobals { camera: ShaderCamera, + light: ShaderLight, } impl ShaderGlobals { - pub fn new(camera_uniform: ShaderCamera) -> Self { + pub fn new(camera_uniform: ShaderCamera, light_uniform: ShaderLight) -> Self { Self { camera: camera_uniform, + light: light_uniform, } } } @@ -234,19 +258,19 @@ impl ShaderGlobals { #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct ShaderVertex { - pub position: Vec2f32, - pub normal: Vec2f32, + pub position: Vec3f32, + pub normal: Vec3f32, } impl ShaderVertex { - pub fn new(position: Vec2f32, normal: Vec2f32) -> Self { + pub fn new(position: Vec3f32, normal: Vec3f32) -> Self { Self { position, normal } } } impl Default for ShaderVertex { fn default() -> Self { - ShaderVertex::new([0.0, 0.0], [0.0, 0.0]) + ShaderVertex::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) } } diff --git a/maplibre/src/render/shaders/tile.vertex.wgsl b/maplibre/src/render/shaders/tile.vertex.wgsl index fd3ed512c..b648fcf90 100644 --- a/maplibre/src/render/shaders/tile.vertex.wgsl +++ b/maplibre/src/render/shaders/tile.vertex.wgsl @@ -3,21 +3,28 @@ struct ShaderCamera { view_position: vec4, }; +struct ShaderLight { + direction: vec4, + color: vec4, +} + struct ShaderGlobals { camera: ShaderCamera, + light: ShaderLight, }; @group(0) @binding(0) var globals: ShaderGlobals; struct VertexOutput { - @location(0) v_color: vec4, @builtin(position) position: vec4, + @location(0) v_color: vec4, + @location(1) normal: vec3, }; @vertex fn main( - @location(0) position: vec2, - @location(1) normal: vec2, + @location(0) position: vec3, + @location(1) normal: vec3, @location(4) translate1: vec4, @location(5) translate2: vec4, @location(6) translate3: vec4, @@ -27,7 +34,6 @@ fn main( @location(10) z_index: f32, @builtin(instance_index) instance_idx: u32 // instance_index is used when we have multiple instances of the same "object" ) -> VertexOutput { - let z = 0.0; let width = 3.0 * zoom_factor; // The following code moves all "invisible" vertices to (0, 0, 0) @@ -35,9 +41,9 @@ fn main( // return VertexOutput(color, vec4(0.0, 0.0, 0.0, 1.0)); //} - var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(position + normal * width, z, 1.0); + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(position + normal * width, 1.0); // FIXME: how to fix z-fighting? position.z = z_index; - return VertexOutput(color, position); + return VertexOutput(position, color, normal); } diff --git a/maplibre/src/render/stages/upload_stage.rs b/maplibre/src/render/stages/upload_stage.rs index 6da45dd19..8b9d2b8f9 100644 --- a/maplibre/src/render/stages/upload_stage.rs +++ b/maplibre/src/render/stages/upload_stage.rs @@ -9,7 +9,10 @@ use crate::{ render::{ camera::ViewProjection, eventually::Eventually::Initialized, - shaders::{ShaderCamera, ShaderFeatureStyle, ShaderGlobals, ShaderLayerMetadata, Vec4f32}, + shaders::{ + ShaderCamera, ShaderFeatureStyle, ShaderGlobals, ShaderLayerMetadata, ShaderLight, + Vec4f32, + }, RenderState, Renderer, }, schedule::Stage, @@ -43,16 +46,19 @@ impl Stage for UploadStage { queue.write_buffer( &globals_bind_group.uniform_buffer, 0, - bytemuck::cast_slice(&[ShaderGlobals::new(ShaderCamera::new( - view_proj.downcast().into(), - view_state - .camera() - .position() - .to_homogeneous() - .cast::() - .unwrap() // TODO: Remove unwrap - .into(), - ))]), + bytemuck::cast_slice(&[ShaderGlobals::new( + ShaderCamera::new( + view_proj.downcast().into(), + view_state + .camera() + .position() + .to_homogeneous() + .cast::() + .unwrap() // TODO: Remove unwrap + .into(), + ), + ShaderLight::default(), + )]), ); } diff --git a/maplibre/src/render/tile_pipeline.rs b/maplibre/src/render/tile_pipeline.rs index 56750d7dd..43515424c 100644 --- a/maplibre/src/render/tile_pipeline.rs +++ b/maplibre/src/render/tile_pipeline.rs @@ -85,7 +85,7 @@ impl RenderPipeline for TilePipeline { layout: if self.bind_globals { Some(vec![vec![wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, diff --git a/maplibre/src/stages/request_stage.rs b/maplibre/src/stages/request_stage.rs index 824657289..e2228dceb 100644 --- a/maplibre/src/stages/request_stage.rs +++ b/maplibre/src/stages/request_stage.rs @@ -131,7 +131,7 @@ impl RequestStage { for coords in view_region.iter() { if coords.build_quad_key().is_some() { // TODO: Make tesselation depend on style? - self.request_tile(tile_repository, coords, &source_layers); + self.request_tile(tile_repository, coords, &source_layers, style); } } } @@ -141,6 +141,7 @@ impl RequestStage { tile_repository: &mut TileRepository, coords: WorldTileCoords, layers: &HashSet, + style: &Style, ) { if tile_repository.is_tile_pending_or_done(&coords) { tile_repository.mark_tile_pending(coords).unwrap(); // TODO: Remove unwrap @@ -153,6 +154,7 @@ impl RequestStage { Input::TileRequest(TileRequest { coords, layers: layers.clone(), + style: style.clone(), }), schedule::< E, diff --git a/maplibre/src/style/layer.rs b/maplibre/src/style/layer.rs index 0a28d1297..8fa2988c8 100644 --- a/maplibre/src/style/layer.rs +++ b/maplibre/src/style/layer.rs @@ -78,6 +78,12 @@ pub struct StyleLayer { pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub source_layer: Option, + #[serde(default = "default_extrusion")] + pub extrusion: bool, +} + +fn default_extrusion() -> bool { + false } impl Default for StyleLayer { @@ -92,6 +98,7 @@ impl Default for StyleLayer { paint: None, source: None, source_layer: Some("does not exist".to_string()), + extrusion: false, } } } diff --git a/maplibre/src/style/style.rs b/maplibre/src/style/style.rs index b8d890a5f..9153d1344 100644 --- a/maplibre/src/style/style.rs +++ b/maplibre/src/style/style.rs @@ -46,6 +46,7 @@ impl Default for Style { })), source: None, source_layer: Some("park".to_string()), + extrusion: false, }, StyleLayer { index: 1, @@ -59,6 +60,7 @@ impl Default for Style { })), source: None, source_layer: Some("landuse".to_string()), + extrusion: false, }, StyleLayer { index: 2, @@ -72,6 +74,7 @@ impl Default for Style { })), source: None, source_layer: Some("landcover".to_string()), + extrusion: false, }, StyleLayer { index: 3, @@ -85,6 +88,7 @@ impl Default for Style { })), source: None, source_layer: Some("transportation".to_string()), + extrusion: false, }, StyleLayer { index: 4, @@ -98,6 +102,7 @@ impl Default for Style { })), source: None, source_layer: Some("building".to_string()), + extrusion: true, }, StyleLayer { index: 4, @@ -111,6 +116,7 @@ impl Default for Style { })), source: None, source_layer: Some("water".to_string()), + extrusion: false, }, StyleLayer { index: 6, @@ -124,6 +130,7 @@ impl Default for Style { })), source: None, source_layer: Some("waterway".to_string()), + extrusion: false, }, StyleLayer { index: 7, @@ -137,6 +144,7 @@ impl Default for Style { })), source: None, source_layer: Some("boundary".to_string()), + extrusion: false, }, ], } diff --git a/maplibre/src/tessellation/mod.rs b/maplibre/src/tessellation/mod.rs index 56bfce8be..b1058e4d8 100644 --- a/maplibre/src/tessellation/mod.rs +++ b/maplibre/src/tessellation/mod.rs @@ -19,15 +19,18 @@ pub struct VertexConstructor {} impl FillVertexConstructor for VertexConstructor { fn new_vertex(&mut self, vertex: FillVertex) -> ShaderVertex { - ShaderVertex::new(vertex.position().to_array(), [0.0, 0.0]) + ShaderVertex::new( + [vertex.position().x, vertex.position().y, 0.0], + [0.0, 0.0, 1.0], + ) } } impl StrokeVertexConstructor for VertexConstructor { fn new_vertex(&mut self, vertex: StrokeVertex) -> ShaderVertex { ShaderVertex::new( - vertex.position_on_path().to_array(), - vertex.normal().to_array(), + [vertex.position().x, vertex.position().y, 0.0], + [0.0, 0.0, 1.0], ) } } diff --git a/web/flatbuffer/layer_tessellated.fbs b/web/flatbuffer/layer_tessellated.fbs index 1ada4d938..80185f880 100644 --- a/web/flatbuffer/layer_tessellated.fbs +++ b/web/flatbuffer/layer_tessellated.fbs @@ -1,8 +1,8 @@ include "basic.fbs"; struct FlatShaderVertex { - position: [float:2]; - normal: [float:2]; + position: [float:3]; + normal: [float:3]; } table FlatLayerTessellated {