乐趣区

关于webgl:webgl智慧楼宇发光效果算法系列之高斯模糊

webgl 智慧楼宇发光成果算法系列之高斯含糊

如果应用过 PS 之类的图像处理软件,置信对于含糊滤镜不会生疏,图像处理软件提供了泛滥的含糊算法。高斯含糊是其中的一种。

在咱们的智慧楼宇的我的项目中,要求对楼宇实现楼宇发光的成果。比方如下图所示的简略楼宇成果:

楼宇发光成果须要用的算法之一就是高斯含糊。

高斯含糊简介

高斯含糊算法是计算机图形学畛域中一种应用宽泛的技术, 是一种图像空间成果,用于对图像进行含糊解决,创立原始图像的柔和含糊版本。
应用高斯含糊的成果,联合一些其余的算法,还能够产生发光,光晕,景深,热雾和含糊玻璃成果。

高斯含糊的原理阐明

图像含糊的原理,简略而言,就是针对图像的每一个像素,其色彩取其周边像素的平均值。不同的含糊算法,对周边的定义不一样,均匀的算法也不一样。比方之前写 #过的一篇文章,webgl 实现径向含糊, 就是含糊算法中的一种。

均值含糊

在了解高斯含糊之前,咱们先了解比拟容易的均值含糊。所谓均值含糊
其原理就是取像素点四周(上下左右)像素的平均值(其中也会包含本身)。如下图所示:

能够看出,对于某个像素点,当搜寻半径为 1 的时候,影响其色彩值的像素是 9 个像素(包含本人和周边的 8 个像素)。假如每个像素对于核心像素的影响都是一样的,那么每个像素的影响度就是 1 /9。如下图所示:

下面这个 3 * 3 的影响度的数字矩阵,通常称之为卷积核。

那么最终中心点的值的求和如下图所示:

最终的值是:

(8 *  1 + 1 * 2 / (8 + 1) ) = 10/9

当计算像素的色彩时候,对于像素的 RGB 每一个通道都进行的上述均匀计算即可。

下面的计算过程就是一种卷积滤镜。所谓卷积滤镜,艰深来说,就是一种组合一组数值的算法。

如果搜寻半径变成 2,则会变成 25 个像素的均匀,搜寻半径越大,就会越含糊。像素个数与搜寻半径的关系如下:

(1 + r * 2)的平方 // r = 1,后果为 9,r=2,后果为 25,r=3 后果为 49.

通常 NxN 会被称之卷积核的大小。比方 3 ×3,5×5。

在均值含糊的计算中,参加的每个像素,对核心像素的奉献值都是一样的,这是均值含糊的特点。也就是,每个像素的权重都是一样的。

正态分布

如果应用简略均匀,显然不是很正当,因为图像都是间断的,越凑近的点关系越亲密,越远离的点关系越疏远。因而,加权均匀更正当,间隔越近的点权重越大,间隔越远的点权重越小。

正态分布整好满足上述的的散布需要,如下图所示:

能够看出,正态分布是一种钟形曲线,越靠近核心,取值越大,越远离核心,取值越小。

在计算平均值的时候,咱们只须要将 ” 中心点 ” 作为原点,其余点依照其在正态曲线上的地位,调配权重,就能够失去一个加权平均值。

高斯函数

高斯函数是形容正态分布的数学公式。公式如下:

其中,μ 是 x 的均值,能够了解为正态分布的核心地位,σ 是 x 的方差。因为计算平均值的时候,中心点就是原点,所以 μ 等于 0。

如果是二维,则有:

能够看出二维高斯函数中,x 和 y 绝对是独立的。也就是说:

G(x,y) = G(x) + G(y)

这个个性的益处是,能够把二维的高斯函数,拆解成两个独立的一维高斯函数。能够提高效率。实际上,高斯含糊使用的一维高斯函数,而不是应用二维。

高斯含糊

高斯含糊的原理和后面介绍的均值含糊的原理基本上一样,只是均值含糊在计算平均值的时候,周边像素的权重都是一样的。而高斯含糊下,周边像素的权重值却应用高斯函数进行计算,这也是高斯含糊的之所以被称为高斯含糊的起因。

比方当 σ 取值为则含糊半径为 1 的权重矩阵如下:

这 9 个点的权重总和等于 0.4787147,如果只计算这 9 个点的加权均匀,还必须让它们的权重之和等于 1,因而下面 9 个值还要别离除以 0.4787147,失去最终的权重矩阵。

渲染流程

理解了高斯含糊的基本原理之后,来看看高斯含糊在 webgl 中根本渲染流程:

  1. 首先,依照失常流程把场景或者图像渲染到一个纹理对象下面,须要应用 FrameBuffer 性能。
  2. 对纹理对象进行施加高斯含糊算法,失去最终的高斯含糊的纹理对象。

下面第二部,施加高斯含糊算法,个别又会分成两步:

  1. 先施加垂直方向的高斯含糊算法;
  2. 在垂直含糊的根底上进行程度方向的高斯含糊算法。

