[Impeller] Use a squircle-sdf-based algorithm for fast blurs (flutter/engine#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)
This commit is contained in:
parent
8973b6e97a
commit
7f99da5b92
@ -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;
|
||||
|
||||
|
@ -464,6 +464,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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For symmetrically mask blurred solid RRects, absorb the mask blur and use
|
||||
// a faster SDF approximation.
|
||||
|
||||
|
@ -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()) {
|
||||
@ -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)},
|
||||
@ -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));
|
||||
|
@ -2,6 +2,10 @@
|
||||
// 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>
|
||||
@ -9,9 +13,14 @@ precision highp float;
|
||||
|
||||
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;
|
||||
|
||||
@ -19,94 +28,36 @@ in vec2 v_position;
|
||||
|
||||
out f16vec4 frag_color;
|
||||
|
||||
const int kSampleCount = 4;
|
||||
const float kTwoOverSqrtPi = 2.0 / sqrt(3.1415926);
|
||||
|
||||
/// 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);
|
||||
|
||||
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;
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -3858,10 +3858,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"longest_path_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.453125,
|
||||
1.5,
|
||||
0.625,
|
||||
0.625,
|
||||
0.21875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -3880,10 +3880,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"shortest_path_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.421875,
|
||||
1.5,
|
||||
0.625,
|
||||
0.625,
|
||||
0.1875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -3893,10 +3893,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"total_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.453125,
|
||||
1.5,
|
||||
0.625,
|
||||
0.625,
|
||||
0.21875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -3904,8 +3904,8 @@
|
||||
},
|
||||
"stack_spill_bytes": 0,
|
||||
"thread_occupancy": 100,
|
||||
"uniform_registers_used": 22,
|
||||
"work_registers_used": 32
|
||||
"uniform_registers_used": 18,
|
||||
"work_registers_used": 23
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3922,7 +3922,7 @@
|
||||
"arithmetic"
|
||||
],
|
||||
"longest_path_cycles": [
|
||||
12.869999885559082,
|
||||
6.599999904632568,
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
@ -3935,7 +3935,7 @@
|
||||
"arithmetic"
|
||||
],
|
||||
"shortest_path_cycles": [
|
||||
12.869999885559082,
|
||||
6.599999904632568,
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
@ -3943,14 +3943,14 @@
|
||||
"arithmetic"
|
||||
],
|
||||
"total_cycles": [
|
||||
13.333333015441895,
|
||||
7.0,
|
||||
1.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"thread_occupancy": 50,
|
||||
"uniform_registers_used": 3,
|
||||
"work_registers_used": 7
|
||||
"thread_occupancy": 100,
|
||||
"uniform_registers_used": 4,
|
||||
"work_registers_used": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6816,10 +6816,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"longest_path_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.421875,
|
||||
1.5,
|
||||
0.6875,
|
||||
0.6875,
|
||||
0.1875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -6838,10 +6838,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"shortest_path_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.421875,
|
||||
1.5,
|
||||
0.6875,
|
||||
0.6875,
|
||||
0.1875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -6851,10 +6851,10 @@
|
||||
"arith_fma"
|
||||
],
|
||||
"total_cycles": [
|
||||
1.59375,
|
||||
1.59375,
|
||||
0.421875,
|
||||
1.5,
|
||||
0.6875,
|
||||
0.6875,
|
||||
0.1875,
|
||||
0.5,
|
||||
0.0,
|
||||
0.25,
|
||||
0.0
|
||||
@ -6862,8 +6862,8 @@
|
||||
},
|
||||
"stack_spill_bytes": 0,
|
||||
"thread_occupancy": 100,
|
||||
"uniform_registers_used": 22,
|
||||
"work_registers_used": 32
|
||||
"uniform_registers_used": 18,
|
||||
"work_registers_used": 13
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -813,9 +813,15 @@ impeller_Play_AiksTest_SetContentsWithRegion_Vulkan.png
|
||||
impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_Metal.png
|
||||
impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_OpenGLES.png
|
||||
impeller_Play_AiksTest_SiblingSaveLayerBoundsAreRespected_Vulkan.png
|
||||
impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_Metal.png
|
||||
impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_OpenGLES.png
|
||||
impeller_Play_AiksTest_SolidColorCircleMaskBlurTinySigma_Vulkan.png
|
||||
impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_Metal.png
|
||||
impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_OpenGLES.png
|
||||
impeller_Play_AiksTest_SolidColorCirclesOvalsRRectsMaskBlurCorrectly_Vulkan.png
|
||||
impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_Metal.png
|
||||
impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_OpenGLES.png
|
||||
impeller_Play_AiksTest_SolidColorOvalsMaskBlurTinySigma_Vulkan.png
|
||||
impeller_Play_AiksTest_SolidStrokesRenderCorrectly_Metal.png
|
||||
impeller_Play_AiksTest_SolidStrokesRenderCorrectly_OpenGLES.png
|
||||
impeller_Play_AiksTest_SolidStrokesRenderCorrectly_Vulkan.png
|
||||
|
Loading…
x
Reference in New Issue
Block a user