From a541e021aa6f972d1eb6006505e02ba1c2923a3d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 29 Mar 2024 20:29:42 +0100 Subject: [PATCH] Add `RectShape::blur_width` to implement shadows (#4267) This is mostly a refactor, but has some performance benefits: * We (re)use the same tessellator as for everything else, leading to less allocations * We cull shapes before rendering them Adding `RectShape::blur_width` means it can also be used for other effects, such as glow. --- crates/egui/src/containers/frame.rs | 5 ++-- crates/egui/src/widgets/image.rs | 1 + crates/egui/src/widgets/slider.rs | 10 ++----- crates/epaint/src/shadow.rs | 42 +++------------------------- crates/epaint/src/shape.rs | 26 ++++++++++++++++- crates/epaint/src/shape_transform.rs | 1 + crates/epaint/src/tessellator.rs | 28 ++++++++++++++++++- 7 files changed, 62 insertions(+), 51 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 2847fdfd..6d7ba6e6 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -291,9 +291,8 @@ impl Frame { if shadow == Default::default() { frame_shape } else { - let shadow = shadow.tessellate(outer_rect, rounding); - let shadow = Shape::Mesh(shadow); - Shape::Vec(vec![shadow, frame_shape]) + let shadow = shadow.as_shape(outer_rect, rounding); + Shape::Vec(vec![Shape::from(shadow), frame_shape]) } } } diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 90176018..945c497d 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -762,6 +762,7 @@ pub fn paint_texture_at( rounding: options.rounding, fill: options.tint, stroke: Stroke::NONE, + blur_width: 0.0, fill_texture_id: texture.id, uv: options.uv, }); diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index c5c60b92..a428fb7d 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -739,14 +739,8 @@ impl<'a> Slider<'a> { }; let v = v + Vec2::splat(visuals.expansion); let rect = Rect::from_center_size(center, 2.0 * v); - ui.painter().add(epaint::RectShape { - fill: visuals.bg_fill, - stroke: visuals.fg_stroke, - rect, - rounding: visuals.rounding, - fill_texture_id: Default::default(), - uv: Rect::ZERO, - }); + ui.painter() + .rect(rect, visuals.rounding, visuals.bg_fill, visuals.fg_stroke); } } } diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index 8b73c07e..fc7de267 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,5 +1,3 @@ -use emath::NumExt as _; - use super::*; /// The color and fuzziness of a fuzzy shape. @@ -37,11 +35,10 @@ impl Shadow { color: Color32::TRANSPARENT, }; - pub fn tessellate(&self, rect: Rect, rounding: impl Into) -> Mesh { + /// The argument is the rectangle of the shadow caster. + pub fn as_shape(&self, rect: Rect, rounding: impl Into) -> RectShape { // tessellator.clip_rect = clip_rect; // TODO(emilk): culling - use crate::tessellator::*; - let Self { offset, blur, @@ -50,40 +47,9 @@ impl Shadow { } = *self; let rect = rect.translate(offset).expand(spread); + let rounding = rounding.into() + Rounding::same(spread.abs()); - // We simulate a blurry shadow by tessellating a solid rectangle using a very large feathering. - // Feathering is usually used to make the edges of a shape softer for anti-aliasing. - // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. - // Thats because the tessellator approximate very thin rectangles as line segments, - // and these line segments don't have rounded corners. - // When the feathering is small (the size of a pixel), this is usually fine, - // but here we have a huge feathering to simulate blur, - // so we need to avoid this optimization in the tessellator, - // which is also why we add this rather big epsilon: - let eps = 0.1; - let blur = blur.at_most(rect.size().min_elem() - eps).at_least(0.0); - - // TODO(emilk): if blur <= 0, return a simple `Shape::Rect` instead of using the tessellator - - let rounding_expansion = spread.abs() + 0.5 * blur; - let rounding = rounding.into() + Rounding::same(rounding_expansion); - - let rect = RectShape::filled(rect, rounding, color); - let pixels_per_point = 1.0; // doesn't matter here - let font_tex_size = [1; 2]; // unused since we are not tessellating text. - let mut tessellator = Tessellator::new( - pixels_per_point, - TessellationOptions { - feathering: true, - feathering_size_in_pixels: blur * pixels_per_point, - ..Default::default() - }, - font_tex_size, - vec![], - ); - let mut mesh = Mesh::default(); - tessellator.tessellate_rect(&rect, &mut mesh); - mesh + RectShape::filled(rect, rounding, color).with_blur_width(blur) } /// How much larger than the parent rect are we in each direction? diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 3c6f1703..7922a92d 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -668,6 +668,14 @@ pub struct RectShape { /// The thickness and color of the outline. pub stroke: Stroke, + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. + pub blur_width: f32, + /// If the rect should be filled with a texture, which one? /// /// The texture is multiplied with [`Self::fill`]. @@ -695,6 +703,7 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: stroke.into(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } @@ -711,6 +720,7 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: Default::default(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } @@ -723,18 +733,32 @@ impl RectShape { rounding: rounding.into(), fill: Default::default(), stroke: stroke.into(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } } + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space. + #[inline] + pub fn with_blur_width(mut self, blur_width: f32) -> Self { + self.blur_width = blur_width; + self + } + /// The visual bounding rectangle (includes stroke width) #[inline] pub fn visual_bounding_rect(&self) -> Rect { if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { Rect::NOTHING } else { - self.rect.expand(self.stroke.width / 2.0) + self.rect + .expand((self.stroke.width + self.blur_width) / 2.0) } } } diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 7f4d3358..8ff65d2a 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -37,6 +37,7 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { rounding: _, fill, stroke, + blur_width: _, fill_texture_id: _, uv: _, }) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index f626b9f3..f5b3d9e3 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1503,9 +1503,10 @@ impl Tessellator { pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) { let RectShape { mut rect, - rounding, + mut rounding, fill, stroke, + mut blur_width, fill_texture_id, uv, } = *rect; @@ -1524,6 +1525,29 @@ impl Tessellator { rect.min = rect.min.at_least(pos2(-1e7, -1e7)); rect.max = rect.max.at_most(pos2(1e7, 1e7)); + let old_feathering = self.feathering; + + if old_feathering < blur_width { + // We accomplish the blur by using a larger-than-normal feathering. + // Feathering is usually used to make the edges of a shape softer for anti-aliasing. + + // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. + // Thats because the tessellator approximate very thin rectangles as line segments, + // and these line segments don't have rounded corners. + // When the feathering is small (the size of a pixel), this is usually fine, + // but here we have a huge feathering to simulate blur, + // so we need to avoid this optimization in the tessellator, + // which is also why we add this rather big epsilon: + let eps = 0.1; + blur_width = blur_width + .at_most(rect.size().min_elem() - eps) + .at_least(0.0); + + rounding += Rounding::same(0.5 * blur_width); + + self.feathering = self.feathering.max(blur_width); + } + if rect.width() < self.feathering { // Very thin - approximate by a vertical line-segment: let line = [rect.center_top(), rect.center_bottom()]; @@ -1566,6 +1590,8 @@ impl Tessellator { path.stroke_closed(self.feathering, stroke, out); } + + self.feathering = old_feathering; // restore } /// Tessellate a single [`TextShape`] into a [`Mesh`].