| /* |
| * Copyright 2020 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "gm/gm.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkData.h" |
| #include "include/core/SkPaint.h" |
| #include "include/core/SkSize.h" |
| #include "include/core/SkString.h" |
| #include "include/core/SkSurface.h" |
| #include "include/effects/SkRuntimeEffect.h" |
| #include "tools/Resources.h" |
| |
| class KawaseBlurFilter { |
| public: |
| // Downsample scale factor used to improve performance |
| static constexpr float kInputScale = 0.25f; |
| // Downsample scale factor used to improve performance |
| static constexpr float kInverseInputScale = 1.0f / kInputScale; |
| // Maximum number of render passes |
| static constexpr uint32_t kMaxPasses = 4; |
| // To avoid downscaling artifacts, we interpolate the blurred fbo with the full composited |
| // image, up to this radius. |
| static constexpr float kMaxCrossFadeRadius = 30.0f; |
| |
| KawaseBlurFilter() { |
| SkString blurString(R"( |
| uniform shader src; |
| uniform float in_inverseScale; |
| uniform float2 in_blurOffset; |
| |
| half4 main(float2 xy) { |
| float2 scaled_xy = float2(xy.x * in_inverseScale, xy.y * in_inverseScale); |
| |
| half4 c = src.eval(scaled_xy); |
| c += src.eval(scaled_xy + float2( in_blurOffset.x, in_blurOffset.y)); |
| c += src.eval(scaled_xy + float2( in_blurOffset.x, -in_blurOffset.y)); |
| c += src.eval(scaled_xy + float2(-in_blurOffset.x, in_blurOffset.y)); |
| c += src.eval(scaled_xy + float2(-in_blurOffset.x, -in_blurOffset.y)); |
| |
| return half4(c.rgb * 0.2, 1.0); |
| } |
| )"); |
| |
| SkString mixString(R"( |
| uniform shader in_blur; |
| uniform shader in_original; |
| uniform float in_inverseScale; |
| uniform float in_mix; |
| |
| half4 main(float2 xy) { |
| float2 scaled_xy = float2(xy.x * in_inverseScale, xy.y * in_inverseScale); |
| |
| half4 blurred = in_blur.eval(scaled_xy); |
| half4 composition = in_original.eval(xy); |
| return mix(composition, blurred, in_mix); |
| } |
| )"); |
| |
| auto [blurEffect, error] = SkRuntimeEffect::MakeForShader(blurString); |
| if (!blurEffect) { |
| SkDEBUGFAILF("RuntimeShader error: %s\n", error.c_str()); |
| } |
| fBlurEffect = std::move(blurEffect); |
| |
| auto [mixEffect, error2] = SkRuntimeEffect::MakeForShader(mixString); |
| if (!mixEffect) { |
| SkDEBUGFAILF("RuntimeShader error: %s\n", error2.c_str()); |
| } |
| fMixEffect = std::move(mixEffect); |
| } |
| |
| static sk_sp<SkSurface> MakeSurface(SkCanvas* canvas, const SkImageInfo& info) { |
| if (sk_sp<SkSurface> surface = canvas->makeSurface(info)) { |
| return surface; |
| } |
| // serialize-8888 returns null from makeSurface; fallback to a raster surface. |
| return SkSurface::MakeRaster(info); |
| } |
| |
| void draw(SkCanvas* canvas, sk_sp<SkImage> input, int blurRadius) { |
| // NOTE: this is only experimental and the current blur params cause points to be sampled |
| // beyond the input blur radius. |
| |
| // Kawase is an approximation of Gaussian, but it behaves differently from it. |
| // A radius transformation is required for approximating them, and also to introduce |
| // non-integer steps, necessary to smoothly interpolate large radii. |
| float tmpRadius = (float)blurRadius / 6.0f; |
| float numberOfPasses = std::min(kMaxPasses, (uint32_t)ceil(tmpRadius)); |
| float radiusByPasses = tmpRadius / (float)numberOfPasses; |
| |
| SkImageInfo scaledInfo = SkImageInfo::MakeN32Premul((float)input->width() * kInputScale, |
| (float)input->height() * kInputScale); |
| auto drawSurface = MakeSurface(canvas, scaledInfo); |
| |
| const float stepX = radiusByPasses; |
| const float stepY = radiusByPasses; |
| |
| // start by drawing and downscaling and doing the first blur pass |
| SkRuntimeShaderBuilder blurBuilder(fBlurEffect); |
| blurBuilder.child("src") = input->makeShader(SkSamplingOptions(SkFilterMode::kLinear)); |
| blurBuilder.uniform("in_inverseScale") = kInverseInputScale; |
| blurBuilder.uniform("in_blurOffset") = SkV2{stepX * kInverseInputScale, |
| stepY * kInverseInputScale}; |
| SkPaint paint; |
| paint.setShader(blurBuilder.makeShader()); |
| drawSurface->getCanvas()->drawIRect(scaledInfo.bounds(), paint); |
| |
| // DEBUG draw each of the stages |
| canvas->save(); |
| canvas->drawImage(drawSurface->makeImageSnapshot(), input->width() / 4, 0, |
| SkSamplingOptions()); |
| canvas->translate(input->width() / 4, input->height() * 0.75); |
| drawSurface->flush(); |
| |
| // And now we'll ping pong between our surfaces, to accumulate the result of various |
| // offsets. |
| auto lastDrawTarget = drawSurface; |
| if (numberOfPasses > 1) { |
| auto readSurface = drawSurface; |
| drawSurface = MakeSurface(canvas, scaledInfo); |
| |
| for (auto i = 1; i < numberOfPasses; i++) { |
| const float stepScale = (float)i * kInputScale; |
| |
| blurBuilder.child("src") = readSurface->makeImageSnapshot()->makeShader( |
| SkSamplingOptions(SkFilterMode::kLinear)); |
| blurBuilder.uniform("in_inverseScale") = 1.0f; |
| blurBuilder.uniform("in_blurOffset") = SkV2{stepX * stepScale , stepY * stepScale}; |
| |
| paint.setShader(blurBuilder.makeShader()); |
| drawSurface->getCanvas()->drawIRect(scaledInfo.bounds(), paint); |
| |
| // DEBUG draw each of the stages |
| canvas->drawImage(drawSurface->makeImageSnapshot(), 0, 0, SkSamplingOptions()); |
| canvas->translate(0, input->height() * 0.75); |
| |
| // Swap buffers for next iteration |
| auto tmp = drawSurface; |
| drawSurface = readSurface; |
| readSurface = tmp; |
| } |
| lastDrawTarget = readSurface; |
| } |
| |
| // restore translations done for debug and offset |
| canvas->restore(); |
| SkAutoCanvasRestore acr(canvas, true); |
| canvas->translate(input->width(), 0); |
| |
| // do the final composition and when we scale our blur up. It will be interpolated |
| // with the larger composited texture to hide downscaling artifacts. |
| SkRuntimeShaderBuilder mixBuilder(fMixEffect); |
| mixBuilder.child("in_blur") = lastDrawTarget->makeImageSnapshot()->makeShader( |
| SkSamplingOptions(SkFilterMode::kLinear)); |
| mixBuilder.child("in_original") = |
| input->makeShader(SkSamplingOptions(SkFilterMode::kLinear)); |
| mixBuilder.uniform("in_inverseScale") = kInputScale; |
| mixBuilder.uniform("in_mix") = std::min(1.0f, (float)blurRadius / kMaxCrossFadeRadius); |
| |
| paint.setShader(mixBuilder.makeShader()); |
| canvas->drawIRect(input->bounds(), paint); |
| } |
| |
| private: |
| sk_sp<SkRuntimeEffect> fBlurEffect; |
| sk_sp<SkRuntimeEffect> fMixEffect; |
| }; |
| |
| class KawaseBlurRT : public skiagm::GM { |
| public: |
| KawaseBlurRT() {} |
| SkString onShortName() override { return SkString("kawase_blur_rt"); } |
| SkISize onISize() override { return {1280, 768}; } |
| |
| void onOnceBeforeDraw() override { |
| fMandrill = GetResourceAsImage("images/mandrill_256.png"); |
| } |
| |
| void onDraw(SkCanvas* canvas) override { |
| canvas->drawImage(fMandrill, 0, 0); |
| canvas->translate(256, 0); |
| KawaseBlurFilter blurFilter; |
| blurFilter.draw(canvas, fMandrill, 45); |
| canvas->translate(512, 0); |
| blurFilter.draw(canvas, fMandrill, 55); |
| } |
| |
| private: |
| sk_sp<SkImage> fMandrill; |
| }; |
| DEF_GM(return new KawaseBlurRT;) |