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.
This commit is contained in:
Emil Ernerfeldt 2022-03-23 11:41:38 +01:00 committed by GitHub
parent a9fd03709e
commit 1387d6e9d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 107 deletions

View file

@ -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

View file

@ -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)

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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<Pos2>,
@ -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<ClippedShape>,
pixels_per_point: f32,
options: TessellationOptions,
shapes: Vec<ClippedShape>,
tex_size: [usize; 2],
) -> Vec<ClippedPrimitive> {
let mut tessellator = Tessellator::from_options(options);
let mut tessellator = Tessellator::new(pixels_per_point, options);
let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();

View file

@ -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