diff --git a/.idea/maplibre-rs.iml b/.idea/maplibre-rs.iml
index 339ac96a3..ccd2fb668 100644
--- a/.idea/maplibre-rs.iml
+++ b/.idea/maplibre-rs.iml
@@ -15,6 +15,9 @@
+
+
+
diff --git a/Cargo.toml b/Cargo.toml
index de596bc9b..dce6b0973 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,8 @@ members = [
"web",
"benchmarks",
+
+ "experiments/wgpu_for_fontrendering"
]
[profile.release]
diff --git a/experiments/wgpu_for_fontrendering/.gitignore b/experiments/wgpu_for_fontrendering/.gitignore
new file mode 100644
index 000000000..ea8c4bf7f
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/experiments/wgpu_for_fontrendering/.vscode/launch.json b/experiments/wgpu_for_fontrendering/.vscode/launch.json
new file mode 100644
index 000000000..191c9e573
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/.vscode/launch.json
@@ -0,0 +1,27 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "wgpu_for_fontrendering",
+ "cargo": {
+ "args": [
+ "build",
+ "--target=aarch64-apple-darwin",
+ "--bin=wgpu_for_fontrendering",
+ "--package=wgpu_for_fontrendering"
+ ],
+ "filter": {
+ "name": "wgpu_for_fontrendering",
+ "kind": "bin"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ },
+ ]
+}
\ No newline at end of file
diff --git a/experiments/wgpu_for_fontrendering/Cargo.toml b/experiments/wgpu_for_fontrendering/Cargo.toml
new file mode 100644
index 000000000..60b1e787f
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "wgpu_for_fontrendering"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[dependencies]
+winit = "0.27.2"
+env_logger = "0.9"
+log = "0.4"
+wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "94ce763" }
+pollster = "0.2.5"
+bytemuck = { version = "1.4", features = [ "derive" ] }
+cgmath = "0.18"
+ttf-parser = "0.15.0"
+rustybuzz = "0.5"
+rand = "0.8"
diff --git a/experiments/wgpu_for_fontrendering/README.md b/experiments/wgpu_for_fontrendering/README.md
new file mode 100644
index 000000000..9929f49dd
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/README.md
@@ -0,0 +1,51 @@
+# GPU-based font rendering experiment
+
+This is a standalone wgpu/winit application which serves as an experimentation platform for font rendering on the GPU.
+
+> The goal is not (yet) to provide a fully fleshed out gpu-based font rendering library but rather see w
+
+
+## Current Approach:
+We can render arbitrary text with a .ttf font under arbitrary 3-d transformations on the GPU:
+![](./doc/perspective_transform.png)
+
+
+### Algorithm
+[Lightweight bezier curve rendering](https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac) was reimplemented:
+
+* Convert ttf outlines into triangle meshes (cpu side, once)
+* Use winding order trick to produce correct glyph shape
+ - First pass: overdraw pixels into a texture
+ - Second pass: render texture but only the pixels that were drawn an uneven number of times
+![](./doc/animation.gif)
+Animation taken from [here](https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac).
+* Quadratic curve segments get two triangles, one with special uv coordinates to enable [simple curve evaluation in the fragment shader](https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-25-rendering-vector-art-gpu)
+
+A rough overview of the setup and render routine:
+![](./doc/overview.png)
+
+### Performance
+* Parsing/tesselation of glyphs is done with the help of `ttfparser` -> currently unoptimized. With glyph caching this should be ok
+* Rendering times degrade massively with number of glyphs, but is independent of screen resolution
+![](./doc/benchmark_2022-04-24.png)
+
+### Issues
+
+The main issue with this approach (besides performance) is that the trick with using overdrawing of pixels to decide whether to fill them or not produces artifacts when two separate glyphs overlap in screen space:
+![](./doc/overlapping_problem.png)
+
+However, this should not be a serious problem for our use case (labels on maps) due to two reasons:
+1. Text on a map should never overlap because it would be detrimental to readability. Looking at e.g. Google Maps one can see that they have a system in place to detect overlaps and hide text following some sort of importance rating.
+2. If we actually want to allow overlapping text, we should get away with a simple painter's algorithm:
+ * Sort text entities (i.e., entire labels) by their distance to the camera
+ * Draw sorted from closest to farthest
+ * Use a depth buffer -> this way all overlapping fragments between texts further back than the closest one are discarded and won't mess with the winding order
+
+### TODOs
+* Cache glyph meshes, so they are not recreated whenever they appear in a word and render them as instances
+* Anti-aliasing!
+
+## Build setup
+This is a separate project from the maplibre-rs, therefore it is excluded from the maplibre-rs workspace and defines its own workspace.
+
+> Running on Mac did not work with a simple `cargo run` (linker error) but with `cargo run --target aarch64-apple-darwin` (on a M1) it worked fine.
\ No newline at end of file
diff --git a/experiments/wgpu_for_fontrendering/doc/animation.gif b/experiments/wgpu_for_fontrendering/doc/animation.gif
new file mode 100644
index 000000000..1f2c16bd6
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/doc/animation.gif differ
diff --git a/experiments/wgpu_for_fontrendering/doc/benchmark_2022-04-24.png b/experiments/wgpu_for_fontrendering/doc/benchmark_2022-04-24.png
new file mode 100644
index 000000000..f12d7424a
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/doc/benchmark_2022-04-24.png differ
diff --git a/experiments/wgpu_for_fontrendering/doc/overlapping_problem.png b/experiments/wgpu_for_fontrendering/doc/overlapping_problem.png
new file mode 100644
index 000000000..227c4d7ef
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/doc/overlapping_problem.png differ
diff --git a/experiments/wgpu_for_fontrendering/doc/overview.png b/experiments/wgpu_for_fontrendering/doc/overview.png
new file mode 100644
index 000000000..ab2b67517
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/doc/overview.png differ
diff --git a/experiments/wgpu_for_fontrendering/doc/perspective_transform.png b/experiments/wgpu_for_fontrendering/doc/perspective_transform.png
new file mode 100644
index 000000000..7d68955ce
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/doc/perspective_transform.png differ
diff --git a/experiments/wgpu_for_fontrendering/src/lib.rs b/experiments/wgpu_for_fontrendering/src/lib.rs
new file mode 100644
index 000000000..c5866e36d
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/lib.rs
@@ -0,0 +1,133 @@
+mod rendering;
+mod text_system;
+
+use cgmath::Quaternion;
+use rand::Rng;
+use std::time::Instant;
+
+use text_system::{FontID, SceneTextSystem};
+
+use winit::{
+ event::*,
+ event_loop::{ControlFlow, EventLoop},
+ window::WindowBuilder,
+};
+
+pub async fn run() {
+ env_logger::init();
+ let event_loop = EventLoop::new();
+ let size: winit::dpi::PhysicalSize = winit::dpi::PhysicalSize::new(4000, 2000);
+ let window = WindowBuilder::new()
+ .with_inner_size(size)
+ .build(&event_loop)
+ .unwrap();
+
+ {
+ let mut state: rendering::State = rendering::State::new(&window).await;
+ let mut text_system: SceneTextSystem = SceneTextSystem::new(&state).unwrap();
+
+ let font_id: FontID = String::from("Aparaj");
+ if let Err(_e) = text_system.load_font(&font_id, "tests/fonts/aparaj.ttf") {
+ panic!("Couldn't add font!");
+ }
+
+ let step_x = 0.42;
+ let step_y = 0.15;
+ let z_jitter: f32 = 1.0;
+
+ let limit = 30;
+
+ let mut rng = rand::thread_rng();
+
+ let title = format!("{} individual glyphs", (2 * limit) * (2 * limit) * 4);
+
+ if let Err(_) = text_system.add_text_to_scene(
+ &state,
+ &title,
+ (-0.8, (limit + 2) as f32 * step_y, 0.0).into(),
+ Quaternion::new(0.0, 0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0).into(),
+ 0.00015,
+ &font_id,
+ ) {
+ panic!("Problem!");
+ }
+
+ for i in -limit..limit {
+ for j in -limit..limit {
+ let letter_1: char = rng.gen_range(b'A'..b'Z') as char;
+ let letter_2: char = rng.gen_range(b'A'..b'Z') as char;
+ let number: u32 = rng.gen_range(0..99);
+ let text = format!("{}{}{:2}", letter_1, letter_2, number);
+
+ let r: f32 = rng.gen_range(0.0..1.0);
+ let g: f32 = rng.gen_range(0.0..1.0);
+ let b: f32 = rng.gen_range(0.0..1.0);
+
+ let z = rng.gen_range(-z_jitter..z_jitter);
+
+ if let Err(_) = text_system.add_text_to_scene(
+ &state,
+ &text,
+ (i as f32 * step_x, j as f32 * step_y, z).into(),
+ Quaternion::new(0.0, 0.0, 0.0, 0.0),
+ (r, g, b).into(),
+ 0.0001,
+ &font_id,
+ ) {
+ panic!("Problem!");
+ }
+ }
+ }
+
+ event_loop.run(move |event, _, control_flow| {
+ match event {
+ Event::WindowEvent {
+ ref event,
+ window_id,
+ } if window_id == window.id() => {
+ if !state.input(event) {
+ match event {
+ WindowEvent::CloseRequested
+ | WindowEvent::KeyboardInput {
+ input:
+ KeyboardInput {
+ state: ElementState::Pressed,
+ virtual_keycode: Some(VirtualKeyCode::Escape),
+ ..
+ },
+ ..
+ } => *control_flow = ControlFlow::Exit,
+ WindowEvent::Resized(physical_size) => state.resize(*physical_size),
+ WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
+ // new_inner_size is &&mut so we have to dereference it twice
+ state.resize(**new_inner_size);
+ }
+ _ => {}
+ }
+ }
+ }
+ Event::RedrawRequested(window_id) if window_id == window.id() => {
+ state.update();
+ let now = Instant::now();
+ match state.render(&mut text_system) {
+ Ok(_) => {}
+ // Reconfigure the surface if lost
+ Err(wgpu::SurfaceError::Lost) => state.resize(state.size),
+ // The system is out of memory, we should probably quit
+ Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
+ // All other errors (Outdated, Timeout) should be resolved by the next frame
+ Err(e) => eprintln!("{:?}", e),
+ }
+ println!("Frame: {}ms", now.elapsed().as_millis());
+ }
+ Event::MainEventsCleared => {
+ // RedrawRequested will only trigger once, unless we manually
+ // request it.
+ window.request_redraw();
+ }
+ _ => {}
+ }
+ });
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/main.rs b/experiments/wgpu_for_fontrendering/src/main.rs
new file mode 100644
index 000000000..78fb8d119
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/main.rs
@@ -0,0 +1,9 @@
+use wgpu_for_fontrendering::run;
+
+fn run_gui() {
+ pollster::block_on(run());
+}
+
+fn main() {
+ run_gui();
+}
diff --git a/experiments/wgpu_for_fontrendering/src/rendering/camera.rs b/experiments/wgpu_for_fontrendering/src/rendering/camera.rs
new file mode 100644
index 000000000..17afe9321
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/rendering/camera.rs
@@ -0,0 +1,142 @@
+use winit::event::*;
+
+pub struct Camera {
+ pub eye: cgmath::Point3,
+ pub target: cgmath::Point3,
+ pub up: cgmath::Vector3,
+ pub aspect: f32,
+ pub fovy: f32,
+ pub znear: f32,
+ pub zfar: f32,
+}
+
+#[rustfmt::skip]
+pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new(
+ 1.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.5, 0.0,
+ 0.0, 0.0, 0.5, 1.0,
+);
+
+impl Camera {
+ fn build_view_projection_matrix(&self) -> cgmath::Matrix4 {
+ // 1.
+ let view = cgmath::Matrix4::look_at_rh(self.eye, self.target, self.up);
+ // 2.
+ let proj = cgmath::perspective(cgmath::Deg(self.fovy), self.aspect, self.znear, self.zfar);
+
+ // 3.
+ return OPENGL_TO_WGPU_MATRIX * proj * view;
+ }
+}
+
+// We need this for Rust to store our data correctly for the shaders
+#[repr(C)]
+// This is so we can store this in a buffer
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct CameraUniform {
+ // We can't use cgmath with bytemuck directly so we'll have
+ // to convert the Matrix4 into a 4x4 f32 array
+ view_proj: [[f32; 4]; 4],
+}
+
+impl CameraUniform {
+ pub fn new() -> Self {
+ use cgmath::SquareMatrix;
+ Self {
+ view_proj: cgmath::Matrix4::identity().into(),
+ }
+ }
+
+ pub fn update_view_proj(&mut self, camera: &Camera) {
+ self.view_proj = camera.build_view_projection_matrix().into();
+ }
+}
+
+pub struct CameraController {
+ pub speed: f32,
+ pub is_forward_pressed: bool,
+ pub is_backward_pressed: bool,
+ pub is_left_pressed: bool,
+ pub is_right_pressed: bool,
+}
+
+impl CameraController {
+ pub fn new(speed: f32) -> Self {
+ Self {
+ speed,
+ is_forward_pressed: false,
+ is_backward_pressed: false,
+ is_left_pressed: false,
+ is_right_pressed: false,
+ }
+ }
+
+ pub fn process_events(&mut self, event: &WindowEvent) -> bool {
+ match event {
+ WindowEvent::KeyboardInput {
+ input:
+ KeyboardInput {
+ state,
+ virtual_keycode: Some(keycode),
+ ..
+ },
+ ..
+ } => {
+ let is_pressed = *state == ElementState::Pressed;
+ match keycode {
+ VirtualKeyCode::W | VirtualKeyCode::Up => {
+ self.is_forward_pressed = is_pressed;
+ true
+ }
+ VirtualKeyCode::A | VirtualKeyCode::Left => {
+ self.is_left_pressed = is_pressed;
+ true
+ }
+ VirtualKeyCode::S | VirtualKeyCode::Down => {
+ self.is_backward_pressed = is_pressed;
+ true
+ }
+ VirtualKeyCode::D | VirtualKeyCode::Right => {
+ self.is_right_pressed = is_pressed;
+ true
+ }
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+ }
+
+ pub fn update_camera(&self, camera: &mut Camera) {
+ use cgmath::InnerSpace;
+ let forward = camera.target - camera.eye;
+ let forward_norm = forward.normalize();
+ let forward_mag = forward.magnitude();
+
+ // Prevents glitching when camera gets too close to the
+ // center of the scene.
+ if self.is_forward_pressed && forward_mag > self.speed {
+ camera.eye += forward_norm * self.speed;
+ }
+ if self.is_backward_pressed {
+ camera.eye -= forward_norm * self.speed;
+ }
+
+ let right = forward_norm.cross(camera.up);
+
+ // Redo radius calc in case the fowrard/backward is pressed.
+ let forward = camera.target - camera.eye;
+ let forward_mag = forward.magnitude();
+
+ if self.is_right_pressed {
+ // Rescale the distance between the target and eye so
+ // that it doesn't change. The eye therefore still
+ // lies on the circle made by the target and eye.
+ camera.eye = camera.target - (forward + right * self.speed).normalize() * forward_mag;
+ }
+ if self.is_left_pressed {
+ camera.eye = camera.target - (forward - right * self.speed).normalize() * forward_mag;
+ }
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/rendering/mod.rs b/experiments/wgpu_for_fontrendering/src/rendering/mod.rs
new file mode 100644
index 000000000..4e24f11a5
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/rendering/mod.rs
@@ -0,0 +1,159 @@
+mod camera;
+
+use camera::*;
+use wgpu::util::DeviceExt;
+use winit::{event::*, window::Window};
+
+pub trait Renderable {
+ /// Render to the given output texture and return it
+ fn render(
+ &mut self,
+ rendering_state: &State,
+ output_texture: wgpu::SurfaceTexture,
+ ) -> wgpu::SurfaceTexture;
+}
+
+pub struct State {
+ pub surface: wgpu::Surface,
+ pub device: wgpu::Device,
+ pub queue: wgpu::Queue,
+ pub config: wgpu::SurfaceConfiguration,
+ pub size: winit::dpi::PhysicalSize,
+ pub camera: Camera,
+ pub camera_uniform: CameraUniform,
+ pub camera_buffer: wgpu::Buffer,
+ pub camera_bind_group_layout: wgpu::BindGroupLayout,
+ pub camera_bind_group: wgpu::BindGroup,
+ pub camera_controller: CameraController,
+}
+
+impl State {
+ // Creating some of the wgpu types requires async code
+ pub async fn new(window: &Window) -> State {
+ let size = window.inner_size();
+
+ // The instance is a handle to our GPU
+ // Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
+ let instance = wgpu::Instance::new(wgpu::Backends::all());
+ let surface = unsafe { instance.create_surface(window) };
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::default(),
+ compatible_surface: Some(&surface),
+ force_fallback_adapter: false,
+ })
+ .await
+ .unwrap();
+
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ features: wgpu::Features::empty(),
+ // WebGL doesn't support all of wgpu's features, so if
+ // we're building for the web we'll have to disable some.
+ limits: wgpu::Limits::default(),
+ label: None,
+ },
+ None, // Trace path
+ )
+ .await
+ .unwrap();
+
+ let config = wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format: surface.get_supported_formats(&adapter)[0],
+ width: size.width,
+ height: size.height,
+ present_mode: wgpu::PresentMode::Fifo,
+ };
+ surface.configure(&device, &config);
+
+ let camera = Camera {
+ // position the camera one unit up and 2 units back
+ // +z is out of the screen
+ eye: (0.0, 0.0, 5.0).into(),
+ // have it look at the origin
+ target: (0.0, 0.0, 0.0).into(),
+ // which way is "up"
+ up: cgmath::Vector3::unit_y(),
+ aspect: config.width as f32 / config.height as f32,
+ fovy: 45.0,
+ znear: 0.1,
+ zfar: 1000.0,
+ };
+ let mut camera_uniform = CameraUniform::new();
+ camera_uniform.update_view_proj(&camera);
+ let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Camera Buffer"),
+ contents: bytemuck::cast_slice(&[camera_uniform]),
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ });
+ let camera_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ label: Some("camera_bind_group_layout"),
+ });
+ let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: &camera_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: camera_buffer.as_entire_binding(),
+ }],
+ label: Some("camera_bind_group"),
+ });
+ let camera_controller = CameraController::new(0.2);
+
+ State {
+ surface,
+ device,
+ queue,
+ config,
+ size,
+ camera,
+ camera_uniform,
+ camera_buffer,
+ camera_bind_group_layout,
+ camera_bind_group,
+ camera_controller,
+ }
+ }
+
+ pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) {
+ if new_size.width > 0 && new_size.height > 0 {
+ self.size = new_size;
+ self.config.width = new_size.width;
+ self.config.height = new_size.height;
+ self.surface.configure(&self.device, &self.config);
+ }
+ }
+
+ pub fn input(&mut self, event: &WindowEvent) -> bool {
+ self.camera_controller.process_events(event)
+ }
+
+ pub fn update(&mut self) {
+ self.camera_controller.update_camera(&mut self.camera);
+ self.camera_uniform.update_view_proj(&self.camera);
+ self.queue.write_buffer(
+ &self.camera_buffer,
+ 0,
+ bytemuck::cast_slice(&[self.camera_uniform]),
+ );
+ }
+
+ pub fn render(&mut self, renderable: &mut dyn Renderable) -> Result<(), wgpu::SurfaceError> {
+ let output_texture = self.surface.get_current_texture()?;
+ let output = renderable.render(self, output_texture);
+ output.present();
+ Ok(())
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/text_system/geom.rs b/experiments/wgpu_for_fontrendering/src/text_system/geom.rs
new file mode 100644
index 000000000..895c4069d
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/text_system/geom.rs
@@ -0,0 +1,137 @@
+use std::vec;
+
+use wgpu::util::DeviceExt;
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct Vertex {
+ pub position: [f32; 3],
+ pub uv: [f32; 2],
+}
+
+impl Vertex {
+ pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[
+ wgpu::VertexAttribute {
+ offset: 0,
+ shader_location: 0,
+ format: wgpu::VertexFormat::Float32x3,
+ },
+ wgpu::VertexAttribute {
+ offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
+ shader_location: 1,
+ format: wgpu::VertexFormat::Float32x2,
+ },
+ ],
+ }
+ }
+
+ pub fn new_2d(position: cgmath::Vector2) -> Vertex {
+ Vertex {
+ position: position.extend(0.0).into(),
+ uv: [0.0, 0.0],
+ }
+ }
+
+ pub fn new_2d_uv(position: cgmath::Vector2, uv: cgmath::Vector2) -> Vertex {
+ Vertex {
+ position: position.extend(0.0).into(),
+ uv: uv.into(),
+ }
+ }
+
+ pub fn new_3d(position: cgmath::Vector3) -> Vertex {
+ Vertex {
+ position: position.into(),
+ uv: [0.0, 0.0],
+ }
+ }
+
+ pub fn new_3d_uv(position: cgmath::Vector3, uv: cgmath::Vector2) -> Vertex {
+ Vertex {
+ position: position.into(),
+ uv: uv.into(),
+ }
+ }
+
+ pub fn scale(&mut self, s: f32) {
+ self.position[0] *= s;
+ self.position[1] *= s;
+ self.position[2] *= s;
+ }
+
+ pub fn scale_3d(&mut self, s: &cgmath::Vector3) {
+ self.position[0] *= s.x;
+ self.position[1] *= s.y;
+ self.position[2] *= s.z;
+ }
+}
+
+pub struct Mesh {
+ vertices: Vec,
+ indices: Vec,
+ pub vertex_buffer: wgpu::Buffer,
+ pub index_buffer: wgpu::Buffer,
+ pub num_indices: usize,
+}
+
+impl Mesh {
+ pub fn new(vertices: Vec, indices: Vec, device: &wgpu::Device) -> Mesh {
+ let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("TestVertices"),
+ contents: bytemuck::cast_slice(&vertices),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+
+ let num_indices = indices.len();
+
+ let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("TestIndices"),
+ contents: bytemuck::cast_slice(&indices),
+ usage: wgpu::BufferUsages::INDEX,
+ });
+
+ Mesh {
+ // Take ownership of the data underlying the buffers!
+ vertices,
+ indices,
+ vertex_buffer,
+ index_buffer,
+ num_indices,
+ }
+ }
+}
+
+pub trait Meshable {
+ fn as_mesh(&self, device: &wgpu::Device) -> Mesh;
+}
+
+pub struct Quad {
+ pub center: cgmath::Vector3,
+ pub width: f32,
+ pub height: f32,
+}
+
+impl Meshable for Quad {
+ fn as_mesh(&self, device: &wgpu::Device) -> Mesh {
+ let half_width = self.width * 0.5;
+ let half_height = self.height * 0.5;
+ let mut vertices = Vec::new();
+
+ let top_left = self.center + cgmath::Vector3::new(-half_width, half_height, 0.0);
+ let bottom_left = self.center + cgmath::Vector3::new(-half_width, -half_height, 0.0);
+ let bottom_right = self.center + cgmath::Vector3::new(half_width, -half_height, 0.0);
+ let top_right = self.center + cgmath::Vector3::new(half_width, half_height, 0.0);
+
+ vertices.push(Vertex::new_3d(top_left));
+ vertices.push(Vertex::new_3d(bottom_left));
+ vertices.push(Vertex::new_3d(bottom_right));
+ vertices.push(Vertex::new_3d(top_right));
+ let indices: Vec = vec![0, 1, 2, 2, 3, 0];
+ let mesh = Mesh::new(vertices, indices, device);
+ mesh
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/text_system/glyph_tesselation.rs b/experiments/wgpu_for_fontrendering/src/text_system/glyph_tesselation.rs
new file mode 100644
index 000000000..4033d07d0
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/text_system/glyph_tesselation.rs
@@ -0,0 +1,174 @@
+use crate::text_system::geom::{Mesh, Meshable, Vertex};
+use std::fmt::Write;
+use ttf_parser as ttf;
+
+pub struct SVGBuilder(pub String);
+
+impl ttf::OutlineBuilder for SVGBuilder {
+ fn move_to(&mut self, x: f32, y: f32) {
+ write!(&mut self.0, "M {} {} ", x, y).unwrap();
+ }
+
+ fn line_to(&mut self, x: f32, y: f32) {
+ write!(&mut self.0, "L {} {} ", x, y).unwrap();
+ }
+
+ fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
+ write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
+ }
+
+ fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
+ write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
+ }
+
+ fn close(&mut self) {
+ write!(&mut self.0, "Z ").unwrap();
+ }
+}
+
+#[derive(Debug)]
+pub struct GlyphBuilder {
+ // Take lines from path description and turn into triangles with an arbitrary point (0, 0).
+ vertices: Vec,
+ indices: Vec,
+ current_index: u16,
+ added_points: u16,
+ last_start_index: u16,
+ word_offset: cgmath::Vector3, // offset in glyph coordinates (i.e. x and y advance!)
+}
+
+impl GlyphBuilder {
+ pub fn new() -> GlyphBuilder {
+ let mut builder = GlyphBuilder {
+ vertices: Vec::new(),
+ indices: Vec::new(),
+ current_index: 0,
+ added_points: 0,
+ last_start_index: 0,
+ word_offset: (0.0, 0.0, 0.0).into(),
+ };
+
+ builder.vertices.push(Vertex::new_2d((0.0, 0.0).into()));
+
+ builder
+ }
+
+ pub fn new_with_offset(word_offset: cgmath::Vector3) -> GlyphBuilder {
+ let mut builder = GlyphBuilder {
+ vertices: Vec::new(),
+ indices: Vec::new(),
+ current_index: 0,
+ added_points: 0,
+ last_start_index: 0,
+ word_offset,
+ };
+
+ builder.add_vertex(0.0, 0.0); // Base Vertex for normal triangles, will be adjusted in finalize()
+
+ builder
+ }
+
+ fn add_vertex(&mut self, x: f32, y: f32) {
+ let point = cgmath::Vector3::new(x, y, 0.0);
+ self.vertices.push(Vertex::new_3d(point));
+ }
+
+ fn add_vertex_uv(&mut self, x: f32, y: f32, u: f32, v: f32) {
+ let point = cgmath::Vector3::new(x, y, 0.0);
+ self.vertices.push(Vertex::new_3d_uv(point, (u, v).into()));
+ }
+
+ fn make_triangle(&mut self) {
+ self.indices.push(0);
+ self.indices.push(self.current_index);
+ self.indices.push(self.current_index + 1);
+
+ self.current_index += 1;
+ self.added_points = 1;
+ }
+
+ pub fn finalize(&mut self, bbox: &ttf::Rect) {
+ // Move the first vertex (base for all the triangles) into the center of the bounding box
+ // This hopefully avoids overlapping triangles between different glyphs (which would torpedo the winding number hack of the fragment shader)
+ if let Some(first_vertex) = self.vertices.first_mut() {
+ (*first_vertex).position[0] = bbox.width() as f32 * 0.5;
+ (*first_vertex).position[1] = bbox.height() as f32 * 0.5;
+ }
+ }
+}
+
+impl ttf::OutlineBuilder for GlyphBuilder {
+ fn move_to(&mut self, x: f32, y: f32) {
+ self.add_vertex(x, y);
+
+ // Move-to starts a new shape
+ self.last_start_index = self.vertices.len() as u16 - 1;
+ self.added_points = 1;
+ self.current_index += 1;
+ }
+
+ fn line_to(&mut self, x: f32, y: f32) {
+ self.add_vertex(x, y);
+ self.added_points += 1;
+ if self.added_points == 2 {
+ self.make_triangle();
+ }
+ }
+
+ fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
+ // Quadratic curve (control point, end point), start point is endpoint of previous segment
+
+ // The last pushed vertex is the start point of the curve
+ // We need to construct 2 triangles
+ // A "normal" triangle as with line segments
+ // And another special triangle for the rounded area of the curve,
+ // which is equipped with special uv coordinates which the pixel shader can use to check if a pixel is inside or outside the curve
+ // Because the endpoint is the start point of the next path segment, we first construct the special triangle
+
+ self.add_vertex_uv(x1, y1, 0.5, 0.0);
+ self.add_vertex_uv(x, y, 1.0, 1.0);
+
+ // The special triangle
+ self.indices.push(self.current_index);
+ self.indices.push(self.current_index + 1);
+ self.indices.push(self.current_index + 2);
+
+ self.added_points += 1;
+ if self.added_points == 2 {
+ self.add_vertex(x, y); // duplicate of the end point without special uv coordinates
+
+ self.indices.push(0);
+ self.indices.push(self.current_index);
+ self.indices.push(self.current_index + 3);
+
+ self.added_points = 1;
+ self.current_index += 3;
+ }
+ }
+
+ fn curve_to(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _x: f32, _y: f32) {
+ // Cubic curve (control point 1, control point 2, end point)
+ panic!("Cubic bezier curves not yet supported!");
+ }
+
+ fn close(&mut self) {
+ // triangle from current point (i.e. the last one that was read in) and the start point of this shape
+ self.indices.push(0);
+ self.indices.push(self.current_index);
+ self.indices.push(self.last_start_index);
+
+ self.indices.push(0);
+ self.indices.push(self.last_start_index);
+ self.indices.push(self.current_index);
+
+ // the next command MUST be a move to if there are more shapes
+ // This will reset the necessary counters
+ }
+}
+
+impl Meshable for GlyphBuilder {
+ fn as_mesh(&self, device: &wgpu::Device) -> Mesh {
+ let mesh = Mesh::new(self.vertices.clone(), self.indices.clone(), device);
+ mesh
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/text_system/mod.rs b/experiments/wgpu_for_fontrendering/src/text_system/mod.rs
new file mode 100644
index 000000000..877e942d7
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/text_system/mod.rs
@@ -0,0 +1,610 @@
+mod geom;
+mod glyph_tesselation;
+mod texture;
+
+use geom::{Mesh, Meshable, Quad, Vertex};
+use glyph_tesselation::GlyphBuilder;
+use std::collections::HashMap;
+use std::io::Error;
+use std::mem;
+use ttf_parser as ttf;
+use wgpu::util::DeviceExt;
+
+use crate::rendering::{Renderable, State};
+
+// A piece of text in the scene, which is styled and moved as a unit
+pub struct TextEntity {
+ pub text: String,
+ pub shaping_info: rustybuzz::GlyphBuffer,
+ pub position: cgmath::Vector3,
+ pub rotation: cgmath::Quaternion,
+ pub color: cgmath::Vector3,
+ pub size: f32,
+}
+
+pub type TextEntityID = u32;
+
+pub enum TextSystemError {
+ FontNotLoaded(&'static str),
+ GlyphNotSupported(&'static str),
+}
+
+// The values for the instance buffer
+#[repr(C)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+struct GlyphInstanceAttributes {
+ pub transform: [[f32; 4]; 4],
+ pub color: [f32; 3],
+}
+
+// Struct of arrays to allow simple instance buffer generation from 'attributes'
+// While at the same time allow removing and editing of glyphs that are part of
+// a certain text entity
+struct GlyphInstances {
+ text_entity_ids: Vec,
+ attributes: Vec,
+ buffer: Option,
+}
+
+impl GlyphInstances {
+ fn new() -> Self {
+ Self {
+ text_entity_ids: Vec::new(),
+ attributes: Vec::new(),
+ buffer: None,
+ }
+ }
+
+ fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::() as wgpu::BufferAddress,
+ // We need to switch from using a step mode of Vertex to Instance
+ // This means that our shaders will only change to use the next
+ // instance when the shader starts processing a new instance
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &[
+ // A mat4 takes up 4 vertex slots as it is technically 4 vec4s. We need to define a slot
+ // for each vec4. We'll have to reassemble the mat4 in
+ // the shader.
+ wgpu::VertexAttribute {
+ offset: 0,
+ shader_location: 2,
+ format: wgpu::VertexFormat::Float32x4,
+ },
+ wgpu::VertexAttribute {
+ offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
+ shader_location: 3,
+ format: wgpu::VertexFormat::Float32x4,
+ },
+ wgpu::VertexAttribute {
+ offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
+ shader_location: 4,
+ format: wgpu::VertexFormat::Float32x4,
+ },
+ wgpu::VertexAttribute {
+ offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
+ shader_location: 5,
+ format: wgpu::VertexFormat::Float32x4,
+ },
+ wgpu::VertexAttribute {
+ offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress,
+ shader_location: 6,
+ format: wgpu::VertexFormat::Float32x3,
+ },
+ ],
+ }
+ }
+
+ fn add(&mut self, entity_id: TextEntityID, glyph_attributes: GlyphInstanceAttributes) {
+ self.text_entity_ids.push(entity_id);
+ self.attributes.push(glyph_attributes);
+ }
+
+ fn compute_buffer(&mut self, device: &wgpu::Device) {
+ self.buffer = Some(
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("Instance Buffer"),
+ contents: bytemuck::cast_slice(&self.attributes),
+ usage: wgpu::BufferUsages::VERTEX,
+ }),
+ );
+ }
+
+ fn num_instances(&self) -> u32 {
+ self.attributes.len() as u32
+ }
+}
+
+impl TextEntity {
+ fn new(
+ text: &str,
+ face: rustybuzz::Face,
+ position: cgmath::Vector3,
+ rotation: cgmath::Quaternion,
+ color: cgmath::Vector3,
+ size: f32,
+ ) -> Self {
+ let mut buffer = rustybuzz::UnicodeBuffer::new();
+ buffer.push_str(text);
+
+ TextEntity {
+ text: String::from(text),
+ shaping_info: rustybuzz::shape(&face, &[], buffer),
+ position,
+ rotation,
+ color,
+ size,
+ }
+ }
+}
+
+pub type FontID = String;
+
+pub struct Font {
+ pub font_name: FontID,
+ pub font_file_path: String,
+ font_data: Vec,
+}
+
+impl Font {
+ pub fn new(font_name: FontID, font_path: &str) -> Result {
+ let font_file_path = String::from(font_path);
+
+ let font_data_res = std::fs::read(&font_file_path);
+ if font_data_res.is_err() {
+ return Err(TextSystemError::FontNotLoaded("Could not read font file!"));
+ }
+
+ let font_data = font_data_res.unwrap();
+
+ Ok(Self {
+ font_name,
+ font_file_path,
+ font_data,
+ })
+ }
+
+ pub fn get_face(&self) -> rustybuzz::Face {
+ // TODO: try using "owningFace" instead and return ref, so we don't recompute the face every time!
+ rustybuzz::Face::from_slice(&self.font_data, 0).unwrap()
+ }
+}
+
+struct GlyphRenderData {
+ mesh: Mesh,
+ instances: GlyphInstances,
+}
+
+// System that takes care of rendering the text
+// Offers a simple interface to add / transform / remove bits of text and then render them to the screen
+// TODO: cleaner separation of rendering algorithm and text system management (i.e. -> interface that supplies mesh representation and shaders + some factory)
+// TODO: add prioritzed collision detection
+// TODO: add way to handle duplicate text on different map zoom levels
+// TODO: test performance with very dynamic text (i.e. lots of movement / appearing / vanishing of labels on the map, ...)
+pub struct SceneTextSystem {
+ // The loaded fonts
+ fonts: HashMap,
+ // Cache for triangulated glyphs (vertices are in font coordinate system -> relatively large numbers)
+ glyph_mesh_cache: HashMap,
+ // All texts in the scene
+ text_entities: HashMap,
+ // Internal counter to assign a unique ID to each text in the scene
+ next_text_entity_id: TextEntityID,
+ // ######### Rendering related stuff #########################################################
+ prepass_target_texture: texture::Texture,
+ prepass_target_texture_bind_group: wgpu::BindGroup,
+ prepass_pipeline: wgpu::RenderPipeline,
+ main_pipeline: wgpu::RenderPipeline,
+ full_screen_quad: Mesh,
+}
+
+impl SceneTextSystem {
+ pub fn new(rendering_state: &State) -> Result {
+ let prepass_target_texture = texture::Texture::empty(
+ &rendering_state.device,
+ rendering_state.config.width,
+ rendering_state.config.height,
+ wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
+ Some("textPrepassTarget"),
+ );
+ let texture_bind_group_layout =
+ rendering_state
+ .device
+ .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ // This should match the filterable field of the
+ // corresponding Texture entry above.
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ label: Some("text_prepass_texture_bind_group_layout"),
+ });
+
+ let prepass_target_texture_bind_group =
+ rendering_state
+ .device
+ .create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: &texture_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(
+ &prepass_target_texture.view,
+ ),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(
+ &prepass_target_texture.sampler,
+ ),
+ },
+ ],
+ label: Some("text_prepass_texture_bind_group"),
+ });
+
+ let shaders = rendering_state
+ .device
+ .create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Text Shaders"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("textsystem_shaders.wgsl").into()),
+ });
+
+ // PREPASS PIPELINE
+
+ let pre_pass_render_pipeline_layout =
+ rendering_state
+ .device
+ .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Prepass Pipeline Layout"),
+ bind_group_layouts: &[&rendering_state.camera_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let prepass_pipeline =
+ rendering_state
+ .device
+ .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Prepass Pipeline"),
+ layout: Some(&pre_pass_render_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shaders,
+ entry_point: "prepass_vs",
+ buffers: &[Vertex::desc(), GlyphInstances::desc()],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shaders,
+ entry_point: "prepass_fs",
+ targets: &[Some(wgpu::ColorTargetState {
+ format: rendering_state.config.format,
+ blend: Some(wgpu::BlendState {
+ // Overwrite color without alpha blending applied
+ // -> A glyph instance's color will be transferred unmodified through the prepass
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::Zero,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent::OVER,
+ }),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None, // no culling because glyph tesselation yields cw and ccw triangles
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ });
+
+ // MAIN PASS PIPELINE
+
+ let main_pass_render_pipeline_layout =
+ rendering_state
+ .device
+ .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Text main pass Pipeline Layout"),
+ bind_group_layouts: &[
+ &rendering_state.camera_bind_group_layout,
+ &texture_bind_group_layout,
+ ],
+ push_constant_ranges: &[],
+ });
+
+ let main_pipeline =
+ rendering_state
+ .device
+ .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Text main pipeline"),
+ layout: Some(&main_pass_render_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shaders,
+ entry_point: "mainpass_vs",
+ buffers: &[Vertex::desc()],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shaders,
+ entry_point: "mainpass_fs",
+ targets: &[Some(wgpu::ColorTargetState {
+ format: rendering_state.config.format,
+ blend: Some(wgpu::BlendState::REPLACE),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: Some(wgpu::Face::Back),
+ // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
+ polygon_mode: wgpu::PolygonMode::Fill,
+ // Requires Features::DEPTH_CLIP_CONTROL
+ unclipped_depth: false,
+ // Requires Features::CONSERVATIVE_RASTERIZATION
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ });
+
+ let quad = Quad {
+ center: (0.0, 0.0, 0.0).into(),
+ width: 1.0,
+ height: 1.0,
+ };
+ let full_screen_quad = quad.as_mesh(&rendering_state.device);
+
+ Ok(Self {
+ fonts: HashMap::new(),
+ glyph_mesh_cache: HashMap::new(),
+ text_entities: HashMap::new(),
+ next_text_entity_id: 0,
+ prepass_target_texture,
+ prepass_target_texture_bind_group,
+ prepass_pipeline,
+ main_pipeline,
+ full_screen_quad,
+ })
+ }
+
+ pub fn load_font(&mut self, id: &FontID, font_path: &str) -> Result<(), TextSystemError> {
+ self.fonts
+ .insert(id.clone(), Font::new(id.clone(), font_path)?);
+ Ok(())
+ }
+
+ pub fn add_text_to_scene(
+ &mut self,
+ rendering_state: &State,
+ text: &str,
+ base_position: cgmath::Vector3,
+ rotation: cgmath::Quaternion,
+ color: cgmath::Vector3,
+ size: f32,
+ font_id: &FontID,
+ ) -> Result<(), TextSystemError> {
+ let new_text_id = self.next_text_entity_id;
+
+ let font_opt = self.fonts.get(font_id);
+
+ if font_opt.is_none() {
+ return Err(TextSystemError::FontNotLoaded(
+ "Font must be loaded before adding text that uses it!",
+ ));
+ }
+
+ let font: &Font = font_opt.unwrap();
+
+ self.text_entities.insert(
+ new_text_id,
+ TextEntity::new(text, font.get_face(), base_position, rotation, color, size),
+ );
+ let text_entity = self.text_entities.get(&new_text_id).unwrap(); // This MUST be present, as it was added in the line above.
+
+ // Construct instances for each glyph of the text (optionally create mesh for glyph if needed)
+ let infos = text_entity.shaping_info.glyph_infos();
+ let posistions = text_entity.shaping_info.glyph_positions();
+ let mut glyph_offset = base_position;
+
+ // For each glyph in the layed out word
+ for (info, pos) in infos.iter().zip(posistions) {
+ let glyph_id = ttf::GlyphId(info.glyph_id.try_into().unwrap()); // ttfparser for some reason wants a u16 ?!
+
+ let mut inserted = true;
+
+ // Create and add glyph mesh to cache if not present
+ if !self.glyph_mesh_cache.contains_key(&glyph_id) {
+ let mut glyph_builder = GlyphBuilder::new();
+ if let Some(bbox) = font.get_face().outline_glyph(glyph_id, &mut glyph_builder) {
+ glyph_builder.finalize(&bbox);
+ self.glyph_mesh_cache.insert(
+ glyph_id,
+ GlyphRenderData {
+ mesh: glyph_builder.as_mesh(&rendering_state.device),
+ instances: GlyphInstances::new(),
+ },
+ );
+ } else {
+ // now new mesh -> we don't need and instance infos for it!
+ inserted = false;
+ // TODO: most likely: white space? -> detect that so we can detect actual errors here
+ // return Err(TextSystemError::GlyphNotSupported("Glyph not supported!"));
+ }
+ }
+
+ if inserted {
+ // Construct instance by passing the attributes. Currently only the position changes for each letter.
+ // TODO: support different styles for each letter in a text entity
+ let glyph_render_data: &mut GlyphRenderData =
+ self.glyph_mesh_cache.get_mut(&glyph_id).unwrap();
+ // we don't scale text in the z-direction as it is a 2-D flat object positioned in 3-D
+ let glyph_instances: &mut GlyphInstances = &mut glyph_render_data.instances;
+ glyph_instances.add(
+ new_text_id,
+ GlyphInstanceAttributes {
+ transform: (cgmath::Matrix4::from_translation(glyph_offset)
+ * cgmath::Matrix4::from_nonuniform_scale(size, size, 1.0)
+ * cgmath::Matrix4::from(rotation))
+ .into(),
+ color: color.into(),
+ },
+ );
+ }
+
+ // Move offset to position of next letter in the text
+ // Apply scaling because advances are in the fonts local coordinate system
+ let x_advance = pos.x_advance as f32 * size;
+ let y_advance = pos.y_advance as f32 * size;
+ glyph_offset += cgmath::Vector3::new(x_advance, y_advance, 0.0);
+ }
+ self.next_text_entity_id += 1;
+ Ok(())
+ }
+
+ fn render_prepass(&mut self, encoder: &mut wgpu::CommandEncoder, rendering_state: &State) {
+ // Draw all the meshes into the target texture
+
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("TextSystemPrePass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &self.prepass_target_texture.view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: 0.0,
+ g: 0.0,
+ b: 0.0,
+ a: 0.0, // important, otherwise all pixels will pass the uneven winding number test in the main pass...
+ }),
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: None,
+ });
+
+ render_pass.set_pipeline(&self.prepass_pipeline);
+ render_pass.set_bind_group(0, &rendering_state.camera_bind_group, &[]);
+
+ for (_, glyph_render_data) in &mut self.glyph_mesh_cache {
+ render_pass.set_vertex_buffer(0, glyph_render_data.mesh.vertex_buffer.slice(..));
+ glyph_render_data
+ .instances
+ .compute_buffer(&rendering_state.device);
+ render_pass.set_vertex_buffer(
+ 1,
+ glyph_render_data
+ .instances
+ .buffer
+ .as_ref()
+ .unwrap()
+ .slice(..),
+ );
+
+ render_pass.set_index_buffer(
+ glyph_render_data.mesh.index_buffer.slice(..),
+ wgpu::IndexFormat::Uint16,
+ );
+ render_pass.draw_indexed(
+ 0..(glyph_render_data.mesh.num_indices as u32),
+ 0,
+ 0..glyph_render_data.instances.num_instances(),
+ );
+ }
+ }
+
+ fn render_mainpass(
+ &mut self,
+ encoder: &mut wgpu::CommandEncoder,
+ rendering_state: &State,
+ output_texture: wgpu::SurfaceTexture,
+ ) -> wgpu::SurfaceTexture {
+ let view = output_texture
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("TextSystemMainPass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: 1.0,
+ g: 1.0,
+ b: 1.0,
+ a: 1.0,
+ }),
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: None,
+ });
+
+ render_pass.set_pipeline(&self.main_pipeline);
+ render_pass.set_bind_group(0, &rendering_state.camera_bind_group, &[]);
+ render_pass.set_bind_group(1, &self.prepass_target_texture_bind_group, &[]);
+
+ let mesh = &self.full_screen_quad;
+ render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(0..4));
+ render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
+ let num_indices = mesh.num_indices as u32;
+ render_pass.draw_indexed(0..num_indices, 0, 0..1);
+
+ output_texture
+ }
+}
+
+impl Renderable for SceneTextSystem {
+ fn render(
+ &mut self,
+ rendering_state: &State,
+ output_texture: wgpu::SurfaceTexture,
+ ) -> wgpu::SurfaceTexture {
+ let mut encoder =
+ rendering_state
+ .device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Render Encoder"),
+ });
+
+ // Prepass to fill in the glyph meshes into the texture
+ self.render_prepass(&mut encoder, rendering_state);
+
+ // Actual pass to flip the pixels (and compute anti-aliasing?)
+ let output = self.render_mainpass(&mut encoder, rendering_state, output_texture);
+
+ rendering_state
+ .queue
+ .submit(std::iter::once(encoder.finish()));
+ output
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/src/text_system/textsystem_shaders.wgsl b/experiments/wgpu_for_fontrendering/src/text_system/textsystem_shaders.wgsl
new file mode 100644
index 000000000..4a93ec8da
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/text_system/textsystem_shaders.wgsl
@@ -0,0 +1,110 @@
+// Vertex shader
+
+struct CameraUniform {
+ view_proj: mat4x4
+};
+
+@group(0) @binding(0)
+var camera: CameraUniform;
+
+@group(1) @binding(0)
+var prepass_target_texture: texture_2d;
+@group(1) @binding(1)
+var prepass_target_texture_sampler: sampler;
+
+struct VertexInput {
+ @location(0) position: vec3,
+ @location(1) uv: vec2,
+};
+
+struct InstanceInput {
+ @location(2) model_matrix_0: vec4,
+ @location(3) model_matrix_1: vec4,
+ @location(4) model_matrix_2: vec4,
+ @location(5) model_matrix_3: vec4,
+ @location(6) color: vec3,
+};
+
+struct VertexOutputPrePass {
+ @builtin(position) clip_position: vec4,
+ @location(0) uv_coords: vec2,
+ @location(1) color: vec4,
+};
+
+
+// ########### Pre Pass ################
+// Regular transformation to NDC
+// Then additive fragment rendering into a texture
+
+@vertex
+fn prepass_vs(
+ model: VertexInput,
+ instance: InstanceInput,
+) -> VertexOutputPrePass {
+ let model_matrix = mat4x4(
+ instance.model_matrix_0,
+ instance.model_matrix_1,
+ instance.model_matrix_2,
+ instance.model_matrix_3,
+ );
+ var out: VertexOutputPrePass;
+ out.clip_position = camera.view_proj * model_matrix * vec4(model.position, 1.0);
+ out.uv_coords = model.uv;
+ out.color = vec4(instance.color.rgb, 1.0);
+ return out;
+}
+
+@fragment
+fn prepass_fs(in: VertexOutputPrePass) -> @location(0) vec4 {
+ // Discard fragments outside the curve defined by u^2 - v
+ if ((in.uv_coords.x * in.uv_coords.x) - in.uv_coords.y > 0.0) {
+ discard;
+ }
+ let color = in.color;
+ return vec4(color.xyz, 1.0 / 255.0); // 1/255 so overlapping triangles add up to alpha values of n * 1/255
+}
+
+// ########## Main Pass #################
+// Create a full screen quad (with uv's from 0 - 1) (assumes 6 input vertices, but disregards their coordinates and creates a screen-sized quad instead)
+// Read from the prepass texture and only paint the pixels with odd alpha value
+
+struct VertexOutputMainPass {
+ @builtin(position) clip_position: vec4,
+ @location(0) tex_coords: vec2,
+};
+
+@vertex
+fn mainpass_vs(@builtin(vertex_index) index: u32) -> VertexOutputMainPass {
+ // map the vertices that are passed in to a screen-sized quad
+ var pos = array, 4>(
+ vec2(-1.0, 1.0),
+ vec2(-1.0, -1.0),
+ vec2( 1.0, -1.0),
+ vec2( 1.0, 1.0),
+ );
+
+ var uv = array, 4>(
+ vec2(0.0, 0.0),
+ vec2(0.0, 1.0),
+ vec2(1.0, 1.0),
+ vec2(1.0, 0.0),
+ );
+
+ var output : VertexOutputMainPass;
+ output.clip_position = vec4(pos[index], 0.0, 1.0);
+ output.tex_coords = uv[index];
+ return output;
+}
+
+@fragment
+fn mainpass_fs(in: VertexOutputMainPass) -> @location(0) vec4 {
+ // look up color in texture -> TODO: currently this is all very inefficient, because we're only using the alpha of the texture!!!!
+ let color = textureSample(prepass_target_texture, prepass_target_texture_sampler, in.tex_coords);
+ var windingNumber: u32 = u32(color.a * 255.0);
+ if (windingNumber % 2u == 1u) {
+ return vec4(color.rgb, 1.0);
+ } else {
+ discard;
+ }
+}
+
diff --git a/experiments/wgpu_for_fontrendering/src/text_system/texture.rs b/experiments/wgpu_for_fontrendering/src/text_system/texture.rs
new file mode 100644
index 000000000..1fdccd5d3
--- /dev/null
+++ b/experiments/wgpu_for_fontrendering/src/text_system/texture.rs
@@ -0,0 +1,46 @@
+pub struct Texture {
+ pub texture: wgpu::Texture,
+ pub view: wgpu::TextureView,
+ pub sampler: wgpu::Sampler,
+}
+
+impl Texture {
+ pub fn empty(
+ device: &wgpu::Device,
+ width: u32,
+ height: u32,
+ usages: wgpu::TextureUsages,
+ label: Option<&str>,
+ ) -> Self {
+ let size = wgpu::Extent3d {
+ width,
+ height,
+ depth_or_array_layers: 1,
+ };
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
+ label,
+ size,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ usage: usages,
+ });
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Nearest,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ Self {
+ texture,
+ view,
+ sampler,
+ }
+ }
+}
diff --git a/experiments/wgpu_for_fontrendering/tests/fonts/aparaj.ttf b/experiments/wgpu_for_fontrendering/tests/fonts/aparaj.ttf
new file mode 100644
index 000000000..8b0c36f58
Binary files /dev/null and b/experiments/wgpu_for_fontrendering/tests/fonts/aparaj.ttf differ