From acb8a2b1cb6bfcc6f11778e52335ec4e7ffbaca1 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Fri, 16 Dec 2022 11:51:07 +0100 Subject: [PATCH 1/8] Start work on SDF font rendering --- maplibre/src/render/shaders/sdf.fragment.wgsl | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 maplibre/src/render/shaders/sdf.fragment.wgsl diff --git a/maplibre/src/render/shaders/sdf.fragment.wgsl b/maplibre/src/render/shaders/sdf.fragment.wgsl new file mode 100644 index 000000000..bd458d751 --- /dev/null +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -0,0 +1,45 @@ +struct VertexOutput { + @location(0) tex_coords: vec2, + @builtin(position) position: vec4, +}; + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + + +//layout(set = 1, binding = 0) uniform texture2D t_sprites; + //layout(set = 1, binding = 1) uniform sampler s_sprites; + // + //layout(set = 2, binding = 0) uniform texture2D t_glyphs; + //layout(set = 2, binding = 1) uniform sampler s_glyphs; + +@fragment +//layout(location=0) flat in uint f_glyph; + //layout(location=1) in vec2 f_tex_coords; + //layout(location=2) in vec4 color; +fn main(in: VertexOutput) -> @location(0) vec4 { + + // Note: we access both textures to ensure uniform control flow: + // https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Non-uniform_flow_control + + vec4 tex_color = texture(sampler2D(t_sprites, s_sprites), f_tex_coords); + + // 0 => border, < 0 => inside, > 0 => outside + // dist(ance) is scaled to [0.75, -0.25] + float glyphDist = 0.75 - texture(sampler2D(t_glyphs, s_glyphs), f_tex_coords).r; + + if (f_glyph == 0) { + f_color = tex_color.bgra; + } else { + // TODO: support: + // - outline + // - blur + + float alpha = smoothstep(0.10, 0, glyphDist); + f_color = vec4(color.bgr, color.a * alpha); + } + + //return textureSample(t_diffuse, s_diffuse, in.tex_coords); +} From 5ee82be3be33dd0c6e7ed4dbb45a1d70456671eb Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Fri, 16 Dec 2022 15:35:22 +0100 Subject: [PATCH 2/8] Fix shader --- maplibre/src/render/shaders/sdf.fragment.wgsl | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/maplibre/src/render/shaders/sdf.fragment.wgsl b/maplibre/src/render/shaders/sdf.fragment.wgsl index bd458d751..f821b4b1d 100644 --- a/maplibre/src/render/shaders/sdf.fragment.wgsl +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -1,45 +1,43 @@ struct VertexOutput { - @location(0) tex_coords: vec2, + @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_diffuse: texture_2d; +var t_sprites: texture_2d; @group(0) @binding(1) -var s_diffuse: sampler; - +var s_sprites: sampler; -//layout(set = 1, binding = 0) uniform texture2D t_sprites; - //layout(set = 1, binding = 1) uniform sampler s_sprites; - // - //layout(set = 2, binding = 0) uniform texture2D t_glyphs; - //layout(set = 2, binding = 1) uniform sampler s_glyphs; +@group(1) @binding(0) +var t_glyphs: texture_2d; +@group(1) @binding(1) +var s_glyphs: sampler; @fragment -//layout(location=0) flat in uint f_glyph; - //layout(location=1) in vec2 f_tex_coords; - //layout(location=2) in vec4 color; -fn main(in: VertexOutput) -> @location(0) vec4 { - +fn main(in: VertexOutput) -> Output { // Note: we access both textures to ensure uniform control flow: // https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Non-uniform_flow_control - vec4 tex_color = texture(sampler2D(t_sprites, s_sprites), f_tex_coords); + let tex_color = textureSample(t_sprites, s_sprites, in.tex_coords); // 0 => border, < 0 => inside, > 0 => outside // dist(ance) is scaled to [0.75, -0.25] - float glyphDist = 0.75 - texture(sampler2D(t_glyphs, s_glyphs), f_tex_coords).r; + let glyphDist = 0.75 - textureSample(t_glyphs, s_glyphs, in.tex_coords).r; - if (f_glyph == 0) { - f_color = tex_color.bgra; + if (in.is_glyph == 0) { + return Output(tex_color); } else { // TODO: support: // - outline // - blur - float alpha = smoothstep(0.10, 0, glyphDist); - f_color = vec4(color.bgr, color.a * alpha); + let alpha: f32 = smoothstep(0.10, 0, glyphDist); + return Output(vec4(in.color.bgr, in.color.a * alpha)); } - - //return textureSample(t_diffuse, s_diffuse, in.tex_coords); } From c7a13587963da4416c2135c636b25989a3d1af43 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Fri, 16 Dec 2022 21:12:26 +0100 Subject: [PATCH 3/8] Generating quads and sending them to the main thread --- data/0-255.pbf | Bin 0 -> 87258 bytes data/sprites.json | 37 +++ data/sprites.png | Bin 0 -> 2614 bytes maplibre/Cargo.toml | 5 + maplibre/build.rs | 22 ++ maplibre/proto/glyphs.proto | 33 +++ maplibre/src/io/apc.rs | 1 + maplibre/src/io/geometry_index.rs | 3 +- maplibre/src/io/pipeline.rs | 11 +- maplibre/src/io/tile_pipelines.rs | 46 +++- maplibre/src/io/tile_repository.rs | 28 +- maplibre/src/io/transferables.rs | 57 +++- maplibre/src/lib.rs | 1 + maplibre/src/render/main_pass.rs | 4 + maplibre/src/render/mod.rs | 15 +- maplibre/src/render/render_commands.rs | 10 +- maplibre/src/render/shaders/mod.rs | 15 + .../src/render/stages/phase_sort_stage.rs | 6 +- maplibre/src/render/stages/queue_stage.rs | 37 ++- maplibre/src/render/stages/resource_stage.rs | 4 + maplibre/src/render/stages/upload_stage.rs | 26 +- maplibre/src/render/tile_view_pattern.rs | 4 +- maplibre/src/stages/mod.rs | 21 +- .../src/stages/populate_tile_store_stage.rs | 10 +- maplibre/src/style/style.rs | 13 + maplibre/src/tessellation/mod.rs | 1 + maplibre/src/tessellation/text_tesselator.rs | 257 ++++++++++++++++++ maplibre/src/text/glyph.rs | 118 ++++++++ maplibre/src/text/mod.rs | 62 +++++ maplibre/src/util/math.rs | 25 +- 30 files changed, 829 insertions(+), 43 deletions(-) create mode 100644 data/0-255.pbf create mode 100644 data/sprites.json create mode 100644 data/sprites.png create mode 100644 maplibre/proto/glyphs.proto create mode 100644 maplibre/src/tessellation/text_tesselator.rs create mode 100644 maplibre/src/text/glyph.rs create mode 100644 maplibre/src/text/mod.rs diff --git a/data/0-255.pbf b/data/0-255.pbf new file mode 100644 index 0000000000000000000000000000000000000000..92d76f170de8fb0fb10f8a79fff49d9990b6413b GIT binary patch literal 87258 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Ð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 z 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%0D;o+-(0(yToh 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 + + 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/main_pass.rs b/maplibre/src/render/main_pass.rs index 635f1063e..2e88f8dd2 100644 --- a/maplibre/src/render/main_pass.rs +++ b/maplibre/src/render/main_pass.rs @@ -95,6 +95,10 @@ impl Node for MainPassNode { DrawTiles::render(state, item, &mut tracked_pass); } + for item in &state.symbol_tile_phase.items { + DrawTiles::render(state, item, &mut tracked_pass); + } + Ok(()) } } diff --git a/maplibre/src/render/mod.rs b/maplibre/src/render/mod.rs index 30c8a4fc8..e6ed4d8b9 100644 --- a/maplibre/src/render/mod.rs +++ b/maplibre/src/render/mod.rs @@ -53,7 +53,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::{ @@ -81,6 +81,16 @@ pub struct RenderState { ShaderFeatureStyle, >, >, + symbol_buffer_pool: Eventually< + BufferPool< + wgpu::Queue, + wgpu::Buffer, + SymbolVertex, + IndexDataType, + ShaderLayerMetadata, + ShaderFeatureStyle, + >, + >, tile_view_pattern: Eventually>, tile_pipeline: Eventually, @@ -96,6 +106,7 @@ pub struct RenderState { mask_phase: RenderPhase, tile_phase: RenderPhase<(IndexEntry, TileShape)>, + symbol_tile_phase: RenderPhase<(IndexEntry, TileShape)>, } impl RenderState { @@ -103,6 +114,7 @@ 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(), @@ -113,6 +125,7 @@ impl RenderState { surface, mask_phase: Default::default(), tile_phase: Default::default(), + symbol_tile_phase: Default::default(), } } diff --git a/maplibre/src/render/render_commands.rs b/maplibre/src/render/render_commands.rs index 5416f1d40..d63aeffe6 100644 --- a/maplibre/src/render/render_commands.rs +++ b/maplibre/src/render/render_commands.rs @@ -159,12 +159,10 @@ impl RenderCommand<(IndexEntry, TileShape)> for DrawTile { .metadata() .slice(entry.layer_metadata_buffer_range()), ); - pass.set_vertex_buffer( - 3, - buffer_pool - .feature_metadata() - .slice(entry.feature_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 } diff --git a/maplibre/src/render/shaders/mod.rs b/maplibre/src/render/shaders/mod.rs index 0bdfc679a..01c41916b 100644 --- a/maplibre/src/render/shaders/mod.rs +++ b/maplibre/src/render/shaders/mod.rs @@ -283,3 +283,18 @@ 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 origin: [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, +} 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..0fd7b18bf 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,27 @@ 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()) { + log::info!("queueing {:?}", &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..58c8f9a13 100644 --- a/maplibre/src/render/stages/resource_stage.rs +++ b/maplibre/src/render/stages/resource_stage.rs @@ -79,6 +79,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"), 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_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..76df125cb --- /dev/null +++ b/maplibre/src/tessellation/text_tesselator.rs @@ -0,0 +1,257 @@ +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> { + pub quad_buffer: VertexBuffers, + + pub feature_indices: Vec, + + current_index: usize, + current_text: Option, + current_bbox: Option>, +} + +impl + MaxIndex> Default + for TextTessellator +{ + fn default() -> Self { + Self { + 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], + glyphs: &GlyphSet, + font_size: f32, + label_text: &str, + zoom: f32, + color: Color, + ) -> Option> { + let mut tessellator = FillTessellator::new(); + + let font_scale = font_size / 24.; + let m_p_px = meters_per_pixel(zoom.floor()) * font_scale; + + let mut next_glyph_origin = origin; + + let texture_dimensions = glyphs.get_texture_dimensions(); + let texture_dimensions = ( + texture_dimensions.0 as f32 * m_p_px, + texture_dimensions.1 as f32 * m_p_px, + ); + + // TODO: silently drops unknown characters + // TODO: handle line wrapping / line height + let mut bbox = None; + for glyph in label_text + .chars() + .filter_map(|c| glyphs.glyphs.get(&c)) + .collect::>() + { + let glyph_dims = glyph.buffered_dimensions(); + let meter_width = glyph_dims.0 as f32 * m_p_px; + let meter_height = glyph_dims.1 as f32 * m_p_px; + + let anchor = [ + next_glyph_origin[0] + glyph.left_bearing as f32 * m_p_px, + next_glyph_origin[1] - meter_height + glyph.top_bearing as f32 * m_p_px, + 0., + ]; + + let glyph_rect = Box2D::new( + (anchor[0], anchor[1]).into(), + (meter_width, meter_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 { + anchor, + texture_dimensions, + sprite_dimensions: (meter_width, meter_height), + sprite_offset: ( + glyph.origin_offset().0 as f32 * m_p_px, + glyph.origin_offset().1 as f32 * m_p_px, + ), + color: color.to_rgba8(), // TODO: is this conversion oke? + glyph: true, // Set here to true to use SDF rendering + }, + ), + ) + .ok()?; + + next_glyph_origin[0] += glyph.advance() as f32 * m_p_px; + } + + 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"), + }; + let data = fs::read("./data/0-255.pbf").unwrap(); + let glyphs = GlyphSet::from(Glyphs::decode(data.as_slice()).unwrap()); + self.tessellate_glyph_quads( + origin, + &glyphs, + 16.0, + text.as_str(), + 10.0, + 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(()) + } +} + +#[inline] +pub fn tile_scale_for_zoom(zoom: f32) -> f32 { + const MERCATOR_RADIUS: f32 = 6378137.; // in meters + const MERCATOR_RADIUS_PI: f32 = MERCATOR_RADIUS * std::f32::consts::PI; + const MERCATOR_RADIUS_2PI: f32 = 2. * MERCATOR_RADIUS_PI; + // Each zoom should show Mercator points 2x larger than the previous zoom + // level. + MERCATOR_RADIUS_2PI / 2f32.powf(zoom) +} + +#[inline] +pub fn meters_per_pixel(zoom: f32) -> f32 { + tile_scale_for_zoom(zoom) / 512.0 // FIXME +} 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..03066207d --- /dev/null +++ b/maplibre/src/text/mod.rs @@ -0,0 +1,62 @@ +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 { + /// In meters + pub anchor: [f32; 3], + /// In meters + pub texture_dimensions: (f32, f32), + /// In meters + pub sprite_dimensions: (f32, f32), + /// In meters + 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 p = 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 + ((p.x - self.anchor[0]) / self.sprite_dimensions.0) * sprite_ratio_x, + y_offset + + (sprite_ratio_y + - ((p.y - self.anchor[1]) / self.sprite_dimensions.1) * sprite_ratio_y), + ]; + + SymbolVertex { + position: [p.x, p.y, 0.], + origin: self.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 { From 98ce0964820b03ca9e51b38813e49c65a1597a0c Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Fri, 16 Dec 2022 21:39:35 +0100 Subject: [PATCH 4/8] Rendering quads works --- maplibre/src/render/main_pass.rs | 4 +- maplibre/src/render/mod.rs | 2 + maplibre/src/render/render_commands.rs | 50 ++++++++++ maplibre/src/render/shaders/mod.rs | 93 +++++++++++++++++++ maplibre/src/render/shaders/sdf.fragment.wgsl | 2 +- maplibre/src/render/shaders/sdf.vertex.wgsl | 45 +++++++++ maplibre/src/render/stages/resource_stage.rs | 20 ++++ maplibre/src/tessellation/text_tesselator.rs | 5 +- 8 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 maplibre/src/render/shaders/sdf.vertex.wgsl diff --git a/maplibre/src/render/main_pass.rs b/maplibre/src/render/main_pass.rs index 2e88f8dd2..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, @@ -96,7 +96,7 @@ impl Node for MainPassNode { } for item in &state.symbol_tile_phase.items { - DrawTiles::render(state, item, &mut tracked_pass); + DrawSymbols::render(state, item, &mut tracked_pass); } Ok(()) diff --git a/maplibre/src/render/mod.rs b/maplibre/src/render/mod.rs index e6ed4d8b9..eb89c9e84 100644 --- a/maplibre/src/render/mod.rs +++ b/maplibre/src/render/mod.rs @@ -96,6 +96,7 @@ pub struct RenderState { tile_pipeline: Eventually, mask_pipeline: Eventually, debug_pipeline: Eventually, + symbol_pipeline: Eventually, globals_bind_group: Eventually, @@ -119,6 +120,7 @@ impl RenderState { tile_pipeline: Default::default(), mask_pipeline: Default::default(), debug_pipeline: Default::default(), + symbol_pipeline: Default::default(), globals_bind_group: Default::default(), depth_texture: Default::default(), multisampling_texture: Default::default(), diff --git a/maplibre/src/render/render_commands.rs b/maplibre/src/render/render_commands.rs index d63aeffe6..cad34434b 100644 --- a/maplibre/src/render/render_commands.rs +++ b/maplibre/src/render/render_commands.rs @@ -62,6 +62,19 @@ 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(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>( @@ -168,8 +181,45 @@ impl RenderCommand<(IndexEntry, TileShape)> for DrawTile { } } +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( + 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 + } +} + 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/shaders/mod.rs b/maplibre/src/render/shaders/mod.rs index 01c41916b..ddac493ef 100644 --- a/maplibre/src/render/shaders/mod.rs +++ b/maplibre/src/render/shaders/mod.rs @@ -298,3 +298,96 @@ pub struct SymbolVertex { // 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, + }, + /* // normal + wgpu::VertexAttribute { + offset: wgpu::VertexFormat::Float32x2.size(), + format: wgpu::VertexFormat::Float32x2, + shader_location: 1, + },*/ + ], + }, + // 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!("basic.fragment.wgsl"), + entry_point: "main", + targets: vec![Some(wgpu::ColorTargetState { + format: self.format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + } + } +} diff --git a/maplibre/src/render/shaders/sdf.fragment.wgsl b/maplibre/src/render/shaders/sdf.fragment.wgsl index f821b4b1d..6d769e281 100644 --- a/maplibre/src/render/shaders/sdf.fragment.wgsl +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -37,7 +37,7 @@ fn main(in: VertexOutput) -> Output { // - outline // - blur - let alpha: f32 = smoothstep(0.10, 0, glyphDist); + let alpha: f32 = smoothstep(0.10, 0.0, glyphDist); return Output(vec4(in.color.bgr, in.color.a * alpha)); } } diff --git a/maplibre/src/render/shaders/sdf.vertex.wgsl b/maplibre/src/render/shaders/sdf.vertex.wgsl new file mode 100644 index 000000000..97bb212a0 --- /dev/null +++ b/maplibre/src/render/shaders/sdf.vertex.wgsl @@ -0,0 +1,45 @@ +struct ShaderCamera { + view_proj: mat4x4, + view_position: vec4, +}; + +struct ShaderGlobals { + camera: ShaderCamera, +}; + +@group(0) @binding(0) var globals: ShaderGlobals; + +/*struct VertexOutput { + @location(0) is_glyph: i32, + @location(1) tex_coords: vec2, + @location(2) color: vec4, + @builtin(position) position: vec4, +};*/ +struct VertexOutput { + @location(0) v_color: vec4, + @builtin(position) position: vec4, +}; + + +@vertex +fn main( + @location(0) position: 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 z = 0.0; + let width = 3.0 * zoom_factor; + let normal = vec2(0.0,0.0); + + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(vec2(position.x, position.y) + normal * width, z, 1.0); + position.z = z_index; + + //return VertexOutput(1, tex_coords, color, position); + return VertexOutput(vec4(1.0,0.0, 0.0, 1.0), position); +} diff --git a/maplibre/src/render/stages/resource_stage.rs b/maplibre/src/render/stages/resource_stage.rs index 58c8f9a13..7ef305362 100644 --- a/maplibre/src/render/stages/resource_stage.rs +++ b/maplibre/src/render/stages/resource_stage.rs @@ -167,5 +167,25 @@ impl Stage for ResourceStage { .describe_render_pipeline() .initialize(device) }); + + state.symbol_pipeline.initialize(|| { + let mask_shader = shaders::SymbolTileShader { + format: surface.surface_format(), + }; + + TilePipeline::new( + *settings, + mask_shader.describe_vertex(), + mask_shader.describe_fragment(), + false, + true, + false, + true, + false, + true, + ) + .describe_render_pipeline() + .initialize(device) + }); } } diff --git a/maplibre/src/tessellation/text_tesselator.rs b/maplibre/src/tessellation/text_tesselator.rs index 76df125cb..952aa410c 100644 --- a/maplibre/src/tessellation/text_tesselator.rs +++ b/maplibre/src/tessellation/text_tesselator.rs @@ -56,6 +56,7 @@ impl + MaxIndex> TextTesse let font_scale = font_size / 24.; let m_p_px = meters_per_pixel(zoom.floor()) * font_scale; + let m_p_px = 6.0; let mut next_glyph_origin = origin; @@ -85,8 +86,10 @@ impl + MaxIndex> TextTesse let glyph_rect = Box2D::new( (anchor[0], anchor[1]).into(), - (meter_width, meter_height).into(), + (anchor[0] + meter_width, anchor[1] + meter_height).into(), ); + //let glyph_rect = Box2D::new((0.0, 0.0).into(), (100.0, 100.0).into()); + bbox = bbox.map_or_else( || Some(glyph_rect), |bbox: Box2D<_>| Some(bbox.union(&glyph_rect)), From 17cfde8749e970e93505352f7e73a7887844ba30 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Sat, 17 Dec 2022 11:44:58 +0100 Subject: [PATCH 5/8] First adequately working version of text rendering --- maplibre/src/render/eventually.rs | 7 +- maplibre/src/render/mod.rs | 7 ++ maplibre/src/render/render_commands.rs | 5 +- maplibre/src/render/resource/glyph_texture.rs | 39 ++++++++++ maplibre/src/render/resource/mod.rs | 2 + maplibre/src/render/shaders/mod.rs | 31 ++++++-- maplibre/src/render/shaders/sdf.fragment.wgsl | 32 ++++----- maplibre/src/render/shaders/sdf.vertex.wgsl | 34 +++------ .../src/render/shaders/tile_debug.vertex.wgsl | 8 +-- .../src/render/shaders/tile_mask.vertex.wgsl | 8 +-- maplibre/src/render/stages/queue_stage.rs | 2 - maplibre/src/render/stages/resource_stage.rs | 62 +++++++++++++++- maplibre/src/render/tile_pipeline.rs | 22 ++++++ maplibre/src/tessellation/text_tesselator.rs | 71 +++++++------------ maplibre/src/text/mod.rs | 26 ++++--- 15 files changed, 231 insertions(+), 125 deletions(-) create mode 100644 maplibre/src/render/resource/glyph_texture.rs 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/mod.rs b/maplibre/src/render/mod.rs index eb89c9e84..d62a5d36a 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, @@ -62,6 +64,7 @@ use crate::{ error::RenderError, graph::{EmptyNode, RenderGraph, RenderGraphError}, main_pass::{MainPassDriverNode, MainPassNode}, + resource::GlyphTexture, }, window::{HeadedMapWindow, MapWindow}, }; @@ -98,10 +101,12 @@ pub struct RenderState { 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, @@ -121,9 +126,11 @@ impl RenderState { 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(), diff --git a/maplibre/src/render/render_commands.rs b/maplibre/src/render/render_commands.rs index cad34434b..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, }; @@ -69,6 +69,9 @@ impl RenderCommand

