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

[Impeller] Use a squircle-sdf-based algorithm for fast blurs #55604

Merged
merged 10 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions impeller/display_list/aiks_dl_blur_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,56 @@ namespace testing {

using namespace flutter;

// The shapes of these ovals should appear equal. They are demonstrating the
// difference between the fast pass and not.
TEST_P(AiksTest, SolidColorOvalsMaskBlurTinySigma) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);

std::vector<float> sigmas = {0.0, 0.01, 1.0};
std::vector<DlColor> colors = {DlColor::kGreen(), DlColor::kYellow(),
DlColor::kRed()};
for (uint32_t i = 0; i < sigmas.size(); ++i) {
DlPaint paint;
paint.setColor(colors[i]);
paint.setMaskFilter(
DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i]));

builder.Save();
builder.Translate(100 + (i * 100), 100);
SkRRect rrect =
SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 60.0f, 160.0f), 50, 100);
builder.DrawRRect(rrect, paint);
builder.Restore();
}

ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}

TEST_P(AiksTest, SolidColorCircleMaskBlurTinySigma) {
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);

std::vector<float> sigmas = {0.0, 0.01, 1.0};
std::vector<DlColor> colors = {DlColor::kGreen(), DlColor::kYellow(),
DlColor::kRed()};
for (uint32_t i = 0; i < sigmas.size(); ++i) {
DlPaint paint;
paint.setColor(colors[i]);
paint.setMaskFilter(
DlBlurMaskFilter::Make(DlBlurStyle::kNormal, sigmas[i]));

builder.Save();
builder.Translate(100 + (i * 100), 100);
SkRRect rrect =
SkRRect::MakeRectXY(SkRect::MakeXYWH(0, 0, 100.0f, 100.0f), 100, 100);
builder.DrawRRect(rrect, paint);
builder.Restore();
}

ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}

TEST_P(AiksTest, CanRenderMaskBlurHugeSigma) {
DisplayListBuilder builder;

Expand Down
5 changes: 5 additions & 0 deletions impeller/display_list/canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect,
return false;
}

// The current rrect blur math doesn't work on ovals.
if (fabsf(corner_radii.width - corner_radii.height) > kEhCloseEnough) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding this case you could also adjust the logic in IsNearlySimpleRRect to force all corner radii to be equal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that would also eliminate fast tessellation of simple rrects. The only deficiency here is that we can no longer fast_blur them so it's a test specific to this method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agh, yes don't do that then! :)

return false;
}

// For symmetrically mask blurred solid RRects, absorb the mask blur and use
// a faster SDF approximation.

Expand Down
76 changes: 63 additions & 13 deletions impeller/entity/contents/solid_rrect_blur_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,56 @@ Color SolidRRectBlurContents::GetColor() const {
return color_;
}

static Point eccentricity(Point v, double sInverse) {
Point vOverS = v * sInverse * 0.5;
Point vOverS_squared = -(vOverS * vOverS);
return {std::exp(vOverS_squared.x), std::exp(vOverS_squared.y)};
}

static Scalar kTwoOverSqrtPi = 2.0 / std::sqrt(kPi);

// use crate::math::compute_erf7;
static Scalar computeErf7(Scalar x) {
x *= kTwoOverSqrtPi;
float xx = x * x;
x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
return x / sqrt(1.0 + x * x);
}

static Point NegPos(Scalar v) {
return {std::min(v, 0.0f), std::max(v, 0.0f)};
}

static void SetupFragInfo(
RRectBlurPipeline::FragmentShader::FragInfo& frag_info,
Scalar blurSigma,
Point center,
Point rSize,
Scalar radius) {
Scalar sigma = std::max(blurSigma * kSqrt2, 1.f);

frag_info.center = rSize * 0.5f;
frag_info.minEdge = std::min(rSize.x, rSize.y);
double rMax = 0.5 * frag_info.minEdge;
double r0 = std::min(std::hypot(radius, sigma * 1.15), rMax);
frag_info.r1 = std::min(std::hypot(radius, sigma * 2.0), rMax);

frag_info.exponent = 2.0 * frag_info.r1 / r0;

frag_info.sInv = 1.0 / sigma;

// Pull in long end (make less eccentric).
Point eccentricV = eccentricity(rSize, frag_info.sInv);
double delta = 1.25 * sigma * (eccentricV.x - eccentricV.y);
rSize += NegPos(delta);

frag_info.adjust = rSize * 0.5 - frag_info.r1;
frag_info.exponentInv = 1.0 / frag_info.exponent;
frag_info.scale =
0.5 * computeErf7(frag_info.sInv * 0.5 *
(std::max(rSize.x, rSize.y) - 0.5 * radius));
}

