|  | /* | 
|  | * 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/DecodeUtils.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 SkSurfaces::Raster(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); | 
|  |  | 
|  | // 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 getName() const override { return SkString("kawase_blur_rt"); } | 
|  | SkISize getISize() override { return {1280, 768}; } | 
|  |  | 
|  | void onOnceBeforeDraw() override { | 
|  | fMandrill = ToolUtils::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;) |