for SetSymbolPipeline { _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 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/shaders/mod.rs b/maplibre/src/render/shaders/mod.rs index ddac493ef..f581098ec 100644 --- a/maplibre/src/render/shaders/mod.rs +++ b/maplibre/src/render/shaders/mod.rs @@ -290,7 +290,7 @@ pub struct SymbolVertex { // 4 bytes * 3 = 12 bytes pub position: [f32; 3], // 4 bytes * 3 = 12 bytes - pub origin: [f32; 3], + pub text_anchor: [f32; 3], // 4 bytes * 2 = 8 bytes pub tex_coords: [f32; 2], // 1 byte * 4 = 4 bytes @@ -320,12 +320,18 @@ impl Shader for SymbolTileShader { format: wgpu::VertexFormat::Float32x3, shader_location: 0, }, - /* // normal + // text_anchor wgpu::VertexAttribute { - offset: wgpu::VertexFormat::Float32x2.size(), - format: wgpu::VertexFormat::Float32x2, + 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 @@ -381,12 +387,23 @@ impl Shader for SymbolTileShader { fn describe_fragment(&self) -> FragmentState { FragmentState { - source: include_str!("basic.fragment.wgsl"), + source: include_str!("sdf.fragment.wgsl"), entry_point: "main", targets: vec![Some(wgpu::ColorTargetState { format: self.format, - blend: None, 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 index 6d769e281..04b1d4f10 100644 --- a/maplibre/src/render/shaders/sdf.fragment.wgsl +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -10,34 +10,32 @@ struct Output { }; @group(0) @binding(0) -var t_sprites: texture_2d; -@group(0) @binding(1) -var s_sprites: sampler; - -@group(1) @binding(0) var t_glyphs: texture_2d; -@group(1) @binding(1) +@group(0) @binding(1) var s_glyphs: sampler; @fragment fn main(in: VertexOutput) -> Output { - // Note: we access both textures to ensure uniform control flow: + // Note: Ensure uniform control flow! // https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Non-uniform_flow_control - let tex_color = textureSample(t_sprites, s_sprites, in.tex_coords); - // 0 => border, < 0 => inside, > 0 => outside // dist(ance) is scaled to [0.75, -0.25] let glyphDist = 0.75 - textureSample(t_glyphs, s_glyphs, in.tex_coords).r; - if (in.is_glyph == 0) { - return Output(tex_color); - } else { - // TODO: support: - // - outline - // - blur + // TODO: support: + // - outline + // - blur + + let alpha: f32 = smoothstep(0.10, 0.0, glyphDist); - let alpha: f32 = smoothstep(0.10, 0.0, glyphDist); - return Output(vec4(in.color.bgr, in.color.a * alpha)); + // "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(in.color.rgb, in.color.a * alpha)); } diff --git a/maplibre/src/render/shaders/sdf.vertex.wgsl b/maplibre/src/render/shaders/sdf.vertex.wgsl index 97bb212a0..28c80182b 100644 --- a/maplibre/src/render/shaders/sdf.vertex.wgsl +++ b/maplibre/src/render/shaders/sdf.vertex.wgsl @@ -1,45 +1,31 @@ -struct ShaderCamera { - view_proj: mat4x4, - view_position: vec4, -}; - -struct ShaderGlobals { - camera: ShaderCamera, -}; - -@group(0) @binding(0) var globals: ShaderGlobals; - -/*struct VertexOutput { +struct VertexOutput { @location(0) is_glyph: i32, @location(1) tex_coords: vec2, @location(2) color: vec4, @builtin(position) position: vec4, -};*/ -struct VertexOutput { - @location(0) v_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, + @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 z = 0.0; - let width = 3.0 * zoom_factor; - let normal = vec2(0.0,0.0); + let scaling: mat3x3 = mat3x3( + vec3(zoom_factor, 0.0, 0.0), + vec3(0.0, zoom_factor, 0.0), + vec3(0.0, 0.0, 1.0) + ); - var position = mat4x4(translate1, translate2, translate3, translate4) * vec4(vec2(position.x, position.y) + normal * width, z, 1.0); + var position = mat4x4(translate1, translate2, translate3, translate4) * vec4((scaling * (position - text_anchor) + text_anchor), 1.0); position.z = z_index; - //return VertexOutput(1, tex_coords, color, position); - return VertexOutput(vec4(1.0,0.0, 0.0, 1.0), position); + return VertexOutput(1, tex_coords, vec4(0.0, 0.0, 0.0, 1.0), 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/queue_stage.rs b/maplibre/src/render/stages/queue_stage.rs index 0fd7b18bf..f3c97b395 100644 --- a/maplibre/src/render/stages/queue_stage.rs +++ b/maplibre/src/render/stages/queue_stage.rs @@ -61,8 +61,6 @@ impl Stage for QueueStage { }; if let Some(entries) = symbol_pool_index.get_layers(&source_shape.coords()) { - log::info!("queueing {:?}", &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); diff --git a/maplibre/src/render/stages/resource_stage.rs b/maplibre/src/render/stages/resource_stage.rs index 7ef305362..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, .. }, .. @@ -113,6 +119,7 @@ impl Stage for ResourceStage { false, false, true, + false, ) .describe_render_pipeline() .initialize(device); @@ -141,6 +148,7 @@ impl Stage for ResourceStage { false, false, true, + false, ) .describe_render_pipeline() .initialize(device) @@ -163,6 +171,7 @@ impl Stage for ResourceStage { true, false, false, + false, ) .describe_render_pipeline() .initialize(device) @@ -173,7 +182,7 @@ impl Stage for ResourceStage { format: surface.surface_format(), }; - TilePipeline::new( + let pipeline = TilePipeline::new( *settings, mask_shader.describe_vertex(), mask_shader.describe_fragment(), @@ -183,9 +192,56 @@ impl Stage for ResourceStage { true, false, true, + true, ) .describe_render_pipeline() - .initialize(device) + .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/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/tessellation/text_tesselator.rs b/maplibre/src/tessellation/text_tesselator.rs index 952aa410c..fc53398d9 100644 --- a/maplibre/src/tessellation/text_tesselator.rs +++ b/maplibre/src/tessellation/text_tesselator.rs @@ -19,10 +19,13 @@ type GeoResult = geozero::error::Result; /// Build tessellations with vectors. pub struct TextTessellator + MaxIndex> { - pub quad_buffer: VertexBuffers, + glyphs: GlyphSet, + // output + pub quad_buffer: VertexBuffers, pub feature_indices: Vec, + // iteration variables current_index: usize, current_text: Option, current_bbox: Option>, @@ -32,7 +35,10 @@ 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, @@ -46,24 +52,19 @@ impl + MaxIndex> TextTesse pub fn tessellate_glyph_quads( &mut self, origin: [f32; 2], - glyphs: &GlyphSet, - font_size: f32, label_text: &str, - zoom: f32, color: Color, ) -> Option> { let mut tessellator = FillTessellator::new(); - let font_scale = font_size / 24.; - let m_p_px = meters_per_pixel(zoom.floor()) * font_scale; - let m_p_px = 6.0; + let font_scale = 3.0; - let mut next_glyph_origin = origin; + let mut next_origin = origin; - let texture_dimensions = glyphs.get_texture_dimensions(); + let texture_dimensions = self.glyphs.get_texture_dimensions(); let texture_dimensions = ( - texture_dimensions.0 as f32 * m_p_px, - texture_dimensions.1 as f32 * m_p_px, + texture_dimensions.0 as f32 * font_scale, + texture_dimensions.1 as f32 * font_scale, ); // TODO: silently drops unknown characters @@ -71,24 +72,23 @@ impl + MaxIndex> TextTesse let mut bbox = None; for glyph in label_text .chars() - .filter_map(|c| glyphs.glyphs.get(&c)) + .filter_map(|c| self.glyphs.glyphs.get(&c)) .collect::>() { let glyph_dims = glyph.buffered_dimensions(); - let meter_width = glyph_dims.0 as f32 * m_p_px; - let meter_height = glyph_dims.1 as f32 * m_p_px; + let width = glyph_dims.0 as f32 * font_scale; + let height = glyph_dims.1 as f32 * font_scale; - let anchor = [ - next_glyph_origin[0] + glyph.left_bearing as f32 * m_p_px, - next_glyph_origin[1] - meter_height + glyph.top_bearing as f32 * m_p_px, + let glyph_anchor = [ + next_origin[0] + glyph.left_bearing as f32 * font_scale, + next_origin[1] - glyph.top_bearing as f32 * font_scale, 0., ]; let glyph_rect = Box2D::new( - (anchor[0], anchor[1]).into(), - (anchor[0] + meter_width, anchor[1] + meter_height).into(), + (glyph_anchor[0], glyph_anchor[1]).into(), + (glyph_anchor[0] + width, glyph_anchor[1] + height).into(), ); - //let glyph_rect = Box2D::new((0.0, 0.0).into(), (100.0, 100.0).into()); bbox = bbox.map_or_else( || Some(glyph_rect), @@ -102,12 +102,13 @@ impl + MaxIndex> TextTesse &mut BuffersBuilder::new( &mut self.quad_buffer, SymbolVertexBuilder { - anchor, + glyph_anchor, + text_anchor: [origin[0], origin[1], 0.0], texture_dimensions, - sprite_dimensions: (meter_width, meter_height), + sprite_dimensions: (width, height), sprite_offset: ( - glyph.origin_offset().0 as f32 * m_p_px, - glyph.origin_offset().1 as f32 * m_p_px, + glyph.origin_offset().0 as f32 * font_scale, + glyph.origin_offset().1 as f32 * font_scale, ), color: color.to_rgba8(), // TODO: is this conversion oke? glyph: true, // Set here to true to use SDF rendering @@ -116,7 +117,7 @@ impl + MaxIndex> TextTesse ) .ok()?; - next_glyph_origin[0] += glyph.advance() as f32 * m_p_px; + next_origin[0] += glyph.advance() as f32 * font_scale; } bbox @@ -221,14 +222,9 @@ impl + MaxIndex> FeaturePr Anchor::Center => bbox.center().to_array(), _ => unimplemented!("no support for this anchor"), }; - let data = fs::read("./data/0-255.pbf").unwrap(); - let glyphs = GlyphSet::from(Glyphs::decode(data.as_slice()).unwrap()); self.tessellate_glyph_quads( origin, - &glyphs, - 16.0, text.as_str(), - 10.0, Color::from_linear_rgba(1.0, 0., 0., 1.), ); @@ -243,18 +239,3 @@ impl + MaxIndex> FeaturePr Ok(()) } } - -#[inline] -pub fn tile_scale_for_zoom(zoom: f32) -> f32 { - const MERCATOR_RADIUS: f32 = 6378137.; // in meters - const MERCATOR_RADIUS_PI: f32 = MERCATOR_RADIUS * std::f32::consts::PI; - const MERCATOR_RADIUS_2PI: f32 = 2. * MERCATOR_RADIUS_PI; - // Each zoom should show Mercator points 2x larger than the previous zoom - // level. - MERCATOR_RADIUS_2PI / 2f32.powf(zoom) -} - -#[inline] -pub fn meters_per_pixel(zoom: f32) -> f32 { - tile_scale_for_zoom(zoom) / 512.0 // FIXME -} diff --git a/maplibre/src/text/mod.rs b/maplibre/src/text/mod.rs index 03066207d..99ea5ee56 100644 --- a/maplibre/src/text/mod.rs +++ b/maplibre/src/text/mod.rs @@ -9,13 +9,15 @@ pub mod sdf_glyphs { } pub struct SymbolVertexBuilder { - /// In meters - pub anchor: [f32; 3], - /// In meters + /// 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), - /// In meters + /// Size of individual glyph * font_scale pub sprite_dimensions: (f32, f32), - /// In meters + /// where in the sheet is the sprite * font_scale pub sprite_offset: (f32, f32), pub glyph: bool, pub color: [u8; 4], @@ -23,7 +25,7 @@ pub struct SymbolVertexBuilder { impl FillVertexConstructor for SymbolVertexBuilder { fn new_vertex(&mut self, vertex: FillVertex) -> SymbolVertex { - let p = vertex.position(); + 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; @@ -32,15 +34,17 @@ impl FillVertexConstructor for SymbolVertexBuilder { let y_offset = self.sprite_offset.1 / self.texture_dimensions.1; let tex_coords = [ - x_offset + ((p.x - self.anchor[0]) / self.sprite_dimensions.0) * sprite_ratio_x, + x_offset + + ((vertex_position.x - self.glyph_anchor[0]) / self.sprite_dimensions.0) + * sprite_ratio_x, y_offset - + (sprite_ratio_y - - ((p.y - self.anchor[1]) / self.sprite_dimensions.1) * sprite_ratio_y), + + ((vertex_position.y - self.glyph_anchor[1]) / self.sprite_dimensions.1) + * sprite_ratio_y, ]; SymbolVertex { - position: [p.x, p.y, 0.], - origin: self.anchor, + 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, From 24699a78e51b7e462cc7154e3a59b533f6a01e67 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Sat, 17 Dec 2022 12:40:30 +0100 Subject: [PATCH 6/8] Move font size to shader --- maplibre/src/render/shaders/sdf.vertex.wgsl | 6 ++++-- maplibre/src/tessellation/text_tesselator.rs | 21 ++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/maplibre/src/render/shaders/sdf.vertex.wgsl b/maplibre/src/render/shaders/sdf.vertex.wgsl index 28c80182b..1ccd130eb 100644 --- a/maplibre/src/render/shaders/sdf.vertex.wgsl +++ b/maplibre/src/render/shaders/sdf.vertex.wgsl @@ -18,9 +18,11 @@ fn main( @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, 0.0, 0.0), - vec3(0.0, zoom_factor, 0.0), + 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) ); diff --git a/maplibre/src/tessellation/text_tesselator.rs b/maplibre/src/tessellation/text_tesselator.rs index fc53398d9..aff11ba7e 100644 --- a/maplibre/src/tessellation/text_tesselator.rs +++ b/maplibre/src/tessellation/text_tesselator.rs @@ -57,15 +57,10 @@ impl + MaxIndex> TextTesse ) -> Option> { let mut tessellator = FillTessellator::new(); - let font_scale = 3.0; - let mut next_origin = origin; let texture_dimensions = self.glyphs.get_texture_dimensions(); - let texture_dimensions = ( - texture_dimensions.0 as f32 * font_scale, - texture_dimensions.1 as f32 * font_scale, - ); + let texture_dimensions = (texture_dimensions.0 as f32, texture_dimensions.1 as f32); // TODO: silently drops unknown characters // TODO: handle line wrapping / line height @@ -76,12 +71,12 @@ impl + MaxIndex> TextTesse .collect::>() { let glyph_dims = glyph.buffered_dimensions(); - let width = glyph_dims.0 as f32 * font_scale; - let height = glyph_dims.1 as f32 * font_scale; + 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 * font_scale, - next_origin[1] - glyph.top_bearing as f32 * font_scale, + next_origin[0] + glyph.left_bearing as f32, + next_origin[1] - glyph.top_bearing as f32, 0., ]; @@ -107,8 +102,8 @@ impl + MaxIndex> TextTesse texture_dimensions, sprite_dimensions: (width, height), sprite_offset: ( - glyph.origin_offset().0 as f32 * font_scale, - glyph.origin_offset().1 as f32 * font_scale, + 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 @@ -117,7 +112,7 @@ impl + MaxIndex> TextTesse ) .ok()?; - next_origin[0] += glyph.advance() as f32 * font_scale; + next_origin[0] += glyph.advance() as f32; } bbox From b4ba1a955ed3aca6e1dc280c62e79f3a1e93b91e Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Wed, 21 Dec 2022 17:30:40 +0100 Subject: [PATCH 7/8] Add : force_fallback_adapter --- maplibre/src/render/mod.rs | 4 ++-- maplibre/src/render/settings.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/maplibre/src/render/mod.rs b/maplibre/src/render/mod.rs index d62a5d36a..4c3ee7aa9 100644 --- a/maplibre/src/render/mod.rs +++ b/maplibre/src/render/mod.rs @@ -197,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), }, ) @@ -236,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/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, From 89eb8e1a4ff21e641aa172eb801b5d56aaa8f611 Mon Sep 17 00:00:00 2001 From: Maximilian Ammann Date: Wed, 21 Dec 2022 17:54:40 +0100 Subject: [PATCH 8/8] Add fragment shader with an outline --- maplibre/src/render/shaders/sdf.fragment.wgsl | 52 +++++++++++++++---- maplibre/src/render/shaders/sdf.vertex.wgsl | 4 +- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/maplibre/src/render/shaders/sdf.fragment.wgsl b/maplibre/src/render/shaders/sdf.fragment.wgsl index 04b1d4f10..df98bc977 100644 --- a/maplibre/src/render/shaders/sdf.fragment.wgsl +++ b/maplibre/src/render/shaders/sdf.fragment.wgsl @@ -14,20 +14,28 @@ 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 { - // Note: Ensure uniform control flow! - // https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Non-uniform_flow_control + 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 - // dist(ance) is scaled to [0.75, -0.25] - let glyphDist = 0.75 - textureSample(t_glyphs, s_glyphs, in.tex_coords).r; + let dist = textureSample(t_glyphs, s_glyphs, in.tex_coords).r; - // TODO: support: - // - outline - // - blur + 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 alpha: f32 = smoothstep(0.10, 0.0, glyphDist); + 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. @@ -37,5 +45,31 @@ fn main(in: VertexOutput) -> Output { discard; } - return Output(vec4(in.color.rgb, in.color.a * alpha)); + 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 index 1ccd130eb..d7dc3ff90 100644 --- a/maplibre/src/render/shaders/sdf.vertex.wgsl +++ b/maplibre/src/render/shaders/sdf.vertex.wgsl @@ -29,5 +29,7 @@ fn main( var position = mat4x4(translate1, translate2, translate3, translate4) * vec4((scaling * (position - text_anchor) + text_anchor), 1.0); position.z = z_index; - return VertexOutput(1, tex_coords, vec4(0.0, 0.0, 0.0, 1.0), position); + 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); }