std::optional<Rect> SolidRRectBlurContents::GetCoverage(
const Entity& entity) const {
if (!rect_.has_value()) {
Expand All @@ -72,15 +122,15 @@ bool SolidRRectBlurContents::Render(const ContentContext& renderer,
// Clamp the max kernel width/height to 1000 to limit the extent
// of the blur and to kEhCloseEnough to prevent NaN calculations
// trying to evaluate a Guassian distribution with a sigma of 0.
auto blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f);
Scalar blur_sigma = std::clamp(sigma_.sigma, kEhCloseEnough, 250.0f);
// Increase quality by making the radius a bit bigger than the typical
// sigma->radius conversion we use for slower blurs.
auto blur_radius = PadForSigma(blur_sigma);
auto positive_rect = rect_->GetPositive();
auto left = -blur_radius;
auto top = -blur_radius;
auto right = positive_rect.GetWidth() + blur_radius;
auto bottom = positive_rect.GetHeight() + blur_radius;
Scalar blur_radius = PadForSigma(blur_sigma);
Rect positive_rect = rect_->GetPositive();
Scalar left = -blur_radius;
Scalar top = -blur_radius;
Scalar right = positive_rect.GetWidth() + blur_radius;
Scalar bottom = positive_rect.GetHeight() + blur_radius;

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

FS::FragInfo frag_info;
frag_info.color = color;
frag_info.blur_sigma = blur_sigma;
frag_info.rect_size = Point(positive_rect.GetSize());
frag_info.corner_radii = {std::clamp(corner_radii_.width, kEhCloseEnough,
positive_rect.GetWidth() * 0.5f),
std::clamp(corner_radii_.height, kEhCloseEnough,
positive_rect.GetHeight() * 0.5f)};
Scalar radius = std::min(std::clamp(corner_radii_.width, kEhCloseEnough,
positive_rect.GetWidth() * 0.5f),
std::clamp(corner_radii_.height, kEhCloseEnough,
positive_rect.GetHeight() * 0.5f));
SetupFragInfo(frag_info, blur_sigma, positive_rect.GetCenter(),
Point(positive_rect.GetSize()), radius);
auto& host_buffer = renderer.GetTransientsBuffer();
pass.SetCommandLabel("RRect Shadow");
pass.SetPipeline(renderer.GetRRectBlurPipeline(opts));
Expand Down
119 changes: 35 additions & 84 deletions impeller/entity/shaders/rrect_blur.frag
Original file line number Diff line number Diff line change
Expand Up @@ -2,111 +2,62 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The math for this shader was based on the work done in Raph Levien's blog
// post "Blurred rounded rectangles":
// https://web.archive.org/web/20231103044404/https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html

precision highp float;

#include <impeller/gaussian.glsl>
#include <impeller/types.glsl>

uniform FragInfo {
f16vec4 color;
vec2 rect_size;
float blur_sigma;
vec2 corner_radii;
vec2 center;
vec2 adjust;
float minEdge;
float r1;
float exponent;
float sInv;
float exponentInv;
float scale;
}
frag_info;

in vec2 v_position;

out f16vec4 frag_color;

const int kSampleCount = 4;

/// Closed form unidirectional rounded rect blur mask solution using the
/// analytical Gaussian integral (with approximated erf).
vec4 RRectBlurX(float sample_position_x,
vec4 sample_position_y,
vec2 half_size) {
// The vertical edge of the rrect consists of a flat portion and a curved
// portion, the two of which vary in size depending on the size of the
// corner radii, both adding up to half_size.y.
// half_size.y - corner_radii.y is the size of the vertical flat
// portion of the rrect.
// subtracting the absolute value of the Y sample_position will be
// negative (and then clamped to 0) for positions that are located
// vertically in the flat part of the rrect, and will be the relative
// distance from the center of curvature otherwise.
vec4 space_y = min(vec4(0.0), half_size.y - frag_info.corner_radii.y -
abs(sample_position_y));
// space is now in the range [0.0, corner_radii.y]. If the y sample was
// in the flat portion of the rrect, it will be 0.0

// We will now calculate rrect_distance as the distance from the centerline
// of the rrect towards the near side of the rrect.
// half_size.x - frag_info.corner_radii.x is the size of the horizontal
// flat portion of the rrect.
// We add to that the X size (space_x) of the curved corner measured at
// the indicated Y coordinate we calculated as space_y, such that:
// (space_y / corner_radii.y)^2 + (space_x / corner_radii.x)^2 == 1.0
// Since we want the space_x, we rearrange the equation as:
// space_x = corner_radii.x * sqrt(1.0 - (space_y / corner_radii.y)^2)
// We need to prevent negative values inside the sqrt which can occur
// when the Y sample was beyond the vertical size of the rrect and thus
// space_y was larger than corner_radii.y.
// The calling function RRectBlur will never provide a Y sample outside
// of that range, though, so the max(0.0) is mostly a precaution.
vec4 unit_space_y = space_y / frag_info.corner_radii.y;
vec4 unit_space_x = sqrt(max(vec4(0.0), 1.0 - unit_space_y * unit_space_y));
vec4 rrect_distance =
half_size.x - frag_info.corner_radii.x * (1.0 - unit_space_x);
const float kTwoOverSqrtPi = 2.0 / sqrt(3.1415926);

vec4 result;
// Now we integrate the Gaussian over the range of the relative positions
// of the left and right sides of the rrect relative to the sampling
// X coordinate.
vec4 integral = IPVec4FastGaussianIntegral(
float(sample_position_x) + vec4(-rrect_distance[0], rrect_distance[0],
-rrect_distance[1], rrect_distance[1]),
float(frag_info.blur_sigma));
// integral.y contains the evaluation of the indefinite gaussian integral
// function at (X + rrect_distance) and integral.x contains the evaluation
// of it at (X - rrect_distance). Subtracting the two produces the
// integral result over the range from one to the other.
result.xy = integral.yw - integral.xz;
integral = IPVec4FastGaussianIntegral(
float(sample_position_x) + vec4(-rrect_distance[2], rrect_distance[2],
-rrect_distance[3], rrect_distance[3]),
float(frag_info.blur_sigma));
result.zw = integral.yw - integral.xz;

return result;
float maxXY(vec2 v) {
return max(v.x, v.y);
}

float RRectBlur(vec2 sample_position, vec2 half_size) {
// Limit the sampling range to 3 standard deviations in the Y direction from
// the kernel center to incorporate 99.7% of the color contribution.
float half_sampling_range = frag_info.blur_sigma * 3.0;

// We want to cover the range [Y - half_range, Y + half_range], but we
// don't want to sample beyond the edge of the rrect (where the RRectBlurX
// function produces bad information and where the real answer at those
// locations will be 0.0 anyway).
float begin_y = max(-half_sampling_range, sample_position.y - half_size.y);
float end_y = min(half_sampling_range, sample_position.y + half_size.y);
float interval = (end_y - begin_y) / kSampleCount;
// use crate::math::compute_erf7;
float computeErf7(float x) {
x *= kTwoOverSqrtPi;
float xx = x * x;
x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
return x / sqrt(1.0 + x * x);
}

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

void main() {
frag_color = frag_info.color;
vec2 adjusted = abs(v_position - frag_info.center) - frag_info.adjust;

vec2 half_size = frag_info.rect_size * 0.5;
vec2 sample_position = v_position - half_size;
float dPos = powerDistance(max(adjusted, 0.0));
float dNeg = min(maxXY(adjusted), 0.0);
float d = dPos + dNeg - frag_info.r1;
float z =
frag_info.scale * (computeErf7(frag_info.sInv * (frag_info.minEdge + d)) -
computeErf7(frag_info.sInv * d));

frag_color *= float16_t(RRectBlur(sample_position, half_size));
frag_color = frag_info.color * float16_t(z);
}
5 changes: 5 additions & 0 deletions impeller/geometry/point.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ constexpr TPoint<T> operator/(const TSize<U>& s, const TPoint<T>& p) {
return {static_cast<T>(s.width) / p.x, static_cast<T>(s.height) / p.y};
}

template <class T>
constexpr TPoint<T> operator-(const TPoint<T>& p, T v) {
return {p.x - v, p.y - v};
}

using Point = TPoint<Scalar>;
using IPoint = TPoint<int64_t>;
using IPoint32 = TPoint<int32_t>;
Expand Down
Loading