GrCircleBlurFragmentProcessor.cpp revision 3865711259e25a90a1d72480f848863ada202067
1/*
2 * Copyright 2015 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "GrCircleBlurFragmentProcessor.h"
9
10#if SK_SUPPORT_GPU
11
12#include "GrContext.h"
13#include "GrInvariantOutput.h"
14#include "GrTextureProvider.h"
15
16#include "glsl/GrGLSLFragmentProcessor.h"
17#include "glsl/GrGLSLFragmentShaderBuilder.h"
18#include "glsl/GrGLSLProgramDataManager.h"
19#include "glsl/GrGLSLUniformHandler.h"
20
21#include "SkFixed.h"
22
23class GrCircleBlurFragmentProcessor::GLSLProcessor : public GrGLSLFragmentProcessor {
24public:
25    void emitCode(EmitArgs&) override;
26
27protected:
28    void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override;
29
30private:
31    GrGLSLProgramDataManager::UniformHandle fDataUniform;
32
33    typedef GrGLSLFragmentProcessor INHERITED;
34};
35
36void GrCircleBlurFragmentProcessor::GLSLProcessor::emitCode(EmitArgs& args) {
37    const char *dataName;
38
39    // The data is formatted as:
40    // x,y  - the center of the circle
41    // z    - inner radius that should map to 0th entry in the texture.
42    // w    - the inverse of the distance over which the texture is stretched.
43    fDataUniform = args.fUniformHandler->addUniform(kFragment_GrShaderFlag,
44                                                    kVec4f_GrSLType,
45                                                    kDefault_GrSLPrecision,
46                                                    "data",
47                                                    &dataName);
48
49    GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder;
50
51    if (args.fInputColor) {
52        fragBuilder->codeAppendf("vec4 src=%s;", args.fInputColor);
53    } else {
54        fragBuilder->codeAppendf("vec4 src=vec4(1);");
55    }
56
57    // We just want to compute "(length(vec) - %s.z + 0.5) * %s.w" but need to rearrange
58    // for precision.
59    fragBuilder->codeAppendf("vec2 vec = vec2( (sk_FragCoord.x - %s.x) * %s.w, "
60                                              "(sk_FragCoord.y - %s.y) * %s.w );",
61                             dataName, dataName, dataName, dataName);
62    fragBuilder->codeAppendf("float dist = length(vec) + (0.5 - %s.z) * %s.w;",
63                             dataName, dataName);
64
65    fragBuilder->codeAppendf("float intensity = ");
66    fragBuilder->appendTextureLookup(args.fTexSamplers[0], "vec2(dist, 0.5)");
67    fragBuilder->codeAppend(".a;");
68
69    fragBuilder->codeAppendf("%s = src * intensity;\n", args.fOutputColor );
70}
71
72void GrCircleBlurFragmentProcessor::GLSLProcessor::onSetData(const GrGLSLProgramDataManager& pdman,
73                                                             const GrProcessor& proc) {
74    const GrCircleBlurFragmentProcessor& cbfp = proc.cast<GrCircleBlurFragmentProcessor>();
75    const SkRect& circle = cbfp.fCircle;
76
77    // The data is formatted as:
78    // x,y  - the center of the circle
79    // z    - inner radius that should map to 0th entry in the texture.
80    // w    - the inverse of the distance over which the profile texture is stretched.
81    pdman.set4f(fDataUniform, circle.centerX(), circle.centerY(), cbfp.fSolidRadius,
82                1.f / cbfp.fTextureRadius);
83}
84
85///////////////////////////////////////////////////////////////////////////////
86
87GrCircleBlurFragmentProcessor::GrCircleBlurFragmentProcessor(const SkRect& circle,
88                                                             float textureRadius,
89                                                             float solidRadius,
90                                                             GrTexture* blurProfile)
91        : INHERITED(kModulatesInput_OptimizationFlag)
92        , fCircle(circle)
93        , fSolidRadius(solidRadius)
94        , fTextureRadius(textureRadius)
95        , fBlurProfileSampler(blurProfile, GrSamplerParams::kBilerp_FilterMode) {
96    this->initClassID<GrCircleBlurFragmentProcessor>();
97    this->addTextureSampler(&fBlurProfileSampler);
98}
99
100GrGLSLFragmentProcessor* GrCircleBlurFragmentProcessor::onCreateGLSLInstance() const {
101    return new GLSLProcessor;
102}
103
104void GrCircleBlurFragmentProcessor::onGetGLSLProcessorKey(const GrShaderCaps& caps,
105                                                          GrProcessorKeyBuilder* b) const {
106    // The code for this processor is always the same so there is nothing to add to the key.
107    return;
108}
109
110void GrCircleBlurFragmentProcessor::onComputeInvariantOutput(GrInvariantOutput* inout) const {
111    inout->mulByUnknownSingleComponent();
112}
113
114// Computes an unnormalized half kernel (right side). Returns the summation of all the half kernel
115// values.
116static float make_unnormalized_half_kernel(float* halfKernel, int halfKernelSize, float sigma) {
117    const float invSigma = 1.f / sigma;
118    const float b = -0.5f * invSigma * invSigma;
119    float tot = 0.0f;
120    // Compute half kernel values at half pixel steps out from the center.
121    float t = 0.5f;
122    for (int i = 0; i < halfKernelSize; ++i) {
123        float value = expf(t * t * b);
124        tot += value;
125        halfKernel[i] = value;
126        t += 1.f;
127    }
128    return tot;
129}
130
131// Create a Gaussian half-kernel (right side) and a summed area table given a sigma and number of
132// discrete steps. The half kernel is normalized to sum to 0.5.
133static void make_half_kernel_and_summed_table(float* halfKernel, float* summedHalfKernel,
134                                              int halfKernelSize, float sigma) {
135    // The half kernel should sum to 0.5 not 1.0.
136    const float tot = 2.f * make_unnormalized_half_kernel(halfKernel, halfKernelSize, sigma);
137    float sum = 0.f;
138    for (int i = 0; i < halfKernelSize; ++i) {
139        halfKernel[i] /= tot;
140        sum += halfKernel[i];
141        summedHalfKernel[i] = sum;
142    }
143}
144
145// Applies the 1D half kernel vertically at points along the x axis to a circle centered at the
146// origin with radius circleR.
147void apply_kernel_in_y(float* results, int numSteps, float firstX, float circleR,
148                       int halfKernelSize, const float* summedHalfKernelTable) {
149    float x = firstX;
150    for (int i = 0; i < numSteps; ++i, x += 1.f) {
151        if (x < -circleR || x > circleR) {
152            results[i] = 0;
153            continue;
154        }
155        float y = sqrtf(circleR * circleR - x * x);
156        // In the column at x we exit the circle at +y and -y
157        // The summed table entry j is actually reflects an offset of j + 0.5.
158        y -= 0.5f;
159        int yInt = SkScalarFloorToInt(y);
160        SkASSERT(yInt >= -1);
161        if (y < 0) {
162            results[i] = (y + 0.5f) * summedHalfKernelTable[0];
163        } else if (yInt >= halfKernelSize - 1) {
164            results[i] = 0.5f;
165        } else {
166            float yFrac = y - yInt;
167            results[i] = (1.f - yFrac) * summedHalfKernelTable[yInt] +
168                         yFrac * summedHalfKernelTable[yInt + 1];
169        }
170    }
171}
172
173// Apply a Gaussian at point (evalX, 0) to a circle centered at the origin with radius circleR.
174// This relies on having a half kernel computed for the Gaussian and a table of applications of
175// the half kernel in y to columns at (evalX - halfKernel, evalX - halfKernel + 1, ..., evalX +
176// halfKernel) passed in as yKernelEvaluations.
177static uint8_t eval_at(float evalX, float circleR, const float* halfKernel, int halfKernelSize,
178                       const float* yKernelEvaluations) {
179    float acc = 0;
180
181    float x = evalX - halfKernelSize;
182    for (int i = 0; i < halfKernelSize; ++i, x += 1.f) {
183        if (x < -circleR || x > circleR) {
184            continue;
185        }
186        float verticalEval = yKernelEvaluations[i];
187        acc += verticalEval * halfKernel[halfKernelSize - i - 1];
188    }
189    for (int i = 0; i < halfKernelSize; ++i, x += 1.f) {
190        if (x < -circleR || x > circleR) {
191            continue;
192        }
193        float verticalEval = yKernelEvaluations[i + halfKernelSize];
194        acc += verticalEval * halfKernel[i];
195    }
196    // Since we applied a half kernel in y we multiply acc by 2 (the circle is symmetric about the
197    // x axis).
198    return SkUnitScalarClampToByte(2.f * acc);
199}
200
201// This function creates a profile of a blurred circle. It does this by computing a kernel for
202// half the Gaussian and a matching summed area table. The summed area table is used to compute
203// an array of vertical applications of the half kernel to the circle along the x axis. The table
204// of y evaluations has 2 * k + n entries where k is the size of the half kernel and n is the size
205// of the profile being computed. Then for each of the n profile entries we walk out k steps in each
206// horizontal direction multiplying the corresponding y evaluation by the half kernel entry and
207// sum these values to compute the profile entry.
208static uint8_t* create_circle_profile(float sigma, float circleR, int profileTextureWidth) {
209    const int numSteps = profileTextureWidth;
210    uint8_t* weights = new uint8_t[numSteps];
211
212    // The full kernel is 6 sigmas wide.
213    int halfKernelSize = SkScalarCeilToInt(6.0f*sigma);
214    // round up to next multiple of 2 and then divide by 2
215    halfKernelSize = ((halfKernelSize + 1) & ~1) >> 1;
216
217    // Number of x steps at which to apply kernel in y to cover all the profile samples in x.
218    int numYSteps = numSteps + 2 * halfKernelSize;
219
220    SkAutoTArray<float> bulkAlloc(halfKernelSize + halfKernelSize + numYSteps);
221    float* halfKernel = bulkAlloc.get();
222    float* summedKernel = bulkAlloc.get() + halfKernelSize;
223    float* yEvals = bulkAlloc.get() + 2 * halfKernelSize;
224    make_half_kernel_and_summed_table(halfKernel, summedKernel, halfKernelSize, sigma);
225
226    float firstX = -halfKernelSize + 0.5f;
227    apply_kernel_in_y(yEvals, numYSteps, firstX, circleR, halfKernelSize, summedKernel);
228
229    for (int i = 0; i < numSteps - 1; ++i) {
230        float evalX = i + 0.5f;
231        weights[i] = eval_at(evalX, circleR, halfKernel, halfKernelSize, yEvals + i);
232    }
233    // Ensure the tail of the Gaussian goes to zero.
234    weights[numSteps - 1] = 0;
235    return weights;
236}
237
238static uint8_t* create_half_plane_profile(int profileWidth) {
239    SkASSERT(!(profileWidth & 0x1));
240    // The full kernel is 6 sigmas wide.
241    float sigma = profileWidth / 6.f;
242    int halfKernelSize = profileWidth / 2;
243
244    SkAutoTArray<float> halfKernel(halfKernelSize);
245    uint8_t* profile = new uint8_t[profileWidth];
246
247    // The half kernel should sum to 0.5.
248    const float tot = 2.f * make_unnormalized_half_kernel(halfKernel.get(), halfKernelSize, sigma);
249    float sum = 0.f;
250    // Populate the profile from the right edge to the middle.
251    for (int i = 0; i < halfKernelSize; ++i) {
252        halfKernel[halfKernelSize - i - 1] /= tot;
253        sum += halfKernel[halfKernelSize - i - 1];
254        profile[profileWidth - i - 1] = SkUnitScalarClampToByte(sum);
255    }
256    // Populate the profile from the middle to the left edge (by flipping the half kernel and
257    // continuing the summation).
258    for (int i = 0; i < halfKernelSize; ++i) {
259        sum += halfKernel[i];
260        profile[halfKernelSize - i - 1] = SkUnitScalarClampToByte(sum);
261    }
262    // Ensure tail goes to 0.
263    profile[profileWidth - 1] = 0;
264    return profile;
265}
266
267static GrTexture* create_profile_texture(GrTextureProvider* textureProvider, const SkRect& circle,
268                                         float sigma, float* solidRadius, float* textureRadius) {
269    float circleR = circle.width() / 2.0f;
270    // Profile textures are cached by the ratio of sigma to circle radius and by the size of the
271    // profile texture (binned by powers of 2).
272    SkScalar sigmaToCircleRRatio = sigma / circleR;
273    // When sigma is really small this becomes a equivalent to convolving a Gaussian with a half-
274    // plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to the Guassian
275    // and the profile texture is a just a Gaussian evaluation. However, we haven't yet implemented
276    // this latter optimization.
277    sigmaToCircleRRatio = SkTMin(sigmaToCircleRRatio, 8.f);
278    SkFixed sigmaToCircleRRatioFixed;
279    static const SkScalar kHalfPlaneThreshold = 0.1f;
280    bool useHalfPlaneApprox = false;
281    if (sigmaToCircleRRatio <= kHalfPlaneThreshold) {
282        useHalfPlaneApprox = true;
283        sigmaToCircleRRatioFixed = 0;
284        *solidRadius = circleR - 3 * sigma;
285        *textureRadius = 6 * sigma;
286    } else {
287        // Convert to fixed point for the key.
288        sigmaToCircleRRatioFixed = SkScalarToFixed(sigmaToCircleRRatio);
289        // We shave off some bits to reduce the number of unique entries. We could probably shave
290        // off more than we do.
291        sigmaToCircleRRatioFixed &= ~0xff;
292        sigmaToCircleRRatio = SkFixedToScalar(sigmaToCircleRRatioFixed);
293        sigma = circleR * sigmaToCircleRRatio;
294        *solidRadius = 0;
295        *textureRadius = circleR + 3 * sigma;
296    }
297
298    static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
299    GrUniqueKey key;
300    GrUniqueKey::Builder builder(&key, kDomain, 1);
301    builder[0] = sigmaToCircleRRatioFixed;
302    builder.finish();
303
304    GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key);
305    if (!blurProfile) {
306        static constexpr int kProfileTextureWidth = 512;
307        GrSurfaceDesc texDesc;
308        texDesc.fWidth = kProfileTextureWidth;
309        texDesc.fHeight = 1;
310        texDesc.fConfig = kAlpha_8_GrPixelConfig;
311
312        std::unique_ptr<uint8_t[]> profile(nullptr);
313        if (useHalfPlaneApprox) {
314            profile.reset(create_half_plane_profile(kProfileTextureWidth));
315        } else {
316            // Rescale params to the size of the texture we're creating.
317            SkScalar scale = kProfileTextureWidth / *textureRadius;
318            profile.reset(create_circle_profile(sigma * scale, circleR * scale,
319                                                kProfileTextureWidth));
320        }
321
322        blurProfile = textureProvider->createTexture(texDesc, SkBudgeted::kYes, profile.get(), 0);
323        if (blurProfile) {
324            textureProvider->assignUniqueKeyToTexture(key, blurProfile);
325        }
326    }
327
328    return blurProfile;
329}
330
331//////////////////////////////////////////////////////////////////////////////
332
333sk_sp<GrFragmentProcessor> GrCircleBlurFragmentProcessor::Make(GrTextureProvider*textureProvider,
334                                                               const SkRect& circle, float sigma) {
335    float solidRadius;
336    float textureRadius;
337    sk_sp<GrTexture> profile(create_profile_texture(textureProvider, circle, sigma,
338                                                    &solidRadius, &textureRadius));
339    if (!profile) {
340        return nullptr;
341    }
342    return sk_sp<GrFragmentProcessor>(
343            new GrCircleBlurFragmentProcessor(circle, textureRadius, solidRadius, profile.get()));
344}
345
346//////////////////////////////////////////////////////////////////////////////
347
348GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrCircleBlurFragmentProcessor);
349
350#if GR_TEST_UTILS
351sk_sp<GrFragmentProcessor> GrCircleBlurFragmentProcessor::TestCreate(GrProcessorTestData* d) {
352    SkScalar wh = d->fRandom->nextRangeScalar(100.f, 1000.f);
353    SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f);
354    SkRect circle = SkRect::MakeWH(wh, wh);
355    return GrCircleBlurFragmentProcessor::Make(d->fContext->textureProvider(), circle, sigma);
356}
357#endif
358
359#endif
360