webgl智慧樓宇發光效果算法系列之高斯模糊
阿新 • • 發佈:2020-12-01
# webgl智慧樓宇發光效果算法系列之高斯模糊
如果使用過PS之類的影象處理軟體,相信對於模糊濾鏡不會陌生,影象處理軟體提供了眾多的模糊演算法。高斯模糊是其中的一種。
在我們的智慧樓宇的專案中,要求對樓宇實現樓宇發光的效果。 比如如下圖所示的簡單樓宇效果:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/22455304fbba498690d98078a14be1e0~tplv-k3u1fbpfcp-zoom-1.image)
樓宇發光效果需要用的演算法之一就是高斯模糊。
# 高斯模糊簡介
高斯模糊演算法是計算機圖形學領域中一種使用廣泛的技術, 是一種影象空間效果,用於對影象進行模糊處理,建立原始影象的柔和模糊版本。
使用高斯模糊的效果,結合一些其他的演算法,還可以產生髮光,光暈,景深,熱霧和模糊玻璃效果。
# 高斯模糊的原理說明
影象模糊的原理,簡單而言,就是針對影象的每一個畫素,其顏色取其周邊畫素的平均值。不同的模糊演算法,對周邊的定義不一樣,平均的演算法也不一樣。 比如之前寫#過的一篇文章,[webgl實現徑向模糊](https://mp.weixin.qq.com/s/0MCkDg2N84jozamEICQRKA),就是模糊演算法中的一種。
### 均值模糊
在理解高斯模糊之前,我們先理解比較容易的均值模糊。所謂均值模糊
其原理就是取畫素點周圍(上下左右)畫素的平均值(其中也會包括自身)。如下圖所示:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0527bc9407634e35b9ef212bdb02694e~tplv-k3u1fbpfcp-zoom-1.image)
可以看出,對於某個畫素點,當搜尋半徑為1的時候,影響其顏色值的畫素是9個畫素(包括自己和周邊的8個畫素)。假設每個畫素對於中心畫素的影響都是一樣的,那麼每個畫素的影響度就是1/9。如下圖所示:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83680bb9e40044fa82eddc5e248cbe41~tplv-k3u1fbpfcp-zoom-1.image)
上面這個3*3的影響度的數字矩陣,通常稱之為卷積核。
那麼最終中心點的值的求和如下圖所示:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2a48baa0cbe48e6b149b7243ec04ee2~tplv-k3u1fbpfcp-zoom-1.image)
最終的值是:
```
(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。
在均值模糊的計算中,參與的每個畫素,對中心畫素的貢獻值都是一樣的,這是均值模糊的特點。也就是,每個畫素的權重都是一樣的。
### 正態分佈
如果使用簡單平均,顯然不是很合理,因為影象都是連續的,越靠近的點關係越密切,越遠離的點關係越疏遠。因此,加權平均更合理,距離越近的點權重越大,距離越遠的點權重越小。
正態分佈整好滿足上述的的分佈需求,如下圖所示:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c2af29067dbc48d5b253ebce2caf2110~tplv-k3u1fbpfcp-zoom-1.image)
可以看出,正態分佈是一種鐘形曲線,越接近中心,取值越大,越遠離中心,取值越小。
在計算平均值的時候,我們只需要將"中心點"作為原點,其他點按照其在正態曲線上的位置,分配權重,就可以得到一個加權平均值。
### 高斯函式
高斯函式是描述正態分佈的數學公式。公式如下:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f243cd5b01b47e083de00ae6275cb48~tplv-k3u1fbpfcp-zoom-1.image)
其中,μ是x的均值,可以理解為正態分佈的中心位置,σ是x的方差。因為計算平均值的時候,中心點就是原點,所以μ等於0。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2c7ec1b1bf684ecd9b0e0cd28e7aa1ab~tplv-k3u1fbpfcp-zoom-1.image)
如果是二維,則有:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8577eafe1d354c92bcfb172a43a3dd11~tplv-k3u1fbpfcp-zoom-1.image)
可以看出二維高斯函式中,x和y相對是獨立的。也就是說:
```
G(x,y) = G(x) + G(y)
```
這個特性的好處是,可以把二維的高斯函式,拆解成兩個獨立的一維高斯函式。可以提高效率。實際上,高斯模糊運用的一維高斯函式,而不是使用二維。
### 高斯模糊
高斯模糊的原理和前面介紹的均值模糊的原理基本上一樣,只是均值模糊在計算平均值的時候,周邊畫素的權重都是一樣的。而高斯模糊下,周邊畫素的權重值卻使用高斯函式進行計算,這也是高斯模糊的之所以被稱為高斯模糊的原因。
比如當σ取值為則模糊半徑為1的權重矩陣如下:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18ae3d2475bf433d95377e740ab26a4d~tplv-k3u1fbpfcp-zoom-1.image)
這9個點的權重總和等於0.4787147,如果只計算這9個點的加權平均,還必須讓它們的權重之和等於1,因此上面9個值還要分別除以0.4787147,得到最終的權重矩陣。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60ae2b7274424ab5ad836ef759eb5ddd~tplv-k3u1fbpfcp-zoom-1.image)
# 渲染流程
瞭解了高斯模糊的基本原理之後,來看看高斯模糊在webgl中基本渲染流程:
1. 首先,按照正常流程把場景或者影象渲染到一個紋理物件上面,需要使用FrameBuffer功能。
1. 對紋理物件進行施加高斯模糊演算法,得到最終的高斯模糊的紋理物件。
上面第二部,施加高斯模糊演算法,一般又會分成兩步:
1. 先施加垂直方向的高斯模糊演算法;
1. 在垂直模糊的基礎上進行水平方向的高斯模糊演算法。
當然,也可以先水平後垂直,結果是一樣的。 分兩步高斯模糊演算法和一步進行兩個方向的高斯模糊演算法的結果基本是一致的,但是卻可以提高演算法的效率。 有人可能說,多模糊了一步,為啥還提高了效率。 這麼來說吧,如果是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;
}
`
```
最終渲染的效果如下,案例中渲染的是一個球體的線框:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/553c7e7f376e42a6bfa25b30be137d74~tplv-k3u1fbpfcp-zoom-1.image)
## 應用案例
目前專案中用到的主要是發光樓宇的效果。 下面是幾個案例圖,分享給大家看看:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36fa01212e254a87ad5c46c4de4627d8~tplv-k3u1fbpfcp-zoom-1.image)
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8614f7cccf354f87bb78dddf133fe70f~tplv-k3u1fbpfcp-zoom-1.image)
當然還有更多的應用場景,讀者可以自行探索。
# 參考文件
[http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html](http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html)
# 結語
如果對視覺化感興趣,可以和我交流,微信541002349. 另外關注公眾號“ITMan彪叔” 可以及時收到更多有價值的