当然,也能够先程度后垂直,后果是一样的。分两步高斯含糊算法和一步进行两个方向的高斯含糊算法的后果根本是统一的,然而却能够进步算法的效率。有人可能说,多含糊了一步,为啥还进步了效率。这么来说吧,如果是 3 ×3 大小的高斯含糊:
分两步要获取的像素数量是 3 + 3 = 6;而一步却是 3 x 3 = 9。如果是 5 ×5 大小的高斯含糊:分两步要获取的像素数量是 5+5=10;而一步却是 5 x 5=25。显然能够算法执行效率。

渲染流程代码

对于第一步,首先是渲染到纹理对象,这输出渲染到纹理的常识,此处不再赘述,大抵大代码构造如下:
···
frameBuffer.bind();
renderScene();
frameBuffer.unbind();
···

把 renderScene 放到 frameBuffer.bind 之后,会把场景绘制到 frameBuffer 关联的纹理对象下面。

而后是第二步,执行高斯含糊算法进行

pass(params={},count = 1,inputFrameBuffer){let {options,fullScreen} = this;
        inputFrameBuffer = inputFrameBuffer || this.inputFrameBuffer;
        let {gl,gaussianBlurProgram,verticalBlurFrameBuffer,horizontalBlurFrameBuffer} = this;
        let {width,height} = options;    

        gl.useProgram(gaussianBlurProgram);
        if(width == null){
          width = verticalBlurFrameBuffer.width;
          height = verticalBlurFrameBuffer.height;
        }
        verticalBlurFrameBuffer.bind();
        fullScreen.enable(gaussianBlurProgram,true);
        gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); //  激活 gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
    

        fullScreen.draw();
        verticalBlurFrameBuffer.unbind();

        if(horizontalBlurFrameBuffer){  // renderToScreen
          horizontalBlurFrameBuffer.bind(gl);
        }
        gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); //  激活 gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 程度方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);

        fullScreen.draw();
        if(horizontalBlurFrameBuffer){horizontalBlurFrameBuffer.unbind();
        }
        if(count > 1){this.pass(params,count - 1,this.horizontalBlurFrameBuffer);
        }
        return horizontalBlurFrameBuffer;
        
    }

其中 inputFrameBuffer 是第一步渲染时候的 frameBuffer 对象,作为输出参数传递过去。而后开始执行垂直方向的高斯含糊算法,

verticalBlurFrameBuffer.bind();
        fullScreen.enable(gaussianBlurProgram,true);
        gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); //  激活 gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
    

        fullScreen.draw();
        verticalBlurFrameBuffer.unbind();

在之后执行程度方向的含糊算法:

 if(horizontalBlurFrameBuffer){  // renderToScreen
          horizontalBlurFrameBuffer.bind(gl);
        }
        gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); //  激活 gl.TEXTURE0
        gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 绑定贴图对象
        gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
        gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
        gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 程度方向
        gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2); 
        gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
        gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);

        fullScreen.draw();
        if(horizontalBlurFrameBuffer){horizontalBlurFrameBuffer.unbind();
        }

shader 代码

shader 代码分成两局部,一个顶点着色器代码:

const gaussianBlurVS =  `
  attribute vec3 aPosition;
  attribute vec2 aUv;
  varying vec2 vUv;
  void main() {
    vUv = aUv;
    gl_Position = vec4(aPosition, 1.0);
  }
`;

另外一个是片元着色器代码:

const gaussianBlurFS = `
precision highp float;
precision highp int;
#define HIGH_PRECISION
#define SHADER_NAME ShaderMaterial
#define MAX_KERNEL_RADIUS 49
#define SIGMA 11
varying vec2 vUv;
uniform sampler2D uColorTexture;
uniform vec2 uTexSize;
uniform vec2 uDirection;
uniform float uExposure;
uniform bool uUseLinear;
uniform float uRadius;

float gaussianPdf(in float x, in float sigma) {return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
}
void main() {
  vec2 invSize = 1.0 / uTexSize;
  float fSigma = float(SIGMA);
  float weightSum = gaussianPdf(0.0, fSigma);
  vec4 diffuseSum = texture2D(uColorTexture, vUv).rgba * weightSum;
  float radius = uRadius;

  for(int i = 1; i < MAX_KERNEL_RADIUS; i ++) {float x = float(i);
    if(x > radius){break;}
    float gaussianPdf(x, fSigma),t = x;
    vec2 uvOffset = uDirection * invSize * t;
    vec4 sample1 = texture2D(uColorTexture, vUv + uvOffset).rgba;
    vec4 sample2 = texture2D(uColorTexture, vUv - uvOffset).rgba;
    diffuseSum += (sample1 + sample2) * w;
    weightSum += 2.0 * w;
   
  }
  vec4 result = vec4(1.0) - exp(-diffuseSum/weightSum * uExposure);
  gl_FragColor = result;
}
`

最终渲染的成果如下,案例中渲染的是一个球体的线框:

利用案例

目前我的项目中用到的次要是发光楼宇的成果。上面是几个案例图,分享给大家看看:

当然还有更多的利用场景,读者能够自行摸索。

参考文档

http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html

结语

如果对可视化感兴趣,能够和我交换,微信 541002349. 另外关注公众号“ITMan 彪叔”能够及时收到更多有价值的文章。

退出移动版