mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Improve tessellation quality (#5669)
## Defining what `Rounding` is This PR defines what `Rounding` means: it is the corner radius of underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this means the rounding is of the outer part of the stroke. Conversely, if you use `StrokeKind::Outside`, the stroke is outside the rounded rectangle, so the stroke has an inner radius or `rounding`, and an outer radius that is larger by `stroke.width`. This definitions is the same as Figma uses. ## Improving general shape rendering The rendering of filled shapes (rectangles, circles, paths, bezier) has been rewritten. Instead of first painting the fill with the stroke on top, we now paint them as one single mesh with shared vertices at the border. This has several benefits: * Less work (faster and with fewer vertices produced) * No overdraw (nicer rendering of translucent shapes) * Correct blending of stroke and fill The logic for rendering thin strokes has also been improved, so that the width of a stroke of `StrokeKind::Outside` never affects the filled area (this used to be wrong for thin strokes). ## Improving of rectangle rendering Rectangles also has specific improvements in how thin rectangles are painted. The handling of "Blur width" is also a lot better, and now works for rectangles with strokes. There also used to be bugs with specific combinations of corner radius and stroke width, that are now fixed. ## But why? With the new `egui::Scene` we end up with a lot of zoomed out shapes, with sub-pixel strokes. These need to look good! One thing led to another, and then I became obsessive 😅 ## Tessellation Test In order to investigate the rendering, I created a Tessellation Test in the `egui_demo_lib`. [Try it here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator)  
This commit is contained in:
@@ -100,6 +100,7 @@ impl Default for DemoGroups {
|
||||
Box::<super::tests::InputTest>::default(),
|
||||
Box::<super::tests::LayoutTest>::default(),
|
||||
Box::<super::tests::ManualLayoutTest>::default(),
|
||||
Box::<super::tests::TessellationTest>::default(),
|
||||
Box::<super::tests::WindowResizeTest>::default(),
|
||||
]),
|
||||
}
|
||||
|
||||
@@ -124,9 +124,9 @@ impl View for MiscDemoWindow {
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
self.checklist
|
||||
.iter_mut()
|
||||
.for_each(|checked| *checked = all_checked);
|
||||
for check in &mut self.checklist {
|
||||
*check = all_checked;
|
||||
}
|
||||
}
|
||||
for (i, checked) in self.checklist.iter_mut().enumerate() {
|
||||
ui.checkbox(checked, format!("Item {}", i + 1));
|
||||
|
||||
@@ -6,6 +6,7 @@ mod input_event_history;
|
||||
mod input_test;
|
||||
mod layout_test;
|
||||
mod manual_layout_test;
|
||||
mod tessellation_test;
|
||||
mod window_resize_test;
|
||||
|
||||
pub use clipboard_test::ClipboardTest;
|
||||
@@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory;
|
||||
pub use input_test::InputTest;
|
||||
pub use layout_test::LayoutTest;
|
||||
pub use manual_layout_test::ManualLayoutTest;
|
||||
pub use tessellation_test::TessellationTest;
|
||||
pub use window_resize_test::WindowResizeTest;
|
||||
|
||||
364
crates/egui_demo_lib/src/demo/tests/tessellation_test.rs
Normal file
364
crates/egui_demo_lib/src/demo/tests/tessellation_test.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use egui::{
|
||||
emath::{GuiRounding, TSTransform},
|
||||
epaint::{self, RectShape},
|
||||
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TessellationTest {
|
||||
shape: RectShape,
|
||||
|
||||
magnification_pixel_size: f32,
|
||||
tessellation_options: epaint::TessellationOptions,
|
||||
paint_edges: bool,
|
||||
}
|
||||
|
||||
impl Default for TessellationTest {
|
||||
fn default() -> Self {
|
||||
let shape = Self::interesting_shapes()[0].1.clone();
|
||||
Self {
|
||||
shape,
|
||||
magnification_pixel_size: 12.0,
|
||||
tessellation_options: Default::default(),
|
||||
paint_edges: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TessellationTest {
|
||||
fn interesting_shapes() -> Vec<(&'static str, RectShape)> {
|
||||
fn sized(size: impl Into<Vec2>) -> Rect {
|
||||
Rect::from_center_size(Pos2::ZERO, size.into())
|
||||
}
|
||||
|
||||
let baby_blue = Color32::from_rgb(0, 181, 255);
|
||||
|
||||
let mut shapes = vec![
|
||||
(
|
||||
"Normal",
|
||||
RectShape::new(
|
||||
sized([20.0, 16.0]),
|
||||
2.0,
|
||||
baby_blue,
|
||||
(1.0, Color32::WHITE),
|
||||
StrokeKind::Inside,
|
||||
),
|
||||
),
|
||||
(
|
||||
"Minimal rounding",
|
||||
RectShape::new(
|
||||
sized([20.0, 16.0]),
|
||||
1.0,
|
||||
baby_blue,
|
||||
(1.0, Color32::WHITE),
|
||||
StrokeKind::Inside,
|
||||
),
|
||||
),
|
||||
(
|
||||
"Thin filled",
|
||||
RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue),
|
||||
),
|
||||
(
|
||||
"Thin stroked",
|
||||
RectShape::new(
|
||||
sized([20.0, 0.5]),
|
||||
2.0,
|
||||
baby_blue,
|
||||
(0.5, Color32::WHITE),
|
||||
StrokeKind::Inside,
|
||||
),
|
||||
),
|
||||
(
|
||||
"Blurred",
|
||||
RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0),
|
||||
),
|
||||
(
|
||||
"Thick stroke, minimal rounding",
|
||||
RectShape::new(
|
||||
sized([20.0, 16.0]),
|
||||
1.0,
|
||||
baby_blue,
|
||||
(3.0, Color32::WHITE),
|
||||
StrokeKind::Inside,
|
||||
),
|
||||
),
|
||||
(
|
||||
"Blurred stroke",
|
||||
RectShape::new(
|
||||
sized([20.0, 16.0]),
|
||||
0.0,
|
||||
baby_blue,
|
||||
(5.0, Color32::WHITE),
|
||||
StrokeKind::Inside,
|
||||
)
|
||||
.with_blur_width(5.0),
|
||||
),
|
||||
];
|
||||
|
||||
for (_name, shape) in &mut shapes {
|
||||
shape.round_to_pixels = Some(true);
|
||||
}
|
||||
|
||||
shapes
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Demo for TessellationTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Tessellation Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::View for TessellationTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
egui::reset_button(ui, self, "Reset");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
rect_shape_ui(ui, &mut self.shape);
|
||||
});
|
||||
});
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.heading("Real size");
|
||||
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
|
||||
let (resp, painter) =
|
||||
ui.allocate_painter(Vec2::splat(128.0), Sense::hover());
|
||||
let canvas = resp.rect;
|
||||
|
||||
let pixels_per_point = ui.pixels_per_point();
|
||||
let pixel_size = 1.0 / pixels_per_point;
|
||||
let mut shape = self.shape.clone();
|
||||
shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size())
|
||||
.round_to_pixel_center(pixels_per_point)
|
||||
.translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding
|
||||
painter.add(shape);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.heading("Zoomed in");
|
||||
let magnification_pixel_size = &mut self.magnification_pixel_size;
|
||||
let tessellation_options = &mut self.tessellation_options;
|
||||
|
||||
egui::Grid::new("TessellationOptions")
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 8.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Magnification");
|
||||
ui.add(
|
||||
egui::DragValue::new(magnification_pixel_size)
|
||||
.speed(0.5)
|
||||
.range(0.0..=64.0),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Feathering width");
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut tessellation_options.feathering, "");
|
||||
ui.add_enabled(
|
||||
tessellation_options.feathering,
|
||||
egui::DragValue::new(
|
||||
&mut tessellation_options.feathering_size_in_pixels,
|
||||
)
|
||||
.speed(0.1)
|
||||
.range(0.0..=4.0)
|
||||
.suffix(" px"),
|
||||
);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Paint edges");
|
||||
ui.checkbox(&mut self.paint_edges, "");
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
let magnification_pixel_size = *magnification_pixel_size;
|
||||
|
||||
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
|
||||
let (resp, painter) = ui.allocate_painter(
|
||||
magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)),
|
||||
Sense::hover(),
|
||||
);
|
||||
let canvas = resp.rect;
|
||||
|
||||
let mut shape = self.shape.clone();
|
||||
shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding
|
||||
|
||||
let mut mesh = epaint::Mesh::default();
|
||||
let mut tessellator = epaint::Tessellator::new(
|
||||
1.0,
|
||||
*tessellation_options,
|
||||
ui.fonts(|f| f.font_image_size()),
|
||||
vec![],
|
||||
);
|
||||
tessellator.tessellate_rect(&shape, &mut mesh);
|
||||
|
||||
// Scale and position the mesh:
|
||||
mesh.transform(
|
||||
TSTransform::from_translation(canvas.center().to_vec2())
|
||||
* TSTransform::from_scaling(magnification_pixel_size),
|
||||
);
|
||||
let mesh = std::sync::Arc::new(mesh);
|
||||
painter.add(epaint::Shape::mesh(mesh.clone()));
|
||||
|
||||
if self.paint_edges {
|
||||
let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA);
|
||||
for triangle in mesh.triangles() {
|
||||
let a = mesh.vertices[triangle[0] as usize];
|
||||
let b = mesh.vertices[triangle[1] as usize];
|
||||
let c = mesh.vertices[triangle[2] as usize];
|
||||
|
||||
painter.line_segment([a.pos, b.pos], stroke);
|
||||
painter.line_segment([b.pos, c.pos], stroke);
|
||||
painter.line_segment([c.pos, a.pos], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw pixel centers:
|
||||
let pixel_radius = 0.75;
|
||||
let pixel_color = Color32::GRAY;
|
||||
for yi in 0.. {
|
||||
let y = (yi as f32 + 0.5) * magnification_pixel_size;
|
||||
if y > canvas.height() / 2.0 {
|
||||
break;
|
||||
}
|
||||
for xi in 0.. {
|
||||
let x = (xi as f32 + 0.5) * magnification_pixel_size;
|
||||
if x > canvas.width() / 2.0 {
|
||||
break;
|
||||
}
|
||||
for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] {
|
||||
painter.circle_filled(
|
||||
canvas.center() + offset,
|
||||
pixel_radius,
|
||||
pixel_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
|
||||
egui::ComboBox::from_id_salt("prefabs")
|
||||
.selected_text("Prefabs")
|
||||
.show_ui(ui, |ui| {
|
||||
for (name, prefab) in TessellationTest::interesting_shapes() {
|
||||
ui.selectable_value(shape, prefab, name);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
let RectShape {
|
||||
rect,
|
||||
rounding,
|
||||
fill,
|
||||
stroke,
|
||||
stroke_kind,
|
||||
blur_width,
|
||||
round_to_pixels,
|
||||
brush: _,
|
||||
} = shape;
|
||||
|
||||
let round_to_pixels = round_to_pixels.get_or_insert(true);
|
||||
|
||||
egui::Grid::new("RectShape")
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 8.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Size");
|
||||
ui.horizontal(|ui| {
|
||||
let mut size = rect.size();
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut size.x)
|
||||
.speed(0.2)
|
||||
.range(0.0..=64.0),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut size.y)
|
||||
.speed(0.2)
|
||||
.range(0.0..=64.0),
|
||||
);
|
||||
*rect = Rect::from_center_size(Pos2::ZERO, size);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Rounding");
|
||||
ui.add(rounding);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Fill");
|
||||
ui.color_edit_button_srgba(fill);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Stroke");
|
||||
ui.add(stroke);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Stroke kind");
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(stroke_kind, StrokeKind::Inside, "Inside");
|
||||
ui.selectable_value(stroke_kind, StrokeKind::Middle, "Middle");
|
||||
ui.selectable_value(stroke_kind, StrokeKind::Outside, "Outside");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Blur width");
|
||||
ui.add(
|
||||
egui::DragValue::new(blur_width)
|
||||
.speed(0.5)
|
||||
.range(0.0..=20.0),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Round to pixels");
|
||||
ui.checkbox(round_to_pixels, "");
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::View as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_tessellation_test() {
|
||||
for (name, shape) in TessellationTest::interesting_shapes() {
|
||||
let mut test = TessellationTest {
|
||||
shape,
|
||||
..Default::default()
|
||||
};
|
||||
let mut harness = egui_kittest::Harness::new_ui(|ui| {
|
||||
test.ui(ui);
|
||||
});
|
||||
|
||||
harness.fit_contents();
|
||||
harness.run();
|
||||
|
||||
harness.snapshot(&format!("tessellation_test/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user