Optimize tessellation of filled circles (#1616)
When painting a scatter plot we sometimes want to paint hundreds of thousands of points (filled circles) on screen every frame. In this PR the font texture atlas is pre-populated with some filled circled of various radii. These are then used when painting (small) filled circled, which means A LOT less triangles and vertices are generated for them. In a new benchmark we can see a 10x speedup in circle tessellation, but the the real benefit comes in the painting of these circles: since we generate a lot less vertices, the backend painter has less to do. In a real-life scenario with a lot of things being painted (including around 100k points) I saw tessellation go from 35ms -> 7ms and painting go from 45ms -> 1ms. This means the total frame time went from 80ms to 8ms, or a 10x speedup.
This commit is contained in:
parent
28efc0e1c8
commit
7b18fab7a4
12 changed files with 232 additions and 23 deletions
|
@ -5,7 +5,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
* Add `*_released` & `*_clicked` methods for `PointerState`.
|
* Add `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)).
|
||||||
|
* Optimize painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).
|
||||||
|
|
||||||
|
|
||||||
## 0.18.1 - 2022-05-01
|
## 0.18.1 - 2022-05-01
|
||||||
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
|
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
|
||||||
|
|
|
@ -830,14 +830,17 @@ impl Context {
|
||||||
|
|
||||||
let pixels_per_point = self.pixels_per_point();
|
let pixels_per_point = self.pixels_per_point();
|
||||||
let tessellation_options = *self.tessellation_options();
|
let tessellation_options = *self.tessellation_options();
|
||||||
let font_image_size = self.fonts().font_image_size();
|
let texture_atlas = self.fonts().texture_atlas();
|
||||||
|
let font_tex_size = texture_atlas.lock().size();
|
||||||
|
let prepared_discs = texture_atlas.lock().prepared_discs();
|
||||||
|
|
||||||
let paint_stats = PaintStats::from_shapes(&shapes);
|
let paint_stats = PaintStats::from_shapes(&shapes);
|
||||||
let clipped_primitives = tessellator::tessellate_shapes(
|
let clipped_primitives = tessellator::tessellate_shapes(
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
tessellation_options,
|
tessellation_options,
|
||||||
|
font_tex_size,
|
||||||
|
prepared_discs,
|
||||||
shapes,
|
shapes,
|
||||||
font_image_size,
|
|
||||||
);
|
);
|
||||||
self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives);
|
self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives);
|
||||||
clipped_primitives
|
clipped_primitives
|
||||||
|
|
|
@ -142,6 +142,7 @@ impl Widget for &mut epaint::TessellationOptions {
|
||||||
feathering,
|
feathering,
|
||||||
feathering_size_in_pixels,
|
feathering_size_in_pixels,
|
||||||
coarse_tessellation_culling,
|
coarse_tessellation_culling,
|
||||||
|
prerasterized_discs,
|
||||||
round_text_to_pixels,
|
round_text_to_pixels,
|
||||||
debug_paint_clip_rects,
|
debug_paint_clip_rects,
|
||||||
debug_paint_text_rects,
|
debug_paint_text_rects,
|
||||||
|
@ -158,6 +159,8 @@ impl Widget for &mut epaint::TessellationOptions {
|
||||||
.text("Feathering size in pixels");
|
.text("Feathering size in pixels");
|
||||||
ui.add_enabled(*feathering, feathering_slider);
|
ui.add_enabled(*feathering, feathering_slider);
|
||||||
|
|
||||||
|
ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization");
|
||||||
|
|
||||||
ui.add(
|
ui.add(
|
||||||
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
|
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
|
||||||
.logarithmic(true)
|
.logarithmic(true)
|
||||||
|
|
|
@ -124,8 +124,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
|
||||||
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
|
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
|
||||||
let font_image_size = fonts.font_image_size();
|
let font_image_size = fonts.font_image_size();
|
||||||
let mut tessellator =
|
let prepared_discs = fonts.texture_atlas().lock().prepared_discs();
|
||||||
egui::epaint::Tessellator::new(1.0, Default::default(), font_image_size);
|
let mut tessellator = egui::epaint::Tessellator::new(
|
||||||
|
1.0,
|
||||||
|
Default::default(),
|
||||||
|
font_image_size,
|
||||||
|
prepared_discs,
|
||||||
|
);
|
||||||
let mut mesh = egui::epaint::Mesh::default();
|
let mut mesh = egui::epaint::Mesh::default();
|
||||||
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
|
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
|
||||||
c.bench_function("tessellate_text", |b| {
|
c.bench_function("tessellate_text", |b| {
|
||||||
|
|
|
@ -158,6 +158,20 @@ impl View for MiscDemoWindow {
|
||||||
painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke);
|
painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CollapsingHeader::new("Many circles of different sizes")
|
||||||
|
.default_open(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
for i in 0..100 {
|
||||||
|
let r = i as f32 * 0.5;
|
||||||
|
let size = Vec2::splat(2.0 * r + 5.0);
|
||||||
|
let (rect, _response) = ui.allocate_at_least(size, Sense::hover());
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(rect.center(), r, ui.visuals().text_color());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ All notable changes to the epaint crate will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).
|
||||||
|
|
||||||
|
|
||||||
## 0.18.1 - 2022-05-01
|
## 0.18.1 - 2022-05-01
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use epaint::{pos2, Color32, Shape, Stroke};
|
use epaint::*;
|
||||||
|
|
||||||
fn single_dashed_lines(c: &mut Criterion) {
|
fn single_dashed_lines(c: &mut Criterion) {
|
||||||
c.bench_function("single_dashed_lines", move |b| {
|
c.bench_function("single_dashed_lines", move |b| {
|
||||||
|
@ -39,5 +39,43 @@ fn many_dashed_lines(c: &mut Criterion) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, single_dashed_lines, many_dashed_lines);
|
fn tessellate_circles(c: &mut Criterion) {
|
||||||
|
c.bench_function("tessellate_circles_100k", move |b| {
|
||||||
|
let radii: [f32; 10] = [1.0, 2.0, 3.6, 4.0, 5.7, 8.0, 10.0, 13.0, 15.0, 17.0];
|
||||||
|
let mut clipped_shapes = vec![];
|
||||||
|
for r in radii {
|
||||||
|
for _ in 0..10_000 {
|
||||||
|
let clip_rect = Rect::from_min_size(Pos2::ZERO, Vec2::splat(1024.0));
|
||||||
|
let shape = Shape::circle_filled(Pos2::new(10.0, 10.0), r, Color32::WHITE);
|
||||||
|
clipped_shapes.push(ClippedShape(clip_rect, shape));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(clipped_shapes.len(), 100_000);
|
||||||
|
|
||||||
|
let pixels_per_point = 2.0;
|
||||||
|
let options = TessellationOptions::default();
|
||||||
|
|
||||||
|
let atlas = TextureAtlas::new([4096, 256]);
|
||||||
|
let font_tex_size = atlas.size();
|
||||||
|
let prepared_discs = atlas.prepared_discs();
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let clipped_primitive = tessellate_shapes(
|
||||||
|
pixels_per_point,
|
||||||
|
options,
|
||||||
|
font_tex_size,
|
||||||
|
prepared_discs.clone(),
|
||||||
|
clipped_shapes.clone(),
|
||||||
|
);
|
||||||
|
black_box(clipped_primitive);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(
|
||||||
|
benches,
|
||||||
|
single_dashed_lines,
|
||||||
|
many_dashed_lines,
|
||||||
|
tessellate_circles
|
||||||
|
);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|
|
@ -73,6 +73,7 @@ impl Shadow {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
font_tex_size,
|
font_tex_size,
|
||||||
|
vec![],
|
||||||
);
|
);
|
||||||
let mut mesh = Mesh::default();
|
let mut mesh = Mesh::default();
|
||||||
tessellator.tessellate_rect(&rect, &mut mesh);
|
tessellator.tessellate_rect(&rect, &mut mesh);
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#![allow(clippy::identity_op)]
|
#![allow(clippy::identity_op)]
|
||||||
|
|
||||||
|
use crate::texture_atlas::PreparedDisc;
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use emath::*;
|
use emath::*;
|
||||||
|
|
||||||
|
@ -618,6 +619,10 @@ pub struct TessellationOptions {
|
||||||
/// This likely makes
|
/// This likely makes
|
||||||
pub coarse_tessellation_culling: bool,
|
pub coarse_tessellation_culling: bool,
|
||||||
|
|
||||||
|
/// If `true`, small filled circled will be optimized by using pre-rasterized circled
|
||||||
|
/// from the font atlas.
|
||||||
|
pub prerasterized_discs: bool,
|
||||||
|
|
||||||
/// If `true` (default) align text to mesh grid.
|
/// If `true` (default) align text to mesh grid.
|
||||||
/// This makes the text sharper on most platforms.
|
/// This makes the text sharper on most platforms.
|
||||||
pub round_text_to_pixels: bool,
|
pub round_text_to_pixels: bool,
|
||||||
|
@ -644,6 +649,7 @@ impl Default for TessellationOptions {
|
||||||
feathering: true,
|
feathering: true,
|
||||||
feathering_size_in_pixels: 1.0,
|
feathering_size_in_pixels: 1.0,
|
||||||
coarse_tessellation_culling: true,
|
coarse_tessellation_culling: true,
|
||||||
|
prerasterized_discs: true,
|
||||||
round_text_to_pixels: true,
|
round_text_to_pixels: true,
|
||||||
debug_paint_text_rects: false,
|
debug_paint_text_rects: false,
|
||||||
debug_paint_clip_rects: false,
|
debug_paint_clip_rects: false,
|
||||||
|
@ -968,6 +974,8 @@ pub struct Tessellator {
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
options: TessellationOptions,
|
options: TessellationOptions,
|
||||||
font_tex_size: [usize; 2],
|
font_tex_size: [usize; 2],
|
||||||
|
/// See [`TextureAtlas::prepared_discs`].
|
||||||
|
prepared_discs: Vec<PreparedDisc>,
|
||||||
/// size of feathering in points. normally the size of a physical pixel. 0.0 if disabled
|
/// size of feathering in points. normally the size of a physical pixel. 0.0 if disabled
|
||||||
feathering: f32,
|
feathering: f32,
|
||||||
/// Only used for culling
|
/// Only used for culling
|
||||||
|
@ -980,10 +988,12 @@ impl Tessellator {
|
||||||
/// Create a new [`Tessellator`].
|
/// Create a new [`Tessellator`].
|
||||||
///
|
///
|
||||||
/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text.
|
/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text.
|
||||||
|
/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
options: TessellationOptions,
|
options: TessellationOptions,
|
||||||
font_tex_size: [usize; 2],
|
font_tex_size: [usize; 2],
|
||||||
|
prepared_discs: Vec<PreparedDisc>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let feathering = if options.feathering {
|
let feathering = if options.feathering {
|
||||||
let pixel_size = 1.0 / pixels_per_point;
|
let pixel_size = 1.0 / pixels_per_point;
|
||||||
|
@ -995,6 +1005,7 @@ impl Tessellator {
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
options,
|
options,
|
||||||
font_tex_size,
|
font_tex_size,
|
||||||
|
prepared_discs,
|
||||||
feathering,
|
feathering,
|
||||||
clip_rect: Rect::EVERYTHING,
|
clip_rect: Rect::EVERYTHING,
|
||||||
scratchpad_points: Default::default(),
|
scratchpad_points: Default::default(),
|
||||||
|
@ -1137,7 +1148,7 @@ impl Tessellator {
|
||||||
let CircleShape {
|
let CircleShape {
|
||||||
center,
|
center,
|
||||||
radius,
|
radius,
|
||||||
fill,
|
mut fill,
|
||||||
stroke,
|
stroke,
|
||||||
} = shape;
|
} = shape;
|
||||||
|
|
||||||
|
@ -1154,6 +1165,30 @@ impl Tessellator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.options.prerasterized_discs && fill != Color32::TRANSPARENT {
|
||||||
|
let radius_px = radius * self.pixels_per_point;
|
||||||
|
// strike the right balance between some circles becoming too blurry, and some too sharp.
|
||||||
|
let cutoff_radius = radius_px * 2.0_f32.powf(0.25);
|
||||||
|
|
||||||
|
// Find the right disc radius for a crisp edge:
|
||||||
|
// TODO: perhaps we can do something faster than this linear search.
|
||||||
|
for disc in &self.prepared_discs {
|
||||||
|
if cutoff_radius <= disc.r {
|
||||||
|
let side = radius_px * disc.w / (self.pixels_per_point * disc.r);
|
||||||
|
let rect = Rect::from_center_size(center, Vec2::splat(side));
|
||||||
|
out.add_rect_with_uv(rect, disc.uv, fill);
|
||||||
|
|
||||||
|
if stroke.is_empty() {
|
||||||
|
return; // we are done
|
||||||
|
} else {
|
||||||
|
// we still need to do the stroke
|
||||||
|
fill = Color32::TRANSPARENT; // don't fill again below
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.scratchpad_path.clear();
|
self.scratchpad_path.clear();
|
||||||
self.scratchpad_path.add_circle(center, radius);
|
self.scratchpad_path.add_circle(center, radius);
|
||||||
self.scratchpad_path.fill(self.feathering, fill, out);
|
self.scratchpad_path.fill(self.feathering, fill, out);
|
||||||
|
@ -1476,7 +1511,8 @@ impl Tessellator {
|
||||||
/// * `pixels_per_point`: number of physical pixels to each logical point
|
/// * `pixels_per_point`: number of physical pixels to each logical point
|
||||||
/// * `options`: tessellation quality
|
/// * `options`: tessellation quality
|
||||||
/// * `shapes`: what to tessellate
|
/// * `shapes`: what to tessellate
|
||||||
/// * `font_tex_size`: size of the font texture (required to normalize glyph uv rectangles)
|
/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text.
|
||||||
|
/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec.
|
||||||
///
|
///
|
||||||
/// The implementation uses a [`Tessellator`].
|
/// The implementation uses a [`Tessellator`].
|
||||||
///
|
///
|
||||||
|
@ -1485,10 +1521,12 @@ impl Tessellator {
|
||||||
pub fn tessellate_shapes(
|
pub fn tessellate_shapes(
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
options: TessellationOptions,
|
options: TessellationOptions,
|
||||||
shapes: Vec<ClippedShape>,
|
|
||||||
font_tex_size: [usize; 2],
|
font_tex_size: [usize; 2],
|
||||||
|
prepared_discs: Vec<PreparedDisc>,
|
||||||
|
shapes: Vec<ClippedShape>,
|
||||||
) -> Vec<ClippedPrimitive> {
|
) -> Vec<ClippedPrimitive> {
|
||||||
let mut tessellator = Tessellator::new(pixels_per_point, options, font_tex_size);
|
let mut tessellator =
|
||||||
|
Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs);
|
||||||
|
|
||||||
let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();
|
let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();
|
||||||
|
|
||||||
|
@ -1562,6 +1600,15 @@ fn test_tessellator() {
|
||||||
let shape = Shape::Vec(shapes);
|
let shape = Shape::Vec(shapes);
|
||||||
let clipped_shapes = vec![ClippedShape(rect, shape)];
|
let clipped_shapes = vec![ClippedShape(rect, shape)];
|
||||||
|
|
||||||
let primitives = tessellate_shapes(1.0, Default::default(), clipped_shapes, [100, 100]);
|
let font_tex_size = [1024, 1024]; // unused
|
||||||
|
let prepared_discs = vec![]; // unused
|
||||||
|
|
||||||
|
let primitives = tessellate_shapes(
|
||||||
|
1.0,
|
||||||
|
Default::default(),
|
||||||
|
font_tex_size,
|
||||||
|
prepared_discs,
|
||||||
|
clipped_shapes,
|
||||||
|
);
|
||||||
assert_eq!(primitives.len(), 2);
|
assert_eq!(primitives.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -394,6 +394,12 @@ impl Fonts {
|
||||||
self.lock().fonts.max_texture_side
|
self.lock().fonts.max_texture_side
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The font atlas.
|
||||||
|
/// Pass this to [`crate::Tessellator`].
|
||||||
|
pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
|
||||||
|
self.lock().fonts.atlas.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Current size of the font image.
|
/// Current size of the font image.
|
||||||
/// Pass this to [`crate::Tessellator`].
|
/// Pass this to [`crate::Tessellator`].
|
||||||
pub fn font_image_size(&self) -> [usize; 2] {
|
pub fn font_image_size(&self) -> [usize; 2] {
|
||||||
|
@ -535,14 +541,7 @@ impl FontsImpl {
|
||||||
|
|
||||||
let texture_width = max_texture_side.at_most(8 * 1024);
|
let texture_width = max_texture_side.at_most(8 * 1024);
|
||||||
let initial_height = 64;
|
let initial_height = 64;
|
||||||
let mut atlas = TextureAtlas::new([texture_width, initial_height]);
|
let atlas = TextureAtlas::new([texture_width, initial_height]);
|
||||||
|
|
||||||
{
|
|
||||||
// Make the top left pixel fully white:
|
|
||||||
let (pos, image) = atlas.allocate((1, 1));
|
|
||||||
assert_eq!(pos, (0, 0));
|
|
||||||
image[pos] = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let atlas = Arc::new(Mutex::new(atlas));
|
let atlas = Arc::new(Mutex::new(atlas));
|
||||||
|
|
||||||
|
|
|
@ -401,7 +401,7 @@ pub struct Glyph {
|
||||||
pub pos: Pos2,
|
pub pos: Pos2,
|
||||||
/// Advance width and font row height.
|
/// Advance width and font row height.
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
/// Position of the glyph in the font texture.
|
/// Position of the glyph in the font texture, in texels.
|
||||||
pub uv_rect: UvRect,
|
pub uv_rect: UvRect,
|
||||||
/// Index into [`LayoutJob::sections`]. Decides color etc.
|
/// Index into [`LayoutJob::sections`]. Decides color etc.
|
||||||
pub section_index: u32,
|
pub section_index: u32,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use emath::{remap_clamp, Rect};
|
||||||
|
|
||||||
use crate::{FontImage, ImageDelta};
|
use crate::{FontImage, ImageDelta};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
struct Rectu {
|
struct Rectu {
|
||||||
/// inclusive
|
/// inclusive
|
||||||
min_x: usize,
|
min_x: usize,
|
||||||
|
@ -27,6 +29,26 @@ impl Rectu {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
struct PrerasterizedDisc {
|
||||||
|
r: f32,
|
||||||
|
uv: Rectu,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pre-rasterized disc (filled circle), somewhere in the texture atlas.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct PreparedDisc {
|
||||||
|
/// The radius of this disc in texels.
|
||||||
|
pub r: f32,
|
||||||
|
|
||||||
|
/// Width in texels.
|
||||||
|
pub w: f32,
|
||||||
|
|
||||||
|
/// Where in the texture atlas the disc is.
|
||||||
|
/// Normalized in 0-1 range.
|
||||||
|
pub uv: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
/// Contains font data in an atlas, where each character occupied a small rectangle.
|
||||||
///
|
///
|
||||||
/// More characters can be added, possibly expanding the texture.
|
/// More characters can be added, possibly expanding the texture.
|
||||||
|
@ -42,24 +64,98 @@ pub struct TextureAtlas {
|
||||||
|
|
||||||
/// Set when someone requested more space than was available.
|
/// Set when someone requested more space than was available.
|
||||||
overflowed: bool,
|
overflowed: bool,
|
||||||
|
|
||||||
|
/// pre-rasterized discs of radii `2^i`, where `i` is the index.
|
||||||
|
discs: Vec<PrerasterizedDisc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextureAtlas {
|
impl TextureAtlas {
|
||||||
pub fn new(size: [usize; 2]) -> Self {
|
pub fn new(size: [usize; 2]) -> Self {
|
||||||
assert!(size[0] >= 1024, "Tiny texture atlas");
|
assert!(size[0] >= 1024, "Tiny texture atlas");
|
||||||
Self {
|
let mut atlas = Self {
|
||||||
image: FontImage::new(size),
|
image: FontImage::new(size),
|
||||||
dirty: Rectu::EVERYTHING,
|
dirty: Rectu::EVERYTHING,
|
||||||
cursor: (0, 0),
|
cursor: (0, 0),
|
||||||
row_height: 0,
|
row_height: 0,
|
||||||
overflowed: false,
|
overflowed: false,
|
||||||
|
discs: vec![], // will be filled in below
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
|
||||||
|
let (pos, image) = atlas.allocate((1, 1));
|
||||||
|
assert_eq!(pos, (0, 0));
|
||||||
|
image[pos] = 1.0;
|
||||||
|
|
||||||
|
// Allocate a series of anti-aliased discs used to render small filled circles:
|
||||||
|
// TODO: these circles can be packed A LOT better.
|
||||||
|
// In fact, the whole texture atlas could be packed a lot better.
|
||||||
|
// for r in [1, 2, 4, 8, 16, 32, 64] {
|
||||||
|
// let w = 2 * r + 3;
|
||||||
|
// let hw = w as i32 / 2;
|
||||||
|
const LARGEST_CIRCLE_RADIUS: f32 = 64.0;
|
||||||
|
for i in 0.. {
|
||||||
|
let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0);
|
||||||
|
if r > LARGEST_CIRCLE_RADIUS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let hw = (r + 0.5).ceil() as i32;
|
||||||
|
let w = (2 * hw + 1) as usize;
|
||||||
|
let ((x, y), image) = atlas.allocate((w, w));
|
||||||
|
for dx in -hw..=hw {
|
||||||
|
for dy in -hw..=hw {
|
||||||
|
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
||||||
|
let coverage = remap_clamp(
|
||||||
|
distance_to_center,
|
||||||
|
(r as f32 - 0.5)..=(r as f32 + 0.5),
|
||||||
|
1.0..=0.0,
|
||||||
|
);
|
||||||
|
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
|
||||||
|
coverage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atlas.discs.push(PrerasterizedDisc {
|
||||||
|
r,
|
||||||
|
uv: Rectu {
|
||||||
|
min_x: x,
|
||||||
|
min_y: y,
|
||||||
|
max_x: x + w,
|
||||||
|
max_y: y + w,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
atlas
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> [usize; 2] {
|
pub fn size(&self) -> [usize; 2] {
|
||||||
self.image.size
|
self.image.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the locations and sizes of pre-rasterized discs (filled circles) in this atlas.
|
||||||
|
pub fn prepared_discs(&self) -> Vec<PreparedDisc> {
|
||||||
|
let size = self.size();
|
||||||
|
let inv_w = 1.0 / size[0] as f32;
|
||||||
|
let inv_h = 1.0 / size[1] as f32;
|
||||||
|
self.discs
|
||||||
|
.iter()
|
||||||
|
.map(|disc| {
|
||||||
|
let r = disc.r;
|
||||||
|
let Rectu {
|
||||||
|
min_x,
|
||||||
|
min_y,
|
||||||
|
max_x,
|
||||||
|
max_y,
|
||||||
|
} = disc.uv;
|
||||||
|
let w = max_x - min_x;
|
||||||
|
let uv = Rect::from_min_max(
|
||||||
|
emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h),
|
||||||
|
emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h),
|
||||||
|
);
|
||||||
|
PreparedDisc { r, w: w as f32, uv }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn max_height(&self) -> usize {
|
fn max_height(&self) -> usize {
|
||||||
// the initial width is likely the max texture side size
|
// the initial width is likely the max texture side size
|
||||||
self.image.width()
|
self.image.width()
|
||||||
|
|
Loading…
Reference in a new issue