diff --git a/data/0-255.pbf b/data/0-255.pbf new file mode 100644 index 000000000..92d76f170 Binary files /dev/null and b/data/0-255.pbf differ diff --git a/data/sprites.json b/data/sprites.json new file mode 100644 index 000000000..2a52ff087 --- /dev/null +++ b/data/sprites.json @@ -0,0 +1,37 @@ +{ + "circle-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 51, + "y": 0 + }, + "circle-alt-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 0, + "y": 84 + }, + "pin-standard": { + "height": 84, + "pixelRatio": 2, + "width": 51, + "x": 0, + "y": 0 + }, + "star-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 34, + "y": 84 + }, + "star-alt-11": { + "height": 34, + "pixelRatio": 2, + "width": 34, + "x": 68, + "y": 84 + } +} \ No newline at end of file diff --git a/data/sprites.png b/data/sprites.png new file mode 100644 index 000000000..a8487dedc Binary files /dev/null and b/data/sprites.png differ diff --git a/maplibre/Cargo.toml b/maplibre/Cargo.toml index a3dae933d..64b8aee69 100644 --- a/maplibre/Cargo.toml +++ b/maplibre/Cargo.toml @@ -86,5 +86,10 @@ smallvec = "1.9.0" # Headless png = { version = "0.17.5", optional = true } +# SDF font +image = { version = "0.24.5", features = ["png"] } +prost-types = "0.10.1" + [build-dependencies] maplibre-build-tools = { path = "../maplibre-build-tools", version = "0.1.0" } +prost-build = "0.10.4" diff --git a/maplibre/build.rs b/maplibre/build.rs index 4429fb1a6..409d519a5 100644 --- a/maplibre/build.rs +++ b/maplibre/build.rs @@ -3,6 +3,8 @@ //! This script is built and executed just before building the package. //! It will validate the WGSL (WebGPU Shading Language) shaders and embed static files. +use std::{fs, path::PathBuf}; + use maplibre_build_tools::wgsl::validate_project_wgsl; #[cfg(feature = "embed-static-tiles")] @@ -50,9 +52,29 @@ fn embed_tiles_statically() { } } +fn generate_protobuf() { + let proto_paths = fs::read_dir("./proto") + .unwrap() + .filter_map(|entry| { + let entry = entry.ok()?; + println!( + "cargo:rerun-if-changed={}", + entry.path().display().to_string() + ); + Some(entry.path()) + }) + .collect::>(); + + if !proto_paths.is_empty() { + prost_build::compile_protos(&proto_paths, &[PathBuf::from("./proto/")]).unwrap(); + } +} + fn main() { validate_project_wgsl(); #[cfg(feature = "embed-static-tiles")] embed_tiles_statically(); + + generate_protobuf(); } diff --git a/maplibre/proto/glyphs.proto b/maplibre/proto/glyphs.proto new file mode 100644 index 000000000..fb7229726 --- /dev/null +++ b/maplibre/proto/glyphs.proto @@ -0,0 +1,33 @@ +// Protocol Version 1 + +package glyphs; + +option optimize_for = LITE_RUNTIME; + +// Stores a glyph with metrics and optional SDF bitmap information. +message glyph { + required uint32 id = 1; + + // A signed distance field of the glyph with a border of 3 pixels. + optional bytes bitmap = 2; + + // Glyph metrics. + required uint32 width = 3; + required uint32 height = 4; + required sint32 left = 5; + required sint32 top = 6; + required uint32 advance = 7; +} + +// Stores fontstack information and a list of faces. +message fontstack { + required string name = 1; + required string range = 2; + repeated glyph glyphs = 3; +} + +message glyphs { + repeated fontstack stacks = 1; + + extensions 16 to 8191; +} diff --git a/maplibre/src/io/apc.rs b/maplibre/src/io/apc.rs index 1c7df9d55..7fb956f8b 100644 --- a/maplibre/src/io/apc.rs +++ b/maplibre/src/io/apc.rs @@ -28,6 +28,7 @@ pub enum Message { TileTessellated(T::TileTessellated), LayerUnavailable(T::LayerUnavailable), LayerTessellated(T::LayerTessellated), + SymbolLayerTessellated(T::SymbolLayerTessellated), LayerIndexed(T::LayerIndexed), } diff --git a/maplibre/src/io/geometry_index.rs b/maplibre/src/io/geometry_index.rs index f4f7e3c0b..eab54f16b 100644 --- a/maplibre/src/io/geometry_index.rs +++ b/maplibre/src/io/geometry_index.rs @@ -9,7 +9,6 @@ use geozero::{ error::GeozeroError, geo_types::GeoWriter, ColumnValue, FeatureProcessor, GeomProcessor, PropertyProcessor, }; -use log::warn; use rstar::{Envelope, PointDistance, RTree, RTreeObject, AABB}; use crate::{ @@ -313,7 +312,7 @@ impl FeatureProcessor for IndexProcessor { .unwrap(), ), _ => { - warn!("Unknown geometry in index") + log::trace!("Unknown geometry in index") } }; diff --git a/maplibre/src/io/pipeline.rs b/maplibre/src/io/pipeline.rs index 580c7c4b1..476a09f15 100644 --- a/maplibre/src/io/pipeline.rs +++ b/maplibre/src/io/pipeline.rs @@ -7,7 +7,7 @@ use thiserror::Error; use crate::{ coords::WorldTileCoords, io::{apc::SendError, geometry_index::IndexedGeometry}, - render::ShaderVertex, + render::{ShaderVertex, SymbolVertex}, tessellation::{IndexDataType, OverAlignedVertexBuffer}, }; @@ -42,6 +42,15 @@ pub trait PipelineProcessor: Downcast { ) -> Result<(), PipelineError> { Ok(()) } + fn symbol_layer_tesselation_finished( + &mut self, + _coords: &WorldTileCoords, + _buffer: OverAlignedVertexBuffer, + _feature_indices: Vec, + _layer_data: tile::Layer, + ) -> Result<(), PipelineError> { + Ok(()) + } fn layer_indexing_finished( &mut self, _coords: &WorldTileCoords, diff --git a/maplibre/src/io/tile_pipelines.rs b/maplibre/src/io/tile_pipelines.rs index 2029bf5ab..750f38764 100644 --- a/maplibre/src/io/tile_pipelines.rs +++ b/maplibre/src/io/tile_pipelines.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use geozero::GeozeroDatasource; +use geozero::{mvt::tile::Layer, GeozeroDatasource}; use prost::Message; use crate::{ @@ -9,7 +9,9 @@ use crate::{ pipeline::{DataPipeline, PipelineContext, PipelineEnd, PipelineError, Processable}, TileRequest, }, - tessellation::{zero_tessellator::ZeroTessellator, IndexDataType}, + tessellation::{ + text_tesselator::TextTessellator, zero_tessellator::ZeroTessellator, IndexDataType, + }, }; #[derive(Default)] @@ -134,12 +136,48 @@ impl Processable for TessellateLayer { } } +#[derive(Default)] +pub struct TessellateGlyphQuads; + +impl Processable for TessellateGlyphQuads { + type Input = (TileRequest, geozero::mvt::Tile); + type Output = (TileRequest, geozero::mvt::Tile); + + #[tracing::instrument(skip_all)] + fn process( + &self, + (tile_request, mut tile): Self::Input, + context: &mut PipelineContext, + ) -> Result { + let coords = &tile_request.coords; + + let mut tessellator = TextTessellator::::default(); + for layer in &mut tile.layers { + layer.process(&mut tessellator).unwrap(); + } + + let mut layer1 = Layer::default(); + layer1.name = "text".to_string(); // FIXME + context.processor_mut().symbol_layer_tesselation_finished( + coords, + tessellator.quad_buffer.into(), + tessellator.feature_indices, + layer1, + )?; + + Ok((tile_request, tile)) + } +} + pub fn build_vector_tile_pipeline() -> impl Processable::Input> { DataPipeline::new( ParseTile, DataPipeline::new( - TessellateLayer, - DataPipeline::new(IndexLayer, PipelineEnd::default()), + TessellateGlyphQuads::default(), + DataPipeline::new( + TessellateLayer, + DataPipeline::new(IndexLayer, PipelineEnd::default()), + ), ), ) } diff --git a/maplibre/src/io/tile_repository.rs b/maplibre/src/io/tile_repository.rs index a38aa645c..b124d0acc 100644 --- a/maplibre/src/io/tile_repository.rs +++ b/maplibre/src/io/tile_repository.rs @@ -9,7 +9,7 @@ use crate::{ coords::{Quadkey, WorldTileCoords}, render::{ resource::{BufferPool, Queue}, - ShaderVertex, + ShaderVertex, SymbolVertex, }, tessellation::{IndexDataType, OverAlignedVertexBuffer}, }; @@ -28,6 +28,13 @@ pub enum StoredLayer { /// Holds for each feature the count of indices. feature_indices: Vec, }, + TessellatedSymbolLayer { + coords: WorldTileCoords, + layer_name: String, + buffer: OverAlignedVertexBuffer, + /// Holds for each feature the count of indices. + feature_indices: Vec, + }, } impl StoredLayer { @@ -35,6 +42,7 @@ impl StoredLayer { match self { StoredLayer::UnavailableLayer { coords, .. } => *coords, StoredLayer::TessellatedLayer { coords, .. } => *coords, + StoredLayer::TessellatedSymbolLayer { coords, .. } => *coords, } } @@ -42,6 +50,7 @@ impl StoredLayer { match self { StoredLayer::UnavailableLayer { layer_name, .. } => layer_name.as_str(), StoredLayer::TessellatedLayer { layer_name, .. } => layer_name.as_str(), + StoredLayer::TessellatedSymbolLayer { layer_name, .. } => layer_name.as_str(), } } } @@ -151,16 +160,25 @@ impl TileRepository { /// Returns the list of tessellated layers at the given world tile coords, which are loaded in /// the BufferPool - pub fn iter_loaded_layers_at, B, V: Pod, I: Pod, TM: Pod, FM: Pod>( + pub fn loaded_layers_at, B, V1: Pod, V2: Pod, I: Pod, TM: Pod, FM: Pod>( &self, - buffer_pool: &BufferPool, + buffer_pool1: &BufferPool, + buffer_pool2: &BufferPool, coords: &WorldTileCoords, ) -> Option> { - let loaded_layers = buffer_pool.get_loaded_layers_at(coords).unwrap_or_default(); + let loaded_layers1 = buffer_pool1 + .get_loaded_layers_at(coords) + .unwrap_or_default(); + let loaded_layers2 = buffer_pool2 + .get_loaded_layers_at(coords) + .unwrap_or_default(); self.iter_layers_at(coords).map(|layers| { layers - .filter(|result| !loaded_layers.contains(&result.layer_name())) + .filter(|result| { + !loaded_layers1.contains(&result.layer_name()) + && !loaded_layers2.contains(&result.layer_name()) + }) .collect::>() }) } diff --git a/maplibre/src/io/transferables.rs b/maplibre/src/io/transferables.rs index 99b846e79..518d69476 100644 --- a/maplibre/src/io/transferables.rs +++ b/maplibre/src/io/transferables.rs @@ -3,7 +3,7 @@ use geozero::mvt::tile::Layer; use crate::{ coords::WorldTileCoords, io::{geometry_index::TileIndex, tile_repository::StoredLayer}, - render::ShaderVertex, + render::{ShaderVertex, SymbolVertex}, tessellation::{IndexDataType, OverAlignedVertexBuffer}, }; @@ -41,6 +41,21 @@ pub trait LayerTessellated: Send { fn to_stored_layer(self) -> StoredLayer; } +pub trait SymbolLayerTessellated: Send { + fn build_from( + coords: WorldTileCoords, + buffer: OverAlignedVertexBuffer, + feature_indices: Vec, + layer_data: Layer, + ) -> Self + where + Self: Sized; + + fn coords(&self) -> WorldTileCoords; + + fn to_stored_layer(self) -> StoredLayer; +} + pub trait LayerIndexed: Send { fn build_from(coords: WorldTileCoords, index: TileIndex) -> Self where @@ -129,6 +144,44 @@ impl LayerTessellated for DefaultLayerTesselated { } } +#[derive(Clone)] +pub struct DefaultSymbolLayerTesselated { + pub coords: WorldTileCoords, + pub buffer: OverAlignedVertexBuffer, + /// Holds for each feature the count of indices. + pub feature_indices: Vec, + pub layer_data: Layer, // FIXME (perf): Introduce a better structure for this +} + +impl SymbolLayerTessellated for DefaultSymbolLayerTesselated { + fn build_from( + coords: WorldTileCoords, + buffer: OverAlignedVertexBuffer, + feature_indices: Vec, + layer_data: Layer, + ) -> Self { + Self { + coords, + buffer, + feature_indices, + layer_data, + } + } + + fn coords(&self) -> WorldTileCoords { + self.coords + } + + fn to_stored_layer(self) -> StoredLayer { + StoredLayer::TessellatedSymbolLayer { + coords: self.coords, + layer_name: self.layer_data.name, + buffer: self.buffer, + feature_indices: self.feature_indices, + } + } +} + pub struct DefaultLayerIndexed { coords: WorldTileCoords, index: TileIndex, @@ -152,6 +205,7 @@ pub trait Transferables: 'static { type TileTessellated: TileTessellated; type LayerUnavailable: LayerUnavailable; type LayerTessellated: LayerTessellated; + type SymbolLayerTessellated: SymbolLayerTessellated; type LayerIndexed: LayerIndexed; } @@ -162,5 +216,6 @@ impl Transferables for DefaultTransferables { type TileTessellated = DefaultTileTessellated; type LayerUnavailable = DefaultLayerUnavailable; type LayerTessellated = DefaultLayerTesselated; + type SymbolLayerTessellated = DefaultSymbolLayerTesselated; type LayerIndexed = DefaultLayerIndexed; } diff --git a/maplibre/src/lib.rs b/maplibre/src/lib.rs index 6a0b962da..288e3c24f 100644 --- a/maplibre/src/lib.rs +++ b/maplibre/src/lib.rs @@ -46,6 +46,7 @@ pub mod benchmarking; pub mod event_loop; pub mod kernel; pub mod map; +pub mod text; pub mod world; // Export tile format diff --git a/maplibre/src/render/eventually.rs b/maplibre/src/render/eventually.rs index 23d2b3953..c66a4cf70 100644 --- a/maplibre/src/render/eventually.rs +++ b/maplibre/src/render/eventually.rs @@ -45,10 +45,15 @@ where } impl Eventually { #[tracing::instrument(name = "initialize", skip_all)] - pub fn initialize(&mut self, f: impl FnOnce() -> T) { + pub fn initialize(&mut self, f: impl FnOnce() -> T) -> &T { if let Eventually::Uninitialized = self { *self = Eventually::Initialized(f()); } + + match self { + Eventually::Initialized(data) => data, + Eventually::Uninitialized => panic!("not initialized"), + } } pub fn take(&mut self) -> Eventually { diff --git a/maplibre/src/render/main_pass.rs b/maplibre/src/render/main_pass.rs index 635f1063e..fd902ae82 100644 --- a/maplibre/src/render/main_pass.rs +++ b/maplibre/src/render/main_pass.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use crate::render::{ draw_graph, graph::{Node, NodeRunError, RenderContext, RenderGraphContext, SlotInfo}, - render_commands::{DrawMasks, DrawTiles}, + render_commands::{DrawMasks, DrawSymbols, DrawTiles}, render_phase::RenderCommand, resource::TrackedRenderPass, Eventually::Initialized, @@ -95,6 +95,10 @@ impl Node for MainPassNode { DrawTiles::render(state, item, &mut tracked_pass); } + for item in &state.symbol_tile_phase.items { + DrawSymbols::render(state, item, &mut tracked_pass); + } + Ok(()) } } diff --git a/maplibre/src/render/mod.rs b/maplibre/src/render/mod.rs index 30c8a4fc8..4c3ee7aa9 100644 --- a/maplibre/src/render/mod.rs +++ b/maplibre/src/render/mod.rs @@ -20,6 +20,8 @@ use std::sync::Arc; +use wgpu::Sampler; + use crate::{ render::{ eventually::Eventually, @@ -53,7 +55,7 @@ pub mod error; pub mod eventually; pub mod settings; -pub use shaders::ShaderVertex; +pub use shaders::{ShaderVertex, SymbolVertex}; pub use stages::register_default_render_stages; use crate::{ @@ -62,6 +64,7 @@ use crate::{ error::RenderError, graph::{EmptyNode, RenderGraph, RenderGraphError}, main_pass::{MainPassDriverNode, MainPassNode}, + resource::GlyphTexture, }, window::{HeadedMapWindow, MapWindow}, }; @@ -81,21 +84,35 @@ pub struct RenderState { ShaderFeatureStyle, >, >, + symbol_buffer_pool: Eventually< + BufferPool< + wgpu::Queue, + wgpu::Buffer, + SymbolVertex, + IndexDataType, + ShaderLayerMetadata, + ShaderFeatureStyle, + >, + >, tile_view_pattern: Eventually>, tile_pipeline: Eventually, mask_pipeline: Eventually, debug_pipeline: Eventually, + symbol_pipeline: Eventually, + glyph_texture_bind_group: Eventually, globals_bind_group: Eventually, depth_texture: Eventually, multisampling_texture: Eventually>, + glyph_texture_sampler: Eventually<(wgpu::Texture, Sampler)>, surface: Surface, mask_phase: RenderPhase, tile_phase: RenderPhase<(IndexEntry, TileShape)>, + symbol_tile_phase: RenderPhase<(IndexEntry, TileShape)>, } impl RenderState { @@ -103,16 +120,21 @@ impl RenderState { Self { render_target: Default::default(), buffer_pool: Default::default(), + symbol_buffer_pool: Default::default(), tile_view_pattern: Default::default(), tile_pipeline: Default::default(), mask_pipeline: Default::default(), debug_pipeline: Default::default(), + symbol_pipeline: Default::default(), + glyph_texture_bind_group: Default::default(), globals_bind_group: Default::default(), depth_texture: Default::default(), multisampling_texture: Default::default(), + glyph_texture_sampler: Default::default(), surface, mask_phase: Default::default(), tile_phase: Default::default(), + symbol_tile_phase: Default::default(), } } @@ -175,7 +197,7 @@ impl Renderer { &wgpu_settings, &wgpu::RequestAdapterOptions { power_preference: wgpu_settings.power_preference, - force_fallback_adapter: false, + force_fallback_adapter: wgpu_settings.force_fallback_adapter, compatible_surface: Some(&surface), }, ) @@ -214,7 +236,7 @@ impl Renderer { &wgpu_settings, &wgpu::RequestAdapterOptions { power_preference: wgpu_settings.power_preference, - force_fallback_adapter: false, + force_fallback_adapter: wgpu_settings.force_fallback_adapter, compatible_surface: None, }, ) diff --git a/maplibre/src/render/render_commands.rs b/maplibre/src/render/render_commands.rs index 5416f1d40..95e30b6e2 100644 --- a/maplibre/src/render/render_commands.rs +++ b/maplibre/src/render/render_commands.rs @@ -4,7 +4,7 @@ use crate::render::{ eventually::Eventually::Initialized, render_phase::{PhaseItem, RenderCommand, RenderCommandResult}, - resource::{Globals, IndexEntry, TrackedRenderPass}, + resource::{Globals, GlyphTexture, IndexEntry, TrackedRenderPass}, tile_view_pattern::TileShape, RenderState, INDEX_FORMAT, }; @@ -62,6 +62,22 @@ impl RenderCommand

