From 1387d6e9d6a978862623fef030f3b00304a7013c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 23 Mar 2022 11:41:38 +0100 Subject: [PATCH] Refactor TessellationOptions to expose slider for feathering size (#1408) The epaint tessellator uses "feathering" to accomplish anti-aliasing. This PS allows you to control the feathering size, i.e. how blurry the edges of epaint shapes are. This changes the interface of Tessellator slightly, and renames some options in TessellationOptions. --- egui/src/context.rs | 12 +- egui/src/introspection.rs | 14 ++- egui_demo_lib/benches/benchmark.rs | 2 +- epaint/CHANGELOG.md | 2 + epaint/src/shadow.rs | 14 ++- epaint/src/tessellator.rs | 172 ++++++++++++++--------------- epaint/src/text/text_layout.rs | 6 +- 7 files changed, 115 insertions(+), 107 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index 536461de..6666ba33 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -807,14 +807,16 @@ impl Context { // shapes are the same, but just comparing the shapes takes about 50% of the time // it takes to tessellate them, so it is not a worth optimization. - let mut tessellation_options = *self.tessellation_options(); - tessellation_options.pixels_per_point = self.pixels_per_point(); - tessellation_options.aa_size = 1.0 / self.pixels_per_point(); + let pixels_per_point = self.pixels_per_point(); + let tessellation_options = *self.tessellation_options(); + let font_image_size = self.fonts().font_image_size(); + let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = tessellator::tessellate_shapes( - shapes, + pixels_per_point, tessellation_options, - self.fonts().font_image_size(), + shapes, + font_image_size, ); self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); clipped_primitives diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 7e286d89..4956918c 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -139,9 +139,8 @@ impl Widget for &mut epaint::TessellationOptions { fn ui(self, ui: &mut Ui) -> Response { ui.vertical(|ui| { let epaint::TessellationOptions { - pixels_per_point: _, - aa_size: _, - anti_alias, + feathering, + feathering_size_in_pixels, coarse_tessellation_culling, round_text_to_pixels, debug_paint_clip_rects, @@ -150,8 +149,15 @@ impl Widget for &mut epaint::TessellationOptions { bezier_tolerance, epsilon: _, } = self; - ui.checkbox(anti_alias, "Antialias") + + ui.checkbox(feathering, "Feathering (antialias)") .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); + let feathering_slider = crate::Slider::new(feathering_size_in_pixels, 0.0..=10.0) + .smallest_positive(0.1) + .logarithmic(true) + .text("Feathering size in pixels"); + ui.add_enabled(*feathering, feathering_slider); + ui.add( crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0) .logarithmic(true) diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index 9038187f..b3e86bbc 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -123,7 +123,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width); - let mut tessellator = egui::epaint::Tessellator::from_options(Default::default()); + let mut tessellator = egui::epaint::Tessellator::new(1.0, Default::default()); let mut mesh = egui::epaint::Mesh::default(); let text_shape = TextShape::new(egui::Pos2::ZERO, galley); let font_image_size = fonts.font_image_size(); diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index ec709f69..de93bc15 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased * Add `Shape::Callback` for backend-specific painting ([#1351](https://github.com/emilk/egui/pull/1351)). * Removed the `single_threaded/multi_threaded` flags - epaint is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)). +* `Tessellator::from_options` is now `Tessellator::new` ([#1408](https://github.com/emilk/egui/pull/1408)). +* Renamed `TessellationOptions::anti_alias` to `feathering` ([#1408](https://github.com/emilk/egui/pull/1408)). ## 0.17.0 - 2022-02-22 diff --git a/epaint/src/shadow.rs b/epaint/src/shadow.rs index d8895fee..35ce41cd 100644 --- a/epaint/src/shadow.rs +++ b/epaint/src/shadow.rs @@ -63,11 +63,15 @@ impl Shadow { use crate::tessellator::*; let rect = RectShape::filled(rect.expand(half_ext), ext_rounding, color); - let mut tessellator = Tessellator::from_options(TessellationOptions { - aa_size: extrusion, - anti_alias: true, - ..Default::default() - }); + let pixels_per_point = 1.0; // doesn't matter here + let mut tessellator = Tessellator::new( + pixels_per_point, + TessellationOptions { + feathering: true, + feathering_size_in_pixels: extrusion * pixels_per_point, + ..Default::default() + }, + ); let mut mesh = Mesh::default(); tessellator.tessellate_rect(&rect, &mut mesh); mesh diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index 440f356e..1f9fe1a0 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -163,23 +163,17 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, stroke: Stroke, options: &TessellationOptions, out: &mut Mesh) { - stroke_path(&self.0, PathType::Open, stroke, options, out); + pub fn stroke_open(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + stroke_path(feathering, &self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, stroke: Stroke, options: &TessellationOptions, out: &mut Mesh) { - stroke_path(&self.0, PathType::Closed, stroke, options, out); + pub fn stroke_closed(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + stroke_path(feathering, &self.0, PathType::Closed, stroke, out); } - pub fn stroke( - &self, - path_type: PathType, - stroke: Stroke, - options: &TessellationOptions, - out: &mut Mesh, - ) { - stroke_path(&self.0, path_type, stroke, options, out); + pub fn stroke(&self, feathering: f32, path_type: PathType, stroke: Stroke, out: &mut Mesh) { + stroke_path(feathering, &self.0, path_type, stroke, out); } /// The path is taken to be closed (i.e. returning to the start again). @@ -187,8 +181,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. - pub fn fill(&mut self, color: Color32, options: &TessellationOptions, out: &mut Mesh) { - fill_closed_path(&mut self.0, color, options, out); + pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, out); } } @@ -286,18 +280,23 @@ pub enum PathType { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct TessellationOptions { - /// Size of a point in pixels (DPI scaling), e.g. 2.0. Used to snap text to pixel boundaries. - pub pixels_per_point: f32, - - /// The size of a pixel (in points), used for anti-aliasing (smoothing of edges). - /// This is normally the inverse of [`Self::pixels_per_point`], - /// but you can make it larger if you want more blurry edges. - pub aa_size: f32, - - /// Anti-aliasing makes shapes appear smoother, but requires more triangles and is therefore slower. + /// Use "feathering" to smooth out the edges of shapes as a form of anti-aliasing. + /// + /// Feathering works by making each edge into a thin gradient into transparency. + /// The size of this edge is controlled by [`Self::feathering_size_in_pixels`]. + /// + /// This makes shapes appear smoother, but requires more triangles and is therefore slower. + /// /// This setting does not affect text. + /// /// Default: `true`. - pub anti_alias: bool, + pub feathering: bool, + + /// The size of the the feathering, in physical pixels. + /// + /// The default, and suggested, value for this is `1.0`. + /// If you use a larger value, edges will appear blurry. + pub feathering_size_in_pixels: f32, /// If `true` (default) cull certain primitives before tessellating them. /// This likely makes @@ -326,9 +325,8 @@ pub struct TessellationOptions { impl Default for TessellationOptions { fn default() -> Self { Self { - pixels_per_point: 1.0, - aa_size: 1.0, - anti_alias: true, + feathering: true, + feathering_size_in_pixels: 1.0, coarse_tessellation_culling: true, round_text_to_pixels: true, debug_paint_text_rects: false, @@ -340,27 +338,6 @@ impl Default for TessellationOptions { } } -impl TessellationOptions { - pub fn from_pixels_per_point(pixels_per_point: f32) -> Self { - Self { - pixels_per_point, - aa_size: 1.0 / pixels_per_point, - ..Default::default() - } - } -} - -impl TessellationOptions { - #[inline(always)] - pub fn round_to_pixel(&self, point: f32) -> f32 { - if self.round_text_to_pixels { - (point * self.pixels_per_point).round() / self.pixels_per_point - } else { - point - } - } -} - fn cw_signed_area(path: &[PathPoint]) -> f64 { if let Some(last) = path.last() { let mut previous = last.pos; @@ -380,18 +357,13 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. -fn fill_closed_path( - path: &mut [PathPoint], - color: Color32, - options: &TessellationOptions, - out: &mut Mesh, -) { +fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out: &mut Mesh) { if color == Color32::TRANSPARENT { return; } let n = path.len() as u32; - if options.anti_alias { + if feathering > 0.0 { if cw_signed_area(path) < 0.0 { // Wrong winding order - fix: path.reverse(); @@ -415,7 +387,7 @@ fn fill_closed_path( let mut i0 = n - 1; for i1 in 0..n { let p1 = &path[i1 as usize]; - let dm = 0.5 * options.aa_size * p1.normal; + let dm = 0.5 * feathering * p1.normal; out.colored_vertex(p1.pos - dm, color); out.colored_vertex(p1.pos + dm, color_outer); out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); @@ -438,10 +410,10 @@ fn fill_closed_path( /// Tessellate the given path as a stroke with thickness. fn stroke_path( + feathering: f32, path: &[PathPoint], path_type: PathType, stroke: Stroke, - options: &TessellationOptions, out: &mut Mesh, ) { let n = path.len() as u32; @@ -452,21 +424,21 @@ fn stroke_path( let idx = out.vertices.len() as u32; - if options.anti_alias { + if feathering > 0.0 { let color_inner = stroke.color; let color_outer = Color32::TRANSPARENT; - let thin_line = stroke.width <= options.aa_size; + let thin_line = stroke.width <= feathering; if thin_line { /* We paint the line using three edges: outer, inner, outer. . o i o outer, inner, outer - . |---| aa_size (pixel width) + . |---| feathering (pixel width) */ // Fade out as it gets thinner: - let color_inner = mul_color(color_inner, stroke.width / options.aa_size); + let color_inner = mul_color(color_inner, stroke.width / feathering); if color_inner == Color32::TRANSPARENT { return; } @@ -480,9 +452,9 @@ fn stroke_path( let p1 = &path[i1 as usize]; let p = p1.pos; let n = p1.normal; - out.colored_vertex(p + n * options.aa_size, color_outer); + out.colored_vertex(p + n * feathering, color_outer); out.colored_vertex(p, color_inner); - out.colored_vertex(p - n * options.aa_size, color_outer); + out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0); @@ -500,14 +472,14 @@ fn stroke_path( We paint the line using four edges: outer, inner, inner, outer . o i p i o outer, inner, point, inner, outer - . |---| aa_size (pixel width) + . |---| feathering (pixel width) . |--------------| width . |---------| outer_rad . |-----| inner_rad */ - let inner_rad = 0.5 * (stroke.width - options.aa_size); - let outer_rad = 0.5 * (stroke.width + options.aa_size); + let inner_rad = 0.5 * (stroke.width - feathering); + let outer_rad = 0.5 * (stroke.width + feathering); match path_type { PathType::Closed => { @@ -542,7 +514,7 @@ fn stroke_path( // | aa | | aa | // _________________ ___ - // | \ added / | aa_size + // | \ added / | feathering // | \ ___p___ / | ___ // | | | | // | | opa | | @@ -558,7 +530,7 @@ fn stroke_path( let end = &path[0]; let p = end.pos; let n = end.normal; - let back_extrude = n.rot90() * options.aa_size; + let back_extrude = n.rot90() * feathering; out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(p - n * inner_rad, color_inner); @@ -595,7 +567,7 @@ fn stroke_path( let end = &path[i1 as usize]; let p = end.pos; let n = end.normal; - let back_extrude = -n.rot90() * options.aa_size; + let back_extrude = -n.rot90() * feathering; out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); out.colored_vertex(p + n * inner_rad, color_inner); out.colored_vertex(p - n * inner_rad, color_inner); @@ -640,11 +612,11 @@ fn stroke_path( ); } - let thin_line = stroke.width <= options.aa_size; + let thin_line = stroke.width <= feathering; if thin_line { // Fade out thin lines rather than making them thinner - let radius = options.aa_size / 2.0; - let color = mul_color(stroke.color, stroke.width / options.aa_size); + let radius = feathering / 2.0; + let color = mul_color(stroke.color, stroke.width / feathering); if color == Color32::TRANSPARENT { return; } @@ -677,7 +649,10 @@ fn mul_color(color: Color32, factor: f32) -> Color32 { /// /// Se also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. pub struct Tessellator { + pixels_per_point: f32, options: TessellationOptions, + /// size of feathering in points. normally the size of a physical pixel. 0.0 if disabled + feathering: f32, /// Only used for culling clip_rect: Rect, scratchpad_points: Vec, @@ -686,9 +661,17 @@ pub struct Tessellator { impl Tessellator { /// Create a new [`Tessellator`]. - pub fn from_options(options: TessellationOptions) -> Self { + pub fn new(pixels_per_point: f32, options: TessellationOptions) -> Self { + let feathering = if options.feathering { + let pixel_size = 1.0 / pixels_per_point; + options.feathering_size_in_pixels * pixel_size + } else { + 0.0 + }; Self { + pixels_per_point, options, + feathering, clip_rect: Rect::EVERYTHING, scratchpad_points: Default::default(), scratchpad_path: Default::default(), @@ -700,6 +683,15 @@ impl Tessellator { self.clip_rect = clip_rect; } + #[inline(always)] + pub fn round_to_pixel(&self, point: f32) -> f32 { + if self.options.round_text_to_pixels { + (point * self.pixels_per_point).round() / self.pixels_per_point + } else { + point + } + } + /// Tessellate a single [`Shape`] into a [`Mesh`]. /// /// * `tex_size`: size of the font texture (required to normalize glyph uv rectangles). @@ -783,9 +775,9 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); - self.scratchpad_path.fill(fill, &self.options, out); + self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(stroke, &self.options, out); + .stroke_closed(self.feathering, stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -826,7 +818,8 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_segment(points); - self.scratchpad_path.stroke_open(stroke, &self.options, out); + self.scratchpad_path + .stroke_open(self.feathering, stroke, out); } /// Tessellate a single [`PathShape`] into a [`Mesh`]. @@ -863,7 +856,7 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(*fill, &self.options, out); + self.scratchpad_path.fill(self.feathering, *fill, out); } let typ = if *closed { PathType::Closed @@ -871,7 +864,7 @@ impl Tessellator { PathType::Open }; self.scratchpad_path - .stroke(typ, *stroke, &self.options, out); + .stroke(self.feathering, typ, *stroke, out); } /// Tessellate a single [`Rect`] into a [`Mesh`]. @@ -904,8 +897,8 @@ impl Tessellator { path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); path.add_line_loop(&self.scratchpad_points); - path.fill(fill, &self.options, out); - path.stroke_closed(stroke, &self.options, out); + path.fill(self.feathering, fill, out); + path.stroke_closed(self.feathering, stroke, out); } /// Tessellate a single [`TextShape`] into a [`Mesh`]. @@ -937,8 +930,8 @@ impl Tessellator { // The contents of the galley is already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: let galley_pos = pos2( - self.options.round_to_pixel(galley_pos.x), - self.options.round_to_pixel(galley_pos.y), + self.round_to_pixel(galley_pos.x), + self.round_to_pixel(galley_pos.y), ); let uv_normalizer = vec2(1.0 / tex_size[0] as f32, 1.0 / tex_size[1] as f32); @@ -1006,7 +999,7 @@ impl Tessellator { self.scratchpad_path .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); self.scratchpad_path - .stroke_open(*underline, &self.options, out); + .stroke_open(self.feathering, *underline, out); } } } @@ -1086,14 +1079,15 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(fill, &self.options, out); + self.scratchpad_path.fill(self.feathering, fill, out); } let typ = if closed { PathType::Closed } else { PathType::Open }; - self.scratchpad_path.stroke(typ, stroke, &self.options, out); + self.scratchpad_path + .stroke(self.feathering, typ, stroke, out); } } @@ -1102,8 +1096,9 @@ impl Tessellator { /// The given shapes will tessellated in the same order as they are given. /// They will be batched together by clip rectangle. /// -/// * `shapes`: what to tessellate +/// * `pixels_per_point`: number of physical pixels to each logical point /// * `options`: tessellation quality +/// * `shapes`: what to tessellate /// * `tex_size`: size of the font texture (required to normalize glyph uv rectangles) /// /// The implementation uses a [`Tessellator`]. @@ -1111,11 +1106,12 @@ impl Tessellator { /// ## Returns /// A list of clip rectangles with matching [`Mesh`]. pub fn tessellate_shapes( - shapes: Vec, + pixels_per_point: f32, options: TessellationOptions, + shapes: Vec, tex_size: [usize; 2], ) -> Vec { - let mut tessellator = Tessellator::from_options(options); + let mut tessellator = Tessellator::new(pixels_per_point, options); let mut clipped_primitives: Vec = Vec::default(); diff --git a/epaint/src/text/text_layout.rs b/epaint/src/text/text_layout.rs index a70127c7..bac97e17 100644 --- a/epaint/src/text/text_layout.rs +++ b/epaint/src/text/text_layout.rs @@ -599,10 +599,8 @@ fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, if antialiased { let mut path = crate::tessellator::Path::default(); // TODO: reuse this to avoid re-allocations. path.add_line_segment([start, stop]); - let options = crate::tessellator::TessellationOptions::from_pixels_per_point( - point_scale.pixels_per_point(), - ); - path.stroke_open(stroke, &options, mesh); + let feathering = 1.0 / point_scale.pixels_per_point(); + path.stroke_open(feathering, stroke, mesh); } else { // Thin lines often lost, so this is a bad idea