Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit bc5f9fc

Browse files
authored
[Impeller] Use a squircle-sdf-based algorithm for fast blurs (#55604)
Use a Squircle Signed Distance Field based algorithm to do a very fast approximation of rrect blurs. This PR is to provide reference within the team to discuss the future of the algorithm compared to the Gaussian approximation functions that are currently in use. It isn't a complete solution, but can be completed easily with a little more work. Notably, it doesn't handle elliptical round rects, only circular corners. Could stand to include an attribution to the source (https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html)
1 parent 3c9069c commit bc5f9fc

File tree

7 files changed

+198
-131
lines changed

7 files changed

+198
-131
lines changed

impeller/display_list/aiks_dl_blur_unittests.cc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,56 @@ namespace testing {
3434

3535
using namespace flutter;
3636

37+
// The shapes of these ovals should appear equal. They are demonstrating the
38+
// difference between the fast pass and not.
39+
TEST_P(AiksTest, SolidColorOvalsMaskBlurTinySigma) {
40+
DisplayListBuilder builder;
41+
builder.Scale(GetContentScale().x, GetContentScale().y);
42+
43+
std::vector<float> sigmas = {0.0, 0.01, 1.0};
44+
std::vector<DlColor> colors = {DlColor::kGreen(), DlColor::kYellow(),
45+
DlColor::kRed()};
46+
for (uint32_t i = 0; i < sigmas.size(); ++i) {
47+
DlPaint paint;
48+
paint.setColor(colors[i]);
49+
paint.setMaskFilter(
50+
DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i]));
51+
52+
builder.Save();
53+
builder.Translate(100 + (i * 100), 100);
54+
SkRRect rrect =
55+
SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 60.0f, 160.0f), 50, 100);
56+
builder.DrawRRect(rrect, paint);
57+
builder.Restore();
58+
}
59+
60+
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
61+
}
62+
63+
TEST_P(AiksTest, SolidColorCircleMaskBlurTinySigma) {
64+
DisplayListBuilder builder;
65+
builder.Scale(GetContentScale().x, GetContentScale().y);
66+
67+
std::vector<float> sigmas = {0.0, 0.01, 1.0};
68+
std::vector<DlColor> colors = {DlColor::kGreen(), DlColor::kYellow(),
69+
DlColor::kRed()};
70+
for (uint32_t i = 0; i < sigmas.size(); ++i) {
71+
DlPaint paint;
72+
paint.setColor(colors[i]);
73+
paint.setMaskFilter(
74+
DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i]));
75+
76+
builder.Save();
77+
builder.Translate(100 + (i * 100), 100);
78+
SkRRect rrect =
79+
SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 100.0f, 100.0f), 100, 100);
80+
builder.DrawRRect(rrect, paint);
81+
builder.Restore();
82+
}
83+
84+
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
85+
}
86+
3787
TEST_P(AiksTest, CanRenderMaskBlurHugeSigma) {
3888
DisplayListBuilder builder;
3989

impeller/display_list/canvas.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,11 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect,
464464
return false;
465465
}
466466

467+
// The current rrect blur math doesn't work on ovals.
468+
if (fabsf(corner_radii.width - corner_radii.height) > kEhCloseEnough) {
469+
return false;
470+
}
471+
467472
// For symmetrically mask blurred solid RRects, absorb the mask blur and use
468473
// a faster SDF approximation.
469474

impeller/entity/contents/solid_rrect_blur_contents.cc

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,56 @@ Color SolidRRectBlurContents::GetColor() const {
4848
return color_;
4949
}
5050

