Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fontrendering #40

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9ac047a
add font rendering experiment branch
FabianWildgrube Apr 20, 2022
0cb7de6
exclude experiments from main workspace
FabianWildgrube Apr 21, 2022
da1bce0
fix fontrendering experiment build; add readme
FabianWildgrube Apr 21, 2022
8839b61
Refactor into two-pass rendering as set up for discarding uneven over…
FabianWildgrube Apr 23, 2022
abd8eb6
Only draw pixels with uneven winding number
FabianWildgrube Apr 24, 2022
5ef894b
Remove backface culling for easier glyph tesselation
FabianWildgrube Apr 24, 2022
d783eab
Fix shader to actually calculate winding number as intended
FabianWildgrube Apr 24, 2022
60491f6
Add proper curve rendering
FabianWildgrube Apr 24, 2022
48f85da
update vscode debug run config
FabianWildgrube Apr 24, 2022
7ff0d40
Use cgmath vector types
FabianWildgrube Apr 24, 2022
c78d007
Render multi letter text at arbitrary positions in the scene
FabianWildgrube Apr 24, 2022
08cfcf2
Add super primitive benchmarking
FabianWildgrube Apr 24, 2022
4ffc70f
Merge branch 'main' into fontrendering
FabianWildgrube Apr 24, 2022
1d2d79a
re-add exclusion of experiments after merge
FabianWildgrube Apr 24, 2022
11726bf
readability
FabianWildgrube Apr 24, 2022
c9351fc
Add font rendering documentation and benchmark
FabianWildgrube Apr 24, 2022
627c63b
Add todo
FabianWildgrube Apr 24, 2022
baa7d6b
Start refactoring into proper text rendering subsystem
FabianWildgrube Apr 25, 2022
353518d
WIP: move text rendering into text sub system [skip ci]
FabianWildgrube Apr 25, 2022
2c84576
Add observation that overlapping text is a non-issue for maps
FabianWildgrube Apr 26, 2022
1ee6cd8
Refactor code structure; add instanced draw calls
FabianWildgrube Apr 28, 2022
4909020
Add experiment to workspace
maxammann May 1, 2022
c24fe96
Fix all errors to render glyphs instanced properly
FabianWildgrube May 1, 2022
1ce4e54
clean up code
FabianWildgrube May 3, 2022
8100c27
Support colorful text
FabianWildgrube May 15, 2022
3327416
Merge branch 'main' into fontrendering
maxammann Jun 1, 2022
300354b
Merge branch 'main' into fontrendering
maxammann Jun 1, 2022
8b6a20e
Merge branch 'main' into fontrendering
maxammann Jun 2, 2022
a6ff369
Merge branch 'main' into fontrendering
maxammann Jun 3, 2022
4f9fc12
Merge branch 'main' into fontrendering
FabianWildgrube Jun 5, 2022
43a6aa3
Merge branch 'main' into fontrendering
FabianWildgrube Jun 16, 2022
754fa7a
Merge branch 'main' into fontrendering
FabianWildgrube Sep 8, 2022
0e24bc5
Update to latest wgpu and winit versions
FabianWildgrube Sep 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/maplibre-rs.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ members = [
"web",

"benchmarks",

"experiments/wgpu_for_fontrendering"
]

[profile.release]
Expand Down
1 change: 1 addition & 0 deletions experiments/wgpu_for_fontrendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
27 changes: 27 additions & 0 deletions experiments/wgpu_for_fontrendering/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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}"
},
]
}
21 changes: 21 additions & 0 deletions experiments/wgpu_for_fontrendering/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
51 changes: 51 additions & 0 deletions experiments/wgpu_for_fontrendering/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions experiments/wgpu_for_fontrendering/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<u32> = 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();
}
_ => {}
}
});
}
}
9 changes: 9 additions & 0 deletions experiments/wgpu_for_fontrendering/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use wgpu_for_fontrendering::run;

fn run_gui() {
pollster::block_on(run());
}

fn main() {
run_gui();
}
Loading