glsl著色器 光照和紋理計算 (有用!)
http://my.oschina.net/sweetdark/blog/214220
以下內容只針對GLSL1.20的版本進行說明的,有些內建的變數在1.20之後,已經被廢棄了。
初次實驗
每個頂點著色器都至少輸出一個裁剪空間的位置座標。光照、紋理座標的生成和其他的一些操作是可選的。例如,你要建立了深度紋理,那你只需要最終的深度值,你就沒必要在著色器中處理顏色和紋理座標,也不需要輸出它們。但至少需要輸出裁剪空間的座標給後面的圖元組裝和光柵化。如果不輸出任何東西,行為將是未定義的。如果要讓顏色在後面的管道中可見,則至少要把輸入的顏色拷貝到輸出顏色,雖然著色器不對其進行任何處理。
舉個簡單的例子來模仿固定管線的方式。在固定管線中,會對頂點進行模型檢視變換和投影變換變為裁剪空間的位置座標。在GLSL中,提供了gl_ModelViewProjectionMatrix,這個矩陣包括模型檢視變換和投影變換。所以我們只要把頂點左乘以這個矩陣就能夠得到裁剪空間的位置座標。
//simple.vs
//執行頂點變換
//拷貝主顏色
#version 120
void main(void)
{
//頂點變換到裁剪空間位置,作為輸出
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
//把主顏色拷貝的正面顏色
gl_FrontColor = gl_Color;
}
上面的gl_ModelViewProjectionMatrx也可以分成兩個來寫,寫成gl_ProjectionMatrix * gl_ModelViewMatrix;
自己執行變換的另一種方式是使用內建函式ftransform,它對需要處理的頂點模擬了固定功能管線的頂點變換。這在混合固定功能和頂點著色器繪製同一個幾何圖形時很有用,可以防止Z值的細微差異導致的Z-fighting。
簡單的寫 就是 gl_Position = ftransform();
效果如下
散射光照
之前介紹過散射的光照,散射的光照要考慮到物體的面與輸入光源的角度。其公式如下:
Cdiff = max{N • L, 0} * Cmat * Cli
其中N代表頂點的單位法線, L代表從頂點指向光源的單位向量。Cmat 是表面的材料顏色, Cli是光源的顏色。Cdiff則計算出來的結果。在例子中我們使用的是白光,所以我們可以直接忽略掉Cli 因為乘以{1, 1, 1, 1}結果不變。下面簡單實現散射光照方程。
//基於白色光的散射光照
uniform vec3 lightPos;
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 - V.xyz);
//計算散射顏色
float NdotL = dot(N, L);
gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));
}
其中gl_NormalMatrix是GLSL內建的變數為法線變換矩陣,gl_Normal代表頂點的法線。dot也是GLSL內建的函式提供向量的點乘, normalize也是內建函式。其餘的已經在前面介紹過了。,那麼我們該如何設定這個lightPos向量呢。GLSL提供了一系列設定uniform變數的方法。這裡用到其中一個。
void glUniform3fv(
GLintlocation, GLsizeicount, const GLfloat *value)
;
你可以在渲染函式中, 隨意設定這個引數值,來改變光源的位置。整個編譯和連結shader並設定變數的函式如下:
float g_lightPos[3] = {20.0f, 10.0f, 20.0f, 1.0f};
void SetupRC()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_DEPTH_TEST);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
glEnable(GL_CULL_FACE);
glEnable(GL_POLYGON_SMOOTH);
glEnable(GL_LINE_SMOOTH);
GLint success; const GLchar* vsSource[1];
vsSource[0] = vsChar;
//這裡的vsChar就是著色器程式碼字串
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, vsSource, NULL);
glCompileShader(vs);
glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
if(!success)
{
GLchar infoLog[MAX_LENGTH];
glGetShaderInfoLog(vs, MAX_LENGTH, NULL, infoLog);
printf(infoLog);
getchar();
exit(0);
}
GLuint program = glCreateProgram();
glAttachShader(program, vs);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success)
{
GLchar infoLog[MAX_LENGTH];
glGetProgramInfoLog(program, MAX_LENGTH, NULL, infoLog);
printf(infoLog);
getchar();
exit(0);
}
glValidateProgram(program);
glGetProgramiv(program, GL_VALIDATE_STATUS, &success);
if (!success)
{
GLchar infoLog[MAX_LENGTH];
glGetProgramInfoLog(program, MAX_LENGTH, NULL, infoLog);
printf(infoLog);
getchar();
exit(0);
}
glUseProgram(program);
lightPosLocation = glGetUniformLocation(program, "lightPos");
if (lightPosLocation != -1)
{
glUniform3fv(lightPosLocation, 1, g_lightPos);
}
}
效果如下:(我光照的位置和物體的位置調的不是很好)
如果你還是想使用固定功能管線的glLight*來設定光源的位置的話,需要改一下shader程式碼。GLSL提供了一個內建的變數gl_LightSource[n].position 其中n為第幾個光源。改一下上面的shader.
#define FIX_FUNCTION 1
char vsChar[] = { "#version 120\n"
"uniform vec3 lightPos;\n"
"void main(void)"
"{"
" gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;"
" vec3 N = normalize(gl_NormalMatrix * gl_Normal);"
" vec4 V = gl_ModelViewMatrix * gl_Vertex;"
#if FIX_FUNCTION
" vec3 L = normalize(gl_LightSource[0].position.xyz - V.xyz);"
#else
" vec3 L = normalize(lightPos - V.xyz);"
#endif
" float NdotL = dot(N, L);"
" gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));"
"}"};
void SetupRC()
{
...
#if FIX_FUNCTION
glLightfv(GL_LIGHT0, GL_POSITION, g_lightPos);
#else
lightPosLocation = glGetUniformLocation(program, "lightPos");
if (lightPosLocation != -1)
{
glUniform3fv(lightPosLocation, 1, g_lightPos);
}
#endif
}
這樣效果是等價的。你會發現我並沒有呼叫
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
來開啟光照。這就是shader的好處,不需要這一堆的開關指令。需要用到的就拿去用吧,沒用到就相當於關閉了。shader 可程式設計管線更加靈活。
鏡面光照
鏡面光照要考慮光的入射方向,以及眼睛所在的位置。由頂點指向光源的向量,頂點的法線,頂點指向照相機的向量就可以決定鏡面光在該頂點的強度。簡單起見預設預設照相機在z的正方向上,假設頂點到照相機的單位向量為(0.0, 0.0, 1.0)。根據鏡面光的公式:
Cspec = max{N • H, 0}Sexp * Cmat * Cli
H是光線向量與視角向量之間夾角正中方向的單位向量。Sexp代表鏡面指數,用於控制鏡面光照的緊聚程度。Cmat是材料的顏色,Cli是光的顏色。Cspec 是最終求得的鏡面顏色。在下面簡單的例子中,假設光是白光(1.0, 1.0, 1.0, 1.0),鏡面材料的鏡面光屬性也為(1.0, 1.0, 1.0, 1.0),所以我們可以忽略掉這一項乘的操作。其中N,L,Cmat 和 Cli 和散射光是一樣的。這裡鏡面指數固定為128.
編寫如下的specular.vs:
#version 120
uniform vec3 lightPos;
void main(void)
{
//MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
//caculate diffuse
//normal
vec3 N = normalize(gl_NormalMatrix * gl_Normal);
//transform to view coordinate
vec4 V = (gl_ModelViewMatrix * gl_Vertex);
//light vector
vec3 L = normalize(lightPos - V.xyz);
float NdotL = dot(N,L);
vec4 diffuse = vec4(max(NdotL, 0.0)) * gl_Color;
//specular
vec3 H = normalize(vec3(0.0, 0.0, 1.0) + L);
float NdotH = max(0.0, dot(N,H));
const float expose = 128.0;
vec4 specular = vec4(0.0);
if (NdotL > 0.0)
specular = vec4(pow(NdotH, expose));
gl_FrontColor = diffuse + specular;
}
效果圖:
提升鏡面光照
由上圖可以看出,鏡面光照的高亮在物體表面變化的非常快。在這裡我們只是逐頂點的計算鏡面亮點然後在三角形內部進行插值。這樣的效果較差。我們並不能獲得一個漂亮的圓形的亮點,亮點看起來是不規則多邊形的。
一種改善的方式是把散射光的效果和鏡面光的效果區分開,把散射光照結果輸出為主顏色,鏡面光照的結果設定為輔助顏色。相比於之前的逐個頂點計算好光照結果,再進行光柵化插值然後進入片段處理。現在是把鏡面光的效果放到輔助顏色,而輔助顏色是在紋理等片段處理之後加到片段上的,這樣就能夠呈現更真實的光照效果。這種基於片段的求和通過啟用GL_COLOR_SUM就可以實現了。
#version 120
uniform vec3 lightPos;
void main(void)
{
// normal MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vec3 N = normalize(gl_NormalMatrix * gl_Normal);
vec4 V = gl_ModelViewMatrix * gl_Vertex;
vec3 L = normalize(lightPos - V.xyz);
vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
const float specularExp = 128.0;
// put diffuse into primary color
float NdotL = max(0.0, dot(N, L));
gl_FrontColor = gl_Color * vec4(NdotL);
// put specular into secondary color
float NdotH = max(0.0, dot(N, H));
gl_FrontSecondaryColor = (NdotL > 0.0) ?
vec4(pow(NdotH, specularExp)) :
vec4(0.0);
}
還需glEanble(GL_COLOR_SUM);
這種方式好像提升了一點點效果。但本質上的原因沒有解決,那就是鏡面指數的問題。隨著鏡面係數的提高(N • H),這種基於頂點插值的方式變化的非常快。如果你的物體沒有很細的分格化,有可能整個物體都沒有得到鏡面加亮(比如一個大三角形的三個頂點,都沒有得到鏡面加亮,那麼這個三角形就沒有鏡面加亮的效果了)。
要避免這個問題的一種有效方法是隻輸出一個鏡面係數(N • H),但是等到片段著色時,才進行冪操作。使用這種方式,可以安全地對變換更慢的鏡面係數進行插值。由於現在還沒接觸到片段著色器。我們可以使用紋理查詢的方式來實現這種功能。我們需要做的就是用一個包含S128 個值的表設定一個1D紋理,然後把鏡面係數輸出為一個紋理座標。然後再用固定功能管線的方式設定紋理環境把從紋理座標查詢到的鏡面顏色與散射光的顏色相加。
著色器程式碼如下:
#version 120
uniform vec3 lightPos;
void main(void)
{
// normal MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vec3 N = normalize(gl_NormalMatrix * gl_Normal);
vec4 V = gl_ModelViewMatrix * gl_Vertex;
vec3 L = normalize(lightPos - V.xyz);
vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
// put diffuse lighting result in primary color
float NdotL = max(0.0, dot(N, L));
gl_FrontColor = gl_Color * vec4(NdotL);
// copy (N.H)*8-7 into texcoord if N.L is positive
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);
}
在這裡N.H的範圍會被擷取為[0,1],但如果你對其進行128次冪,那麼在[0,1]之間的大部分值,都會非常接近0,這樣大部分頂點的紋理座標就是0了。只有[7/8, 1]的值經過冪之後,會有可度量的紋理值。為了充分利用1D紋理,我們可以把幾種在上面的八分之一範圍的值填充到整個紋理中,來提高結果的精度。我們可以把(N • H)放大8倍,然後左移7個單位,那麼[0,1]就被對映為[-7,1],然後使用GL_CLAMP_TO_EDGE環繞模式,[-7,0]的值將會被擷取為0.我們所感興趣的範圍[0,1]中的值將接受(7/8)128 和 1之間的紋理單元值。
//建立一個一維的紋理單元
void CreateTexture(float r, float g, float b)
{
GLfloat texels[512 * 4];
GLint texSize = (maxTexSize > 512) ? 512 : maxTexSize;
GLint x;
for (x = 0; x < texSize; x++)
{
texels[x*4+0] = r * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0);
texels[x*4+1] = g * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0);
texels[x*4+2] = b * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0);
texels[x*4+3] = 1.0f;
}
// Make sure the first texel is exactly zero. Most
// incoming texcoords will clamp to this texel.
texels[0] = texels[1] = texels[2] = 0.0f;
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA16, texSize, 0, GL_RGBA, GL_FLOAT, texels);
}
....//設定紋理模式
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize);
glActiveTexture(GL_TEXTURE0);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexEnvi(GL_TEXTURE_1D, GL_TEXTURE_ENV_MODE, GL_ADD);
CreateTexture(1.0f, 1.0f, 1.0f);
獲得了更好效果
使用前面方式的效果。
下面的shader使用三個光源:
#version 120
uniform vec3 lightPos[3];
varying vec4 gl_TexCoord[3];
uniform vec3 camaraPos;void main(void)
{
//MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
//經過檢視變換後的點
vec4 V = gl_ModelViewMatrix * gl_Vertex;
vec3 N[3], L[3], H[3];
gl_FrontColor = vec4(0.0);
for (int i = 0; i < 3; ++i)
{
N[i] = normalize(gl_NormalMatrix * gl_Normal);
L[i] = normalize(lightPos[i] - V.xyz);
float NdotL = dot(N[i], L[i]);
//accumalte diffuse light
gl_FrontColor += vec4(max(0.0, NdotL)) * gl_Color;
//指向光源的向量,與指向照相機的向量的。半形向量。
H[i] = normalize(L[i] + normalize(camaraPos));
float NdotH = 0.0;if (NdotL > 0.0)
NdotH = max(0.0, dot(N, H) * 8.0 - 7.0);
gl_TexCoord[i] = vec4(NdotH, 0.0, 0.0, 1.0);
}
}
上面的例子也用了對應的三個紋理。
...
glActiveTexture(GL_TEXTURE0);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD);
glBindTexture(GL_TEXTURE_1D, textures[0]);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
CreateTexture(1.0f, 0.25f, 0.25f);
glEnable(GL_TEXTURE_1D);
glActiveTexture(GL_TEXTURE1);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD);
glBindTexture(GL_TEXTURE_1D, textures[1]);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
CreateTexture(0.25, 1.0, 0.25);
glEnable(GL_TEXTURE_1D);
glActiveTexture(GL_TEXTURE2);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD);
glBindTexture(GL_TEXTURE_1D, textures[2]);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
CreateTexture(0.25, 0.25, 1.0);
glEnable(GL_TEXTURE_1D);
...
基於頂點的霧
儘管霧是在片段處理階段中處理的, 但出於效能的考慮,我們可以在頂點階段對其進行處理,而且也不影響真實性。下面是霧的二次方霧因子的方程式:
ff = e-(d * fc)2
其中d代表霧的濃度,fc是霧座標,通常情況下是頂點到照相機的距離。下面我們只在shader中計算 霧座標,霧的方程式用固定功能管線來實現。其中length是GLSL內建的函式,求向量的長度。
float fogColor[4] = {0.5f, 0.8f, 0.5f, 1.0f}; //霧顏色為淺綠
#version 120
uniform vec3 lightPos[1];
uniform vec3 camaraPos;void main(void)
{
//MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vec4 V = gl_ModelViewMatrix * gl_Vertex;
vec3 N = normalize(gl_NormalMatrix * gl_Normal);
vec3 L = normalize(lightPos[0] - V.xyz);
float NdotL = dot(N, L);
vec4 diffuse = max(0.0, NdotL) * gl_Color;
const float expose = 128.0;
vec3 H = normalize(L + normalize(camaraPos));
float NdotH = 0.0;if (NdotL > 0.0)
NdotH = max(0.0, dot(N, H));
vec4 specular = vec4(pow(NdotH, expose));
gl_FrontColor = diffuse + specular;
//計算霧座標
gl_FogFragCoord = length(V);
}
..
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glFogfv(GL_FOG_COLOR, fogColor);
glFogf(GL_FOG_DENSITY, density);
glFogi(GL_FOG_MODE, GL_EXP2);
glFogi(GL_FOG_COORD_SRC, GL_FOG_COORD);
glEnable(GL_FOG);
..
當然我們也可以在shader中直接實現該方程。
#version 120
uniform vec3 lightPos[1];
uniform vec3 camaraPos;
uniform float density;
void main(void)
{
//MVP transform
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vec4 V = gl_ModelViewMatrix * gl_Vertex;
vec3 N = normalize(gl_NormalMatrix * gl_Normal);
vec3 L = normalize(lightPos[0] - V.xyz);
float NdotL = dot(N, L);
vec4 diffuse = max(0.0, NdotL) * gl_Color;
const float expose = 128.0;
vec3 H = normalize(L + normalize(camaraPos));
float NdotH = 0.0;if (NdotL > 0.0)
NdotH = max(0.0, dot(N, H));
vec4 specular = vec4(pow(NdotH, expose));
//計算霧因子
const float e = 2.71828;
float fogFactor = density * length(V);
fogFactor *= fogFactor;
fogFactor = clamp(pow(e, -fogFactor), 0.0, 1.0);
const vec4 fogColor = vec4(0.5, 0.8, 0.5, 1.0);
//把霧顏色和 光的顏色 根據霧因子進行混合
gl_FrontColor = mix(fogColor, clamp(diffuse + specular, 0.0, 1.0), fogFactor);
}
mix在x,y之間進行插值,a是權值。 插值的公式是x⋅(1−a)+y⋅a.
原始碼參考:https://github.com/sweetdark/openglex 專案下 specular multilight 和 fogvs。 shader在 shadersource目錄下。