Home | History | Annotate | Download | only in effects
      1 /*
      2  * Copyright 2018 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 in half4 circleRect;
      9 in half textureRadius;
     10 in half solidRadius;
     11 in uniform sampler2D blurProfileSampler;
     12 
     13 // The data is formatted as:
     14 // x, y - the center of the circle
     15 // z    - inner radius that should map to 0th entry in the texture.
     16 // w    - the inverse of the distance over which the texture is stretched.
     17 uniform half4 circleData;
     18 
     19 @optimizationFlags {
     20     kCompatibleWithCoverageAsAlpha_OptimizationFlag
     21 }
     22 
     23 @make {
     24     static std::unique_ptr<GrFragmentProcessor> Make(GrProxyProvider*,
     25                                                      const SkRect& circle, float sigma);
     26 }
     27 
     28 @setData(data) {
     29     data.set4f(circleData, circleRect.centerX(), circleRect.centerY(), solidRadius,
     30                1.f / textureRadius);
     31 }
     32 
     33 @cpp {
     34     #include "GrProxyProvider.h"
     35 
     36     // Computes an unnormalized half kernel (right side). Returns the summation of all the half
     37     // kernel values.
     38     static float make_unnormalized_half_kernel(float* halfKernel, int halfKernelSize, float sigma) {
     39         const float invSigma = 1.f / sigma;
     40         const float b = -0.5f * invSigma * invSigma;
     41         float tot = 0.0f;
     42         // Compute half kernel values at half pixel steps out from the center.
     43         float t = 0.5f;
     44         for (int i = 0; i < halfKernelSize; ++i) {
     45             float value = expf(t * t * b);
     46             tot += value;
     47             halfKernel[i] = value;
     48             t += 1.f;
     49         }
     50         return tot;
     51     }
     52 
     53     // Create a Gaussian half-kernel (right side) and a summed area table given a sigma and number
     54     // of discrete steps. The half kernel is normalized to sum to 0.5.
     55     static void make_half_kernel_and_summed_table(float* halfKernel, float* summedHalfKernel,
     56                                                   int halfKernelSize, float sigma) {
     57         // The half kernel should sum to 0.5 not 1.0.
     58         const float tot = 2.f * make_unnormalized_half_kernel(halfKernel, halfKernelSize, sigma);
     59         float sum = 0.f;
     60         for (int i = 0; i < halfKernelSize; ++i) {
     61             halfKernel[i] /= tot;
     62             sum += halfKernel[i];
     63             summedHalfKernel[i] = sum;
     64         }
     65     }
     66 
     67     // Applies the 1D half kernel vertically at points along the x axis to a circle centered at the
     68     // origin with radius circleR.
     69     void apply_kernel_in_y(float* results, int numSteps, float firstX, float circleR,
     70                            int halfKernelSize, const float* summedHalfKernelTable) {
     71         float x = firstX;
     72         for (int i = 0; i < numSteps; ++i, x += 1.f) {
     73             if (x < -circleR || x > circleR) {
     74                 results[i] = 0;
     75                 continue;
     76             }
     77             float y = sqrtf(circleR * circleR - x * x);
     78             // In the column at x we exit the circle at +y and -y
     79             // The summed table entry j is actually reflects an offset of j + 0.5.
     80             y -= 0.5f;
     81             int yInt = SkScalarFloorToInt(y);
     82             SkASSERT(yInt >= -1);
     83             if (y < 0) {
     84                 results[i] = (y + 0.5f) * summedHalfKernelTable[0];
     85             } else if (yInt >= halfKernelSize - 1) {
     86                 results[i] = 0.5f;
     87             } else {
     88                 float yFrac = y - yInt;
     89                 results[i] = (1.f - yFrac) * summedHalfKernelTable[yInt] +
     90                              yFrac * summedHalfKernelTable[yInt + 1];
     91             }
     92         }
     93     }
     94 
     95     // Apply a Gaussian at point (evalX, 0) to a circle centered at the origin with radius circleR.
     96     // This relies on having a half kernel computed for the Gaussian and a table of applications of
     97     // the half kernel in y to columns at (evalX - halfKernel, evalX - halfKernel + 1, ..., evalX +
     98     // halfKernel) passed in as yKernelEvaluations.
     99     static uint8_t eval_at(float evalX, float circleR, const float* halfKernel, int halfKernelSize,
    100                            const float* yKernelEvaluations) {
    101         float acc = 0;
    102 
    103         float x = evalX - halfKernelSize;
    104         for (int i = 0; i < halfKernelSize; ++i, x += 1.f) {
    105             if (x < -circleR || x > circleR) {
    106                 continue;
    107             }
    108             float verticalEval = yKernelEvaluations[i];
    109             acc += verticalEval * halfKernel[halfKernelSize - i - 1];
    110         }
    111         for (int i = 0; i < halfKernelSize; ++i, x += 1.f) {
    112             if (x < -circleR || x > circleR) {
    113                 continue;
    114             }
    115             float verticalEval = yKernelEvaluations[i + halfKernelSize];
    116             acc += verticalEval * halfKernel[i];
    117         }
    118         // Since we applied a half kernel in y we multiply acc by 2 (the circle is symmetric about
    119         // the x axis).
    120         return SkUnitScalarClampToByte(2.f * acc);
    121     }
    122 
    123     // This function creates a profile of a blurred circle. It does this by computing a kernel for
    124     // half the Gaussian and a matching summed area table. The summed area table is used to compute
    125     // an array of vertical applications of the half kernel to the circle along the x axis. The
    126     // table of y evaluations has 2 * k + n entries where k is the size of the half kernel and n is
    127     // the size of the profile being computed. Then for each of the n profile entries we walk out k
    128     // steps in each horizontal direction multiplying the corresponding y evaluation by the half
    129     // kernel entry and sum these values to compute the profile entry.
    130     static uint8_t* create_circle_profile(float sigma, float circleR, int profileTextureWidth) {
    131         const int numSteps = profileTextureWidth;
    132         uint8_t* weights = new uint8_t[numSteps];
    133 
    134         // The full kernel is 6 sigmas wide.
    135         int halfKernelSize = SkScalarCeilToInt(6.0f*sigma);
    136         // round up to next multiple of 2 and then divide by 2
    137         halfKernelSize = ((halfKernelSize + 1) & ~1) >> 1;
    138 
    139         // Number of x steps at which to apply kernel in y to cover all the profile samples in x.
    140         int numYSteps = numSteps + 2 * halfKernelSize;
    141 
    142         SkAutoTArray<float> bulkAlloc(halfKernelSize + halfKernelSize + numYSteps);
    143         float* halfKernel = bulkAlloc.get();
    144         float* summedKernel = bulkAlloc.get() + halfKernelSize;
    145         float* yEvals = bulkAlloc.get() + 2 * halfKernelSize;
    146         make_half_kernel_and_summed_table(halfKernel, summedKernel, halfKernelSize, sigma);
    147 
    148         float firstX = -halfKernelSize + 0.5f;
    149         apply_kernel_in_y(yEvals, numYSteps, firstX, circleR, halfKernelSize, summedKernel);
    150 
    151         for (int i = 0; i < numSteps - 1; ++i) {
    152             float evalX = i + 0.5f;
    153             weights[i] = eval_at(evalX, circleR, halfKernel, halfKernelSize, yEvals + i);
    154         }
    155         // Ensure the tail of the Gaussian goes to zero.
    156         weights[numSteps - 1] = 0;
    157         return weights;
    158     }
    159 
    160     static uint8_t* create_half_plane_profile(int profileWidth) {
    161         SkASSERT(!(profileWidth & 0x1));
    162         // The full kernel is 6 sigmas wide.
    163         float sigma = profileWidth / 6.f;
    164         int halfKernelSize = profileWidth / 2;
    165 
    166         SkAutoTArray<float> halfKernel(halfKernelSize);
    167         uint8_t* profile = new uint8_t[profileWidth];
    168 
    169         // The half kernel should sum to 0.5.
    170         const float tot = 2.f * make_unnormalized_half_kernel(halfKernel.get(), halfKernelSize,
    171                                                               sigma);
    172         float sum = 0.f;
    173         // Populate the profile from the right edge to the middle.
    174         for (int i = 0; i < halfKernelSize; ++i) {
    175             halfKernel[halfKernelSize - i - 1] /= tot;
    176             sum += halfKernel[halfKernelSize - i - 1];
    177             profile[profileWidth - i - 1] = SkUnitScalarClampToByte(sum);
    178         }
    179         // Populate the profile from the middle to the left edge (by flipping the half kernel and
    180         // continuing the summation).
    181         for (int i = 0; i < halfKernelSize; ++i) {
    182             sum += halfKernel[i];
    183             profile[halfKernelSize - i - 1] = SkUnitScalarClampToByte(sum);
    184         }
    185         // Ensure tail goes to 0.
    186         profile[profileWidth - 1] = 0;
    187         return profile;
    188     }
    189 
    190     static sk_sp<GrTextureProxy> create_profile_texture(GrProxyProvider* proxyProvider,
    191                                                         const SkRect& circle,
    192                                                         float sigma,
    193                                                         float* solidRadius, float* textureRadius) {
    194         float circleR = circle.width() / 2.0f;
    195         if (circleR < SK_ScalarNearlyZero) {
    196             return nullptr;
    197         }
    198         // Profile textures are cached by the ratio of sigma to circle radius and by the size of the
    199         // profile texture (binned by powers of 2).
    200         SkScalar sigmaToCircleRRatio = sigma / circleR;
    201         // When sigma is really small this becomes a equivalent to convolving a Gaussian with a
    202         // half-plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to the
    203         // Guassian and the profile texture is a just a Gaussian evaluation. However, we haven't yet
    204         // implemented this latter optimization.
    205         sigmaToCircleRRatio = SkTMin(sigmaToCircleRRatio, 8.f);
    206         SkFixed sigmaToCircleRRatioFixed;
    207         static const SkScalar kHalfPlaneThreshold = 0.1f;
    208         bool useHalfPlaneApprox = false;
    209         if (sigmaToCircleRRatio <= kHalfPlaneThreshold) {
    210             useHalfPlaneApprox = true;
    211             sigmaToCircleRRatioFixed = 0;
    212             *solidRadius = circleR - 3 * sigma;
    213             *textureRadius = 6 * sigma;
    214         } else {
    215             // Convert to fixed point for the key.
    216             sigmaToCircleRRatioFixed = SkScalarToFixed(sigmaToCircleRRatio);
    217             // We shave off some bits to reduce the number of unique entries. We could probably
    218             // shave off more than we do.
    219             sigmaToCircleRRatioFixed &= ~0xff;
    220             sigmaToCircleRRatio = SkFixedToScalar(sigmaToCircleRRatioFixed);
    221             sigma = circleR * sigmaToCircleRRatio;
    222             *solidRadius = 0;
    223             *textureRadius = circleR + 3 * sigma;
    224         }
    225 
    226         static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
    227         GrUniqueKey key;
    228         GrUniqueKey::Builder builder(&key, kDomain, 1);
    229         builder[0] = sigmaToCircleRRatioFixed;
    230         builder.finish();
    231 
    232         sk_sp<GrTextureProxy> blurProfile =
    233                       proxyProvider->findOrCreateProxyByUniqueKey(key, kTopLeft_GrSurfaceOrigin);
    234         if (!blurProfile) {
    235             static constexpr int kProfileTextureWidth = 512;
    236             GrSurfaceDesc texDesc;
    237             texDesc.fOrigin = kTopLeft_GrSurfaceOrigin;
    238             texDesc.fWidth = kProfileTextureWidth;
    239             texDesc.fHeight = 1;
    240             texDesc.fConfig = kAlpha_8_GrPixelConfig;
    241 
    242             std::unique_ptr<uint8_t[]> profile(nullptr);
    243             if (useHalfPlaneApprox) {
    244                 profile.reset(create_half_plane_profile(kProfileTextureWidth));
    245             } else {
    246                 // Rescale params to the size of the texture we're creating.
    247                 SkScalar scale = kProfileTextureWidth / *textureRadius;
    248                 profile.reset(create_circle_profile(sigma * scale, circleR * scale,
    249                                                     kProfileTextureWidth));
    250             }
    251 
    252             blurProfile = proxyProvider->createTextureProxy(texDesc, SkBudgeted::kYes,
    253                                                             profile.get(), 0);
    254             if (!blurProfile) {
    255                 return nullptr;
    256             }
    257 
    258             SkASSERT(blurProfile->origin() == kTopLeft_GrSurfaceOrigin);
    259             proxyProvider->assignUniqueKeyToProxy(key, blurProfile.get());
    260         }
    261 
    262         return blurProfile;
    263     }
    264 
    265     std::unique_ptr<GrFragmentProcessor> GrCircleBlurFragmentProcessor::Make(
    266             GrProxyProvider* proxyProvider, const SkRect& circle, float sigma) {
    267         float solidRadius;
    268         float textureRadius;
    269         sk_sp<GrTextureProxy> profile(create_profile_texture(proxyProvider, circle, sigma,
    270                                                              &solidRadius, &textureRadius));
    271         if (!profile) {
    272             return nullptr;
    273         }
    274         return std::unique_ptr<GrFragmentProcessor>(new GrCircleBlurFragmentProcessor(
    275                 circle, textureRadius, solidRadius, std::move(profile)));
    276     }
    277 }
    278 
    279 void main() {
    280     // We just want to compute "(length(vec) - circleData.z + 0.5) * circleData.w" but need to
    281     // rearrange for precision.
    282     half2 vec = half2((sk_FragCoord.x - circleData.x) * circleData.w,
    283                       (sk_FragCoord.y - circleData.y) * circleData.w);
    284     half dist = length(vec) + (0.5 - circleData.z) * circleData.w;
    285     sk_OutColor = sk_InColor * texture(blurProfileSampler, half2(dist, 0.5)).a;
    286 }
    287 
    288 @test(testData) {
    289     SkScalar wh = testData->fRandom->nextRangeScalar(100.f, 1000.f);
    290     SkScalar sigma = testData->fRandom->nextRangeF(1.f,10.f);
    291     SkRect circle = SkRect::MakeWH(wh, wh);
    292     return GrCircleBlurFragmentProcessor::Make(testData->proxyProvider(), circle, sigma);
    293 }
    294