1. 程式人生 > >GLSL/C++ 實現濾鏡效果

GLSL/C++ 實現濾鏡效果

概念 第一個 美麗 函數 form 一個 傳遞 eve 針對

入門效果之浮雕

"浮雕"圖象效果是指圖像的前景前向凸出背景。常見於一些紀念碑的雕刻上。要實現浮雕事實上很easy。我們把圖象的一個象素和左上方的象素進行求差運算。並加上一個灰度。這個灰度就是表示背景顏色。這裏我們設置這個插值為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";

C++版

void relief(int x,int y,BYTE *pre,BYTE *now,int Stride,int index){
	int upperleft;
	upperleft=(y-1)*Stride+4*(x-1);
	Vec4 Pixel,NowPixel;
	NowPixel.SetPixel(pre,index);
	Pixel.SetPixel(pre,upperleft);
	Pixel=NowPixel-Pixel;
	BeGray(Pixel);
	Pixel.GetPixelToNow(now,index);
}

技術分享 技術分享


入門效果之馬賽克

接下來我們完畢一個更加常見的效果—馬賽克.圖片的馬賽克就是把圖片的一個相當大小的區域用同一個點的顏色來表示.能夠覺得是大規模的減少圖像的分辨率,而讓圖像的一些細節隱藏起來, 比方電視中要秀一下某個罪犯的身材,卻又不能展示他的臉,這個時候我們就能夠給他的臉加一個馬賽克.

用HLSL代碼實現馬賽克是很easy的,可是相同的,我們須要一些額外的步驟,第一步就是先把紋理坐標轉換成圖像實際大小的整數坐標.接下來,我們要把圖像這個坐標量化---比方馬賽克塊的大小是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";

C++ 版

void Mosaic(int x,int y,BYTE *pre,BYTE *now,int Stride,int index){
	Vec4 Pixel,Pixel_UpperLeft;
	Pixel.SetPixel(pre,index);
	if (x%8==0&&y%8==0)
	{
		Pixel.GetPixelToNow(now,index);
	} 
	else
	{
		int tmpX,tmpY;
		tmpX=x/8*8;
		tmpY=y/8*8;
		int index_UpperLeft;
		index_UpperLeft=tmpY*Stride+4*(tmpX);
		Pixel_UpperLeft.SetPixel(pre,index_UpperLeft);
		Pixel_UpperLeft.GetPixelToNow(now,index);
	}
}


技術分享


讀者可能會發現這個馬賽克太普通了,確實它不夠新穎,以下我們來改良一下。我們希望達到這樣一個效果:馬賽克區域不是方的,而是圓的,圓形區域以外,我們用圖像原來的顏色覆蓋。這樣我們須要改變一下代碼。

首先求出原來馬賽克區域的正中心(原來是左上角):然後計算圖像採樣點到這個中心的距離,假設在馬賽克圓內。就用區域的中心顏色,否則就用原來的顏色。

改良後的代碼例如以下。這裏我們把馬賽克區域大小調節成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"  ;

C++版

void Mosaic_Point(int x,int y,BYTE *pre,BYTE *now,int Stride,int index){
	Vec4 Pixel,Pixel_UpperLeft;
	Pixel.SetPixel(pre,index);
	int i,j;
	i=x%8;
	j=y%8;
	double dist=sqrt(double((4-i)*(4-i)+(4-j)*(4-j)));
	if (dist>4)
	{
		Pixel.GetPixelToNow(now,index);
	} 
	else
	{
		int tmpX,tmpY;
		tmpX=x/8*8;
		tmpY=y/8*8;
		int index_UpperLeft;
		index_UpperLeft=tmpY*Stride+4*(tmpX);
		Pixel_UpperLeft.SetPixel(pre,index_UpperLeft);
		Pixel_UpperLeft.GetPixelToNow(now,index);
	}
}


技術分享

圖: 改良後的馬賽克效果

l 進階效果之銳化模糊

以上兩個效果相對照較簡單,姑且稱之為入門效果。 它並沒實用到太多數字圖像處理或者信號處理方面的知識。

接下來我們要介紹略微復雜一點的效果。第一個就是圖像的模糊和銳化。

圖像的模糊又成為圖像的平滑(smoothing),我們知道人眼對高頻成分是非常敏感的。假設在一個亮度連續變化的圖像中,突然出現一個亮點,那麽我們非常easy察覺出來,類似的,假設圖像有個突然的跳躍—明顯的邊緣,我們也是非常easy察覺出來的。

這些突然變化的分量就是圖像的高頻成分。

人眼一般是通過低頻成分來辨別輪廓,通過高頻成分來感知細節的(這也是為什麽照片分辨率低的時候,人們僅僅能辨認出照片的大概輪廓,而看不到細節)。可是這些高頻成分通常也包括了噪聲成分。圖像的平滑處理就是濾除圖像的高頻成分。

