OpenGL的著色器是新一代顯卡提供給開發人員一個小程式,為的是讓開發人員對光照、座標轉換以及像素進行一些個人化的處理。OpenGL的著色器有一種專門的語言:GLSL,現在的GLSL應該全面轉向Shader Model5,像我這樣的初學者還需要花費更長的時間來學習才能基本瞭解OpenGL的著色器方面的知識。
下面兩圖展示了OpenGL從固定渲染管線到可程式化渲染管線的變化
我們可以很容易地看出,OpenGL的頂點著色器取代了固定渲染管線的轉換、光照、紋理座標產生和轉換;片斷著色器取代了紋理、顏色求和和霧的操作。在OpenGL3.2版本中加入了幾何著色器(GeometryShader)這個概念,在OpenGL4.0中又添加了分格化控制(Tessellation Control)和分格化評估(Tessellation Evaluation)著色器,最新的OpenGL版本4.3則添加了計算著色器(Compute Shader)。看來OpenGL的著色器真是越來越複雜,越來越重要了。
下面介紹一下光照模型在頂點著色器的實現,所有的內容都可以在《OpenGL超級寶典(第四版)》中找到。
漫反射光照是一種簡單的光照模型,它只考慮漫反射。它的公式是:
N是頂點的單位法線,L是表示從頂點到光源的單位向量方向。Cmat是表面材料的顏色,Cli是光線的顏色,Cdiff是最終的散射顏色。注意,N和L在傳入之前一定要單位化。如果用頂點著色器來實現的話,則是:
uniform vec3 lightPos[1];void main( void ){ // 法線的MVP變換 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize( gl_NormalMatrix * gl_Normal ); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize( lightPos[0] - V.xyz ); // 輸出散射顏色 float NdotL = dot( N, L ); gl_FrontColor = gl_Color * vec4( max( 0.0, NdotL ) );}
注意,這裡lightPos是一個標記為uniform的變數,這意味著可以在運行期傳入的一個值到lightPos中來改變著色器的行為。
下面是漫反射光照模型的運行結果:
只有漫反射,再漂亮的模型也會失去光澤,我們必須找出一個方法來顯示模型的高光,這時應採用鏡面反射光照模型。鏡面反射光照模型的公式是:
H表示光線向量和視圖向量(可通過視圖矩陣轉換)之間的夾角正中的方向。稱為半形向量。Sexp是最終產生的鏡面顏色。N、L、Cmat和Cli的值與散射光方程式相同。下面是用著色器實現的代碼:
uniform vec3 lightPos[1];void main( void ){ // 法線變換 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize( gl_NormalMatrix * gl_Normal ); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize( lightPos[0] - V.xyz ); vec3 H = normalize( L + vec3( 0.0, 0.0,1.0 ) ); const float specularExp = 128.0; // 計算散射光照 float NdotL = max( 0.0, dot( N, L ) ); vec4 diffuse = gl_Color * vec4( NdotL ); // 計算鏡面光照 float NdotH = max( 0.0, dot( N, H ) ); vec4 specular = vec4( 0.0 ); if ( NdotL > 0.0 ) specular = vec4( pow( NdotH, specularExp ) ); // 求散射和鏡面成分之和 gl_FrontColor = diffuse + specular;}
下面是渲染的:
這種渲染模式並不是理想的,因為僅僅是簡單的插值,在面數不多的幾何體上光照非常難看,為此有兩種解決方案。其一是使用輔助顏色(二級顏色,Secondary Color)將鏡面反射顏色與漫反射顏色進行分離,第二種方法則是使用一維紋理來為幾何體貼上高光的“紋理”。下面是採用分離顏色的方法的頂點著色器:
uniform vec3 lightPos[1];void main( void ){ // 法線變換 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize( gl_NormalMatrix * gl_Normal ); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize( lightPos[0] - V.xyz ); vec3 H = normalize( L + vec3( 0.0, 0.0,1.0 ) ); const float specularExp = 128.0; // 把散射顏色放在主顏色中 float NdotL = max( 0.0, dot( N, L ) ); gl_FrontColor = gl_Color * vec4( NdotL ); // 把鏡面顏色放到輔助顏色中 float NdotH = max( 0.0, dot( N, H ) ); gl_FrontSecondaryColor = ( NdotL > 0.0 )? vec4( pow( NdotH, specularExp ) ): vec4( 0.0 );}
下面是:
採用一維紋理的方法較麻煩。首先在初始化的時候建立一維紋理,代碼如下:
bool OpenGLView::SetupTextureSpecular( void ){ …… // 使用0號紋理進行紋理設定,並且開啟一維紋理 glGetIntegerv( GL_MAX_TEXTURE_SIZE, &maxTexSize ); glActiveTexture( GL_TEXTURE0 ); glGenTextures( 1, &texID ); glBindTexture( GL_TEXTURE_1D, texID ); // 建立一維的紋理,並且設定它 GLfloat texture[512 * 4]; GLint texSize = ( maxTexSize > 512 ) ? 512 : maxTexSize; float r = 1.0f, g = 1.0f, b = 1.0f; for ( int x = 0; x < 512; ++x ) { // Incoming N.H has been scaled by 8 and biased by -7 to take better // advantage of the texture space. Otherwise, the texture will be // entirely zeros until ~7/8 of the way into it. This way, we expand // the useful 1/8 of the range and get better precision. texture[x*4+0] = r * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texture[x*4+1] = g * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texture[x*4+2] = b * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texture[x*4+3] = 1.0f; } // Make sure the first texel is exactly zero. Most // incoming texcoords will clamp to this texel. //texture[0] = texture[1] = texture[2] = 0.0f; glTexImage1D( GL_TEXTURE_1D, 0, GL_RGBA16, texSize, 0, GL_RGBA, GL_FLOAT, texture ); return true;}
隨後設定一些狀態:
glEnable( GL_TEXTURE_1D );glActiveTexture( GL_TEXTURE0 );glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD );glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );glBindTexture( GL_TEXTURE_1D, texID );
最後看看頂點著色器是如何寫的:
uniform vec3 lightPos[1];void main( void ){ // 法線變換 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize( gl_NormalMatrix * gl_Normal ); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize( lightPos[0] - V.xyz ); vec3 H = normalize( L + vec3( 0.0, 0.0,1.0 ) ); const float specularExp = 128.0; // 把散射顏色放在主顏色中 float NdotL = max( 0.0, dot( N, L ) ); gl_FrontColor = gl_Color * vec4( NdotL ); // 把鏡面顏色放到輔助顏色中 float NdotH = 0.0;if ( NdotL > 0.0 )NdotH = max( 0.0, dot( N, H ) * 8.0 - 7.0 );gl_TexCoord[0] = vec4( NdotH, 0.0, 0.0, 1.0 );}
執行效果如,發現高光範圍明顯比上面兩個要小,說明光照的精確度很高。最後說明一下,我似乎在Intel的集顯上看不全這些效果,所以大家還是盡量選用高效能獨顯對其進行渲染。
參考:《OpenGL超級寶典(第四版)》