Skip to content

Commit

Permalink
Add RectShape::blur_width to implement shadows (emilk#4267)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
emilk authored and hacknus committed Oct 30, 2024
1 parent 477370e commit 4a9ffcb
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 51 deletions.
5 changes: 2 additions & 3 deletions crates/egui/src/containers/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/widgets/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
10 changes: 2 additions & 8 deletions crates/egui/src/widgets/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
42 changes: 4 additions & 38 deletions crates/epaint/src/shadow.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use emath::NumExt as _;

use super::*;

/// The color and fuzziness of a fuzzy shape.
Expand Down Expand Up @@ -37,11 +35,10 @@ impl Shadow {
color: Color32::TRANSPARENT,
};

pub fn tessellate(&self, rect: Rect, rounding: impl Into<Rounding>) -> Mesh {
/// The argument is the rectangle of the shadow caster.
pub fn as_shape(&self, rect: Rect, rounding: impl Into<Rounding>) -> RectShape {
// tessellator.clip_rect = clip_rect; // TODO(emilk): culling

use crate::tessellator::*;

let Self {
offset,
blur,
Expand All @@ -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?
Expand Down
26 changes: 25 additions & 1 deletion crates/epaint/src/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -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,
}
Expand All @@ -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,
}
Expand All @@ -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)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/epaint/src/shape_transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: _,
})
Expand Down
28 changes: 27 additions & 1 deletion crates/epaint/src/tessellator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()];
Expand Down Expand Up @@ -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`].
Expand Down

0 comments on commit 4a9ffcb

Please sign in to comment.