51+
static Point eccentricity(Point v, double sInverse) {
52+
Point vOverS = v * sInverse * 0.5;
53+
Point vOverS_squared = -(vOverS * vOverS);
54+
return {std::exp(vOverS_squared.x), std::exp(vOverS_squared.y)};
55+
}
56+
57+
static Scalar kTwoOverSqrtPi = 2.0 / std::sqrt(kPi);
58+
59+
// use crate::math::compute_erf7;
60+
static Scalar computeErf7(Scalar x) {
61+
x *= kTwoOverSqrtPi;
62+
float xx = x * x;
63+
x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
64+
return x / sqrt(1.0 + x * x);
65+
}
66+
67+
static Point NegPos(Scalar v) {
68+
return {std::min(v, 0.0f), std::max(v, 0.0f)};
69+
}
70+
71+
static void SetupFragInfo(
72+
RRectBlurPipeline::FragmentShader::FragInfo& frag_info,
73+
Scalar blurSigma,
74+
Point center,
75+
Point rSize,
76+
Scalar radius) {
77+
Scalar sigma = std::max(blurSigma * kSqrt2, 1.f);
78+
79+
frag_info.center = rSize * 0.5f;
80+
frag_info.minEdge = std::min(rSize.x, rSize.y);
81+
double rMax = 0.5 * frag_info.minEdge;
82+
double r0 = std::min(std::hypot(radius, sigma * 1.15), rMax);
83+
frag_info.r1 = std::min(std::hypot(radius, sigma * 2.0), rMax);
84+
85+
frag_info.exponent = 2.0 * frag_info.r1 / r0;
86+
87+
frag_info.sInv = 1.0 / sigma;
88+
89+
// Pull in long end (make less eccentric).
90+
Point eccentricV = eccentricity(rSize, frag_info.sInv);
91+
double delta = 1.25 * sigma * (eccentricV.x - eccentricV.y);
92+
rSize += NegPos(delta);
93+
94+
frag_info.adjust = rSize * 0.5 - frag_info.r1;
95+
frag_info.exponentInv = 1.0 / frag_info.exponent;
96+
frag_info.scale =
97+
0.5 * computeErf7(frag_info.sInv * 0.5 *
98+
(std::max(rSize.x, rSize.y) - 0.5 * radius));
99+
}
100+
51101
std::optional<Rect> SolidRRectBlurContents::GetCoverage(
52102
const Entity& entity) const {
53103
if (!rect_.has_value()) {
@@ -72,15 +122,15 @@ bool SolidRRectBlurContents::Render(const ContentContext& renderer,
72122
// Clamp the max kernel width/height to 1000 to limit the extent
73123
// of the blur and to kEhCloseEnough to prevent NaN calculations
74124
// trying to evaluate a Guassian distribution with a sigma of 0.
75-
auto blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f);
125+
Scalar blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f);
76126
// Increase quality by making the radius a bit bigger than the typical
77127
// sigma->radius conversion we use for slower blurs.
78-
auto blur_radius = PadForSigma(blur_sigma);
79-
auto positive_rect = rect_->GetPositive();
80-
auto left = -blur_radius;
81-
auto top = -blur_radius;
82-
auto right = positive_rect.GetWidth() + blur_radius;
83-
auto bottom = positive_rect.GetHeight() + blur_radius;
128+
Scalar blur_radius = PadForSigma(blur_sigma);
129+
Rect positive_rect = rect_->GetPositive();
130+
Scalar left = -blur_radius;
131+
Scalar top = -blur_radius;
132+
Scalar right = positive_rect.GetWidth() + blur_radius;
133+
Scalar bottom = positive_rect.GetHeight() + blur_radius;
84134

85135
std::array<VS::PerVertexData, 4> vertices = {
86136
VS::PerVertexData{Point(left, top)},
@@ -105,12 +155,12 @@ bool SolidRRectBlurContents::Render(const ContentContext& renderer,
105155

106156
FS::FragInfo frag_info;
107157
frag_info.color = color;
108-
frag_info.blur_sigma = blur_sigma;
109-
frag_info.rect_size = Point(positive_rect.GetSize());
110-
frag_info.corner_radii = {std::clamp(corner_radii_.width, kEhCloseEnough,
111-
positive_rect.GetWidth() * 0.5f),
112-
std::clamp(corner_radii_.height, kEhCloseEnough,
113-
positive_rect.GetHeight() * 0.5f)};
158+
Scalar radius = std::min(std::clamp(corner_radii_.width, kEhCloseEnough,
159+
positive_rect.GetWidth() * 0.5f),
160+
std::clamp(corner_radii_.height, kEhCloseEnough,
161+
positive_rect.GetHeight() * 0.5f));
162+
SetupFragInfo(frag_info, blur_sigma, positive_rect.GetCenter(),
163+
Point(positive_rect.GetSize()), radius);
114164
auto& host_buffer = renderer.GetTransientsBuffer();
115165
pass.SetCommandLabel("RRect Shadow");
116166
pass.SetPipeline(renderer.GetRRectBlurPipeline(opts));

impeller/entity/shaders/rrect_blur.frag

Lines changed: 35 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,111 +2,62 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
// The math for this shader was based on the work done in Raph Levien's blog
6+
// post "Blurred rounded rectangles":
7+
// https://web.archive.org/web/20231103044404/https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html
8+
59
precision highp float;
610

711
#include <impeller/gaussian.glsl>
812
#include <impeller/types.glsl>
913

1014
uniform FragInfo {
1115
f16vec4 color;
12-
vec2 rect_size;
13-
float blur_sigma;
14-
vec2 corner_radii;
16+
vec2 center;
17+
vec2 adjust;
18+
float minEdge;
19+
float r1;
20+
float exponent;
21+
float sInv;
22+
float exponentInv;
23+
float scale;
1524
}
1625
frag_info;
1726

1827
in vec2 v_position;
1928

2029
out f16vec4 frag_color;
2130

22-
const int kSampleCount = 4;
23-
24-
/// Closed form unidirectional rounded rect blur mask solution using the
25-
/// analytical Gaussian integral (with approximated erf).
26-
vec4 RRectBlurX(float sample_position_x,
27-
vec4 sample_position_y,
28-
vec2 half_size) {
29-
// The vertical edge of the rrect consists of a flat portion and a curved
30-
// portion, the two of which vary in size depending on the size of the
31-
// corner radii, both adding up to half_size.y.
32-
// half_size.y - corner_radii.y is the size of the vertical flat
33-
// portion of the rrect.
34-
// subtracting the absolute value of the Y sample_position will be
35-
// negative (and then clamped to 0) for positions that are located
36-
// vertically in the flat part of the rrect, and will be the relative
37-
// distance from the center of curvature otherwise.
38-
vec4 space_y = min(vec4(0.0), half_size.y - frag_info.corner_radii.y -
39-
abs(sample_position_y));
40-
// space is now in the range [0.0, corner_radii.y]. If the y sample was
41-
// in the flat portion of the rrect, it will be 0.0
42-
43-
// We will now calculate rrect_distance as the distance from the centerline
44-
// of the rrect towards the near side of the rrect.
45-
// half_size.x - frag_info.corner_radii.x is the size of the horizontal
46-
// flat portion of the rrect.
47-
// We add to that the X size (space_x) of the curved corner measured at
48-
// the indicated Y coordinate we calculated as space_y, such that:
49-
// (space_y / corner_radii.y)^2 + (space_x / corner_radii.x)^2 == 1.0
50-
// Since we want the space_x, we rearrange the equation as:
51-
// space_x = corner_radii.x * sqrt(1.0 - (space_y / corner_radii.y)^2)
52-
// We need to prevent negative values inside the sqrt which can occur
53-
// when the Y sample was beyond the vertical size of the rrect and thus
54-
// space_y was larger than corner_radii.y.
55-
// The calling function RRectBlur will never provide a Y sample outside
56-
// of that range, though, so the max(0.0) is mostly a precaution.
57-
vec4 unit_space_y = space_y / frag_info.corner_radii.y;
58-
vec4 unit_space_x = sqrt(max(vec4(0.0), 1.0 - unit_space_y * unit_space_y));
59-
vec4 rrect_distance =
60-
half_size.x - frag_info.corner_radii.x * (1.0 - unit_space_x);
31+
const float kTwoOverSqrtPi = 2.0 / sqrt(3.1415926);
6132

62-
vec4 result;
63-
// Now we integrate the Gaussian over the range of the relative positions
64-
// of the left and right sides of the rrect relative to the sampling
65-
// X coordinate.
66-
vec4 integral = IPVec4FastGaussianIntegral(
67-
float(sample_position_x) + vec4(-rrect_distance[0], rrect_distance[0],
68-
-rrect_distance[1], rrect_distance[1]),
69-
float(frag_info.blur_sigma));
70-
// integral.y contains the evaluation of the indefinite gaussian integral
71-
// function at (X + rrect_distance) and integral.x contains the evaluation
72-
// of it at (X - rrect_distance). Subtracting the two produces the
73-
// integral result over the range from one to the other.
74-
result.xy = integral.yw - integral.xz;
75-
integral = IPVec4FastGaussianIntegral(
76-
float(sample_position_x) + vec4(-rrect_distance[2], rrect_distance[2],
77-
-rrect_distance[3], rrect_distance[3]),
78-
float(frag_info.blur_sigma));
79-
result.zw = integral.yw - integral.xz;
80-
81-
return result;
33+
float maxXY(vec2 v) {
34+
return max(v.x, v.y);
8235
}
8336

84-
float RRectBlur(vec2 sample_position, vec2 half_size) {
85-
// Limit the sampling range to 3 standard deviations in the Y direction from
86-
// the kernel center to incorporate 99.7% of the color contribution.
87-
float half_sampling_range = frag_info.blur_sigma * 3.0;
88-
89-
// We want to cover the range [Y - half_range, Y + half_range], but we
90-
// don't want to sample beyond the edge of the rrect (where the RRectBlurX
91-
// function produces bad information and where the real answer at those
92-
// locations will be 0.0 anyway).
93-
float begin_y = max(-half_sampling_range, sample_position.y - half_size.y);
94-
float end_y = min(half_sampling_range, sample_position.y + half_size.y);
95-
float interval = (end_y - begin_y) / kSampleCount;
37+
// use crate::math::compute_erf7;
38+
float computeErf7(float x) {
39+
x *= kTwoOverSqrtPi;
40+
float xx = x * x;
41+
x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
42+
return x / sqrt(1.0 + x * x);
43+
}
9644

97-
// Sample the X blur kSampleCount times, weighted by the Gaussian function.
98-
vec4 ys = vec4(0.5, 1.5, 2.5, 3.5) * interval + begin_y;
99-
vec4 sample_ys = sample_position.y - ys;
100-
vec4 blurx = RRectBlurX(sample_position.x, sample_ys, half_size);
101-
vec4 gaussian_y = IPGaussian(ys, float(frag_info.blur_sigma));
102-
return dot(blurx, gaussian_y * interval);
45+
// The length formula, but with an exponent other than 2
46+
float powerDistance(vec2 p) {
47+
float xp = pow(p.x, frag_info.exponent);
48+
float yp = pow(p.y, frag_info.exponent);
49+
return pow(xp + yp, frag_info.exponentInv);
10350
}
10451

10552
void main() {
106-
frag_color = frag_info.color;
53+
vec2 adjusted = abs(v_position - frag_info.center) - frag_info.adjust;
10754

108-
vec2 half_size = frag_info.rect_size * 0.5;
109-
vec2 sample_position = v_position - half_size;
55+
float dPos = powerDistance(max(adjusted, 0.0));
56+
float dNeg = min(maxXY(adjusted), 0.0);
57+
float d = dPos + dNeg - frag_info.r1;
58+
float z =
59+
frag_info.scale * (computeErf7(frag_info.sInv * (frag_info.minEdge + d)) -
60+
computeErf7(frag_info.sInv * d));
11061

111-
frag_color *= float16_t(RRectBlur(sample_position, half_size));
62+
frag_color = frag_info.color * float16_t(z);
11263
}

impeller/geometry/point.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ constexpr TPoint<T> operator/(const TSize<U>& s, const TPoint<T>& p) {
319319
return {static_cast<T>(s.width) / p.x, static_cast<T>(s.height) / p.y};
320320
}
321321

322+
template <class T>
323+
constexpr TPoint<T> operator-(const TPoint<T>& p, T v) {
324+
return {p.x - v, p.y - v};
325+
}
326+
322327
using Point = TPoint<Scalar>;
323328
using IPoint = TPoint<int64_t>;
324329
using IPoint32 = TPoint<int32_t>;

0 commit comments

Comments
 (0)