Шейдер Размытие по Гауссу

внешний интерфейс алгоритм WebGL

Алгоритм размытия по Гауссу можно увидеть написанным Руаном Ифэном.эта статья, размытие по Гауссу называется размытием по Гауссу, потому что оно использует функцию плотности нормального распределения по Гауссу:

Одномерная форма:

2D форма:

Соответствующие этапы расчета:

Восстанавливаем шейдерПиндуодуо изданиеПростая версия алгоритма размытия по Гауссу:

void main() {
    vec2 st = gl_FragCoord / iResulution;

    vec4 color = vec4(0.0);
    const int coreSize = 3;         // 取 3x3 像素网格
    vec2 texelOffset = vec2(1.)/vec2(375., 667.);  // 每个素纹的间距

    float kernel[9];                // 卷积核,每个像素点的权重
    kernel[0] = 1.; kernel[1] = 2.; kernel[2] = 1.;
    kernel[3] = 2.; kernel[4] = 4.; kernel[5] = 2.;
    kernel[6] = 1.; kernel[7] = 2.; kernel[8] = 1.;

    int index = 0;
    for(int y=0; y<coreSize; y++)
    {
        for(int x = 0; x<coreSize; x++)
        {
            // 这是核心,依次取9个像素点的色值
            vec4 currentColor = texture2D(inputImageTexture, st + vec2(float(-1+x)*texelOffset.x, float(-1+y)*texelOffset.y));

            // 将颜色值和权重相乘,就是卷积运算
            if (index == 0) { color += currentColor * kernel[0]; }
            else if (index == 1) { color += currentColor * kernel[1]; }
            else if (index == 2) { color += currentColor * kernel[2]; }
            else if (index == 3) { color += currentColor * kernel[3]; }
            else if (index == 4) { color += currentColor * kernel[4]; }
            else if (index == 5) { color += currentColor * kernel[5]; }
            else if (index == 6) { color += currentColor * kernel[6]; }
            else if (index == 7) { color += currentColor * kernel[7]; }
            else if (index == 8) { color += currentColor * kernel[8]; }

            index++;
        }
    }
    
    // 除以权重总和
    color /= 16.0;
    gl_FragColor=color;
}

Эффект не очевиден, потому что наши веса не очень точны, а размер изображения относительно большой (750x1334).Если мы размываем только сетку 3x3, эффект недостаточно очевиден.Следующая оптимизация:

void main() {
    vec2 st = gl_FragCoord / iResulution;

    vec4 color = vec4(0.0);
    const int coreSize = 3;
    
    // 把素纹间隔放大,这里做法比较粗暴。合理的做法是把 3x3 变成 9x9 或更大
    vec2 texelOffset = vec2(2.)/vec2(375., 667.);

    // 代入高斯正态分布函数计算出来的权重值
    float kernel[9];
    kernel[0] = .0947416; kernel[1] = .118318; kernel[2] = .0947416;
    kernel[3] = .118318; kernel[4] = .147761; kernel[5] = .118318;
    kernel[6] = .0947416; kernel[7] = .118318; kernel[8] = .0947416;

    int index = 0;
    for(int y=0; y<coreSize; y++)
    {
        for(int x = 0; x<coreSize; x++)
        {
            vec4 currentColor = texture2D(inputImageTexture, st + vec2(float(-1+x)*texelOffset.x, float(-1+y)*texelOffset.y));

            if (index == 0) { color += currentColor * kernel[0]; }
            else if (index == 1) { color += currentColor * kernel[1]; }
            else if (index == 2) { color += currentColor * kernel[2]; }
            else if (index == 3) { color += currentColor * kernel[3]; }
            else if (index == 4) { color += currentColor * kernel[4]; }
            else if (index == 5) { color += currentColor * kernel[5]; }
            else if (index == 6) { color += currentColor * kernel[6]; }
            else if (index == 7) { color += currentColor * kernel[7]; }
            else if (index == 8) { color += currentColor * kernel[8]; }

            index++;
        }
    }
    // 上面的权重已经进行了加权平均,所以这一步不需要了
    // color /= 16.0;
    gl_FragColor = color;
}

Пока мы увеличиваем сетку 3x3, например 9x9/16x16, или напрямую увеличиваем расстояние между пикселями, мы можем усилить эффект размытия.

Однако производительность вышеприведенной реализации относительно низкая. Потому что стоимость прохождения слишком высока. Обычно разбивается на два одномерных вектора, так что временная сложность определяется выражениемNxNxWxHвплоть до2xNxWxH(W — ширина изображения, H — высота изображения).

Возьмем в качестве примера ядро ​​свертки 5x5 и вертикальную выборку:

