197 lines
7.8 KiB
C++
197 lines
7.8 KiB
C++
/*
|
|
* 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;)
|