webgl智慧樓宇發光效果算法系列之高斯模糊

webgl智慧樓宇發光效果算法系列之高斯模糊

若是使用過PS之類的圖像處理軟件,相信對於模糊濾鏡不會陌生,圖像處理軟件提供了衆多的模糊算法。高斯模糊是其中的一種。html

在咱們的智慧樓宇的項目中,要求對樓宇實現樓宇發光的效果。 好比以下圖所示的簡單樓宇效果:web

樓宇發光效果須要用的算法之一就是高斯模糊。算法

高斯模糊簡介

高斯模糊算法是計算機圖形學領域中一種使用普遍的技術, 是一種圖像空間效果,用於對圖像進行模糊處理,建立原始圖像的柔和模糊版本。
使用高斯模糊的效果,結合一些其餘的算法,還能夠產生髮光,光暈,景深,熱霧和模糊玻璃效果。微信

高斯模糊的原理說明

圖像模糊的原理,簡單而言,就是針對圖像的每個像素,其顏色取其周邊像素的平均值。不一樣的模糊算法,對周邊的定義不同,平均的算法也不同。 好比以前寫#過的一篇文章,webgl實現徑向模糊,就是模糊算法中的一種。函數

均值模糊

在理解高斯模糊以前,咱們先理解比較容易的均值模糊。所謂均值模糊
其原理就是取像素點周圍(上下左右)像素的平均值(其中也會包括自身)。以下圖所示:
webgl

能夠看出,對於某個像素點,當搜索半徑爲1的時候,影響其顏色值的像素是9個像素(包括本身和周邊的8個像素)。假設每一個像素對於中心像素的影響都是同樣的,那麼每一個像素的影響度就是1/9。以下圖所示:
this

上面這個3*3的影響度的數字矩陣,一般稱之爲卷積核。spa

那麼最終中心點的值的求和以下圖所示:

最終的值是:3d

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

當計算像素的顏色時候,對於像素的RGB每個通道都進行的上述平均計算便可。code

上面的計算過程就是一種卷積濾鏡。所謂卷積濾鏡,通俗來講,就是一種組合一組數值的算法。

若是搜索半徑變成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彪叔」 能夠及時收到更多有價值的文章。

相關文章
相關標籤/搜索