blob: e5887329ae1018982f6d015e2550f8354ca7f6e0 [file] [log] [blame]
/*
* Copyright 2015 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "GrCircleBlurFragmentProcessor.h"
#if SK_SUPPORT_GPU
#include "GrContext.h"
#include "GrTextureProvider.h"
#include "gl/GrGLFragmentProcessor.h"
#include "gl/builders/GrGLProgramBuilder.h"
#include "glsl/GrGLSLProgramDataManager.h"
class GrGLCircleBlurFragmentProcessor : public GrGLFragmentProcessor {
public:
GrGLCircleBlurFragmentProcessor(const GrProcessor&) {}
void emitCode(EmitArgs&) override;
protected:
void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override;
private:
GrGLSLProgramDataManager::UniformHandle fDataUniform;
typedef GrGLFragmentProcessor INHERITED;
};
void GrGLCircleBlurFragmentProcessor::emitCode(EmitArgs& args) {
const char *dataName;
// The data is formatted as:
// x,y - the center of the circle
// z - the distance at which the intensity starts falling off (e.g., the start of the table)
// w - the size of the profile texture
fDataUniform = args.fBuilder->addUniform(GrGLProgramBuilder::kFragment_Visibility,
kVec4f_GrSLType,
kDefault_GrSLPrecision,
"data",
&dataName);
GrGLFragmentBuilder* fsBuilder = args.fBuilder->getFragmentShaderBuilder();
const char *fragmentPos = fsBuilder->fragmentPosition();
if (args.fInputColor) {
fsBuilder->codeAppendf("vec4 src=%s;", args.fInputColor);
} else {
fsBuilder->codeAppendf("vec4 src=vec4(1);");
}
fsBuilder->codeAppendf("vec2 vec = %s.xy - %s.xy;", fragmentPos, dataName);
fsBuilder->codeAppendf("float dist = (length(vec) - %s.z + 0.5) / %s.w;", dataName, dataName);
fsBuilder->codeAppendf("float intensity = ");
fsBuilder->appendTextureLookup(args.fSamplers[0], "vec2(dist, 0.5)");
fsBuilder->codeAppend(".a;");
fsBuilder->codeAppendf("%s = src * intensity;\n", args.fOutputColor );
}
void GrGLCircleBlurFragmentProcessor::onSetData(const GrGLSLProgramDataManager& pdman,
const GrProcessor& proc) {
const GrCircleBlurFragmentProcessor& cbfp = proc.cast<GrCircleBlurFragmentProcessor>();
const SkRect& circle = cbfp.circle();
// The data is formatted as:
// x,y - the center of the circle
// z - the distance at which the intensity starts falling off (e.g., the start of the table)
// w - the size of the profile texture
pdman.set4f(fDataUniform, circle.centerX(), circle.centerY(), cbfp.offset(),
SkIntToScalar(cbfp.profileSize()));
}
///////////////////////////////////////////////////////////////////////////////
GrCircleBlurFragmentProcessor::GrCircleBlurFragmentProcessor(const SkRect& circle,
float sigma,
float offset,
GrTexture* blurProfile)
: fCircle(circle)
, fSigma(sigma)
, fOffset(offset)
, fBlurProfileAccess(blurProfile, GrTextureParams::kBilerp_FilterMode) {
this->initClassID<GrCircleBlurFragmentProcessor>();
this->addTextureAccess(&fBlurProfileAccess);
this->setWillReadFragmentPosition();
}
GrGLFragmentProcessor* GrCircleBlurFragmentProcessor::onCreateGLInstance() const {
return new GrGLCircleBlurFragmentProcessor(*this);
}
void GrCircleBlurFragmentProcessor::onGetGLProcessorKey(const GrGLSLCaps& caps,
GrProcessorKeyBuilder* b) const {
GrGLCircleBlurFragmentProcessor::GenKey(*this, caps, b);
}
void GrCircleBlurFragmentProcessor::onComputeInvariantOutput(GrInvariantOutput* inout) const {
inout->mulByUnknownSingleComponent();
}
// Evaluate an AA circle function centered at the origin with 'radius' at (x,y)
static inline float disk(float x, float y, float radius) {
float distSq = x*x + y*y;
if (distSq <= (radius-0.5f)*(radius-0.5f)) {
return 1.0f;
} else if (distSq >= (radius+0.5f)*(radius+0.5f)) {
return 0.0f;
} else {
float ramp = radius + 0.5f - sqrtf(distSq);
SkASSERT(ramp >= 0.0f && ramp <= 1.0f);
return ramp;
}
}
// Create the top half of an even-sized Gaussian kernel
static void make_half_kernel(float* kernel, int kernelWH, float sigma) {
SkASSERT(!(kernelWH & 1));
const float kernelOff = (kernelWH-1)/2.0f;
float b = 1.0f / (2.0f * sigma * sigma);
// omit the scale term since we're just going to renormalize
float tot = 0.0f;
for (int y = 0; y < kernelWH/2; ++y) {
for (int x = 0; x < kernelWH/2; ++x) {
// TODO: use a cheap approximation of the 2D Guassian?
float x2 = (x-kernelOff) * (x-kernelOff);
float y2 = (y-kernelOff) * (y-kernelOff);
// The kernel is symmetric so only compute it once for both sides
kernel[y*kernelWH+(kernelWH-x-1)] = kernel[y*kernelWH+x] = expf(-(x2 + y2) * b);
tot += 2.0f * kernel[y*kernelWH+x];
}
}
// Still normalize the half kernel to 1.0 (rather than 0.5) so we don't
// have to scale by 2.0 after convolution.
for (int y = 0; y < kernelWH/2; ++y) {
for (int x = 0; x < kernelWH; ++x) {
kernel[y*kernelWH+x] /= tot;
}
}
}
// Apply the half-kernel at 't' away from the center of the circle
static uint8_t eval_at(float t, float halfWidth, float* halfKernel, int kernelWH) {
SkASSERT(!(kernelWH & 1));
const float kernelOff = (kernelWH-1)/2.0f;
float acc = 0;
for (int y = 0; y < kernelWH/2; ++y) {
if (kernelOff-y > halfWidth+0.5f) {
// All disk() samples in this row will be 0.0f
continue;
}
for (int x = 0; x < kernelWH; ++x) {
float image = disk(t - kernelOff + x, -kernelOff + y, halfWidth);
float kernel = halfKernel[y*kernelWH+x];
acc += kernel * image;
}
}
return SkUnitScalarClampToByte(acc);
}
static inline void compute_profile_offset_and_size(float halfWH, float sigma,
float* offset, int* size) {
if (3*sigma <= halfWH) {
// The circle is bigger than the Gaussian. In this case we know the interior of the
// blurred circle is solid.
*offset = halfWH - 3 * sigma; // This location maps to 0.5f in the weights texture.
// It should always be 255.
*size = SkScalarCeilToInt(6*sigma);
} else {
// The Gaussian is bigger than the circle.
*offset = 0.0f;
*size = SkScalarCeilToInt(halfWH + 3*sigma);
}
}
static uint8_t* create_profile(float halfWH, float sigma) {
int kernelWH = SkScalarCeilToInt(6.0f*sigma);
kernelWH = (kernelWH + 1) & ~1; // make it the next even number up
SkAutoTArray<float> halfKernel(kernelWH*kernelWH/2);
make_half_kernel(halfKernel.get(), kernelWH, sigma);
float offset;
int numSteps;
compute_profile_offset_and_size(halfWH, sigma, &offset, &numSteps);
uint8_t* weights = new uint8_t[numSteps];
for (int i = 0; i < numSteps; ++i) {
weights[i] = eval_at(offset+i, halfWH, halfKernel.get(), kernelWH);
}
return weights;
}
GrTexture* GrCircleBlurFragmentProcessor::CreateCircleBlurProfileTexture(
GrTextureProvider* textureProvider,
const SkRect& circle,
float sigma,
float* offset) {
float halfWH = circle.width() / 2.0f;
int size;
compute_profile_offset_and_size(halfWH, sigma, offset, &size);
GrSurfaceDesc texDesc;
texDesc.fWidth = size;
texDesc.fHeight = 1;
texDesc.fConfig = kAlpha_8_GrPixelConfig;
static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
GrUniqueKey key;
GrUniqueKey::Builder builder(&key, kDomain, 2);
// The profile curve varies with both the sigma of the Gaussian and the size of the
// disk. Quantizing to 16.16 should be close enough though.
builder[0] = SkScalarToFixed(sigma);
builder[1] = SkScalarToFixed(halfWH);
builder.finish();
GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key);
if (!blurProfile) {
SkAutoTDeleteArray<uint8_t> profile(create_profile(halfWH, sigma));
blurProfile = textureProvider->createTexture(texDesc, true, profile.get(), 0);
if (blurProfile) {
textureProvider->assignUniqueKeyToTexture(key, blurProfile);
}
}
return blurProfile;
}
GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrCircleBlurFragmentProcessor);
const GrFragmentProcessor* GrCircleBlurFragmentProcessor::TestCreate(GrProcessorTestData* d) {
SkScalar wh = d->fRandom->nextRangeScalar(100.f, 1000.f);
SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f);
SkRect circle = SkRect::MakeWH(wh, wh);
return GrCircleBlurFragmentProcessor::Create(d->fContext->textureProvider(), circle, sigma);
}
#endif