void main() {
    vec2 iResolution = vec2(375., 667.);

    float offset[6];
    offset[1] = 0.; offset[2] = 1.; offset[3] = 2.; offset[4] = 3.; offset[5] = 4.;

    float weight[6];
    weight[1] = 0.2270270270; weight[2] = 0.1945945946; weight[3] = 0.1216216216;
    weight[4] = 0.0540540541; weight[5] = 0.0162162162; 

    vec4 color = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    for (int i=1; i<=5; i++) {
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(0.0, offset[i]))/iResolution)
                * weight[i];
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(0.0, offset[i]))/iResolution)
                * weight[i];
    }

    gl_FragColor = color;
}

То же самое верно и для горизонтального направления, но описанный выше метод все же нужно выполнить 9 раз.texture2D()операции выборки текстуры,эта статьяМетод линейной выборки представлен в , который сокращает 9 операций выборки текстуры до 5 за счет обработки весов и интервалов:

void main() {
    vec2 iResolution = vec2(375., 667.);

    float offset[4];
    offset[1] = 0.; offset[2] = 1.3846153846; offset[3] = 3.2307692308;

    float weight[4];
    weight[1] = 0.2270270270; weight[2] = 0.3162162162; weight[3] = 0.0702702703;

    vec4 color = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    // 垂直
    for (int i=1; i<=3; i++) {
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(0.0, offset[i]))/iResolution)
                * weight[i];
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(0.0, offset[i]))/iResolution)
                * weight[i];
    }

    vec4 color2 = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    // 水平
    for (int i=1; i<=3; i++) {
        color2 +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(offset[i], 0.0))/iResolution)
                * weight[i];
        color2 +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(offset[i], 0.0))/iResolution)
                * weight[i];
    }

    gl_FragColor = mix(color, color2, .5);
}

Невооруженным глазом разницы не будет видно, но производительность улучшится:

Еще один способ сделать изображение более размытым — усилить эффект размытия, многократно применяя функцию размытия к буферу кадра.


Конечно, есть много других реализаций для реализации размытия по Гауссу:
// 来自 https://github.com/Jam3/glsl-fast-gaussian-blur
// 只包含一个方向,需要自己叠加
// 3x3
vec4 blur5(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.3333333333333333) * direction;
  color += texture2D(image, uv) * 0.29411764705882354;
  color += texture2D(image, uv + (off1 / resolution)) * 0.35294117647058826;
  color += texture2D(image, uv - (off1 / resolution)) * 0.35294117647058826;
  return color; 
}
// 5x5
vec4 blur9(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.3846153846) * direction;
  vec2 off2 = vec2(3.2307692308) * direction;
  color += texture2D(image, uv) * 0.2270270270;
  color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162;
  color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162;
  color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703;
  color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703;
  return color;
}
// 7x7
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, uv) * 0.1964825501511404;
  color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057;
  color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057;
  return color;
}
// 来自 https://www.shadertoy.com/view/XdfGDH
// 正态分布概率密度函数
float normpdf(in float x, in float sigma) {
	return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma;
}
vec3 gaussianblur(int size, sampler2D texture, vec2 resolution) {
    //declare stuff
    const int mSize = size;
    const int kSize = (mSize-1)/2;
    float kernel[mSize];
    vec3 final_colour = vec3(0.0);
    
    //create the 1-D kernel
    float sigma = 7.0;
    float Z = 0.0;
    for (int j = 0; j <= kSize; ++j)
    {
        kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma);
    }
    
    //get the normalization factor (as the gaussian has been clamped)
    for (int j = 0; j < mSize; ++j)
    {
        Z += kernel[j];
    }
    
    //read out the texels
    for (int i=-kSize; i <= kSize; ++i)
    {
        for (int j=-kSize; j <= kSize; ++j)
        {
            final_colour += kernel[kSize+j]*kernel[kSize+i]*texture2D(texture, (gl_FragCoord.xy+vec2(float(i),float(j))) / resolution.xy).rgb;
        }
    }

    return final_colour/(Z*Z);
}
// 来自:https://gl-transitions.com/editor/LinearBlur
vec4 blur(vec2 _uv, sampler2D texture) {
    float disp = 0.;
    float intensity = .2;
    const int passes = 6;
    vec4 c1 = vec4(0.0);
    disp = intensity*(0.5-distance(0.5, .1));
  
    for (int xi=0; xi<passes; xi++) {
        float x = float(xi) / float(passes) - 0.5;
        for (int yi=0; yi<passes; yi++) {
            float y = float(yi) / float(passes) - 0.5;
            vec2 v = vec2(x, y);
            float d = disp;
            c1 += texture2D(texture, _uv + d*v);
        }
    }
    c1 /= float(passes*passes);
    return c1;
}

СледующийШейдер Размытие в движении.

Ссылки по теме: