歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> OpenGL超級寶典學習筆記——頂點著色器示例

OpenGL超級寶典學習筆記——頂點著色器示例

日期:2017/3/1 9:32:09   编辑:Linux編程

以下內容只針對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);
}

genType mix(genTypex, genTypey, genTypea);

mix在x,y之間進行插值,a是權值。 插值的公式是x⋅(1−a)+y⋅a.

源碼參考:https://github.com/sweetdark/openglex 項目下 specular multilight 和 fogvs。 shader在 shadersource目錄下。

OpenGL超級寶典 第4版 中文版PDF+英文版+源代碼 見 http://www.linuxidc.com/Linux/2013-10/91413.htm

OpenGL編程指南(原書第7版)中文掃描版PDF 下載 http://www.linuxidc.com/Linux/2012-08/67925.htm

OpenGL 渲染篇 http://www.linuxidc.com/Linux/2011-10/45756.htm

Ubuntu 13.04 安裝 OpenGL http://www.linuxidc.com/Linux/2013-05/84815.htm

OpenGL三維球體數據生成與繪制【附源碼】 http://www.linuxidc.com/Linux/2013-04/83235.htm

Ubuntu下OpenGL編程基礎解析 http://www.linuxidc.com/Linux/2013-03/81675.htm

如何在Ubuntu使用eclipse for c++配置OpenGL http://www.linuxidc.com/Linux/2012-11/74191.htm

更多《OpenGL超級寶典學習筆記》相關知識 見 http://www.linuxidc.com/search.aspx?where=nkey&keyword=34581

Copyright © Linux教程網 All Rights Reserved