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.
This commit is contained in:
Emil Ernerfeldt 2024-03-29 20:29:42 +01:00 committed by GitHub
parent 73dbfd689b
commit a541e021aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 62 additions and 51 deletions

View File

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

View File

@ -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,
});

View File

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

View File

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

View File

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

View File

@ -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: _,
})

View File

@ -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`].