那麽怎樣才幹濾除圖像的高頻成分呢?我們先來介紹一下圖像數字濾波器的概念。

簡單通俗的來說,圖像的數字濾波器事實上就是一個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";

C++版

void dip_filter(int x,int y,BYTE *pre,BYTE *now,int Stride,int index)
{
	int dir_x[3][3]={-1,0,1,-1,0,1,-1,0,1};
	int dir_y[3][3]={1,1,1,0,0,0,-1,-1,-1};
	int tmpX,tmpY;
	Vec4 Pixel,PrePixel;
	double num=sqrt(2.0);
	double filter[3][3]={-1,0,1,
							-num,0,num,
							-1,0,1};
	Pixel.Clear();
	for (int i=0;i<3;i++)
	{
		for (int j=0;j<3;j++)
		{
			tmpX=x+dir_x[i][j];
			tmpY=y+dir_y[i][j];
			int tmp=tmpY*Stride+4*(tmpX);
			PrePixel.SetPixel(pre,tmp);
			Pixel+=PrePixel*filter[i][j];
		}
	}
	BeGray(Pixel);
	Pixel.GetPixelToNow(now,index);
}


以上的模糊濾波器稱為BOX濾波器,是最簡單的濾波器。假設考慮到離開中心像素的距離對濾波器系數的影響,我們通常採用更加合理的濾波器---高斯濾波器—一種通過2維高斯採樣得到的濾波器。它的模板例如以下:

技術分享

非常easy看出來。離開中心越遠的像素。權重系數越小。

對於銳化操作,經常使用的銳化模板是拉普拉斯(Laplacian)模板,這個模板定義例如以下:

技術分享

easy看出拉普拉斯模板的作法:先將自身與周圍的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";

C++版

void Gaussian_filter(int x,int y,BYTE *pre,BYTE *now,int Stride,int index)
{
	int dir_x[3][3]={-1,0,1,-1,0,1,-1,0,1};
	int dir_y[3][3]={1,1,1,0,0,0,-1,-1,-1};
	int tmpX,tmpY;
	Vec4 Pixel,PrePixel;
	double filter[3][3]={1.0/16,2.0/16,1.0/16,
		2.0/16,4.0/16,2.0/16,
		1.0/16,2.0/16,1.0/16};
	Pixel.Clear();
	for (int i=0;i<3;i++)
	{
		for (int j=0;j<3;j++)
		{
			tmpX=x+dir_x[i][j];
			tmpY=y+dir_y[i][j];
			int tmp=tmpY*Stride+4*(tmpX);
			PrePixel.SetPixel(pre,tmp);
			Pixel+=PrePixel*filter[i][j];
		}
	}
	//BeGray(Pixel);
	Pixel.GetPixelToNow(now,index);
}




上面演示的效果種用到的模板就是一種邊緣檢測器,在信號處理上是一種基於梯度的濾波器。又稱邊緣算子。梯度是有方向的,和邊沿的方向總是正交(垂直)的,在上面的代碼中,我們採用的就是一個梯度為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";

C++版

void HDR(int x,int y,BYTE *pre,BYTE *now,int index,double k){
	Vec4 Pixel;
	double GrayPixel;
	Pixel.SetPixel(pre,index);
	GrayPixel=GetGray(Pixel)/255.0;
	double b=(4*k-1.0);
	double a=1-b;
	double f=GrayPixel*(a*GrayPixel+b);
	Pixel*=f;
	OverFlow(Pixel);
	Pixel.GetPixelToNow(now,index);
}


以下是原圖像和經過處理後圖像的對照:

技術分享 技術分享 技術分享

原圖 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";

C++版

void WaterFilter(int x,int y,BYTE *pre,BYTE *now,int Stride,int index,double _quatLevel,double _waterPower,int width,int height){
	Vec4 nowPixel,RoundPixel;
	int indexRound;
	double Level;
	nowPixel.SetPixel(pre,index);
	int rx=rand()%2,ry=rand()%2;
	indexRound=(y+rx)*Stride+(x+ry)*4;
	RoundPixel.SetPixel(pre,indexRound);
	Level=255/pow(2,_quatLevel);
	RoundPixel.r=floor(RoundPixel.r/Level)*Level;
	RoundPixel.g=floor(RoundPixel.g/Level)*Level;
	RoundPixel.b=floor(RoundPixel.b/Level)*Level;
	OverFlow(RoundPixel);
	RoundPixel.GetPixelToNow(now,index);
}


代碼中的_quatLevel用來表示對圖像的量化比特數。值越小。色塊越明顯。比較合理的取值範圍是2-6。_waterPower則表示圖像顏色擴散範圍,取值範圍在8-64之間的效果比較好。

以下是經過水彩畫處理後的圖像:

技術分享 技術分享

圖:水彩畫效果。左圖量化比特數為6比特,擴散範圍為20象素。

右圖量化比特數為5比特,擴散範圍為40象素


最後貼個C++版用到的函數

