GLSL实现滤镜效果
? http://blog.csdn.net/neng18/article/details/38083987
"浮雕"圖象效果是指圖像的前景前向凸出背景。常見于一些紀念碑的雕刻上,要實現浮雕其實非常簡單。我們把圖象的一個象素和左上方的象素進行求差運算,并加上一個灰度。這個灰度就是表示背景顏色。這里我們設置這個插值為128 (圖象RGB的值是0-255)。同時,我們還應該把這兩個顏色的差值轉換為亮度信息.否則浮雕圖像會出現彩色。
"precision mediump float; \n" "varying vec2 v_texCoord; \n" "uniform sampler2D s_baseMap; \n" "uniform vec2 TexSize; \n" "void main() \n" "{ \n" " vec2 tex =v_texCoord; \n" " vec2 upLeftUV = vec2(tex.x-1.0/TexSize.x,tex.y-1.0/TexSize.y); \n" " vec4 curColor = texture2D(s_baseMap,v_texCoord); \n" " vec4 upLeftColor = texture2D(s_baseMap,upLeftUV); \n" " vec4 delColor = curColor - upLeftColor; \n" " float h = 0.3*delColor.x + 0.59*delColor.y + 0.11*delColor.z; \n" " vec4 bkColor = vec4(0.5, 0.5, 0.5, 1.0); \n" " gl_FragColor = vec4(h,h,h,0.0) +bkColor; \n" "} \n";
???
入門效果之馬賽克
接下來我們完成一個更加常見的效果—馬賽克.圖片的馬賽克就是把圖片的一個相當大小的區域用同一個點的顏色來表示.可以認為是大規模的降低圖像的分辨率,而讓圖像的一些細節隱藏起來, 比如電視中要秀一下某個罪犯的身材,卻又不能展示他的臉,這個時候我們就可以給他的臉加一個馬賽克.
用HLSL代碼實現馬賽克是非常簡單的,但是同樣的,我們需要一些額外的步驟,第一步就是先把紋理坐標轉換成圖像實際大小的整數坐標.接下來,我們要把圖像這個坐標量化---比如馬賽克塊的大小是8x8象素。那么我們可以用下列方法來得到馬賽克后的圖像采樣值,假設[x.y]為圖像的整數坐標:
[x,y]mosaic = [ int(x/8)*8 , int(y/8)*8].
"precision mediump float; \n" "varying vec2 v_texCoord; \n" "uniform sampler2D s_baseMap;\n" "uniform vec2 TexSize; \n" "vec2 mosaicSize = vec2(8,8);\n" "void main() \n" "{ \n" " vec2 intXY = vec2(v_texCoord.x*TexSize.x, v_texCoord.y*TexSize.y); \n" " vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x,floor(intXY.y/mosaicSize.y)*mosaicSize.y); \n" " vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y); \n" " vec4 baseMap = texture2D(s_baseMap,UVMosaic); \n" " gl_FragColor = baseMap; \n" "} \n";
讀者可能會發現這個馬賽克太普通了,確實它不夠新穎,下面我們來改良一下,我們希望達到這樣一個效果:馬賽克區域不是方的,而是圓的,圓形區域以外,我們用圖像原來的顏色覆蓋。這樣我們需要改變一下代碼。
首先求出原來馬賽克區域的正中心(原來是左上角):然后計算圖像采樣點到這個中心的距離,如果在馬賽克圓內,就用區域的中心顏色,否則就用原來的顏色。改良后的代碼如下,這里我們把馬賽克區域大小調節成16x16。這樣效果更明顯。
"precision highp float; \n" "varying vec2 v_texCoord; \n" "uniform sampler2D s_baseMap; \n" "uniform vec2 TexSize; \n" "vec2 mosaicSize = vec2(8,8); \n" "void main() \n" "{ \n" " vec2 intXY = vec2(v_texCoord.x*TexSize.x, v_texCoord.y*TexSize.y); \n" " vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x,floor(intXY.y/mosaicSize.y)*mosaicSize.y) + 0.5*mosaicSize; \n" " vec2 delXY = XYMosaic - intXY; \n" " float delL = length(delXY); \n" " vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y); \n" " vec4 _finalColor; \n" " if(delL< 0.5*mosaicSize.x) \n" " _finalColor = texture2D(s_baseMap,UVMosaic); \n" " else \n" " _finalColor = texture2D(s_baseMap,v_texCoord); \n" " gl_FragColor = _finalColor; \n" "} \n" ;
圖:? 改良后的馬賽克效果
l?進階效果之銳化模糊
以上兩個效果相對比較簡單,姑且稱之為入門效果, 它并沒有用到太多數字圖像處理或者信號處理方面的知識。接下來我們要介紹稍微復雜一點的效果,第一個就是圖像的模糊和銳化。
圖像的模糊又成為圖像的平滑(smoothing),我們知道人眼對高頻成分是非常敏感的,如果在一個亮度連續變化的圖像中,突然出現一個亮點,那么我們很容易察覺出來,類似的,如果圖像有個突然的跳躍—明顯的邊緣,我們也是很容易察覺出來的。這些突然變化的分量就是圖像的高頻成分。人眼通常是通過低頻成分來辨別輪廓,通過高頻成分來感知細節的(這也是為什么照片分辨率低的時候,人們只能辨認出照片的大概輪廓,而看不到細節)。但是這些高頻成分通常也包含了噪聲成分。圖像的平滑處理就是濾除圖像的高頻成分。
那么如何才能濾除圖像的高頻成分呢?我們先來介紹一下圖像數字濾波器的概念。
簡單通俗的來說,圖像的數字濾波器其實就是一個n x n的數組(數組中的元素成為濾波器的系數或者濾波器的權重,n稱為濾波器的階)。對圖像做濾波的時候,把某個像素為中心的nxn個像素的值和這個濾波器做卷積運算(也就是對應位置上的像素和對應位置上的權重的乘積累加起來),公式如下
其中x , y 為當前正在處理的像素坐標。
通常情況下,我們濾波器的階數為3已經足夠了,用于模糊處理的3x3濾波器如下
?????? ??????????????????????????。
經過這樣的濾波器,其實就是等效于把一個像素和周圍8個像素一起求平均值,這是非常合理的---等于把一個像素和周圍幾個像素攪拌在一起—自然就模糊了。
"precision mediump float; \n" "vec4 dip_filter(mat3 _filter, sampler2D _image, vec2 _xy, vec2 texSize) \n" "{ \n" " mat3 _filter_pos_delta_x=mat3(vec3(-1.0, 0.0, 1.0), vec3(0.0, 0.0 ,1.0) ,vec3(1.0,0.0,1.0)); \n" " mat3 _filter_pos_delta_y=mat3(vec3(-1.0,-1.0,-1.0),vec3(-1.0,0.0,0.0),vec3(-1.0,1.0,1.0)); \n" " vec4 final_color = vec4(0.0, 0.0, 0.0, 0.0); \n" " for(int i = 0; i<3; i++) \n" " { \n" " for(int j = 0; j<3; j++) \n" " { \n" " vec2 _xy_new = vec2(_xy.x + _filter_pos_delta_x[i][j], _xy.y + _filter_pos_delta_y[i][j]); \n" " vec2 _uv_new = vec2(_xy_new.x/texSize.x, _xy_new.y/texSize.y); \n" " final_color += texture2D(_image,_uv_new) * _filter[i][j]; \n" " } \n" " } \n" " return final_color; \n" "} \n" "varying vec2 v_texCoord; \n" "uniform vec2 TexSize; \n" "uniform sampler2D s_baseMap; \n" "void main() \n" "{ \n" " vec2 intXY = vec2(v_texCoord.x * TexSize.x, v_texCoord.y * TexSize.y); \n" " mat3 _smooth_fil = mat3(1.0/9.0,1.0/9.0,1.0/9.0, \n" " 1.0/9.0,1.0/9.0,1.0/9.0, \n" " 1.0/9.0,1.0/9.0,1.0/9.0); \n" " vec4 tmp = dip_filter(_smooth_fil, s_baseMap, intXY, TexSize);" " gl_FragColor = tmp; \n" "} \n";以上的模糊濾波器稱為BOX濾波器,是最簡單的濾波器,如果考慮到離開中心像素的距離對濾波器系數的影響,我們通常采用更加合理的濾波器---高斯濾波器—一種通過2維高斯采樣得到的濾波器,它的模板如下:
很容易看出來,離開中心越遠的像素,權重系數越小。
對于銳化操作,常用的銳化模板是拉普拉斯(Laplacian)模板,這個模板定義如下:
容易看出拉普拉斯模板的作法:先將自身與周圍的8個象素相減,表示自身與周圍象素的差別;再將這個差別加上自身作為新象素的灰度。可見,如果一片暗區出現了一個亮點,那么銳化處理的結果是這個亮點變得更亮,這就增強了圖像的細節。
下面三副圖分別表示了經過BOX濾波。高斯濾波和拉普拉斯濾波后的圖像
? ?BOX 模糊????????????????? 高斯模糊??????????????????? 拉普拉斯銳化
高斯模糊和拉普拉斯銳化效果的GLSL和BOX的代碼基本一致,就是filter的系數不同,這里不在列出。
通過這個兩個效果,我們介紹了圖像的濾波操作,這樣的操作,也成為模板操作,它實現了一種鄰域運算(Neighborhood Operation),即某個象素點的結果灰度不僅和該象素灰度有關,而且和其鄰域點的值有關。模板運算在圖象處理中經常要用到,可以看出,它是一項非常耗時的運算。有一種優化的方法稱為可分離式濾波,就是使用兩個pass來進行x/y方向分別濾波,能讓運算次數大大減少。而且濾波器階數越高,優勢越明顯。
數字圖像濾波的時候,同樣還需要注意邊界像素的問題,不過幸好,GLSL能讓邊界處理更加的透明和簡單。
l?進階效果之描邊效果
相對浮雕效果來說,描邊(邊緣檢測)的代碼并不復雜多少,只是在理論上相對來說稍微復雜一點,而且效果看上去更加的討人喜歡一些。
我們知道 ,如果在圖像的邊緣處,灰度值肯定經過一個跳躍,我們可以計算出這個跳躍,并對這個值進行一些處理,來得到邊緣濃黑的描邊效果。
首先我們可以考慮對這個象素的左右兩個象素進行差值,得到一個差量,這個差量越大,表示圖像越處于邊緣,而且這個邊緣應該左右方向的,同樣我們能得到上下方向和兩個對角線上的圖像邊緣。這樣我們構造一個濾波器
經過這個濾波器后,我們得到的是圖像在這個象素處的變化差值,我們把它轉化成灰度值,并求絕對值(差值可能為負),然后我們定義差值的絕對值越大的地方越黑(邊緣顯然是黑的),否則越白,我們便得到如下的效果:
圖:鉛筆描邊效果
該效果的代碼如下(其中dip_filter函數代碼同上):
"precision mediump float; \n" "vec4 dip_filter(mat3 _filter, sampler2D _image, vec2 _xy, vec2 texSize) \n" "{ \n" " mat3 _filter_pos_delta_x=mat3(vec3(-1.0, 0.0, 1.0), vec3(0.0, 0.0 ,1.0) ,vec3(1.0,0.0,1.0)); \n" " mat3 _filter_pos_delta_y=mat3(vec3(-1.0,-1.0,-1.0),vec3(-1.0,0.0,0.0),vec3(-1.0,1.0,1.0)); \n" " vec4 final_color = vec4(0.0, 0.0, 0.0, 0.0); \n" " for(int i = 0; i<3; i++) \n" " { \n" " for(int j = 0; j<3; j++) \n" " { \n" " vec2 _xy_new = vec2(_xy.x + _filter_pos_delta_x[i][j], _xy.y + _filter_pos_delta_y[i][j]); \n" " vec2 _uv_new = vec2(_xy_new.x/texSize.x, _xy_new.y/texSize.y); \n" " final_color += texture2D(_image,_uv_new) * _filter[i][j]; \n" " } \n" " } \n" " return final_color; \n" "} \n" "varying vec2 v_texCoord; \n" "uniform vec2 TexSize; \n" "uniform sampler2D s_baseMap; \n" "void main() \n" "{ \n" " vec2 intXY = vec2(v_texCoord.x * TexSize.x, v_texCoord.y * TexSize.y); \n" " mat3 _smooth_fil = mat3(-0.5,-1.0,0.0, \n" " -1.0,0.0,1.0, \n" " 0.0,1.0,0.5); \n" " vec4 delColor = dip_filter(_smooth_fil, s_baseMap, intXY, TexSize); \n" " float deltaGray = 0.3*delColor.x + 0.59*delColor.y + 0.11*delColor.z; \n" " if(deltaGray < 0.0) deltaGray = -1.0 * deltaGray; \n" " deltaGray = 1.0 - deltaGray; \n" " gl_FragColor = vec4(deltaGray,deltaGray,deltaGray,1.0); \n" "} \n"; 上面演示的效果種用到的模板就是一種邊緣檢測器,在信號處理上是一種基于梯度的濾波器,又稱邊緣算子,梯度是有方向的,和邊沿的方向總是正交(垂直)的,在上面的代碼中,我們采用的就是一個梯度為45度方向模板,它可以檢測出135度方向的邊沿。以上是簡單的邊緣檢測算子,更加嚴格的,我們可以采樣Sobel算子,Sobel 算子有兩個,一個是檢測水平邊沿的,另一個是檢測垂直平邊沿的,同樣,Sobel算子另一種形式是各向同性Sobel算子,也有兩個,一個是檢測水平邊沿的,另一個是檢測垂直邊沿的。各向同性Sobel算子和普通Sobel算子相比,它的位置加權系數更為準確,在檢測不同方向的邊沿時梯度的幅度一致。讀者可以自行嘗試Sobel算子的效果,只要修改pencil_filter的值就可以了。
l?高級效果之偽 HDR/Blow
HDR和Blow在現在主流游戲中是非常時髦的效果。
所謂HDR就是高動態范圍的意思,我們知道,在普通的顯示器和位圖里,每通道都是8-bit,也就是說RGB分量的范圍都是0-255,這用來表示現實中的顏色顯然是遠遠不夠的,現實中的圖像的動態范圍遠遠大的多,那么如何在現有的顯示設備里盡可能的保持更大的動態范圍,而且讓它能更符合人眼的習慣就成了圖形學研究的一個熱點。通常真正的HDR的做法都是采用浮點紋理,把渲染運算的過程中,我們使用16bit的動態范圍來保存運算結果,然后我們對運算結果進行分析,求出這個圖像的中間灰度值,然后對圖像進行調整映射到LDR的設備中。但是這樣的算法有兩個非常耗資源的過程,其中一個是浮點紋理,另外一個就是求圖像中間灰度(通常情況是把圖像不停的渲染到RenderTarget,每渲染一次,圖像大小縮小一半,直到縮小到1x1大,一個1024 x1024的圖像需要渲染10次!)。因此雖然HDR的效果非常漂亮,但是目前還是只有為數不多的游戲采用了這樣的算法,大部分都是采用的偽HDR+blow效果。
偽HDR效果通常是重新調整圖像的亮度曲線,讓亮的更亮,暗的更暗一些,而Blow效果則是圖像的亮度擴散開來,產生很柔的效果。
在這里我們采用一個二次曲線來重新調整圖像的亮度,這個曲線的方程是
x [ (2-4k) x + 4k-1 ).K的取值范圍為0.5 – 2.0
經過這個公式調整以后,圖像上亮的區域將更加的亮,并且整體亮度會提高。那么接下來,我們如何使圖像的亮度擴散開來呢?一種可行的方法就是對場景圖像做一次downsample。把它變成原來的1/4次大小,那樣就等于亮度往外擴散了4x4個象素的區域。
"precision mediump float; \n" "varying vec2 v_texCoord; \n" "uniform sampler2D s_baseMap; \n" "uniform float k; \n" "vec4 xposure(vec4 _color, float gray, float ex) \n" "{ \n" " float b = (4.0*ex - 1.0); \n" " float a = 1.0 - b; \n" " float f = gray*(a*gray + b); \n" " return f*_color; \n" "} \n" "void main() \n" "{ \n" " vec4 _dsColor = texture2D(s_baseMap, v_texCoord); \n" " float _lum = 0.3*_dsColor.x + 0.59*_dsColor.y; \n" " vec4 _fColor = texture2D(s_baseMap, v_texCoord); \n" " gl_FragColor = xposure(_fColor, _lum, k); \n" "} \n";下面是原圖像和經過處理后圖像的對比:
??
原圖????????????????????? k = 1.1????????????????????? k = 1.6
圖:經過偽HDR+Blow處理過的圖像和原圖的對比
l?高級效果之水彩化
真正的水彩效果在shader中是比較難實現的,它需要進行中值濾波后累加等一些操作,還需要處理NPR中的筆觸一類的概念。本文繞開這些概念,只從視覺效果上能盡量模擬出水彩的畫的那種感覺來。
我們知道,水彩畫一個最大的特點是水彩在紙上流動擴散后會和周圍的顏色攪拌在一起,另外一個特點就是水彩通常會形成一個個的色塊,過渡不像照片那樣的平滑。針對這兩個特點。我們可以設計這樣的一個算法來模擬水彩畫的效果。
我們可以采用噪聲紋理的方式,既事先計算好一個nxn的隨機數數組,作為紋理傳遞給Pixel shader,這樣在Pixel Shader里我們就能獲得隨機數了。得到隨機數后,我們將隨機數映射成紋理坐標的偏移值,就能模擬出色彩的擴散了。典型的噪聲紋理是這個樣子的:
圖:噪聲紋理
接下來我們需要處理色塊,我們對顏色的RGB值分別進行量化,把RGB分量由原來的8bit量化成比特數更低的值。這樣顏色的過渡就會顯得不那么的平滑,而是會呈現出一定的色塊效果。
通過以上兩步處理后,我們得到的圖像依然有非常多的細節,尤其是第一步處理中產生的很多細節噪點,很自然的我們就想到通過平滑模糊的方式來過濾掉這些高頻噪聲成分。
算法設計好了,接下來看看我們如何在RenderMonkey里實現這個算法。
類似上一個效果,我們需要兩個pass來完成這個算法,第一個pass叫flow pass,模擬顏色的流動和處理顏色的量化。第二個pass叫Gauss pass,也就是前面提到的高斯模糊算法。我們的重點在第一個pass。
在模擬擴散的pass中,我們同樣需要一個RenderTarget,以把結果保存在其中以便后續處理,然后還需要一個噪聲紋理來產生隨機數。具體代碼如下:
"precision mediump float; \n" "varying vec2 v_texCoord; \n" "uniform sampler2D s_baseMap; \n" "uniform vec2 TexSize; \n" "float _waterPower = 40.0; \n" "float _quatLevel = 5.0; \n" "vec4 quant(vec4 _cl, float n) \n" "{ \n" " _cl.x = floor(_cl.x*255.0/n)*n/255.0; \n" " _cl.y = floor(_cl.y*255.0/n)*n/255.0; \n" " _cl.z = floor(_cl.z*255.0/n)*n/255.0; \n" " return _cl; \n" "} \n" "void main() \n" "{ \n" " vec4 noiseColor = _waterPower*texture2D(s_baseMap,v_texCoord); \n" " vec2 newUV =vec2 (v_texCoord.x + noiseColor.x/TexSize.x,v_texCoord.y + noiseColor.y/TexSize.y); \n" " vec4 _fColor = texture2D(s_baseMap,newUV); \n" " gl_FragColor = quant(_fColor, 255.0/pow(2,_quatLevel)); \n" "} \n";代碼中的_quatLevel用來表示對圖像的量化比特數,值越小,色塊越明顯,比較合理的取值范圍是2-6。_waterPower則表示圖像顏色擴散范圍,取值范圍在8-64之間的效果比較好。
下面是經過水彩畫處理后的圖像:
?
圖:水彩畫效果。左圖量化比特數為6比特,擴散范圍為20象素。
右圖量化比特數為5比特,擴散范圍為40象素
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的GLSL实现滤镜效果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双摄像头测距
- 下一篇: 使用GLSL实现雾化的效果