[Impeller] implements gaussian "blur halo" (#48149)
This makes the blurred item have a halo effect that is rendered outside of the bounds of the original thing that was rendered.
issue: https://github.com/flutter/flutter/issues/131580
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
diff --git a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
index 1500f77..af05805 100644
--- a/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
+++ b/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc
@@ -37,12 +37,19 @@
cmd.BindVertices(vtx_buffer);
}
+Matrix MakeAnchorScale(const Point& anchor, Vector2 scale) {
+ return Matrix::MakeTranslation({anchor.x, anchor.y, 0}) *
+ Matrix::MakeScale(scale) *
+ Matrix::MakeTranslation({-anchor.x, -anchor.y, 0});
+}
+
std::shared_ptr<Texture> MakeDownsampleSubpass(
const ContentContext& renderer,
std::shared_ptr<Texture> input_texture,
const SamplerDescriptor& sampler_descriptor,
const Quad& uvs,
- const ISize& subpass_size) {
+ const ISize& subpass_size,
+ const Vector2 padding) {
ContentContext::SubpassCallback subpass_callback =
[&](const ContentContext& renderer, RenderPass& pass) {
HostBuffer& host_buffer = pass.GetTransientsBuffer();
@@ -58,20 +65,32 @@
frame_info.texture_sampler_y_coord_scale = 1.0;
frame_info.alpha = 1.0;
+ // Insert transparent gutter around the downsampled image so the blur
+ // creates a halo effect.
+ Vector2 texture_size = Vector2(input_texture->GetSize());
+ Quad vertices =
+ MakeAnchorScale({0.5, 0.5},
+ texture_size / (texture_size + padding * 2))
+ .Transform(
+ {Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1)});
+
BindVertices<TextureFillVertexShader>(cmd, host_buffer,
{
- {Point(0, 0), uvs[0]},
- {Point(1, 0), uvs[1]},
- {Point(0, 1), uvs[2]},
- {Point(1, 1), uvs[3]},
+ {vertices[0], uvs[0]},
+ {vertices[1], uvs[1]},
+ {vertices[2], uvs[2]},
+ {vertices[3], uvs[3]},
});
+ SamplerDescriptor linear_sampler_descriptor = sampler_descriptor;
+ linear_sampler_descriptor.mag_filter = MinMagFilter::kLinear;
+ linear_sampler_descriptor.min_filter = MinMagFilter::kLinear;
TextureFillVertexShader::BindFrameInfo(
cmd, host_buffer.EmplaceUniform(frame_info));
TextureFillFragmentShader::BindTextureSampler(
cmd, input_texture,
renderer.GetContext()->GetSamplerLibrary()->GetSampler(
- sampler_descriptor));
+ linear_sampler_descriptor));
pass.AddCommand(std::move(cmd));
@@ -125,14 +144,6 @@
return out_texture;
}
-/// Given a desired |scalar|, will return the scalar that gets close but leaves
-/// |size| in integer sizes.
-Vector2 CalculateIntegerScale(Scalar scalar, ISize size) {
- ISize new_size(size.width / scalar, size.height / scalar);
- return Vector2(size.width / static_cast<Scalar>(new_size.width),
- size.height / static_cast<Scalar>(new_size.height));
-}
-
/// Calculate how much to scale down the texture depending on the blur radius.
/// This curve was taken from |DirectionalGaussianBlurFilterContents|.
Scalar CalculateScale(Scalar radius) {
@@ -200,36 +211,33 @@
}
Scalar blur_radius = CalculateBlurRadius(sigma_);
- Scalar desired_scale = 1.0 / CalculateScale(blur_radius);
- Vector2 downsample =
- CalculateIntegerScale(desired_scale, input_snapshot->texture->GetSize());
+ Scalar desired_scalar = CalculateScale(blur_radius);
+ Vector2 downsample_scalar(desired_scalar, desired_scalar);
+ Vector2 padding(ceil(blur_radius), ceil(blur_radius));
- // TODO(gaaclarke): This isn't taking into account the blur radius to expand
- // the rendered size, so blurred objects are clipped. In
- // order for that to be implemented correctly we'll need to
- // start adjusting the geometry coordinates in the downsample
- // step so that there is a border of transparency around it
- // before the blur steps.
+ Vector2 padded_size =
+ Vector2(input_snapshot->texture->GetSize()) + 2.0 * padding;
+ Vector2 downsampled_size = padded_size * downsample_scalar;
+ // TODO(gaaclarke): I don't think we are correctly handling this fractional
+ // amount we are throwing away.
ISize subpass_size =
- ISize(input_snapshot->texture->GetSize().width / downsample.x,
- input_snapshot->texture->GetSize().height / downsample.y);
+ ISize(round(downsampled_size.x), round(downsampled_size.y));
Quad uvs =
CalculateUVs(inputs[0], entity, input_snapshot->texture->GetSize());
std::shared_ptr<Texture> pass1_out_texture = MakeDownsampleSubpass(
renderer, input_snapshot->texture, input_snapshot->sampler_descriptor,
- uvs, subpass_size);
+ uvs, subpass_size, padding);
- Size pass1_pixel_size(1.0 / pass1_out_texture->GetSize().width,
- 1.0 / pass1_out_texture->GetSize().height);
+ Vector2 pass1_pixel_size = 1.0 / Vector2(pass1_out_texture->GetSize());
std::shared_ptr<Texture> pass2_out_texture = MakeBlurSubpass(
renderer, pass1_out_texture, input_snapshot->sampler_descriptor,
GaussianBlurFragmentShader::BlurInfo{
- .blur_uv_offset = Point(0.0, pass1_pixel_size.height),
- .blur_sigma = sigma_ / downsample.y,
- .blur_radius = blur_radius / downsample.y,
+ .blur_uv_offset = Point(0.0, pass1_pixel_size.y),
+ .blur_sigma = sigma_ * downsample_scalar.y,
+ .blur_radius = blur_radius * downsample_scalar.y,
.step_size = 1.0,
});
@@ -237,9 +245,9 @@
std::shared_ptr<Texture> pass3_out_texture = MakeBlurSubpass(
renderer, pass2_out_texture, input_snapshot->sampler_descriptor,
GaussianBlurFragmentShader::BlurInfo{
- .blur_uv_offset = Point(pass1_pixel_size.width, 0.0),
- .blur_sigma = sigma_ / downsample.x,
- .blur_radius = blur_radius / downsample.x,
+ .blur_uv_offset = Point(pass1_pixel_size.x, 0.0),
+ .blur_sigma = sigma_ * downsample_scalar.x,
+ .blur_radius = blur_radius * downsample_scalar.x,
.step_size = 1.0,
});
@@ -249,14 +257,10 @@
return Entity::FromSnapshot(
Snapshot{
.texture = pass3_out_texture,
- .transform =
- entity.GetTransform() *
- Matrix::MakeScale(
- {input_snapshot->texture->GetSize().width /
- static_cast<Scalar>(pass1_out_texture->GetSize().width),
- input_snapshot->texture->GetSize().height /
- static_cast<Scalar>(pass1_out_texture->GetSize().height),
- 1.0}),
+ .transform = entity.GetTransform() *
+ Matrix::MakeTranslation({-padding.x, -padding.y, 0}) *
+ Matrix::MakeScale(padded_size /
+ Vector2(pass1_out_texture->GetSize())),
.sampler_descriptor = sampler_desc,
.opacity = input_snapshot->opacity},
entity.GetBlendMode(), entity.GetClipDepth());
diff --git a/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc b/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc
index aff2aab..d8378d3 100644
--- a/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc
+++ b/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc
@@ -135,13 +135,10 @@
EXPECT_TRUE(result_coverage.has_value());
EXPECT_TRUE(contents_coverage.has_value());
if (result_coverage.has_value() && contents_coverage.has_value()) {
- // TODO(gaaclarke): This test won't pass until the blur_radius is used to
- // expand the coverage. See note inside of
- // gaussian_blur_filter_contents.cc.
- // EXPECT_TRUE(RectNear(result_coverage.value(),
- // contents_coverage.value()));
+ EXPECT_TRUE(RectNear(contents_coverage.value(),
+ Rect::MakeLTRB(-1, -1, 101, 101)));
EXPECT_TRUE(
- RectNear(result_coverage.value(), Rect::MakeLTRB(0, 0, 100, 100)));
+ RectNear(result_coverage.value(), Rect::MakeLTRB(-1, -1, 101, 101)));
}
}
}
@@ -170,15 +167,10 @@
EXPECT_TRUE(result_coverage.has_value());
EXPECT_TRUE(contents_coverage.has_value());
if (result_coverage.has_value() && contents_coverage.has_value()) {
- // TODO(gaaclarke): This test won't pass until the blur_radius is used to
- // expand the coverage. See note inside of
- // gaussian_blur_filter_contents.cc.
- // EXPECT_TRUE(RectNear(result_coverage.value(),
- // contents_coverage.value()));
EXPECT_TRUE(RectNear(contents_coverage.value(),
Rect::MakeLTRB(99, 199, 201, 301)));
- EXPECT_TRUE(RectNear(result_coverage.value(),
- Rect::MakeLTRB(100, 200, 200, 300)));
+ EXPECT_TRUE(
+ RectNear(result_coverage.value(), Rect::MakeLTRB(99, 199, 201, 301)));
}
}
}
@@ -208,15 +200,10 @@
EXPECT_TRUE(result_coverage.has_value());
EXPECT_TRUE(contents_coverage.has_value());
if (result_coverage.has_value() && contents_coverage.has_value()) {
- // TODO(gaaclarke): This test won't pass until the blur_radius is used to
- // expand the coverage. See note inside of
- // gaussian_blur_filter_contents.cc.
- // EXPECT_TRUE(RectNear(result_coverage.value(),
- // contents_coverage.value()));
EXPECT_TRUE(RectNear(contents_coverage.value(),
Rect::MakeLTRB(99, 99, 401, 501)));
- EXPECT_TRUE(RectNear(result_coverage.value(),
- Rect::MakeLTRB(100, 100, 400, 500)));
+ EXPECT_TRUE(
+ RectNear(result_coverage.value(), Rect::MakeLTRB(99, 99, 401, 501)));
}
}
}