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會被稱之卷積核的大小。比如3x3,5x5。

在均值模糊的計算中,參與的每個像素,對中心像素的貢獻值都是一樣的,這是均值模糊的特點。也就是,每個像素的權重都是一樣的。

正態分佈

如果使用簡單平均,顯然不是很合理,因爲圖像都是連續的,越靠近的點關係越密切,越遠離的點關係越疏遠。因此,加權平均更合理,距離越近的點權重越大,距離越遠的點權重越小。

正態分佈整好滿足上述的的分佈需求,如下圖所示:

可以看出,正態分佈是一種鐘形曲線,越接近中心,取值越大,越遠離中心,取值越小。

在計算平均值的時候,我們只需要將"中心點"作爲原點,其他點按照其在正態曲線上的位置,分配權重,就可以得到一個加權平均值。

高斯函數

高斯函數是描述正態分佈的數學公式。公式如下:

其中,μ是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. 在垂直模糊的基礎上進行水平方向的高斯模糊算法。 當然,也可以先水平後垂直,結果是一樣的。   分兩步高斯模糊算法和一步進行兩個方向的高斯模糊算法的結果基本是一致的,但是卻可以提高算法的效率。 有人可能說,多模糊了一步,爲啥還提高了效率。 這麼來說吧,如果是3x3大小的高斯模糊: 分兩步要獲取的像素數量是 3 + 3 = 6; 而一步卻是3 x 3 = 9。 如果是5x5大小的高斯模糊:分兩步要獲取的像素數量是 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彪叔” 可以及時收到更多有價值的文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章