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 void create_circle_profile(uint8_t* weights, float sigma, float circleR,
    131                                       int profileTextureWidth) {
    132         const int numSteps = profileTextureWidth;
    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     }
    158 
    159     static void create_half_plane_profile(uint8_t* profile, int profileWidth) {
    160         SkASSERT(!(profileWidth & 0x1));
    161         // The full kernel is 6 sigmas wide.
    162         float sigma = profileWidth / 6.f;
    163         int halfKernelSize = profileWidth / 2;
    164 
    165         SkAutoTArray<float> halfKernel(halfKernelSize);
    166 
    167         // The half kernel should sum to 0.5.
    168         const float tot = 2.f * make_unnormalized_half_kernel(halfKernel.get(), halfKernelSize,
    169                                                               sigma);
    170         float sum = 0.f;
    171         // Populate the profile from the right edge to the middle.
    172         for (int i = 0; i < halfKernelSize; ++i) {
    173             halfKernel[halfKernelSize - i - 1] /= tot;
    174             sum += halfKernel[halfKernelSize - i - 1];
    175             profile[profileWidth - i - 1] = SkUnitScalarClampToByte(sum);
    176         }
    177         // Populate the profile from the middle to the left edge (by flipping the half kernel and
    178         // continuing the summation).
    179         for (int i = 0; i < halfKernelSize; ++i) {
    180             sum += halfKernel[i];
    181             profile[halfKernelSize - i - 1] = SkUnitScalarClampToByte(sum);
    182         }
    183         // Ensure tail goes to 0.
    184         profile[profileWidth - 1] = 0;
    185     }
    186 
    187     static sk_sp<GrTextureProxy> create_profile_texture(GrProxyProvider* proxyProvider,
    188                                                         const SkRect& circle,
    189                                                         float sigma,
    190                                                         float* solidRadius, float* textureRadius) {
    191         float circleR = circle.width() / 2.0f;
    192         if (circleR < SK_ScalarNearlyZero) {
    193             return nullptr;
    194         }
    195         // Profile textures are cached by the ratio of sigma to circle radius and by the size of the
    196         // profile texture (binned by powers of 2).
    197         SkScalar sigmaToCircleRRatio = sigma / circleR;
    198         // When sigma is really small this becomes a equivalent to convolving a Gaussian with a
    199         // half-plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to the
    200         // Guassian and the profile texture is a just a Gaussian evaluation. However, we haven't yet
    201         // implemented this latter optimization.
    202         sigmaToCircleRRatio = SkTMin(sigmaToCircleRRatio, 8.f);
    203         SkFixed sigmaToCircleRRatioFixed;
    204         static const SkScalar kHalfPlaneThreshold = 0.1f;
    205         bool useHalfPlaneApprox = false;
    206         if (sigmaToCircleRRatio <= kHalfPlaneThreshold) {
    207             useHalfPlaneApprox = true;
    208             sigmaToCircleRRatioFixed = 0;
    209             *solidRadius = circleR - 3 * sigma;
    210             *textureRadius = 6 * sigma;
    211         } else {
    212             // Convert to fixed point for the key.
    213             sigmaToCircleRRatioFixed = SkScalarToFixed(sigmaToCircleRRatio);
    214             // We shave off some bits to reduce the number of unique entries. We could probably
    215             // shave off more than we do.
    216             sigmaToCircleRRatioFixed &= ~0xff;
    217             sigmaToCircleRRatio = SkFixedToScalar(sigmaToCircleRRatioFixed);
    218             sigma = circleR * sigmaToCircleRRatio;
    219             *solidRadius = 0;
    220             *textureRadius = circleR + 3 * sigma;
    221         }
    222 
    223         static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
    224         GrUniqueKey key;
    225         GrUniqueKey::Builder builder(&key, kDomain, 1, "1-D Circular Blur");
    226         builder[0] = sigmaToCircleRRatioFixed;
    227         builder.finish();
    228 
    229         sk_sp<GrTextureProxy> blurProfile =
    230                       proxyProvider->findOrCreateProxyByUniqueKey(key, kTopLeft_GrSurfaceOrigin);
    231         if (!blurProfile) {
    232             static constexpr int kProfileTextureWidth = 512;
    233 
    234             SkBitmap bm;
    235             if (!bm.tryAllocPixels(SkImageInfo::MakeA8(kProfileTextureWidth, 1))) {
    236                 return nullptr;
    237             }
    238 
    239             if (useHalfPlaneApprox) {
    240                 create_half_plane_profile(bm.getAddr8(0, 0), kProfileTextureWidth);
    241             } else {
    242                 // Rescale params to the size of the texture we're creating.
    243                 SkScalar scale = kProfileTextureWidth / *textureRadius;
    244                 create_circle_profile(bm.getAddr8(0, 0), sigma * scale, circleR * scale,
    245                                       kProfileTextureWidth);
    246             }
    247 
    248             bm.setImmutable();
    249             sk_sp<SkImage> image = SkImage::MakeFromBitmap(bm);
    250 
    251             blurProfile = proxyProvider->createTextureProxy(std::move(image), kNone_GrSurfaceFlags, 1,
    252                                                             SkBudgeted::kYes, SkBackingFit::kExact);
    253             if (!blurProfile) {
    254                 return nullptr;
    255             }
    256 
    257             SkASSERT(blurProfile->origin() == kTopLeft_GrSurfaceOrigin);
    258             proxyProvider->assignUniqueKeyToProxy(key, blurProfile.get());
    259         }
    260 
    261         return blurProfile;
    262     }
    263 
    264     std::unique_ptr<GrFragmentProcessor> GrCircleBlurFragmentProcessor::Make(
    265             GrProxyProvider* proxyProvider, const SkRect& circle, float sigma) {
    266         float solidRadius;
    267         float textureRadius;
    268         sk_sp<GrTextureProxy> profile(create_profile_texture(proxyProvider, circle, sigma,
    269                                                              &solidRadius, &textureRadius));
    270         if (!profile) {
    271             return nullptr;
    272         }
    273         return std::unique_ptr<GrFragmentProcessor>(new GrCircleBlurFragmentProcessor(
    274                 circle, textureRadius, solidRadius, std::move(profile)));
    275     }
    276 }
    277 
    278 void main() {
    279     // We just want to compute "(length(vec) - circleData.z + 0.5) * circleData.w" but need to
    280     // rearrange for precision.
    281     half2 vec = half2(half((sk_FragCoord.x - circleData.x) * circleData.w),
    282                       half((sk_FragCoord.y - circleData.y) * circleData.w));
    283     half dist = length(vec) + (0.5 - circleData.z) * circleData.w;
    284     sk_OutColor = sk_InColor * texture(blurProfileSampler, half2(dist, 0.5)).a;
    285 }
    286 
    287 @test(testData) {
    288     SkScalar wh = testData->fRandom->nextRangeScalar(100.f, 1000.f);
    289     SkScalar sigma = testData->fRandom->nextRangeF(1.f,10.f);
    290     SkRect circle = SkRect::MakeWH(wh, wh);
    291     return GrCircleBlurFragmentProcessor::Make(testData->proxyProvider(), circle, sigma);
    292 }
    293