for SetDebugPipeline { } } +pub struct SetSymbolPipeline; +impl RenderCommand

for SetSymbolPipeline { + fn render<'w>( + state: &'w RenderState, + _item: &P, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Initialized(GlyphTexture { bind_group, .. }) = &state.glyph_texture_bind_group else { return RenderCommandResult::Failure; }; + pass.set_bind_group(0, bind_group, &[]); + + let Initialized(pipeline) = &state.symbol_pipeline else { return RenderCommandResult::Failure; }; + pass.set_render_pipeline(pipeline); + RenderCommandResult::Success + } +} + pub struct SetTilePipeline; impl RenderCommand

for SetTilePipeline { fn render<'w>( @@ -159,12 +175,45 @@ impl RenderCommand<(IndexEntry, TileShape)> for DrawTile { .metadata() .slice(entry.layer_metadata_buffer_range()), ); + let range = entry.feature_metadata_buffer_range(); + if !range.is_empty() { + pass.set_vertex_buffer(3, buffer_pool.feature_metadata().slice(range)); + } + pass.draw_indexed(entry.indices_range(), 0, 0..1); + RenderCommandResult::Success + } +} + +pub struct DrawSymbol; +impl RenderCommand<(IndexEntry, TileShape)> for DrawSymbol { + fn render<'w>( + state: &'w RenderState, + (entry, shape): &(IndexEntry, TileShape), + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let (Initialized(symbol_buffer_pool), Initialized(tile_view_pattern)) = + (&state.symbol_buffer_pool, &state.tile_view_pattern) else { return RenderCommandResult::Failure; }; + + pass.set_index_buffer( + symbol_buffer_pool + .indices() + .slice(entry.indices_buffer_range()), + INDEX_FORMAT, + ); pass.set_vertex_buffer( - 3, - buffer_pool - .feature_metadata() - .slice(entry.feature_metadata_buffer_range()), + 0, + symbol_buffer_pool + .vertices() + .slice(entry.vertices_buffer_range()), ); + pass.set_vertex_buffer(1, tile_view_pattern.buffer().slice(shape.buffer_range())); + pass.set_vertex_buffer( + 2, + symbol_buffer_pool + .metadata() + .slice(entry.layer_metadata_buffer_range()), + ); + pass.draw_indexed(entry.indices_range(), 0, 0..1); RenderCommandResult::Success } @@ -175,3 +224,5 @@ pub type DrawTiles = (SetTilePipeline, SetViewBindGroup<0>, DrawTile); pub type DrawMasks = (SetMaskPipeline, DrawMask); pub type DrawDebugOutlines = (SetDebugPipeline, DrawDebugOutline); + +pub type DrawSymbols = (SetSymbolPipeline, DrawSymbol); diff --git a/maplibre/src/render/resource/glyph_texture.rs b/maplibre/src/render/resource/glyph_texture.rs new file mode 100644 index 000000000..04a448cba --- /dev/null +++ b/maplibre/src/render/resource/glyph_texture.rs @@ -0,0 +1,39 @@ +pub struct GlyphTexture { + pub bind_group: wgpu::BindGroup, +} + +impl GlyphTexture { + pub fn from_device( + device: &wgpu::Device, + texture: &wgpu::Texture, + sampler: &wgpu::Sampler, + layout: &wgpu::BindGroupLayout, + ) -> Self { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture.create_view( + &wgpu::TextureViewDescriptor { + label: Some("Glyph texture view"), + format: Some(wgpu::TextureFormat::R8Unorm), + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }, + )), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + label: Some("Glyph texture bind group"), + }); + Self { bind_group } + } +} diff --git a/maplibre/src/render/resource/mod.rs b/maplibre/src/render/resource/mod.rs index bb7e6e8a2..f88b63f5c 100644 --- a/maplibre/src/render/resource/mod.rs +++ b/maplibre/src/render/resource/mod.rs @@ -3,6 +3,7 @@ mod buffer_pool; mod globals; +mod glyph_texture; mod pipeline; mod shader; mod surface; @@ -11,6 +12,7 @@ mod tracked_render_pass; pub use buffer_pool::*; pub use globals::*; +pub use glyph_texture::*; pub use pipeline::*; pub use shader::*; pub use surface::*; diff --git a/maplibre/src/render/settings.rs b/maplibre/src/render/settings.rs index d8b7dbafb..3df321afb 100644 --- a/maplibre/src/render/settings.rs +++ b/maplibre/src/render/settings.rs @@ -13,6 +13,7 @@ pub struct WgpuSettings { pub device_label: Option>, pub backends: Option, pub power_preference: PowerPreference, + pub force_fallback_adapter: bool, /// The features to ensure are enabled regardless of what the adapter/backend supports. /// Setting these explicitly may cause renderer initialization to fail. pub features: Features, @@ -59,6 +60,7 @@ impl Default for WgpuSettings { device_label: Default::default(), backends, power_preference: PowerPreference::HighPerformance, + force_fallback_adapter: false, features, disabled_features: None, limits, diff --git a/maplibre/src/render/shaders/mod.rs b/maplibre/src/render/shaders/mod.rs index 0bdfc679a..f581098ec 100644 --- a/maplibre/src/render/shaders/mod.rs +++ b/maplibre/src/render/shaders/mod.rs @@ -283,3 +283,128 @@ impl ShaderTileMetadata { } } } + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct SymbolVertex { + // 4 bytes * 3 = 12 bytes + pub position: [f32; 3], + // 4 bytes * 3 = 12 bytes + pub text_anchor: [f32; 3], + // 4 bytes * 2 = 8 bytes + pub tex_coords: [f32; 2], + // 1 byte * 4 = 4 bytes + pub color: [u8; 4], + // 1 byte + pub is_glyph: u32, +} + +pub struct SymbolTileShader { + pub format: wgpu::TextureFormat, +} + +impl Shader for SymbolTileShader { + fn describe_vertex(&self) -> VertexState { + VertexState { + source: include_str!("sdf.vertex.wgsl"), + entry_point: "main", + buffers: vec![ + // vertex data + VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: vec![ + // position + wgpu::VertexAttribute { + offset: 0, + format: wgpu::VertexFormat::Float32x3, + shader_location: 0, + }, + // text_anchor + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }, + // tex coords + wgpu::VertexAttribute { + offset: (std::mem::size_of::<[f32; 3]>() * 2) as wgpu::BufferAddress, + shader_location: 11, + format: wgpu::VertexFormat::Float32x2, + }, + ], + }, + // tile metadata + VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: vec![ + // translate + wgpu::VertexAttribute { + offset: 0, + format: wgpu::VertexFormat::Float32x4, + shader_location: 4, + }, + wgpu::VertexAttribute { + offset: 1 * wgpu::VertexFormat::Float32x4.size(), + format: wgpu::VertexFormat::Float32x4, + shader_location: 5, + }, + wgpu::VertexAttribute { + offset: 2 * wgpu::VertexFormat::Float32x4.size(), + format: wgpu::VertexFormat::Float32x4, + shader_location: 6, + }, + wgpu::VertexAttribute { + offset: 3 * wgpu::VertexFormat::Float32x4.size(), + format: wgpu::VertexFormat::Float32x4, + shader_location: 7, + }, + // zoom_factor + wgpu::VertexAttribute { + offset: 4 * wgpu::VertexFormat::Float32x4.size(), + format: wgpu::VertexFormat::Float32, + shader_location: 9, + }, + ], + }, + // layer metadata + VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: vec![ + // z_index + wgpu::VertexAttribute { + offset: 0, + format: wgpu::VertexFormat::Float32, + shader_location: 10, + }, + ], + }, + ], + } + } + + fn describe_fragment(&self) -> FragmentState { + FragmentState { + source: include_str!("sdf.fragment.wgsl"), + entry_point: "main", + targets: vec![Some(wgpu::ColorTargetState { + format: self.format, + write_mask: wgpu::ColorWrites::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::Zero, + operation: wgpu::BlendOperation::Add, + }, + }), + })], + } + } +} diff --git a/maplibre/src/render/shaders/sdf.fragment.wgsl b/maplibre/src/render/shaders/sdf.fragment.wgsl new file mode 100644 index 000000000..df98bc977 --- /dev/null +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -0,0 +1,75 @@ +struct VertexOutput { + @location(0) is_glyph: i32, + @location(1) tex_coords: vec2, + @location(2) color: vec4, + @builtin(position) position: vec4, +}; + +struct Output { + @location(0) out_color: vec4, +}; + +@group(0) @binding(0) +var t_glyphs: texture_2d; +@group(0) @binding(1) +var s_glyphs: sampler; + +// Note: Ensure uniform control flow! +// https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Non-uniform_flow_control +@fragment +fn main(in: VertexOutput) -> Output { + let buffer_width: f32 = 0.25; + let buffer_center_outline: f32 = 0.8; + + // At which offset is the outline of the SDF? + let outline_center_offset: f32 = -0.25; + + // shift outline by `outline_width` to the ouside + let buffer_center: f32 = buffer_center_outline + outline_center_offset; + + let outline_color = vec3(0.0, 0.0, 0.0); + + // 0 => border, < 0 => inside, > 0 => outside + let dist = textureSample(t_glyphs, s_glyphs, in.tex_coords).r; + + let alpha: f32 = smoothstep(buffer_center - buffer_width / 2.0, buffer_center + buffer_width / 2.0, dist); + let border: f32 = smoothstep(buffer_center_outline - buffer_width / 2.0, buffer_center_outline + buffer_width / 2.0, dist); + + let color_rgb = mix(outline_color.rgb, in.color.rgb, border); + + // "Another Good Trick" from https://www.sjbaker.org/steve/omniv/alpha_sorting.html + // Using discard is an alternative for GL_ALPHA_TEST. + // https://stackoverflow.com/questions/53024693/opengl-is-discard-the-only-replacement-for-deprecated-gl-alpha-test + // Alternative is to disable the depth buffer for the RenderPass using sdf.fragment.wgsl + if (alpha == 0.0) { + discard; + } + + return Output(vec4(color_rgb, in.color.a * alpha)); +} + + +// MapLibre SDF shader: +/* + let SDF_PX = 8.0; + let device_pixel_ratio = 1.0; + let EDGE_GAMMA = 0.105 / device_pixel_ratio; + + let size = 6.0; // TODO + let fontScale = size / 24.0; // TODO Why / 24? + let halo_width = 0.5; // TODO + let halo_blur = 0.5; // TODO + let halo_color = vec4(1.0, 0.0, 0.0, 1.0); + + var color = in.color; + var gamma_scale = 1.0; + var gamma = EDGE_GAMMA / (fontScale * gamma_scale); + var buff = (256.0 - 64.0) / 256.0; + + let is_halo = false; + if (is_halo) { + color = halo_color; + gamma = (halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * gamma_scale); + buff = (6.0 - halo_width / fontScale) / SDF_PX; + } +*/ diff --git a/maplibre/src/render/shaders/sdf.vertex.wgsl b/maplibre/src/render/shaders/sdf.vertex.wgsl new file mode 100644 index 000000000..d7dc3ff90 --- /dev/null +++ b/maplibre/src/render/shaders/sdf.vertex.wgsl @@ -0,0 +1,35 @@ +struct VertexOutput { + @location(0) is_glyph: i32, + @location(1) tex_coords: vec2, + @location(2) color: vec4, + @builtin(position) position: vec4, +}; + +@vertex +fn main( + @location(0) position: vec3, + @location(1) text_anchor: vec3, + @location(4) translate1: vec4, + @location(5) translate2: vec4, + @location(6) translate3: vec4, + @location(7) translate4: vec4, + @location(9) zoom_factor: f32, + @location(10) z_index: f32, + @location(11) tex_coords: vec2, + @builtin(instance_index) instance_idx: u32 // instance_index is used when we have multiple instances of the same "object" +) -> VertexOutput { + let font_scale = 3.0; + + let scaling: mat3x3 = mat3x3( + vec3(zoom_factor * font_scale, 0.0, 0.0), + vec3(0.0, zoom_factor * font_scale, 0.0), + vec3(0.0, 0.0, 1.0) + ); + + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4((scaling * (position - text_anchor) + text_anchor), 1.0); + position.z = z_index; + + let white = vec4(1.0, 1.0, 1.0, 1.0); + let black = vec4(0.0, 0.0, 0.0, 1.0); + return VertexOutput(1, tex_coords, white, position); +} diff --git a/maplibre/src/render/shaders/tile_debug.vertex.wgsl b/maplibre/src/render/shaders/tile_debug.vertex.wgsl index f469a3282..358ae4b1b 100644 --- a/maplibre/src/render/shaders/tile_debug.vertex.wgsl +++ b/maplibre/src/render/shaders/tile_debug.vertex.wgsl @@ -63,13 +63,7 @@ fn main( let a_position = VERTICES[vertex_idx]; - let scaling: mat3x3 = mat3x3( - vec3(target_width, 0.0, 0.0), - vec3(0.0, target_height, 0.0), - vec3(0.0, 0.0, 1.0) - ); - - var position = mat4x4(translate1, translate2, translate3, translate4) * vec4((scaling * a_position), 1.0); + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(a_position, 1.0); position.z = 1.0; return VertexOutput(DEBUG_COLOR, position); } diff --git a/maplibre/src/render/shaders/tile_mask.vertex.wgsl b/maplibre/src/render/shaders/tile_mask.vertex.wgsl index 80ad1ba8c..b34c1dd23 100644 --- a/maplibre/src/render/shaders/tile_mask.vertex.wgsl +++ b/maplibre/src/render/shaders/tile_mask.vertex.wgsl @@ -31,13 +31,7 @@ fn main( ); let a_position = VERTICES[vertex_idx]; - let scaling: mat3x3 = mat3x3( - vec3(target_width, 0.0, 0.0), - vec3(0.0, target_height, 0.0), - vec3(0.0, 0.0, 1.0) - ); - - var position = mat4x4(translate1, translate2, translate3, translate4) * vec4((scaling * a_position), 1.0); + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(a_position, 1.0); // FIXME: how to fix z-fighting? position.z = 1.0; diff --git a/maplibre/src/render/stages/phase_sort_stage.rs b/maplibre/src/render/stages/phase_sort_stage.rs index d253f9f21..48a10feb0 100644 --- a/maplibre/src/render/stages/phase_sort_stage.rs +++ b/maplibre/src/render/stages/phase_sort_stage.rs @@ -19,7 +19,9 @@ impl Stage for PhaseSortStage { ) { let mask_phase: &mut RenderPhase<_> = &mut state.mask_phase; mask_phase.sort(); - let file_phase = &mut state.tile_phase; - file_phase.sort(); + let tile_phase = &mut state.tile_phase; + tile_phase.sort(); + let symbol_tile_phase = &mut state.symbol_tile_phase; + symbol_tile_phase.sort(); } } diff --git a/maplibre/src/render/stages/queue_stage.rs b/maplibre/src/render/stages/queue_stage.rs index 349a2d20c..f3c97b395 100644 --- a/maplibre/src/render/stages/queue_stage.rs +++ b/maplibre/src/render/stages/queue_stage.rs @@ -20,6 +20,8 @@ impl Stage for QueueStage { RenderState { mask_phase, tile_phase, + symbol_buffer_pool, + symbol_tile_phase, tile_view_pattern, buffer_pool, .. @@ -31,11 +33,13 @@ impl Stage for QueueStage { ) { mask_phase.items.clear(); tile_phase.items.clear(); + symbol_tile_phase.items.clear(); - let (Initialized(tile_view_pattern), Initialized(buffer_pool)) = - (tile_view_pattern, &buffer_pool) else { return; }; + let (Initialized(tile_view_pattern), Initialized(buffer_pool), Initialized(symbol_buffer_pool)) = + (tile_view_pattern, &buffer_pool, symbol_buffer_pool) else { return; }; - let index = buffer_pool.index(); + let buffer_pool_index = buffer_pool.index(); + let symbol_pool_index = symbol_buffer_pool.index(); for view_tile in tile_view_pattern.iter() { let coords = &view_tile.coords(); @@ -46,18 +50,25 @@ impl Stage for QueueStage { // Draw masks for all source_shapes mask_phase.add(source_shape.clone()); - let Some(entries) = index.get_layers(&source_shape.coords()) else { - tracing::trace!("No layers found at {}", &source_shape.coords()); - return; + if let Some(entries) = buffer_pool_index.get_layers(&source_shape.coords()) { + let mut layers_to_render: Vec<&IndexEntry> = Vec::from_iter(entries); + layers_to_render.sort_by_key(|entry| entry.style_layer.index); + + for entry in layers_to_render { + // Draw tile + tile_phase.add((entry.clone(), source_shape.clone())); + } }; - let mut layers_to_render: Vec<&IndexEntry> = Vec::from_iter(entries); - layers_to_render.sort_by_key(|entry| entry.style_layer.index); + if let Some(entries) = symbol_pool_index.get_layers(&source_shape.coords()) { + let mut layers_to_render: Vec<&IndexEntry> = Vec::from_iter(entries); + layers_to_render.sort_by_key(|entry| entry.style_layer.index); - for entry in layers_to_render { - // Draw tile - tile_phase.add((entry.clone(), source_shape.clone())) - } + for entry in layers_to_render { + // Draw tile + symbol_tile_phase.add((entry.clone(), source_shape.clone())); + } + }; }); } } diff --git a/maplibre/src/render/stages/resource_stage.rs b/maplibre/src/render/stages/resource_stage.rs index 3521e64ea..236d05c73 100644 --- a/maplibre/src/render/stages/resource_stage.rs +++ b/maplibre/src/render/stages/resource_stage.rs @@ -2,10 +2,15 @@ use std::mem::size_of; +use prost::Message; +use wgpu::util::DeviceExt; + use crate::{ context::MapContext, render::{ - resource::{BackingBufferDescriptor, BufferPool, Globals, RenderPipeline, Texture}, + resource::{ + BackingBufferDescriptor, BufferPool, Globals, GlyphTexture, RenderPipeline, Texture, + }, shaders, shaders::{Shader, ShaderTileMetadata}, tile_pipeline::TilePipeline, @@ -28,6 +33,7 @@ impl Stage for ResourceStage { settings, device, state, + queue, .. }, .. @@ -79,6 +85,10 @@ impl Stage for ResourceStage { .buffer_pool .initialize(|| BufferPool::from_device(device)); + state + .symbol_buffer_pool + .initialize(|| BufferPool::from_device(device)); + state.tile_view_pattern.initialize(|| { let tile_view_buffer_desc = wgpu::BufferDescriptor { label: Some("tile view buffer"), @@ -109,6 +119,7 @@ impl Stage for ResourceStage { false, false, true, + false, ) .describe_render_pipeline() .initialize(device); @@ -137,6 +148,7 @@ impl Stage for ResourceStage { false, false, true, + false, ) .describe_render_pipeline() .initialize(device) @@ -159,9 +171,77 @@ impl Stage for ResourceStage { true, false, false, + false, ) .describe_render_pipeline() .initialize(device) }); + + state.symbol_pipeline.initialize(|| { + let mask_shader = shaders::SymbolTileShader { + format: surface.surface_format(), + }; + + let pipeline = TilePipeline::new( + *settings, + mask_shader.describe_vertex(), + mask_shader.describe_fragment(), + false, + true, + false, + true, + false, + true, + true, + ) + .describe_render_pipeline() + .initialize(device); + + let (texture, sampler) = state.glyph_texture_sampler.initialize(|| { + let data = std::fs::read("./data/0-255.pbf").unwrap(); + let glyphs = crate::text::glyph::GlyphSet::from( + crate::text::sdf_glyphs::Glyphs::decode(data.as_slice()).unwrap(), + ); + + let (width, height) = glyphs.get_texture_dimensions(); + + let texture = device.create_texture_with_data( + &queue, + &wgpu::TextureDescriptor { + label: Some("Glyph Texture"), + size: wgpu::Extent3d { + width: width as _, + height: height as _, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + }, + glyphs.get_texture_bytes(), + ); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + // SDF rendering requires linear interpolation + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + (texture, sampler) + }); + + state.glyph_texture_bind_group.initialize(|| { + GlyphTexture::from_device( + device, + texture, + sampler, + &pipeline.get_bind_group_layout(0), + ) + }); + pipeline + }); } } diff --git a/maplibre/src/render/stages/upload_stage.rs b/maplibre/src/render/stages/upload_stage.rs index 6da45dd19..859a6fb67 100644 --- a/maplibre/src/render/stages/upload_stage.rs +++ b/maplibre/src/render/stages/upload_stage.rs @@ -135,6 +135,7 @@ impl UploadStage { buffer_pool.update_feature_metadata(queue, entry, &feature_metadata); } + _ => {} } } } @@ -156,18 +157,23 @@ impl UploadStage { #[tracing::instrument(skip_all)] pub fn upload_tile_geometry( &self, - RenderState { buffer_pool, .. }: &mut RenderState, + RenderState { + buffer_pool, + symbol_buffer_pool, + .. + }: &mut RenderState, queue: &wgpu::Queue, tile_repository: &TileRepository, style: &Style, view_region: &ViewRegion, ) { let Initialized(buffer_pool) = buffer_pool else { return; }; + let Initialized(symbol_buffer_pool) = symbol_buffer_pool else { return; }; // Upload all tessellated layers which are in view for coords in view_region.iter() { let Some(available_layers) = - tile_repository.iter_loaded_layers_at(buffer_pool, &coords) else { continue; }; + tile_repository.loaded_layers_at(buffer_pool, symbol_buffer_pool, &coords) else { continue; }; for style_layer in &style.layers { let source_layer = style_layer.source_layer.as_ref().unwrap(); // TODO: Remove unwrap @@ -215,6 +221,22 @@ impl UploadStage { &feature_metadata, ); } + StoredLayer::TessellatedSymbolLayer { + coords, + feature_indices, + buffer, + .. + } => { + log::info!("uploading {:?}", &coords); + symbol_buffer_pool.allocate_layer_geometry( + queue, + *coords, + style_layer.clone(), + buffer, + ShaderLayerMetadata::new(style_layer.index as f32), + &[], + ); + } } } } diff --git a/maplibre/src/render/tile_pipeline.rs b/maplibre/src/render/tile_pipeline.rs index 56750d7dd..adcc17065 100644 --- a/maplibre/src/render/tile_pipeline.rs +++ b/maplibre/src/render/tile_pipeline.rs @@ -21,6 +21,7 @@ pub struct TilePipeline { debug_stencil: bool, wireframe: bool, multisampling: bool, + glyph_rendering: bool, settings: RendererSettings, vertex_state: VertexState, @@ -38,6 +39,7 @@ impl TilePipeline { debug_stencil: bool, wireframe: bool, multisampling: bool, + glyph_rendering: bool, ) -> Self { TilePipeline { bind_globals, @@ -46,6 +48,7 @@ impl TilePipeline { debug_stencil, wireframe, multisampling, + glyph_rendering, settings, vertex_state, fragment_state, @@ -93,6 +96,25 @@ impl RenderPipeline for TilePipeline { }, count: None, }]]) + } else if self.glyph_rendering { + Some(vec![vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ]]) } else { None }, diff --git a/maplibre/src/render/tile_view_pattern.rs b/maplibre/src/render/tile_view_pattern.rs index 2c9026fea..c8c74142c 100644 --- a/maplibre/src/render/tile_view_pattern.rs +++ b/maplibre/src/render/tile_view_pattern.rs @@ -159,13 +159,13 @@ impl, B> TileViewPattern { if pool_index.has_tile(&coords) { SourceShapes::SourceEqTarget(TileShape::new(coords, zoom)) } else if let Some(parent_coords) = pool_index.get_available_parent(&coords) { - log::info!("Could not find data at {coords}. Falling back to {parent_coords}"); + log::debug!("Could not find data at {coords}. Falling back to {parent_coords}"); SourceShapes::Parent(TileShape::new(parent_coords, zoom)) } else if let Some(children_coords) = pool_index.get_available_children(&coords, CHILDREN_SEARCH_DEPTH) { - log::info!( + log::debug!( "Could not find data at {coords}. Falling back children: {children_coords:?}" ); diff --git a/maplibre/src/stages/mod.rs b/maplibre/src/stages/mod.rs index 40807ed20..1ddcbcbff 100644 --- a/maplibre/src/stages/mod.rs +++ b/maplibre/src/stages/mod.rs @@ -2,7 +2,7 @@ use std::{marker::PhantomData, rc::Rc}; -use geozero::mvt::tile; +use geozero::mvt::{tile, tile::Layer}; use request_stage::RequestStage; use crate::{ @@ -14,11 +14,12 @@ use crate::{ pipeline::{PipelineError, PipelineProcessor}, source_client::HttpClient, transferables::{ - LayerIndexed, LayerTessellated, LayerUnavailable, TileTessellated, Transferables, + LayerIndexed, LayerTessellated, LayerUnavailable, SymbolLayerTessellated, + TileTessellated, Transferables, }, }, kernel::Kernel, - render::ShaderVertex, + render::{ShaderVertex, SymbolVertex}, schedule::Schedule, stages::populate_tile_store_stage::PopulateTileStore, tessellation::{IndexDataType, OverAlignedVertexBuffer}, @@ -80,6 +81,20 @@ impl> PipelineProcessor .map_err(|e| PipelineError::Processing(Box::new(e))) } + fn symbol_layer_tesselation_finished( + &mut self, + coords: &WorldTileCoords, + buffer: OverAlignedVertexBuffer, + feature_indices: Vec, + layer_data: Layer, + ) -> Result<(), PipelineError> { + self.context + .send(Message::SymbolLayerTessellated( + T::SymbolLayerTessellated::build_from(*coords, buffer, feature_indices, layer_data), + )) + .map_err(|e| PipelineError::Processing(Box::new(e))) + } + fn layer_indexing_finished( &mut self, coords: &WorldTileCoords, diff --git a/maplibre/src/stages/populate_tile_store_stage.rs b/maplibre/src/stages/populate_tile_store_stage.rs index e57bb8031..b3a79597e 100644 --- a/maplibre/src/stages/populate_tile_store_stage.rs +++ b/maplibre/src/stages/populate_tile_store_stage.rs @@ -8,7 +8,10 @@ use crate::{ io::{ apc::{AsyncProcedureCall, Message}, tile_repository::StoredLayer, - transferables::{LayerIndexed, LayerTessellated, LayerUnavailable, TileTessellated}, + transferables::{ + LayerIndexed, LayerTessellated, LayerUnavailable, SymbolLayerTessellated, + TileTessellated, + }, }, kernel::Kernel, schedule::Stage, @@ -86,6 +89,11 @@ impl Stage for PopulateTileStore { geometry_index.index_tile(&coords, message.to_tile_index()); } + Message::SymbolLayerTessellated(message) => { + let layer: StoredLayer = message.to_stored_layer(); + + tile_repository.put_layer(layer); + } } } } diff --git a/maplibre/src/style/style.rs b/maplibre/src/style/style.rs index b8d890a5f..100fcfdff 100644 --- a/maplibre/src/style/style.rs +++ b/maplibre/src/style/style.rs @@ -138,6 +138,19 @@ impl Default for Style { source: None, source_layer: Some("boundary".to_string()), }, + StyleLayer { + index: 8, + id: "text".to_string(), + typ: "symbol".to_string(), + maxzoom: None, + minzoom: None, + metadata: None, + paint: Some(LayerPaint::Line(LinePaint { + line_color: Some(Color::from_str("black").unwrap()), + })), + source: None, + source_layer: Some("text".to_string()), + }, ], } } diff --git a/maplibre/src/tessellation/mod.rs b/maplibre/src/tessellation/mod.rs index 56bfce8be..6f3ad310e 100644 --- a/maplibre/src/tessellation/mod.rs +++ b/maplibre/src/tessellation/mod.rs @@ -7,6 +7,7 @@ use lyon::tessellation::{ use crate::render::ShaderVertex; +pub mod text_tesselator; pub mod zero_tessellator; const DEFAULT_TOLERANCE: f32 = 0.02; diff --git a/maplibre/src/tessellation/text_tesselator.rs b/maplibre/src/tessellation/text_tesselator.rs new file mode 100644 index 000000000..aff11ba7e --- /dev/null +++ b/maplibre/src/tessellation/text_tesselator.rs @@ -0,0 +1,236 @@ +use std::fs; + +use csscolorparser::Color; +use geozero::{ColumnValue, FeatureProcessor, GeomProcessor, PropertyProcessor}; +use lyon::{ + geom::{euclid::Point2D, Box2D}, + tessellation::{ + geometry_builder::MaxIndex, BuffersBuilder, FillOptions, FillTessellator, VertexBuffers, + }, +}; +use prost::Message; + +use crate::{ + render::SymbolVertex, + text::{glyph::GlyphSet, sdf_glyphs::Glyphs, Anchor, SymbolVertexBuilder}, +}; + +type GeoResult = geozero::error::Result; + +/// Build tessellations with vectors. +pub struct TextTessellator + MaxIndex> { + glyphs: GlyphSet, + + // output + pub quad_buffer: VertexBuffers, + pub feature_indices: Vec, + + // iteration variables + current_index: usize, + current_text: Option, + current_bbox: Option>, +} + +impl + MaxIndex> Default + for TextTessellator +{ + fn default() -> Self { + let data = fs::read("./data/0-255.pbf").unwrap(); + let glyphs = GlyphSet::from(Glyphs::decode(data.as_slice()).unwrap()); + Self { + glyphs, + quad_buffer: VertexBuffers::new(), + feature_indices: Vec::new(), + current_index: 0, + current_text: None, + current_bbox: None, + } + } +} + +impl + MaxIndex> TextTessellator { + pub fn tessellate_glyph_quads( + &mut self, + origin: [f32; 2], + label_text: &str, + color: Color, + ) -> Option> { + let mut tessellator = FillTessellator::new(); + + let mut next_origin = origin; + + let texture_dimensions = self.glyphs.get_texture_dimensions(); + let texture_dimensions = (texture_dimensions.0 as f32, texture_dimensions.1 as f32); + + // TODO: silently drops unknown characters + // TODO: handle line wrapping / line height + let mut bbox = None; + for glyph in label_text + .chars() + .filter_map(|c| self.glyphs.glyphs.get(&c)) + .collect::>() + { + let glyph_dims = glyph.buffered_dimensions(); + let width = glyph_dims.0 as f32; + let height = glyph_dims.1 as f32; + + let glyph_anchor = [ + next_origin[0] + glyph.left_bearing as f32, + next_origin[1] - glyph.top_bearing as f32, + 0., + ]; + + let glyph_rect = Box2D::new( + (glyph_anchor[0], glyph_anchor[1]).into(), + (glyph_anchor[0] + width, glyph_anchor[1] + height).into(), + ); + + bbox = bbox.map_or_else( + || Some(glyph_rect), + |bbox: Box2D<_>| Some(bbox.union(&glyph_rect)), + ); + + tessellator + .tessellate_rectangle( + &glyph_rect, + &FillOptions::default(), + &mut BuffersBuilder::new( + &mut self.quad_buffer, + SymbolVertexBuilder { + glyph_anchor, + text_anchor: [origin[0], origin[1], 0.0], + texture_dimensions, + sprite_dimensions: (width, height), + sprite_offset: ( + glyph.origin_offset().0 as f32, + glyph.origin_offset().1 as f32, + ), + color: color.to_rgba8(), // TODO: is this conversion oke? + glyph: true, // Set here to true to use SDF rendering + }, + ), + ) + .ok()?; + + next_origin[0] += glyph.advance() as f32; + } + + bbox + } +} + +impl + MaxIndex> GeomProcessor + for TextTessellator +{ + fn xy(&mut self, x: f64, y: f64, _idx: usize) -> GeoResult<()> { + let new_box = Box2D::new( + Point2D::new(x as f32, y as f32), + Point2D::new(x as f32, y as f32), + ); + if let Some(bbox) = self.current_bbox { + self.current_bbox = Some(bbox.union(&new_box)) + } else { + self.current_bbox = Some(new_box) + } + Ok(()) + } + + fn point_begin(&mut self, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn point_end(&mut self, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multipoint_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multipoint_end(&mut self, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn linestring_begin(&mut self, _tagged: bool, _size: usize, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn linestring_end(&mut self, tagged: bool, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multilinestring_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multilinestring_end(&mut self, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn polygon_begin(&mut self, _tagged: bool, _size: usize, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn polygon_end(&mut self, tagged: bool, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multipolygon_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> { + Ok(()) + } + + fn multipolygon_end(&mut self, _idx: usize) -> GeoResult<()> { + Ok(()) + } +} + +impl + MaxIndex> PropertyProcessor + for TextTessellator +{ + fn property( + &mut self, + idx: usize, + name: &str, + value: &ColumnValue, + ) -> geozero::error::Result { + if name == "name" { + match value { + ColumnValue::String(str) => { + self.current_text = Some(str.to_string()); + } + _ => {} + } + } + Ok(true) + } +} + +impl + MaxIndex> FeatureProcessor + for TextTessellator +{ + fn feature_end(&mut self, _idx: u64) -> geozero::error::Result<()> { + if let (Some(bbox), Some(text)) = (&self.current_bbox, self.current_text.clone()) { + let anchor = Anchor::Center; + // TODO: add more anchor possibilities; only support center right now + // TODO: document how anchor and glyph metrics work together to establish a baseline + let origin = match anchor { + Anchor::Center => bbox.center().to_array(), + _ => unimplemented!("no support for this anchor"), + }; + self.tessellate_glyph_quads( + origin, + text.as_str(), + Color::from_linear_rgba(1.0, 0., 0., 1.), + ); + + let next_index = self.quad_buffer.indices.len(); + let indices = (next_index - self.current_index) as u32; + self.feature_indices.push(indices); + self.current_index = next_index; + } + + self.current_bbox = None; + self.current_text = None; + Ok(()) + } +} diff --git a/maplibre/src/text/glyph.rs b/maplibre/src/text/glyph.rs new file mode 100644 index 000000000..4975ad92a --- /dev/null +++ b/maplibre/src/text/glyph.rs @@ -0,0 +1,118 @@ +use std::{collections::BTreeMap, convert::TryFrom}; + +use image::{GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma}; + +use crate::text::sdf_glyphs::{Glyph as ProtoGlyph, Glyphs}; + +pub type UnicodePoint = char; + +#[derive(Debug)] +pub struct Glyph { + pub codepoint: UnicodePoint, + pub width: u32, + pub height: u32, + pub left_bearing: i32, + pub top_bearing: i32, + h_advance: u32, + /// x origin coordinate within the packed texture + tex_origin_x: u32, + /// y origin coordinate within the packed texture + tex_origin_y: u32, +} + +impl Glyph { + fn from_pbf(g: ProtoGlyph, origin_x: u32, origin_y: u32) -> Self { + Self { + codepoint: char::try_from(g.id).unwrap(), + width: g.width, + height: g.height, + left_bearing: g.left, + top_bearing: g.top, + h_advance: g.advance, + tex_origin_x: origin_x, + tex_origin_y: origin_y, + } + } + + pub fn buffered_dimensions(&self) -> (u32, u32) { + (self.width + 3 * 2, self.height + 3 * 2) + } + pub fn origin_offset(&self) -> (u32, u32) { + (self.tex_origin_x, self.tex_origin_y) + } + pub fn advance(&self) -> u32 { + self.h_advance + } +} + +pub struct GlyphSet { + texture_bytes: Vec, + texture_dimensions: (usize, usize), + pub glyphs: BTreeMap, +} + +impl From for GlyphSet { + fn from(pbf_glyphs: Glyphs) -> Self { + let stacks = pbf_glyphs.stacks; + let mut texture: GrayImage = ImageBuffer::new(4096, 4096); + let mut last_position = (0, 0); + let mut max_height = 0; + + let glyphs = stacks + .into_iter() + .flat_map(|stack| { + stack + .glyphs + .into_iter() + .filter_map(|mut glyph| { + // Save an extra copy operation by taking the bits out directly. + let bitmap = glyph.bitmap.take()?; + + let glyph = Glyph::from_pbf(glyph, last_position.0, last_position.1); + + let buffered_width = glyph.width + 3 * 2; + let buffered_height = glyph.height + 3 * 2; + + let glyph_texture = ImageBuffer::, _>::from_vec( + buffered_width, + buffered_height, + bitmap, + )?; + assert_eq!(buffered_height, glyph_texture.height()); + assert_eq!(buffered_width, glyph_texture.width()); + + // TODO: wraparound on texture width overflow + texture + .copy_from(&glyph_texture, last_position.0, last_position.1) + .expect("Unable to copy glyph texture."); + + last_position.0 += glyph_texture.width(); + max_height = max_height.max(glyph_texture.height()); + + Some((glyph.codepoint, glyph)) + }) + .collect::>() + }) + .collect(); + + Self { + texture_bytes: texture + .view(0, 0, last_position.0, max_height) + .pixels() + .map(|(_x, _y, p)| p[0]) + .collect(), + texture_dimensions: (last_position.0 as _, max_height as _), + glyphs, + } + } +} + +impl GlyphSet { + pub fn get_texture_dimensions(&self) -> (usize, usize) { + self.texture_dimensions + } + + pub fn get_texture_bytes(&self) -> &[u8] { + self.texture_bytes.as_slice() + } +} diff --git a/maplibre/src/text/mod.rs b/maplibre/src/text/mod.rs new file mode 100644 index 000000000..99ea5ee56 --- /dev/null +++ b/maplibre/src/text/mod.rs @@ -0,0 +1,66 @@ +use lyon::tessellation::{FillVertex, FillVertexConstructor}; + +use crate::render::SymbolVertex; + +pub mod glyph; + +pub mod sdf_glyphs { + include!(concat!(env!("OUT_DIR"), "/glyphs.rs")); +} + +pub struct SymbolVertexBuilder { + /// Where is the top-left anchor of the glyph box + pub glyph_anchor: [f32; 3], + /// Where is the top-left anchor of the text box + pub text_anchor: [f32; 3], + /// Size of sprite-sheet * font_scale + pub texture_dimensions: (f32, f32), + /// Size of individual glyph * font_scale + pub sprite_dimensions: (f32, f32), + /// where in the sheet is the sprite * font_scale + pub sprite_offset: (f32, f32), + pub glyph: bool, + pub color: [u8; 4], +} + +impl FillVertexConstructor for SymbolVertexBuilder { + fn new_vertex(&mut self, vertex: FillVertex) -> SymbolVertex { + let vertex_position = vertex.position(); + + let sprite_ratio_x = self.sprite_dimensions.0 / self.texture_dimensions.0; + let sprite_ratio_y = self.sprite_dimensions.1 / self.texture_dimensions.1; + + let x_offset = self.sprite_offset.0 / self.texture_dimensions.0; + let y_offset = self.sprite_offset.1 / self.texture_dimensions.1; + + let tex_coords = [ + x_offset + + ((vertex_position.x - self.glyph_anchor[0]) / self.sprite_dimensions.0) + * sprite_ratio_x, + y_offset + + ((vertex_position.y - self.glyph_anchor[1]) / self.sprite_dimensions.1) + * sprite_ratio_y, + ]; + + SymbolVertex { + position: [vertex_position.x, vertex_position.y, 0.], + text_anchor: self.text_anchor, + is_glyph: if self.glyph { 1 } else { 0 }, + color: self.color, + tex_coords, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum Anchor { + Center, + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} diff --git a/maplibre/src/util/math.rs b/maplibre/src/util/math.rs index 98433a77b..8f3e6e16c 100644 --- a/maplibre/src/util/math.rs +++ b/maplibre/src/util/math.rs @@ -229,7 +229,7 @@ pub(crate) fn max(lhs: S, rhs: S) -> S { } /// A two-dimensional AABB, aka a rectangle. -pub struct Aabb2 { +pub struct Aabb2 { /// Minimum point of the AABB pub min: Point2, /// Maximum point of the AABB @@ -256,6 +256,29 @@ impl Aabb2 { self.max, ] } + + /* /// Returns true if the size is zero, negative or NaN. + #[inline] + pub fn is_empty(&self) -> bool { + !(self.max.x > self.min.x && self.max.y > self.min.y) + } + + /// Computes the union of two boxes. + /// + /// If either of the boxes is empty, the other one is returned. + pub fn union(&self, other: &Aabb2) -> Aabb2 { + if other.is_empty() { + return *self; + } + if self.is_empty() { + return *other; + } + + Aabb2 { + min: Point2::new(min(self.min.x, other.min.x), min(self.min.y, other.min.y)), + max: Point2::new(max(self.max.x, other.max.x), max(self.max.y, other.max.y)), + } + }*/ } impl fmt::Debug for Aabb2 {