struct Vec4{
	double r,g,b,a;
	Vec4 (){
		this->r=0;
		this->g=0;
		this->b=0;
		this->a=0;
	}
	Vec4(double r,double g,double b){
		this->r=r;
		this->g=g;
		this->b=b;
	}
	Vec4 operator+(const double one) const{
		return Vec4(r+one,g+one,b+one);
	}
	Vec4 operator+(const Vec4& rhs)const{
		return Vec4(r+rhs.r,g+rhs.g,b+rhs.b);
	}
	Vec4& operator+=(const double one){
		r+=one;
		g+=one;
		b+=one;
		return *this;
	}
	Vec4& operator+=(const Vec4& rhs){
		r+=rhs.r;
		g+=rhs.g;
		b+=rhs.b;
		return *this;
	}
	Vec4 operator-(const double one) const{
		return Vec4(r-one,g-one,b-one);
	}
	Vec4 operator-(const Vec4& rhs)const{
		return Vec4(r-rhs.r,g-rhs.g,b-rhs.b);
	}
	Vec4& operator-=(const double one){
		r-=one;
		g-=one;
		b-=one;
		return *this;
	}
	Vec4 operator*(const double one) const{
		return Vec4(r*one,g*one,b*one);
	}
	Vec4& operator*=(const double one){
		r*=one;
		g*=one;
		b*=one;
		return *this;
	}
	Vec4 operator/(const double one) const{
		return Vec4(r/one,g/one,b/one);
	}
	Vec4& operator/=(const double one){
		r/=one;
		g/=one;
		b/=one;
		return *this;
	}
	void Clear(){
		r=g=b=0;
	}
	void SetPixel(BYTE *pre,int index){
		this->r=pre[index];
		this->g=pre[index+1];
		this->b=pre[index+2];
		this->a=pre[index+3];
	}
	void SetPixel(double RGB){
		this->r=RGB;
		this->g=RGB;
		this->b=RGB;
	}
	void SetPixel(double R,double G,double B){
		this->r=R;
		this->g=G;
		this->b=B;
	}
	void GetPixelToNow(BYTE *now,int index){
		now[index]=this->r;
		now[index+1]=this->g;
		now[index+2]=this->b;
		
	}
};
void OverFlow(Vec4 &Pixel){
	if (Pixel.r>255.0)
	{
		Pixel.r=255;
	}
	else if (Pixel.r<0.0)
	{
		Pixel.r=-Pixel.r;
	}

	if (Pixel.g>255.0)
	{
		Pixel.g=255;
	}
	else if (Pixel.g<0.0)
	{
		Pixel.g=-Pixel.g;
	}

	if (Pixel.b>255.0)
	{
		Pixel.b=255;
	}
	else if (Pixel.b<0.0)
	{
		Pixel.b=-Pixel.b;
	}
}
void OverFlow(double &Pixel){
	if (Pixel>255.0)
	{
		Pixel=255;
	}
	else if (Pixel<0.0)
	{
		Pixel=-Pixel;
	}
}
void ToGray(Vec4 &Pixel){
	double detaGray;
	detaGray=Pixel.r*0.3+Pixel.g*0.59+Pixel.b*0.11;
	Pixel.SetPixel(detaGray);
	OverFlow(Pixel);
}
double GetGray(Vec4 Pixel){
	double detaGray;
	detaGray=Pixel.r*0.3+Pixel.g*0.59+Pixel.b*0.11;
	OverFlow(detaGray);
	return detaGray;
}
void BeGray(Vec4 &Pixel){
	double detaGray;
	detaGray=Pixel.r*0.3+Pixel.g*0.59+Pixel.b*0.11;
	
	if (detaGray<0.0)
	{
		detaGray=-detaGray;
	}
	else if (detaGray>255.0)
	{
		detaGray=255;
	}
	detaGray=255-detaGray;
	Pixel.SetPixel(detaGray);
}
void Solve(){
    BYTE *pre = (BYTE*)m_srcImg.Scan0;
    BYTE *now = (BYTE*)m_copySrcImg.Scan0;
    //函數在這邊調用就可以。

    int index=0,Stride=m_copySrcImg.Stride;     int width=m_pCopyImg->GetWidth();     int height=m_pCopyImg->GetHeight();     index=Stride+4;     for (int j=1;j<m_copySrcImg.Height-1;j++)     {         for (int i=1;i<m_copySrcImg.Width-1;i++)         {            //Gaussian_filter(i,j,pre,now,Stride,index);             //dip_filter(i,j,pre,now,Stride,index);             //relief(i,j,pre,now,Stride,index);             //Mosaic(i,j,pre,now,Stride,index);             //Mosaic_Point(i,j,pre,now,Stride,index);             //HDR(i,j,pre,now,index,1.1);             //WaterFilter(i,j,pre,now,Stride,index,10,40,width,height);                 index+=4;         }         index+=8;     }     //調用函數。

}



GLSL/C++ 實現濾鏡效果