HLSL的一些常见渲染特效的实现
原文作者:潘李亮
xheartblue@163.com
關于學習,中國有句古話叫“學以致用”,可見把學到的東西用于實際實踐中是多么的重要,現在學習Direct3D/HLSL的人非常多,教程也非常多。但是很多人不知道看完這些教程后該干什么,或者說可以怎么利用學到的知識,本文針對已經學習過Direct3D/HLSL的初學者,講述如果將HLSL用于數字圖像處理,帶領大家一起體會HLSL的強大。
本文會對Direct3D/HLSL做一個簡單的介紹,但是假設讀者已經了解和掌握了Direct3D/HLSL的基本知識。
簡介.
1)Direct3D和HLSL
眾所周知,Direct3D是微軟開發的用于編寫Windows下高性能圖形程序的3D API。通過Direct3D,我們可以訪問高速的圖形加速卡。它是DirectX眾多成員的一部分。
HLSL 全稱High Level Shading Language . 是MS推出Direct3D 9時的一個重要更新。所謂的Shading Language還需要從Direct3D的圖形管道說起,Direct3D在Direct3D 8以前只能工作在固定管道(Fixed Function Pipe-line)的模式下,在固定管道模式下,圖元從提交到被轉化成可以顯示的像素是按照實現定義好的流程和算法來完成,可以認為是固化在硬件中的死功能。
從Direct3D 8開始,微軟在Direct3D中引入了可編程管道(Programable Function
Pipeline)的概念,在可編程管道中,開發人員可以自己編寫用于處理頂點和像素的程序,這些程序是運行在GPU上而不是CPU上的。在Direct3D里面,用于處理頂點的程序叫Vertex Shader,用于處理像素的叫Pixel Shader。(目前最新的Direct3D10中又引入了Geometry Shader的概念)。因為硬件的水平在進步,所以可編程管道的處理能力也在不斷的提高,根據不同的硬件能力,Shader的版本也已經有對應的不同版本。從Direct3D發布的最早的Shader Model 1.0到現在主流的Shader Model 3.0,可編程管道已經能提供一點范圍的通用編程能力了,這就是所謂的GPGPU。
從名字上可知,HLSL是一種高級語言(High Level),那么必然有與之對應的Low Level Shading
Language,這個低級的語言就是ASM的Shader。它是類似于匯編語言,難以編寫和維護,而HLSL則跟我們熟悉的C/C++語言非常類似。大大降低了開發人員學習的成本。HLSL本身就是微軟和nVidia聯合開發的,nVidia的版本稱為Cg,也就是C
for Graphics。可想而知,它和C是有同樣的血統的。
本文不是Direct3D和HLSL的教程,如果讀者覺得以上的概念還比較陌生,請先學習Direct3D的基礎知識。同時關于如何在Direct3D應用程序中使用HLSL編寫的Vertex Shader和Pixel Shader,請參閱其它的教程和微軟的DirectX SDK。
2)RenderMonkey簡介
現在的開發人員可能都比較熟悉IDE的工作模式,尤其是使用Visual Studio一類開發工具的Windows程序開發人員。在一個統一的開發環境中,可以編寫和調試程序。HLSL作為一種新的語言,GPU編程作為一種新事物,目前還沒有很好的IDE能完整的支持編寫,調試一體化的工作方式。在本文我們將使用ATI的一個相對比較好用的開發HLSL的IDE: RenderMonkey。
RenderMonkey是由前ATI開發的,用于編寫Shader,并調試Shader的一個工具。由于RenderMonkey支持插件,所以RenderMonkey既可以編寫OpenGL的GLSL也可以編寫Direct3D的HLSL。它能支持創建RenderTarget,多Pass渲染,可以自由選擇用哪個shader model來編譯代碼。并能加亮顯示shader代碼。
經典的RenderMonkey界面如下圖
?
?
?
?
?
?
?
?
?
?
?
?
左邊為工作區,右邊為預覽區域。下面為信息輸出區。在左邊的工作區里可以看到。我們可以對Shader的工程進行分組,其中每一個可以獨立工作的工程稱為一個Effect。在同一時候預覽區中只能預覽當前激活的Effect。每個Effect由不同的對象組成,其中比較重要的對象如下:
1) Pass . 這個pass就是渲染中常提到的pass.代表一遍的渲染
2) 幾何體。就是類紅色茶壺表示的,它代表在渲染中使用的幾何體。
3) 紋理對象和RenderTarget對象(用一個鉛筆表示)
4) Shader中用到的參數,這些參數可以是自定義的,也可以是預定義的(比如當前的觀察矩陣,攝像機的位置等參數)。
5) 每個pass中用到的Shader。這些shader可以在RenderMonkey的代碼編輯器中進行編輯,并調用命令來編譯。
因為文章篇幅的關系,也不采用編寫Direct3D程序加載HLSL的方式來做演示程序,而是直接使用RenderMonkey來作為演示的平臺。關于如何使用RenderMonkey,請參照RenderMonkey的幫助,或者打開RenderMonkey自帶的例子,很容易就能掌握這個工具的使用方法。
GPGPU
本文將要介紹的是如何用HLSL來實現PhotoShop的濾鏡效果,也就是說需要通過GPU來進行數字圖像處理。這是目前很流行的GPGPU的應用的一種。
我們知道,GPU和CPU的工作方式和用途都是不同的,CPU是通用的處理器,而GPU是專用于處理3D圖形顯示的,因此CPU的指令集更加豐富,而GPU的指令集更加有針對性,因此這就決定了GPU在犧牲了CPU的靈活性的前提上有更快的運行速度。GPU特別適合處理那種可以大規模并行的算法,比如某些數字圖像處理算法。
因為目前我們的程序只能通過Direct3D的API才能訪問到GPU,一般我們采用Pixel
Shader來進行GPGPU,所以我們要使用GPU來處理數據的時候,必須完成以下幾件事:
1).將數據提交給GPU
2).調用對應命令讓GPU開始處理數據
3).從GPU哪里取回處理完畢的數據。
我們可以通過兩種方法將數據提交到GPU,紋理和shader的參數,紋理中一般保存我們需要進行處理的數據,而shader參數則一般是用于數據處理算法需要用到的一些參數。當然這也不是絕對的。
當數據已經準備完畢后,我們調用Direct3D的drawPrimitive函數在屏幕上繪制一個紋理相同大小的矩形,把GPGPU的算法寫到用于繪制這個矩形的Pixel Shader中。當Direct3D開始繪制這個矩形以后,會為每一個象素調用一次整個Pixel Shader,然后把Pixel Shader的輸出寫入到RenderTarget中,因為我們繪制的矩形的大小和紋理的大小是一致的,所以輸出象素和紋理的象素可以做到一一對應的關系,也就是說紋理中的每一個象素在經過Pixel Shader的運算后被輸出掉RenderTarget里,等于對這個數據調用了一次我們需要的算法。我們知道現代的GPU中往往有大量的Pixel Shader處理單元,而這些處理都是可以并行運行的,可想而知,這個處理是非常快速的。
經過前面的步驟,處理完的數據已經到了RenderTarget里了,我們可以事先自己創建一個RenderTarget(通常和輸入紋理等大)來接受步驟2中的數據,然后Lock這個RenderTarget取回數據。也可以在步驟二中直接把圖象繪制到屏幕上,通過Capture屏幕來得到輸出(對于圖象處理也夠了,就是速度慢,而且顯得非常傻)。
GPGPU簡單介紹到這里。詳細的GPGPU資料請參考www.gpgpu.org。 同時nvidia的網站和發布的SDK上也有很多關于GPGPU的例子。
接下來我們使用RenderMonkey來搭建一個用于數字圖像處理的架子,以實現類似PhotoShop的濾鏡效果
RenderMonkey圖像處理的架子-圖像黑白化
下面我們通過一個簡單的例子,先來完成一個最簡單的圖像處理-把圖像黑白化。來說明RenderMonkey如何來處理數字圖像。
Render Monkey和VC類似,內置了一些工程代碼。在這里我們在RenderMonkey的工作區菜單里選擇Add Effect ->
DirectX->Screen-AlignedQuad. 在生成的工程中,我們看到RenderMonkey為我們顯示了一個默認的圖片,首先我們就是要修改這個圖片,我們雙擊那個base圖片對應的節點,選擇一個我們要演示的圖片。如下圖。
接下來,我們要開始進行我們關鍵的一步,編寫處理圖像的算法,我們雙擊剛才建立的項目中的single pass -> pixel shader . 開始編輯Pixel
Shader的代碼。
我們知道,一個RGB顏色的亮度和各個分量之間的關系的公式為:
GrayValue = 0.3 * R + 0.59*G + 0.11 *B
根據這個公式,我們的代碼如下:
sampler2D Texture0;
float4?
main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
float4 _inColor = tex2D( Texture0, texCoord );
??
float h = 0.3 * _inColor.x + 0.59 *
_inColor.y + 0.11* _inColor.z;
?? float4 _outColor = float4(h,h,h,1.0);
?? return _outColor;
}
我來詳細的解釋一下這個Pixel Shader,首先我們定義的的sampler2D Texture0。Texture0就是代表我們輸入的圖像。這個圖像在RenderMonkey的工作區中用兩部分表示,首先需要在工作區中創建一個圖像對象bas,然后需要在用到這個紋理圖像的pass中創建一個紋理對象Texture0,然后讓這個Texture0指向我們剛才創建的紋理圖像bas,讀者應該注意到了紋理對象的名字就是我們Shader里的sampler2D變量的名字,不錯,RenderMonkey就是以這種方法把shader代碼中的變量名字和工作區中的對象關聯起來.不光紋理如此,其它的float4/float3/float2/float變量都如此.
接下來的main函數中,我們通過紋理采樣的方式得到當前需要繪制的像素,float4 _inColor = tex2D( Texture0, texCoord ); 也就是輸入的顏色。得到輸入顏色后,我們可以通過上面給出的公式來計算出這個顏色的灰度值,并用這個值構造一個灰度顏色返回給Direct3D。系統就會把這個顏色作為最終的色彩顯示在窗口中,也就是得到一個黑白的圖像。最終結果如下圖:
圖:圖像去色效果
(注:這個例子是最簡單的HLSL用于圖像處理的例子,如果讀者覺得到目前為止還很有難度,建議重新溫習一遍Direct3D和HLSL的知識)。
通過這個例子,我們已經基本了解了RenderMonkey處理圖像的步驟和流程,下面我們通過分析一些更加復雜一點的例子來體會HLSL的強大能力
l 入門效果之浮雕
"浮雕"圖象效果是指圖像的前景前向凸出背景。常見于一些紀念碑的雕刻上,要實現浮雕其實非常簡單。我們把圖象的一個象素和左上方的象素進行求差運算,并加上一個灰度。這個灰度就是表示背景顏色。這里我們設置這個插值為128 (圖象RGB的值是0-255)。同時,我們還應該把這兩個顏色的差值轉換為亮度信息.否則浮雕圖像會出現彩色J
在使用HLSL處理浮雕效果的時候,兩個問題我們需要注意一下。
其中一個圖象邊界,寫過C++實現浮雕效果的朋友都知道,在處理邊界象素的時候可能是取不到左上角象素的, 這個時候就應該左做特殊處理,通常我們把邊界位置的浮雕結果設置成背景顏色,但是使用HLSL的時候我們不需要在HLSL的shader中去對圖象的邊界做特殊處理,但是我們需要對紋理設置濾波器, 這個濾波器我們設置為CLAMP模式就可以了.
第二個需要處理的問題是, 我們知道PixelShader中,紋理的采樣坐標是0-1.0, 如果我們要取到左上我們需要知道紋理圖象的大小,這樣才能把一個象素的的偏移轉換成0-1.0的值是多少, 假設紋理的大小是[w,h],當前紋理坐標是[u,v],那么它左上角的象素的紋理坐標就是[u -1.0/w, v –
1.0/h].RenderMonkey中無法知道這個紋理圖像的大小,當然如果我們自己用VC++寫一個程序的話,我們可以在加載圖像或者從IDirect3DTexture9對象中得到紋理大小.然后當作一個constant常量設置給HLSL就可以了. 當然也可以偷懶—假設紋理的大小就是1024
x 1024—得到的效果也是可以接受的.
好了,現在我來展示一下用來得到浮雕效果的HLSL的代碼:
sampler2D Texture0;
float2????? TexSize;
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
float2?
upLeftUV = float2(texCoord.x - 1.0/TexSize.x , texCoord.y -
1.0/TexSize.y);
??
float4?
bkColor = float4(0.5 , 0.5 , 0.5 , 1.0);
??
float4?
curColor??? = ?tex2D( Texture0, texCoord );
??
float4?
upLeftColor =? tex2D( Texture0, upLeftUV );
??
//相減得到顏色的差
??
float4?
delColor = curColor - upLeftColor;
??
//需要把這個顏色的差設置
??
float?
h = 0.3 * delColor.x + 0.59 * delColor.y + 0.11* delColor.z;
??
float4?
_outColor = ?float4(h,h,h,0.0)+ bkColor;
??
return?
_outColor;
}
原圖 浮雕化后的圖像
讀者應該會發現,相對于C++版本的代碼,HLSL的代碼顯得非常的干凈和利索.沒有分支,沒有循環.最重要的是它的速度非常快,對一個2048x2048的圖像完全可以做到>30fps的實時處理能力而不會耗費很多的CPU時間!
l 入門效果之馬賽克
接下來我們完成一個更加常見的效果—馬賽克.圖片的馬賽克就是把圖片的一個相當大小的區域用同一個點的顏色來表示.可以認為是大規模的降低圖像的分辨率,而讓圖像的一些細節隱藏起來, 比如電視中要秀一下某個罪犯的身材,卻又不能展示他的臉,這個時候我們就可以給他的臉加一個馬賽克.
用HLSL代碼實現馬賽克是非常簡單的,但是同樣的,我們需要一些額外的步驟,第一步就是先把紋理坐標轉換成圖像實際大小的整數坐標.接下來,我們要把圖像這個坐標量化---比如馬賽克塊的大小是8x8象素。那么我們可以用下列方法來得到馬賽克后的圖像采樣值,假設[x.y]為圖像的整數坐標:
[x,y]mosaic = [ int(x/8)*8 ,
int(y/8)*8].
得到這個坐標后,我們只要用相反的方法,把整數坐標轉換回到0-1.0的紋理坐標。
具體的馬賽克效果代碼如下:
sampler2D Texture0;
float2???
TexSize;
float2???
mosaicSize = float2(8,8);
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
//得到當前紋理坐標相對圖像大小整數值。
??
float2?
intXY = float2(texCoord.x * TexSize.x , texCoord.y * TexSize.y);
??
//根據馬賽克塊大小進行取整。
??
float2?
XYMosaic?? = float2(int(intXY.x/mosaicSize.x) * mosaicSize.x,
?????????????????????????????? int(intXY.y/mosaicSize.y) * mosaicSize.y );
??
//把整數坐標轉換回紋理采樣坐標
??
float2? UVMosaic?? = float2(XYMosaic.x/TexSize.x ,
XYMosaic.y/TexSize.y);
??
return tex2D( Texture0, UVMosaic );
}
經過這個Shader處理后的圖像結果如下:
圖:馬賽克處理效果
讀者可能會發現這個馬賽克太普通了,確實它不夠新穎,下面我們來改良一下,我們希望達到這樣一個效果:馬賽克區域不是方的,而是圓的,圓形區域以外,我們用圖像原來的顏色覆蓋。這樣我們需要改變一下代碼。
首先求出原來馬賽克區域的正中心(原來是左上角):然后計算圖像采樣點到這個中心的距離,如果在馬賽克圓內,就用區域的中心顏色,否則就用原來的顏色。改良后的代碼如下,這里我們把馬賽克區域大小調節成16x16。這樣效果更明顯。
sampler2D?? Texture0;
float2?????
TexSize;
float2???
mosaicSize = float2(16,16);
float4 ps_main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
float2?
intXY = float2(texCoord.x * TexSize.x , texCoord.y * TexSize.y);
??
//馬賽克中心不再是左上角,而是中心
??
float2?
XYMosaic?? = float2(int(intXY.x/mosaicSize.x) *
mosaicSize.x,?? int(intXY.y/mosaicSize.y) * mosaicSize.y )
+ 0.5 * mosaicSize;
??
//求出采樣點到馬賽克中心的距離
?? float2?
delXY = XYMosaic - intXY;
??
float??
delL? = length(delXY);
?? float2?
UVMosaic?? = float2(XYMosaic.x/TexSize.x ,
XYMosaic.y/TexSize.y);
??
float4?
_finalColor;
??
//判斷是不是處于馬賽克圓中。
??
if(delL< 0.5 * mosaicSize.x)
??????
_finalColor = tex2D( Texture0, UVMosaic );
??
else
??????
_finalColor = tex2D( Texture0, texCoord );
??
return _finalColor;
}
這個代碼相對上面的代碼復雜了一些,加了一個分支if/else。注意,GPU是個高度并行的處理器,過多分支會降低Shader的運行速度。這個改良的馬賽克效果如下
圖: 改良后的馬賽克效果
l 進階效果之銳化模糊
以上兩個效果相對比較簡單,姑且稱之為入門效果, 它并沒有用到太多數字圖像處理或者信號處理方面的知識。接下來我們要介紹稍微復雜一點的效果,第一個就是圖像的模糊和銳化。
圖像的模糊又成為圖像的平滑(smoothing),我們知道人眼對高頻成分是非常敏感的,如果在一個亮度連續變化的圖像中,突然出現一個亮點,那么我們很容易察覺出來,類似的,如果圖像有個突然的跳躍—明 顯的邊緣,我們也是很容易察覺出來的。這些突然變化的分量就是圖像的高頻成分。人眼通常是通過低頻成分來辨別輪廓,通過高頻成分來感知細節的(這也是為什
么照片分辨率低的時候,人們只能辨認出照片的大概輪廓,而看不到細節)。但是這些高頻成分通常也包含了噪聲成分。圖像的平滑處理就是濾除圖像的高頻成分。
那么如何才能濾除圖像的高頻成分呢?我們先來介紹一下圖像數字濾波器的概念。
簡單通俗的來說,圖像的數字濾波器其實就是一個n
x n的數組(數組中的元素成為濾波器的系數或者濾波器的權重,n稱為濾波器的階)。對圖像做濾波的時候,把某個像素為中心的nxn個像素的值和這個濾波器做卷積運算(也就是對應位置上的像素和對應位置上的權重的乘積累加起來),公式如下
其中x , y 為當前正在處理的像素坐標。
通常情況下,我們濾波器的階數為3已經足夠了,用于模糊處理的3x3濾波器如下
。
經過這樣的濾波器,其實就是等效于把一個像素和周圍8個像素一起求平均值,這是非常合理的---等于把一個像素和周圍幾個像素攪拌在一起—自然就模糊了J
用來對一個圖像做濾波處理的函數如下:
//用來做濾波操作的函數
float4 dip_filter(float3x3 _filter , sampler2D _image, float2 _xy, float2 texSize)
{
???
//紋理坐標采樣的偏移
????
float2 _filter_pos_delta[3][3] =
????
{
???????
{ float2(-1.0 , -1.0) , float2(0,-1.0), float2(1.0 , -1.0) },
???????
{ float2( 0.0 , -1.0) , float2(0, 0.0), float2(1.0 ,? 0.0) },
???????
{ float2( 1.0 , -1.0) , float2(0, 1.0), float2(1.0 ,? 1.0) },
????
};
????
//最終的輸出顏色
????
float4 final_color = float4(0.0,0.0,0.0,0.0);
????
//對圖像做濾波操作
????
for(int i = 0 ; i < 3 ; i ++ )
????
{
????????
for(int j = 0 ; j < 3 ; j ++)
????????
{
?????????? ?//計算采樣點,得到當前像素附近的像素的坐標
????????????? float2 _xy_new = float2(_xy.x + _filter_pos_delta[i][j].x ,
??????????????????????????????????? _xy.y +
_filter_pos_delta[i][j].y);
??????????? float2 _uv_new = float2(_xy_new.x/texSize.x ,
_xy_new.y/texSize.y);
????
???????//采樣并乘以濾波器權重,然后累加
????????????? final_color += tex2D( _image, _uv_new ) * _filter[i][j];
????????
}
????
}
????
return final_color;
}
剩下的工作,我們就是定義一個用來進行模糊的濾波器模板,并調用dip_filter這個函數就可以了。代碼如下:
sampler2D Texture0;
float2???
TexSize;
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
float2?
intXY = float2(texCoord.x * TexSize.x , texCoord.y * TexSize.y);
??
//用于模糊操作的濾波器
??
float3x3 _smooth_fil = float3x3 (1/9.0 ,1/9.0,1/9.0 ,
??????????????????????????????????? 1/9.0
,1/9.0,1/9.0 ,
??????????????? ????????????????????1/9.0 ,1/9.0,1/9.0 );
??
return dip_filter(_smooth_fil , Texture0 ,
intXY, TexSize);
}
以上的模糊濾波器稱為BOX濾波器,是最簡單的濾波器,如果考慮到離開中心像素的距離對濾波器系數的影響,我們通常采用更加合理的濾波器---高斯濾波器—一種通過2維高斯采樣得到的濾波器,它的模板如下:
很容易看出來,離開中心越遠的像素,權重系數越小。
對于銳化操作,常用的銳化模板是拉普拉斯(Laplacian)模板,這個模板定義如下:
容易看出拉普拉斯模板的作法:先將自身與周圍的8個象素相減,表示自身與周圍象素的差別;再將這個差別加上自身作為新象素的灰度。可見,如果一片暗區出現了一個亮點,那么銳化處理的結果是這個亮點變得更亮,這就增強了圖像的細節。
下面三副圖分別表示了經過BOX濾波。高斯濾波和拉普拉斯濾波后的圖像
BOX 模糊 高斯模糊 拉普拉斯銳化
高斯模糊和拉普拉斯銳化效果的HLSL和BOX的代碼基本一致,就是filter的系數不同,這里不在列出。
通過這個兩個效果,我們介紹了圖像的濾波操作,這樣的操作,也成為模板操作,它實現了一種鄰域運算(Neighborhood
Operation),即某個象素點的結果灰度不僅和該象素灰度有關,而且和其鄰域點的值有關。模板運算在圖象處理中經常要用到,可以看出,它是一項非常耗時的運算。有一種優化的方法稱為可分離式濾波,就是使用兩個pass來進行x/y方向分別濾波,能讓運算次數大大減少。而且濾波器階數越高,優勢越明顯。
數字圖像濾波的時候,同樣還需要注意邊界像素的問題,不過幸好,HLSL能讓邊界處理更加的透明和簡單。
l 進階效果之描邊效果
相對浮雕效果來說,描邊(邊緣檢測)的代碼并不復雜多少,只是在理論上相對來說稍微復雜一點,而且效果看上去更加的討人喜歡一些。
我們知道 ,如果在圖像的邊緣處,灰度值肯定經過一個跳躍,我們可以計算出這個跳躍,并對這個值進行一些處理,來得到邊緣濃黑的描邊效果。
首先我們可以考慮對這個象素的左右兩個象素進行差值,得到一個差量,這個差量越大,表示圖像越處于邊緣,而且這個邊緣應該左右方向的,同樣我們能得到上下方向和兩個對角線上的圖像邊緣。這樣我們構造一個濾波器
經過這個濾波器后,我們得到的是圖像在這個象素處的變化差值,我們把它轉化成灰度值,并求絕對值(差值可能為負),然后我們定義差值的絕對值越大的地方越黑(邊緣顯然是黑的),否則越白,我們便得到如下的效果:
圖:鉛筆描邊效果
該效果的代碼如下(其中dip_filter函數代碼同上):
sampler2D Texture0;
float2???
TexSize;
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
??
float2?
intXY = float2(texCoord.x * TexSize.x , texCoord.y * TexSize.y);
??
float3x3 _pencil_fil = float3x3 (-0.5 ,-1.0? , 0.0 ,
??????????????????????????????????? -1.0 ,? 0.0 , 1.0 ,
?????????????????????????????????? -0.0? , 1.0 , 0.5 );
?
??
float4 delColor =? dip_filter(_pencil_fil , Texture0 , intXY,
TexSize);
??
float?
deltaGray = 0.3 * delColor.x? +
0.59 * delColor.y? + 0.11*
delColor.z;?????????????????
?? if(deltaGray < 0.0) deltaGray = -1.0 *
deltaGray;
??
deltaGray = 1.0 - deltaGray;
??
return float4(deltaGray,deltaGray,deltaGray,1.0);?????? ?????????????
}
上面演示的效果種用到的模板就是一種邊緣檢測器,在信號處理上是一種基于梯度的濾波器,又稱邊緣算子,梯度是有方向的,和邊沿的方向總是正交(垂直)的,在上面的代碼中,我們采用的就是一個梯度為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個象素的區域。
技術方案已經基本有了。接下來我們要在RenderMonkey里實現它,和前面的例子不同,這里我們需要使用RenderMonkey的多pass渲染:
第一個pass我們先對圖像進行downsample操作,首先我們在原來的pass前增加一個新的pass,并命名為downsample pass,然后我們建立一個render to texture的紋理節點,設定它的大小(原圖像大小的1/4),并在新建立的pass里建立一個render target的節點指向它,這樣,這個pass的渲染結果就會保存到這個render to texture中。這個pass的pixel shader只要使用前面我們已經完成的模糊效果的shader就可以了。
接下來,我們給另外一個pass命名為render pass,并增加一個新的紋理對象,并把紋理對象指向剛才我們增加的render
to texture,以便我們能在該pass中使用這個downsample過的圖像。然后我們增加一個float類型的變量,用來表示上面公式中提到的k值;最終工作區的結構如下圖:
接下來編輯render pass的pixel shader。代碼如下:
sampler2D Texture0;
sampler2D TexDownSample;
float???
k ;//控制參數,公式中k值.
float4?
xposure(float4 _color , float
gray , float ex)
{//重新調整場景的亮度
????? float b = ( 4 * ex -? 1 );
????
float a = 1 - b;
????
float f = gray * ( a * gray + b );
???? return??
f * _color;
}
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
???
//亮度信息從downSample后的圖像中獲得
???
float4 _dsColor = tex2D(TexDownSample
, texCoord);
???
float _lum = 0.3 * _dsColor.x + 0.59 *
_dsColor.y + 0.11* _dsColor.z;
??? float4 _fColor = tex2D(Texture0 , texCoord);
???
//對最終顏色進行修正
???
return xposure(_fColor , _lum , k);???????????????????
}
下面是原圖像和經過處理后圖像的對比:
原圖 k = 1.1 k = 1.6
圖:經過偽HDR+Blow處理過的圖像和原圖的對比
l 高級效果之水彩化
真正的水彩效果在shader中是比較難實現的,它需要進行中值濾波后累加等一些操作,還需要處理NPR中的筆觸一類的概念。本文繞開這些概念,只從視覺效果上能盡量模擬出水彩的畫的那種感覺來。
我們知道,水彩畫一個最大的特點是水彩在紙上流動擴散后會和周圍的顏色攪拌在一起,另外一個特點就是水彩通常會形成一個個的色塊,過渡不像照片那樣的平滑。針對這兩個特點。我們可以設計這樣的一個算法來模擬水彩畫的效果。
首先我們模擬擴散,簡單的說,可以通過隨機對附近的象素點進行采樣來模擬顏色的擴散,而這個隨機區域的大小我們可以稱為擴散的力度。這在C++代碼里應該是非常容易實現的,讀者只需要使用Random函數就可以了。但是HLSL并沒有提供這樣的函數(似乎有個noise函數,不過不能用L)。怎么辦呢?我們可以采用噪聲紋理的方式,既事先計算好一個nxn的隨機數數組,作為紋理傳遞給Pixel shader,這樣在Pixel Shader里我們就能獲得隨機數了。得到隨機數后,我們將隨機數映射成紋理坐標的偏移值,就能模擬出色彩的擴散了。典型的噪聲紋理是這個樣子的:
圖:噪聲紋理
接下來我們需要處理色塊,我們對顏色的RGB值分別進行量化,把RGB分量由原來的8bit量化成比特數更低的值。這樣顏色的過渡就會顯得不那么的平滑,而是會呈現出一定的色塊效果。
通過以上兩步處理后,我們得到的圖像依然有非常多的細節,尤其是第一步處理中產生的很多細節噪點,很自然的我們就想到通過平滑模糊的方式來過濾掉這些高頻噪聲成分。
算法設計好了,接下來看看我們如何在RenderMonkey里實現這個算法。
類似上一個效果,我們需要兩個pass來完成這個算法,第一個pass叫flow pass,模擬顏色的流動和處理顏色的量化。第二個pass叫Gauss pass,也就是前面提到的高斯模糊算法。我們的重點在第一個pass。
在模擬擴散的pass中,我們同樣需要一個RenderTarget,以把結果保存在其中以便后續處理,然后還需要一個噪聲紋理來產生隨機數。具體代碼如下:
sampler2D Texture0;
sampler2D noiseTexture;
float?????
_quatLevel ;? //量化的bit數 ,取值2-6比較適合
float?????
_waterPower; //表示擴展力度,單位為象素
float4?
quant(float4 _cl , float n)
{//該函數對顏色的四個分量進行量化
???
_cl.x = int(_cl.x * 255 / n) * n /255;
???
_cl.y = int(_cl.y * 255 / n) * n /255;
???
_cl.z = int(_cl.z * 255 / n) * n /255;
???
return _cl;
}
?
float4 main( float2 texCoord? : TEXCOORD0 ) : COLOR
{
???
//取得隨機數,對紋理坐標進行擾動,形成擴散效果
???
float4 noiseColor = _waterPower *
tex2D(noiseTexture , texCoord);
???
float2 newUV?
= float2(texCoord.x + noiseColor.x / TexSize.x ,
texCoord.y + noiseColor.y / TexSize.y);
???
float4 _fColor = tex2D(Texture0
, newUV);
???
//量化圖像的顏色值,形成色塊
???
return quant(_fColor , 255/pow(2, _quatLevel)
);???????????????????
}
代碼中的_quatLevel用來表示對圖像的量化比特數,值越小,色塊越明顯,比較合理的取值范圍是2-6。_waterPower則表示圖像顏色擴散范圍,取值范圍在8-64之間的效果比較好。
下面是經過水彩畫處理后的圖像:
圖:水彩畫效果。左圖量化比特數為6比特,擴散范圍為20象素。
右圖量化比特數為5比特,擴散范圍為40象素
l 總結
GPU進行數字圖像處理,甚至是使用GPU進行數字視頻編輯是目前非常流行的話題,市場是已經出現很多商業的產品,比如Mac公司的iMotion,就是完全采用GPU加速的視頻非編軟件,iMotion作者的對它的評價是:Play with the images in
real-time。可見它的效率之高,本文只是簡單的介紹了HLSL在圖像處理領域的應用,希望能給沒入門的讀者撥開一些云霧。通過以上介紹的幾種濾鏡效果,讀者應該大致掌握了使用HLSL進行數字圖像處理的一些基本步驟和方法了,為了方便起見,我們并沒有把處理完的圖像保存下來而是僅僅把處理完的圖像顯示在屏幕上,其實在RendererMonkey中也是可以把處理完的結果保存起來的,我們可以創建一個和圖像等大的RenderTarget。并把我們處理的結果繪制到這個RenderTarget中(關于如何設置當前的RenderTarget,以及如何設置多個RenderTarget,留給讀者自己摸索),然后在RenderMonkey的工作區中選擇那個RenderTarget,在右鍵菜單中選擇保存到圖像就可以了。
我們知道C++也好,Basic也好,乃至現在的HLSL/GLSL也好。它們都是語言而已,充分的了解這些語言,熟悉他們的特性都是非常簡單的。但是如何充分發揮他們的作用,用它們做一些有意義的事情,就完全在于我們自己的實踐和在實踐中的創造性。如果讀者在實踐中還能創造處更多,更實用的效果。甚至是應用的商業產品中。
最后還得提一下的是,文中出現不少信號處理和數學的知識,可見多花點時間在數學上是非常值得的J
注:處于閱讀方便,本文代碼都未經過優化。
參考資料:
高等教育出版社《數字圖像處理》
RenderMonkey官方網站:http://ati.amd.com/developer/rendermonkey/index.html
轉載于:https://www.cnblogs.com/guopenglx/archive/2011/04/14/2015550.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的HLSL的一些常见渲染特效的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WebGIS概述:WebGIS基础(1)
- 下一篇: 毕设开发总结-3D游戏框架及网络对战游戏