OpenGL程式設計逐步深入(四)Shaders
OpenGl 中的 Shader在一些中文書籍或資料中都被翻譯為“著色器”, 單從字面意思也看不出Shader到底是什麼,Shader實際上就是一段程式碼,用於完成特定功能的一個模組。Shader分為Vertex Shader(頂點著色器)和Pixel Shader(畫素著色器)兩種,其中Pixel Shader在本文中又被稱為Fragment Shader(片段著色器)
準備知識
從本節開始我們將使用Shader來實現遊戲中的各種特效,Shader是現代3D圖形學中重要的渲染技術。從某種程度上,你可以抱怨這種做法是比較落後的,因為固定渲染管道(fixed function pipeline)提供的3d功能本來只需要開發人員指定配置引數(例如光照屬性、旋轉值)就可以了,現在都要通過編寫Shader程式碼來實現。然而這種可程式設計方式為編寫程式提供了更大的靈活性和創新性。
OpenGl的可程式設計管道可以由下圖直觀的表達:
頂點處理器(vertex processor)負責執行每個通過管道的頂點的vertex shader(數量取決於呼叫繪圖函式是傳入的引數),Vertex shaders並不知道渲染圖元的拓撲結構(是繪製四邊形還是三角形?),因此頂點處理器(vertex processor)是必須的。每個頂點只進入頂點處理器一次,經過變換後沿著管道執行下一步處理。(注:所謂的管道是指從頂點輸入到渲染到螢幕上經歷的整個過程)
接下來是幾何處理器(geometry processor)階段。在這個階段,一組連續的頂點如何構成圖形的資訊將會提供給Shader。這使得我們需要考慮除了頂點自身之外的額外資訊。幾何處理器(geometry processor)能夠改變呼叫繪圖函式時指定的拓撲結構(點、線、三角形等),例如你可以將它用在一組點上,將原指定拓撲結構生成的四邊形變成兩個三角形(公告牌技術的應用)。此外,你還可以讓幾何處理器(geometry processor)忽略多個指定的點,讓這些點以呼叫繪圖函式時指定的拓撲結構來繪製圖形。
管道中的下一階段為裁剪階段(Clipper),這是一個任務較為簡單的固定功能單元,會裁剪掉上一節教程中的正方形以外的圖形元素,除此之外Z軸方向上的近裁剪面和遠裁剪面以外的部分也會被裁剪掉。能夠對映到螢幕的頂點不會被裁剪,光柵化程式會根據繪圖函式指定的拓撲結構(三角形、四邊形等)將圖形渲染在螢幕上。例如:拓撲結構指定為三角形時光柵化程式會找到三角形內部的所有點並對它們進行渲染。對於每個點光柵化程式會呼叫片段處理器。在這裡你可以通過對紋理取樣(或者使用其他技術)確定畫素的顏色。
上面的三個可程式設計階段是可有可無的,如果不對它們繫結Shader,一些預設的功能將會被執行。
Shader的建立和c/c++程式非常相似,首先編寫Shader程式碼,然後確保它在你的程式中能正確執行。可以在程式中使用字元陣列來儲存Shader程式碼或者將Shader寫在一個外部的檔案中,然後在程式中載入它。接著把這些Shader全部的編譯成Shader物件,最後使用連結器將這些Shader連結到一個單獨的program 物件載入到GPU中。連結Shader物件使得驅動能夠對這些Shader進行裁剪並根據它們的關係做優化處理。
專案配置
1.在前幾節專案解決方案中新建控制檯應用。
2.在專案上點選右鍵選擇屬性,將配置屬性->常規->專案預設值->字符集設定為“使用多位元組字符集”。
在配置屬性->VC++目錄下的包含目錄中新增$(SolutionDir)Include和$(SolutionDir)Include\assimp
在庫目錄中新增$(SolutionDir)Lib
在配置屬性->連結器->輸入->附加依賴項中新增freeglut.lib、glew32.lib、assimp.lib
程式程式碼
清單1.主程式 tutorial04.cpp程式碼
/*
Copyright 2010 Etay Meiri
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Tutorial 04 - shaders
*/
#include "stdafx.h"
#include <stdio.h>
#include <GL/glew.h>
#include <GL/freeglut.h>
#include "ogldev_util.h"
GLuint VBO;
const char* pVSFileName = "shader.vs";
const char* pFSFileName = "shader.fs";
//建立頂點結構體,用於表示OpenGL中的頂點
struct Vector3f
{
float x;
float y;
float z;
Vector3f(){}
Vector3f(float _x, float _y, float _z)
{
x = _x;
y = _y;
z = _z;
}
};
static void RenderSceneCB()
{
glClear(GL_COLOR_BUFFER_BIT);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);
glutSwapBuffers();
}
static void InitializeGlutCallbacks()
{
glutDisplayFunc(RenderSceneCB);
}
static void CreateVertexBuffer()
{
Vector3f Vertices[3];
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
Vertices[1] = Vector3f(1.0f, -1.0f, 0.0f);
Vertices[2] = Vector3f(0.0f, 1.0f, 0.0f);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
GLuint ShaderObj = glCreateShader(ShaderType);
if (ShaderObj == 0) {
fprintf(stderr, "Error creating shader type %d\n", ShaderType);
exit(0);
}
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
glCompileShader(ShaderObj);
GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar InfoLog[1024];
glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
exit(1);
}
glAttachShader(ShaderProgram, ShaderObj);
}
static void CompileShaders()
{
GLuint ShaderProgram = glCreateProgram();
if (ShaderProgram == 0) {
fprintf(stderr, "Error creating shader program\n");
exit(1);
}
string vs, fs;
if (!ReadFile(pVSFileName, vs)) {
exit(1);
};
if (!ReadFile(pFSFileName, fs)) {
exit(1);
};
AddShader(ShaderProgram, vs.c_str(), GL_VERTEX_SHADER);
AddShader(ShaderProgram, fs.c_str(), GL_FRAGMENT_SHADER);
GLint Success = 0;
GLchar ErrorLog[1024] = { 0 };
glLinkProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
exit(1);
}
glValidateProgram(ShaderProgram);
glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
if (!Success) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
exit(1);
}
glUseProgram(ShaderProgram);
}
int _tmain(int argc, _TCHAR* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA);
glutInitWindowSize(800, 600);
glutInitWindowPosition(100, 100);
glutCreateWindow("Tutorial 02");
InitializeGlutCallbacks();
// Must be done after glut is initialized!
GLenum res = glewInit();
if (res != GLEW_OK) {
fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
return 1;
}
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
CreateVertexBuffer();
CompileShaders();
glutMainLoop();
return 0;
}
程式碼解讀
GLuint ShaderProgram = glCreateProgram();
這裡我們建立一個Program物件,你可以把它是Shader的容器,我們將會把所有的Shader物件連結到這個Program物件中。
GLuint ShaderObj = glCreateShader(ShaderType);
我們通過glCreateShader函式的呼叫建立兩個Shader物件,其中一個Shader型別為GL_VERTEX_SHADER(Vertex Shader),另外一個為GL_FRAGMENT_SHADER(Fragment Shader)。
Shader物件用於維護我們編寫的Shader程式碼。
const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);
在編譯Shader物件之前我們必須指定它的原始碼,glShaderSource 函式以Shader物件作為引數,提供了一種靈活的方式指定Shader原始碼。原始碼可以分佈在多個字元陣列中,你需要提供一個存放這些字元陣列地址的陣列的指標和一個存放每個陣列長度的陣列的指標。為了簡單起見,我們使用一個字元陣列存放所有的Shader原始碼和只有一個元素的GLint陣列存放字元陣列的長度。 第二個引數指定這兩個陣列元素個數。
glCompileShader(ShaderObj);
呼叫該函式編譯Shader物件。
GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar InfoLog[1024];
glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL, InfoLog);
fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
}
這段程式碼用於輸出Shader物件編譯出錯時的錯誤資訊。
glAttachShader(ShaderProgram, ShaderObj);
將編譯後的Shader物件附加到之前建立的Program物件中,非常類似於在makefile檔案中新增需要連結的物件列表。因為我們沒有一個makefile檔案來效仿gnu make的行為,所以只能呼叫函式的方式為連結處理做準備。
glLinkProgram(ShaderProgram);
在所有的Shader物件經過編譯並把它們附加到Program物件之後,呼叫glLinkProgram來連結它們。需要注意的是,完成Program物件的連結後,可以呼叫glDetachShader 和glDeleteShader 函式來解除附加的Shader物件。OpenGl驅動中維護著它所生成的大多數物件的引用計數,如果一個Shader物件建立之後又被刪除,驅動程式去把它去除,但是如果它被附加到Program物件中,呼叫glDeleteShader 後驅動程式僅僅會把它標記為刪除,你還需呼叫glDetachShader 將它的引用計數置為0,然後它才會被刪除。
glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
}
注意:我們檢測Program物件相關錯誤(例如連結錯誤)和檢測Shader物件錯誤呼叫的函式有些不同,使用glGetProgramiv 代替glGetShaderiv ,使用glGetProgramInfoLog代替glGetShaderInfoLog 。
glValidateProgram(ShaderProgram);
看到這段程式碼,你可能會問為什麼已經成功連結Program物件後還要呼叫glValidateProgram來校驗該物件。所不同的是連結錯誤檢測針對的是Shader物件的合併,而該函式是檢測Program物件在該管道狀態下是否能正確執行。
glUseProgram(ShaderProgram);
最後呼叫呼叫上面這個函式,安裝Program物件作為當前渲染狀態的一部分。這個Program物件會影響所有繪圖函式的呼叫,直到你替換它或使用glUseProgram指定引數為NULL來顯式的禁用它。
清單2.shader.vs程式碼
#version 330
layout (location = 0) in vec3 Position;
void main()
{
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
}
#version 330
告訴編譯器GLSL版本為3.3,如果編譯器不支援將會丟擲異常。
layout (location = 0) in vec3 Position;
這段程式碼在Shader中宣告一個頂點特定屬性(vertex specific attribute)Position,它是由3個float型別構成的向量。頂點特定(vertex specific)意味著在GPU呼叫每一個shader時,在緩衝區中的新頂點的值會被提供。宣告的第一部分layout (location = 0),建立屬性名和緩衝區中屬性的繫結。這樣做是為了防止我們的頂點中有多個屬性(位置、法線、紋理座標等)。我們需要讓編譯器知道頂點中的哪個屬性必須對映到shader中宣告的屬性。有兩種做法,我們可以像上面程式碼一樣不明確的設定(指定為0)。如果這樣我們可以在程式中使用一個硬編碼的值(即呼叫glVertexAttributePointer函式時的第一個引數值)。或者我們可以不管它(即上面語句直接寫成‘in vec3 Position’),然後在執行時使用glGetAttribLocation從程式中查詢該location 。這時我們需要將返回值用在glVertexAttributePointer 函式引數中來取代硬編碼方式。這裡我們選擇較為簡單的方式,但是在更復雜的程式中最好讓編譯器決定屬性的索引並且在執行時查詢它們。這使得把Shader從多個原始檔整合起來變的更簡單,而無需把它們調整到緩衝區佈局中。
void main()
你可以通過把多個Shader物件連結來建立你自己的Shader,然而在每個著色階段(VS,GS,FS)只能有一個main函式作為Shader的入口點。
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);
這裡我們通過硬編碼方式對傳過來的頂點位置進行變換。把X/Y的值減半,Z的值保持不變,gl_Position是一個特殊的內建變數應該包含齊次的頂點座標位置。光柵化程式會找到這個變數,並使用它作為螢幕空間的位置。使X/Y值減半意味著我們能看到的三角形的大小將是前面教程中的1/4。需要注意的是我們把W的值設為1,這對三角形的正確顯示是至關重要的。投影從3D到2D實際上是在兩個獨立的階段完成。首先你需要把所有頂點乘上投影矩陣,在頂點到底光柵化程式之前,GPU會為位置屬性自動執行所謂的“透視分割”。這意味著所有的元件都會除以gl_Position 的W元件值。在本教程中我們還沒有在vertex shader中做任何投影,但是透視分割(perspective divide)階段不可缺少。
清單3. shader.fs程式碼
#version 330
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
out vec4 FragColor;
通常片段著色器(fragment shader)的作用是決定畫素的顏色。此外,fragment shader可以完全丟棄畫素或改變其Z值(將會影響隨後的Z test結果)。在該案例中,螢幕圖形輸出的顏色由上面的變數決定,包含四個元件分別為R、G、B、A(alpha,即透明度),設定到這個變數中的值將會被光柵化程式接收並寫入到幀緩衝區。
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
在前面的教程中沒有用到片段著色器,所有繪製的圖形預設都是白色,這裡通過FragColor 設定為紅色。
編譯執行程式
你可以看到一個紅色的三角形顯示在螢幕中間。