diff --git a/engine/src/flutter/impeller/entity/contents/filters/filter_contents.cc b/engine/src/flutter/impeller/entity/contents/filters/filter_contents.cc index 4829403f2d4..adb25ff305b 100644 --- a/engine/src/flutter/impeller/entity/contents/filters/filter_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/filters/filter_contents.cc @@ -61,12 +61,14 @@ std::shared_ptr FilterContents::MakeBlend( std::shared_ptr FilterContents::MakeDirectionalGaussianBlur( FilterInput::Ref input, - Vector2 blur_vector, + Sigma sigma, + Vector2 direction, BlurStyle blur_style, FilterInput::Ref source_override) { auto blur = std::make_shared(); blur->SetInputs({input}); - blur->SetBlurVector(blur_vector); + blur->SetSigma(sigma); + blur->SetDirection(direction); blur->SetBlurStyle(blur_style); blur->SetSourceOverride(source_override); return blur; @@ -74,13 +76,13 @@ std::shared_ptr FilterContents::MakeDirectionalGaussianBlur( std::shared_ptr FilterContents::MakeGaussianBlur( FilterInput::Ref input, - Scalar sigma_x, - Scalar sigma_y, + Sigma sigma_x, + Sigma sigma_y, BlurStyle blur_style) { - auto x_blur = - MakeDirectionalGaussianBlur(input, Point(sigma_x, 0), BlurStyle::kNormal); - auto y_blur = MakeDirectionalGaussianBlur( - FilterInput::Make(x_blur), Point(0, sigma_y), blur_style, input); + auto x_blur = MakeDirectionalGaussianBlur(input, sigma_x, Point(1, 0), + BlurStyle::kNormal); + auto y_blur = MakeDirectionalGaussianBlur(FilterInput::Make(x_blur), sigma_y, + Point(0, 1), blur_style, input); return y_blur; } diff --git a/engine/src/flutter/impeller/entity/contents/filters/filter_contents.h b/engine/src/flutter/impeller/entity/contents/filters/filter_contents.h index 275ea4d52b3..25bae0f0a53 100644 --- a/engine/src/flutter/impeller/entity/contents/filters/filter_contents.h +++ b/engine/src/flutter/impeller/entity/contents/filters/filter_contents.h @@ -29,19 +29,71 @@ class FilterContents : public Contents { kInner, }; + /// For filters that use a Gaussian distribution, this is the `Radius` size to + /// use per `Sigma` (standard deviation). + /// + /// This cutoff (sqrt(3)) is taken from Flutter and Skia (where the + /// multiplicative inverse of this constant is used (1 / sqrt(3)): + /// https://api.flutter.dev/flutter/dart-ui/Shadow/convertRadiusToSigma.html + /// + /// In practice, this value is somewhat arbitrary, and can be changed to a + /// higher number to integrate more of the Gaussian function and render higher + /// quality blurs (with exponentially diminishing returns for the same sigma + /// input). Making this value any lower results in a noticable loss of + /// quality in the blur. + constexpr static float kKernelRadiusPerSigma = 1.73205080757; + + struct Radius; + + /// @brief In filters that use Gaussian distributions, "sigma" is a size of + /// one standard deviation in terms of the local space pixel grid of + /// the filter input. In other words, this determines how wide the + /// distribution stretches. + struct Sigma { + Scalar sigma = 0.0; + + constexpr Sigma() = default; + + explicit constexpr Sigma(Scalar p_sigma) : sigma(p_sigma) {} + + constexpr operator Radius() const { + return Radius{sigma > 0.5f ? (sigma - 0.5f) * kKernelRadiusPerSigma + : 0.0f}; + }; + }; + + /// @brief For convolution filters, the "radius" is the size of the + /// convolution kernel to use on the local space pixel grid of the + /// filter input. + /// For Gaussian blur kernels, this unit has a linear + /// relationship with `Sigma`. See `kKernelRadiusPerSigma` for + /// details on how this relationship works. + struct Radius { + Scalar radius = 0.0; + + constexpr Radius() = default; + + explicit constexpr Radius(Scalar p_radius) : radius(p_radius) {} + + constexpr operator Sigma() const { + return Sigma{radius > 0 ? radius / kKernelRadiusPerSigma + 0.5f : 0.0f}; + }; + }; + static std::shared_ptr MakeBlend(Entity::BlendMode blend_mode, FilterInput::Vector inputs); static std::shared_ptr MakeDirectionalGaussianBlur( FilterInput::Ref input, - Vector2 blur_vector, + Sigma sigma, + Vector2 direction, BlurStyle blur_style = BlurStyle::kNormal, FilterInput::Ref alpha_mask = nullptr); static std::shared_ptr MakeGaussianBlur( FilterInput::Ref input, - Scalar sigma_x, - Scalar sigma_y, + Sigma sigma_x, + Sigma sigma_y, BlurStyle blur_style = BlurStyle::kNormal); FilterContents(); diff --git a/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc b/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc index 79a77b4d0a1..0b2c19770dc 100644 --- a/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.cc @@ -22,12 +22,23 @@ DirectionalGaussianBlurFilterContents::DirectionalGaussianBlurFilterContents() = DirectionalGaussianBlurFilterContents:: ~DirectionalGaussianBlurFilterContents() = default; -void DirectionalGaussianBlurFilterContents::SetBlurVector(Vector2 blur_vector) { - if (blur_vector.GetLengthSquared() < kEhCloseEnough) { - blur_vector_ = Vector2(0, kEhCloseEnough); +void DirectionalGaussianBlurFilterContents::SetSigma(Sigma sigma) { + if (sigma.sigma < kEhCloseEnough) { + // This cutoff is an implementation detail of the blur that's tied to the + // fragment shader. When the blur is set to 0, having a value slightly above + // zero makes the shader do 1 finite sample to pass the image through with + // no blur (while retaining correct alpha mask behavior). + blur_sigma_ = Sigma{kEhCloseEnough}; return; } - blur_vector_ = blur_vector; + blur_sigma_ = sigma; +} + +void DirectionalGaussianBlurFilterContents::SetDirection(Vector2 direction) { + blur_direction_ = direction.Normalize(); + if (blur_direction_.IsZero()) { + blur_direction_ = Vector2(0, 1); + } } void DirectionalGaussianBlurFilterContents::SetBlurStyle(BlurStyle blur_style) { @@ -93,8 +104,8 @@ bool DirectionalGaussianBlurFilterContents::RenderFilter( return false; } - auto transformed_blur = - entity.GetTransformation().TransformDirection(blur_vector_); + auto transformed_blur = entity.GetTransformation().TransformDirection( + blur_direction_ * blur_sigma_.sigma); // LTRB Scalar uv[4] = { @@ -142,7 +153,8 @@ bool DirectionalGaussianBlurFilterContents::RenderFilter( VS::FrameInfo frame_info; frame_info.texture_size = Point(input_bounds->size); - frame_info.blur_radius = transformed_blur.GetLength(); + frame_info.blur_sigma = transformed_blur.GetLength(); + frame_info.blur_radius = Radius{Sigma{frame_info.blur_sigma}}.radius; frame_info.blur_direction = transformed_blur.Normalize(); frame_info.src_factor = src_color_factor_; frame_info.inner_blur_factor = inner_blur_factor_; @@ -174,10 +186,14 @@ std::optional DirectionalGaussianBlurFilterContents::GetCoverage( return std::nullopt; } - auto transformed_blur = - entity.GetTransformation().TransformDirection(blur_vector_).Abs(); - auto extent = bounds->size + transformed_blur * 2; - return Rect(bounds->origin - transformed_blur, Size(extent.x, extent.y)); + auto transformed_blur_vector = + entity.GetTransformation() + .TransformDirection(blur_direction_ * + ceil(Radius{blur_sigma_}.radius)) + .Abs(); + auto extent = bounds->size + transformed_blur_vector * 2; + return Rect(bounds->origin - transformed_blur_vector, + Size(extent.x, extent.y)); } } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.h b/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.h index 1ec3a710a42..098cab0f9de 100644 --- a/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.h +++ b/engine/src/flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents.h @@ -18,7 +18,9 @@ class DirectionalGaussianBlurFilterContents final : public FilterContents { ~DirectionalGaussianBlurFilterContents() override; - void SetBlurVector(Vector2 blur_vector); + void SetSigma(Sigma sigma); + + void SetDirection(Vector2 direction); void SetBlurStyle(BlurStyle blur_style); @@ -34,8 +36,8 @@ class DirectionalGaussianBlurFilterContents final : public FilterContents { const Entity& entity, RenderPass& pass, const Rect& bounds) const override; - - Vector2 blur_vector_; + Sigma blur_sigma_; + Vector2 blur_direction_; BlurStyle blur_style_ = BlurStyle::kNormal; bool src_color_factor_ = false; bool inner_blur_factor_ = true; diff --git a/engine/src/flutter/impeller/entity/entity_unittests.cc b/engine/src/flutter/impeller/entity/entity_unittests.cc index 68db263d733..b3904fba06b 100644 --- a/engine/src/flutter/impeller/entity/entity_unittests.cc +++ b/engine/src/flutter/impeller/entity/entity_unittests.cc @@ -729,7 +729,8 @@ TEST_F(EntityTest, GaussianBlurFilter) { Entity::BlendMode::kPlus, FilterInput::Make({boston, bridge, bridge})); auto blur = FilterContents::MakeGaussianBlur( - FilterInput::Make(blend), blur_amount[0], blur_amount[1], + FilterInput::Make(blend), FilterContents::Sigma{blur_amount[0]}, + FilterContents::Sigma{blur_amount[1]}, blur_styles[selected_blur_style]); ISize input_size = boston->GetSize(); diff --git a/engine/src/flutter/impeller/entity/shaders/gaussian_blur.frag b/engine/src/flutter/impeller/entity/shaders/gaussian_blur.frag index 939ecf9e20c..501acb3df78 100644 --- a/engine/src/flutter/impeller/entity/shaders/gaussian_blur.frag +++ b/engine/src/flutter/impeller/entity/shaders/gaussian_blur.frag @@ -16,6 +16,7 @@ in vec2 v_texture_coords; in vec2 v_src_texture_coords; in vec2 v_texture_size; in vec2 v_blur_direction; +in float v_blur_sigma; in float v_blur_radius; in float v_src_factor; in float v_inner_blur_factor; @@ -23,12 +24,11 @@ in float v_outer_blur_factor; out vec4 frag_color; -const float kTwoPi = 6.283185307179586; +const float kSqrtTwoPi = 2.50662827463; float Gaussian(float x) { - float stddev = v_blur_radius * 0.5; - float xnorm = x / stddev; - return exp(-0.5 * xnorm * xnorm) / (kTwoPi * stddev * stddev); + float variance = v_blur_sigma * v_blur_sigma; + return exp(-0.5 * x * x / variance) / (kSqrtTwoPi * v_blur_sigma); } // Emulate SamplerAddressMode::ClampToBorder. @@ -38,17 +38,19 @@ vec4 SampleWithBorder(sampler2D tex, vec2 uv) { } void main() { - vec4 total = vec4(0); - float total_gaussian = 0; + vec4 total_color = vec4(0); + float gaussian_integral = 0; vec2 blur_uv_offset = v_blur_direction / v_texture_size; + for (float i = -v_blur_radius; i <= v_blur_radius; i++) { float gaussian = Gaussian(i); - total_gaussian += gaussian; - total += gaussian * SampleWithBorder(texture_sampler, - v_texture_coords + blur_uv_offset * i); + gaussian_integral += gaussian; + total_color += + gaussian * SampleWithBorder(texture_sampler, + v_texture_coords + blur_uv_offset * i); } - vec4 blur_color = total / total_gaussian; + vec4 blur_color = total_color / gaussian_integral; vec4 src_color = SampleWithBorder(alpha_mask_sampler, v_src_texture_coords); float blur_factor = v_inner_blur_factor * float(src_color.a > 0) + diff --git a/engine/src/flutter/impeller/entity/shaders/gaussian_blur.vert b/engine/src/flutter/impeller/entity/shaders/gaussian_blur.vert index a358a2b5719..1a66dc0eb5a 100644 --- a/engine/src/flutter/impeller/entity/shaders/gaussian_blur.vert +++ b/engine/src/flutter/impeller/entity/shaders/gaussian_blur.vert @@ -7,6 +7,7 @@ uniform FrameInfo { vec2 texture_size; vec2 blur_direction; + float blur_sigma; float blur_radius; float src_factor; @@ -23,6 +24,7 @@ out vec2 v_texture_coords; out vec2 v_src_texture_coords; out vec2 v_texture_size; out vec2 v_blur_direction; +out float v_blur_sigma; out float v_blur_radius; out float v_src_factor; out float v_inner_blur_factor; @@ -34,6 +36,7 @@ void main() { v_src_texture_coords = src_texture_coords; v_texture_size = frame_info.texture_size; v_blur_direction = frame_info.blur_direction; + v_blur_sigma = frame_info.blur_sigma; v_blur_radius = frame_info.blur_radius; v_src_factor = frame_info.src_factor; v_inner_blur_factor = frame_info.inner_blur_factor;