【Modern OpenGL】Shader
Shaders
正如在上一篇教程中提到的,shader是在GPU中運行的小程序。如上一個教程中實現的最簡單的vertex shader和fragment shader,一個shader基本上負責圖形渲染流水線中的一個階段的功能。從根本上來說,shader就是將輸入轉化成輸出的操作。而且,它們之間是獨立的,除了以輸入和輸出方式外,他們之間不允許進行通信。
上一篇教程中我們僅僅是知道了關于shader最基本的寫法和用法。在本篇教程中我們將對shader進一步講解,特別是GLSL(OpenGL Shading Language,簡稱GLSL)語言。
GLSL
Shader使用與C類似GLSL語言來書寫的。GLSL是為圖形處理量身定做的語言,它包含很多針對向量或者矩陣操作的特性。
Shader一般以版本聲明開始,接著聲明輸入和輸出變量。uniform變量(先理解成一種全局變量,后面會講到)和主函數(main函數)。每一個shader的入點都是main函數,在main函數中,我們對輸入數據進行處理,然后將處理結果寫到輸出數據中。
一個shader的典型結構如下所示:
//版本聲明 #version version_number //輸入聲明 in type in_variable_name; in type in_variable_name; //輸出聲明 out type out_variable_name; //uniforms uniform type uniform_name; //主函數 void main() {// Process input(s) and do some weird graphics stuff...// Output processed stuff to output variableout_variable_name = weird_stuff_we_processed; }當我們說到具體的shader的時候,比如說vertex shader,每一個輸入變量又叫做頂點屬性(vertex attribute)。對輸入的頂點屬性數量有一個上界,是由硬件決定的。OpenGL保證知道有16個4分量的頂點屬性可用,但是某些硬件可能會支持更多,可以通過查詢GL_MAX_VERTEX_ATTRIBS來獲得自己機器上支持的數量:
GLint nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;一般情況下返回值會大于等于16,無特殊要求是夠用了。我的平臺上也是輸出16。
Types
GLSL有和其他編程語言類似的數據類型用于指定變量的種類。在GLSL中,int, float, double, uint 和 bool是和C一樣的基本數據類型,還有兩種容器類型的變量,我們在后面會經常用到,它們分別是vector(向量)和matrice(矩陣)。我們將在后面的教程中討論矩陣。
向量 Vectors
GLSL中的vector是含有1,2,3,或者4個基本數據類型分量的容器。用如下的形式來聲明向量(其中n代表向量中分量的個數):
- vecn: 默認情況下分量數據類型是float。
- bvecn: bool類型的向量。
- ivecn: 整型類型的向量。
- uvecn: 無符號整型類型的向量。
- dvecn: 雙精度類型的向量。
在大多數情況下我們使用默認情況下的vecn就夠了,因為浮點類型的分量已經夠我們大部分的使用了。
向量的分量可以通過vec.x形式訪問。可以分別使用.x, .y, .z 和 .w來訪問向量的第一、二、三、和四個分量。GLSL還可以使用rgba來訪問顏色向量,或者使用stpq來訪問紋理坐標,他們也能訪問相同的分量值。
GLSL中的向量是十分靈活的,它允許一些有趣的操作——它支持以下類似的語法:
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy;vec2 vect = vec2(0.5f, 0.7f); vec4 result = vec4(vect, 0.0f, 0.0f); vec4 otherResult = vec4(result.xyz, 1.0f);總體來說,向量是一種十分靈活的數據類型,它可以用來聲明各種輸入和輸出。讓我們在教程中根據實例仔細體會吧。
輸入和輸出
從Shader自身來說,他們是小的獨立程序,但是從整體來說,他們是整個圖形渲染流水的組成部分,這也是為什么我們要讓它們有輸入和輸出。GLSL為此專門定義了in和out關鍵字。每個shader都可以定義用這兩個關鍵字來指明自身的輸入和輸出數據,當其它的shader中的輸入和輸出數據類型能夠相匹配,那么這兩個shader就可以連接起來,相應的數據流就可以在連接起來的shader之間流通。我們之前定義的vertex shader和fragment shader明顯是不能夠進行連接的,因為它們的輸入輸出接口是匹配不上的。
Vertex shader應該支持不同種類的輸入,否則它就是低效的。因為它是圖形渲染流水線的最開始的頂點數據輸入,而輸入的數據的類型也是多種多樣的。為了定義頂點數據的組織方式,我們通過location標定輸入的變量,這樣我們可以在CPU中來配置頂點屬性。我們在上一個教程中的vertex shader中的layout (location = 0)就是這種用法。所以vertex shader需要為其輸入額外規定布局(layout),這樣就可以和具體的頂點數據聯系起來。
實際上事先指定輸入數據布局的方法,也就是通過類似layout (location = 0)的聲明完成,可以通過在OpenGL中調用glGetAttribLocation的方式取代。但是這種方式相當于把shader和OpenGL的工作分開來。
fragment shader應該輸出的是顏色值,應該是一個vec4類型的向量。因為片段處理器本質上決定了屏幕上顯示的每個像素的顏色值(當然有可能會被后面的混合器改變),所以如果沒有指定或者錯誤指定輸出顏色值,那么OpenGL渲染得到的可能是錯誤的。
所以,如果我們想要在A shader和B shader之間傳遞數據,比如說從A傳到B,那么至少應該在A中定義輸出變量,在B中定義輸入變量,而且要求這個輸入和輸出變量的數據類型和名稱必須一致。這樣OpenGL才會在圖形渲染流水線中將這兩個shader連接起來以完成數據的傳遞。為了更好的理解上面說的這些,下面修改上個教程創建的vertex shader和fragment shader來進行理解:
Vertex shader
#version 330 core layout (location = 0) in vec3 position; // The position variable has attribute position 0 out vec4 vertexColor; // Specify a color output to the fragment shadervoid main() {gl_Position = vec4(position, 1.0); // See how we directly give a vec3 to vec4's constructorvertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // Set the output variable to a dark-red color }Fragment shader
#version 330 core in vec4 vertexColor; // The input variable from the vertex shader (same name and same type)out vec4 color;void main() {color = vertexColor; } 如上面代碼所示:我們在vertex shader中聲明了一個vec4類型的變量vertexColor作為其輸出;在fragment shader中我們也聲明了一個同名同類型的變量,但是作為其輸入。所以這兩個變量實際上就將這兩個shader“連接”起來了——vertex shader可以利用vertexColor變量給fragment shader傳遞顏色值。在例子中,我們在vertex shader中給vertexColor賦值為一個深紅色的顏色,fragment shader中用這個顏色為其輸出的顏色值賦值,那么我們也應該得到最終的圖形的渲染顏色是深紅色,實際上也是這樣,我得到的結果是:哈哈,我們成功將一個顏色值從vertex shader傳遞到fragment shader中!讓我們再嘗試一下更有趣的:從我們的程序中傳遞一個顏色值給fragment shader,這需要用到我們在開頭提到的uniform。
Uniforms
與頂點屬性類似,uniform是從在CPU中運行的程序向在GPU中運行的shader的另一種方式,但是二者卻有很大的不同。首先,uniform類型的變量是全局的,這就意味著:首先,每個shader都必須有一個唯一命名的uniform變量,并且在任何shader(不需要連接在一起)中都能夠訪問其它shader中的uniform變量;其次,uniform變量的值一直保持不變,直到被重置或者更新才會改變。
在GLSL中聲明一個uniform只需要在變量聲明的時候加上一個關鍵字uniform。在此之后我們就可以使用這個uniform變量。接下來,讓我們嘗試一下是否可以使用uniform來設置fragment shader的數據結果值。原理就是,我們在fragment shader中聲明一個全局變量,并將fragment的最后輸出結果賦值為這個uniform值,然后我們在OpenGL程序中對這個uniform變量進行修改,然后看效果,首先是fragment shader:
#version 330 core out vec4 color;uniform vec4 ourColor; // We set this variable in the OpenGL code.void main() {color = ourColor; }如你所見,在這個fragment shader中,我們定義了一個vec4類型的變量ourcolor,前面的uniform關鍵字標明它是一個uniform類型的變量。然后,我們將fragment shader的輸出值color賦值為ourcolor。實際上,因為uniform類型的變量是全局變量,我們可以在任何的shader中定義,在任何的shader中使用已定義多的uniform變量。
如果你定義了一個uniform類型的變量,但是在GLSL程序中并沒有使用過,那么,編譯器就會在編譯的時候將這個變量給去掉。這可能會造成一些奇怪的錯誤(比如說你在OpenGL中對這個uniform賦值),我們應該記住這一點。
上面的uniform變量當前是空的,因為我們還沒有對它進行任何的賦值操作。下面我們就來對它進行賦值。首先,我們要找到這個uniform變量的索引/位置,然后我們可以對它進行值的更新。我們不想僅僅傳遞單一的顏色給fragment shader,我們讓這個顏色值隨著時間改變,代碼如下:
GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);首先,我們通過glfwGetTime函數取得運行時以秒為單位的時間值,然后我們設置綠色分量的值在0.0-1.0之間隨著時間變化。再然后,我們利用glGetUniformLocation函數取得我們在fragment shader中聲明的uniform變量的索引/位置。最后利用glUniform4f函數對這個位置的值進行更新。
需要注意的是,在調用glGetUniformLocation函數時,我們需要傳遞我們組裝的渲染程序對象名稱,在我們的例子中是”shaderProgram”,它指明了在哪兒查找,同時需要給出我們想要查找哪個uniform,即給出我們要查找的uniform名稱。如果這個函數返回-1,那么表示沒有找到。成功找到后,我們最后通過glUniform4f來根據找到的位置設置這個uniform的值。
需要注意的是,在設置一個渲染程序對象中的uniform變量值的時候,需要用glUseProgram函數來顯示表明我們要修改的渲染程序對象,在本例中,即shaderProgram。
因為OpenGL的核心是一個C庫,所以它沒有提供類型重載的功能。所以,OpenGL為每種需要的函數都定義了一個函數,glUniform是一個很好的例子。glUniform函數需要在一個指定需要設置數據類型的后綴,如本例中的4f,表明這個函數有四個float類型的參數。一些其它可能的后綴如下:
f: 函數有1個float類型的參數 i: 函數有1個int類型的參數 ui: 函數有1個unsigned int類型的參數 3f: 函數有3個float類型的參數 fv: 函數有1個float類型分量的vector參數所以,每當需要重載的時候,只需要在后面添加相應的后綴就可以了。
現在我們已經知道怎樣設置uniform類型變量的值了,我們可以用它們來進行渲染了。如果我們想讓顏色是漸變的,那么我們可以在每次游戲循環(每幀)中對uniform進行更新,否則,如果我們只調用一次,那么顏色值也就只有一種。我們在下面的程序中采用前一種方式:
while(!glfwWindowShouldClose(window)) {// Check and call eventsglfwPollEvents();// Render// Clear the colorbufferglClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// Be sure to activate the shaderglUseProgram(shaderProgram);// Update the uniform colorGLfloat timeValue = glfwGetTime();GLfloat greenValue = (sin(timeValue) / 2) + 0.5;GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);// Now draw the triangleglBindVertexArray(VAO);glDrawArrays(GL_TRIANGLES, 0, 3);glBindVertexArray(0); }上面的代碼看上去是比較簡單的,只是在原有基礎上添加了uniform值的更新,如果正確的話,我們應該能夠得到所繪制的圖形顏色漸變的結果。目前為止的代碼在這兒。
正如你看到的,uniform是一個很好的工具,它可以幫助我們在每次渲染迭代中設置屬性或者在程序和shader之間傳遞數據。但是,如果我,如果我們想要設置每個頂點的顏色呢?如果要使用uniform的方式,那需要定義和點的數量相同的uniform變量。這是復雜和不可接受的。一個更好的解決方法是在頂點屬性中包含更多的值——也就是更多的屬性值。
更多的頂點屬性值
我們在前面的教程中已經知道怎樣填充一個VBO,怎樣配置一個頂點屬性指針和怎樣存儲在VAO中。現在,我們想要為每個頂點數據添加顏色值。具體來說,我們想為每個頂點數據添加3個float類型數據來指定顏色值,這三個數值分別代表rgb分量。
GLfloat vertices[] = {// Positions // Colors0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // Bottom Right-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // Bottom Left0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // Top };因為目前我們有更多的數據要發送到頂點渲染程序,那么有必要對頂點渲染程序進行調整,使其支持我們顏色值的輸入,我們又定義了一個vec3類型的變量color,指定布局中的位置為1,如下所示:
#version 330 core layout (location = 0) in vec3 position; // The position variable has attribute position 0 layout (location = 1) in vec3 color; // The color variable has attribute position 1out vec3 ourColor; // Output a color to the fragment shadervoid main() {gl_Position = vec4(position, 1.0);ourColor = color; // Set ourColor to the input color we got from the vertex data }有了每個頂點的顏色值,我們不再需要通過uniform類型的顏色值對頂點顏色進行設置,所以我們也要修改相應的fragment shader,如下所示:
#version 330 core in vec3 ourColor; out vec4 color;void main() {color = vec4(ourColor, 1.0f); }因為我們在頂點屬性中添加了數值,而且更新了VBO的內存,我們需要重新配置頂點屬性指針。更新后的數據在VBO內存中的組織方式是這樣的:
根據數據的這個布局方式,我們可以利用glVertexAttribPointer函數設置OpenGL解釋這些數據的方式。
// Position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0); // Color attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat))); glEnableVertexAttribArray(1); glVertexAttribPointer函數的前幾個參數相對簡單,在前一個教程中已經說得比較明確。在這里,我們對頂點屬性中的位置屬性的配置和前面教程中的基本一致,只是第五個參數的設置上稍有不同,因為這個參數代表的是兩個頂點數據之間的間隙,而我們新創建的頂點數組中每個頂點屬性的數據大小為6個GLfloat類型長度,所以這里設定的是6 * sizeof(GLfloat)。另外在對頂點屬性中的顏色屬性進行設置的時候,我們指定的location是1(第一個參數),在最后一個參數中,我們設定的偏移量是3,因為每個頂點數據中,顏色數據是在頂點數據開始偏移3個GLfloat類型數據的位置。好的,運行上面的程序,我們應該能夠得到下面的結果,我的是這樣:代碼在這兒.
圖像顯示的效果可能和你想的不是太一樣,因為我們只是設定了三角形的三個顏色為紅綠藍,為什么感覺整個調色板的顏色都顯示出來了呢?這是由在片段處理器中的一種叫做片段插值的技術造成的。在渲染一個三角形的時候,光柵化階段通常會產生比最初設置的多得多的片段(一個片段就是一個要顯示在屏幕上的點的所有的信息)。光柵化程序在此基礎上根據它們在三角形中的相對位置決定每個片段在屏幕中的位置。
根據這些位置,它對片段處理器輸出的顏色值進行線性插值的操作。比如說,我們有一條線,其上端點是是綠色的,下端點是藍色的。如果片段處理程序作用在這條線的靠近綠色30%的地方,那么這個點的顏色值就是30%藍色和70%綠色的線性組合。
這就是我們的三角形呈現出線性變化的多種顏色的原理。雖然我們只設置了三個頂點的三種顏色,但是這個三角形中應該差不多包含了50,000個像素點,對應者50,000個片段。沒有被我們設置顏色的片段就會被通過上述由點的位置決定的線性顏色插值處理,并最終由于顏色的混合得到我們看到的三角形的樣子。
關于shader的寫法,編譯和使用上次教程就已經說到,本次教程又講了shader中的具體的數據結構,輸入輸出變量的設置,uniform變量的使用和改變要輸入的頂點屬性等等,下面作者還想要更深一步,講解shader類的使用。
我們自己的shader類
上述過程中,書寫,編譯和管理shader是比較繁雜的。我們想通過創建一個shader類使得這整個過程變得更容易一些。shader類可以從磁盤中讀取shader源碼、編譯和裝配他們、處理錯誤。這也能夠讓我們對我們到目前學到的只是進行一個有益的抽象,即用類來實現和管理shader。
我們將創建shader類的所有內容放在一個頭文件中,主要是為了學習和移植方面的考慮。讓我們首先來包含必要的頭文件和定義結構體數據類型吧:
#ifndef SHADER_H #define SHADER_H#include <string> #include <fstream> #include <sstream> #include <iostream>#include <GL/glew.h>; // Include glew to get all the required OpenGL headersclass Shader { public:// The program IDGLuint Program;// Constructor reads and builds the shaderShader(const GLchar* vertexPath, const GLchar* fragmentPath);// Use the programvoid Use(); };#endif在文件的一開頭,我們利用兩行預處理指令來保證這個頭文件只會在我們的程序中包含一次,即使在很多源文件中都有定義。這樣能夠避免鏈接時候的重復定義錯誤。
這個shader類保存渲染程序對象的ID號,它的構造函數需要頂點處理程序和片段處理程序的路徑作為參數。它們可以被簡單存儲為字符文件。另外,我們額外增加了一個use函數,雖然瑣碎,但是能夠幫助我們減少我們的工作量。
從文件讀入shader程序
我們將在其構造函數中使用C++文件流來從文件中將shader程序的內容讀入到幾個字符串對象中:
Shader(const GLchar* vertexPath, const GLchar* fragmentPath) {// 1. Retrieve the vertex/fragment source code from filePathstd::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// ensures ifstream objects can throw exceptions:vShaderFile.exceptions(std::ifstream::badbit);fShaderFile.exceptions(std::ifstream::badbit);try {// Open filesvShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// Read file's buffer contents into streamsvShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf(); // close file handlersvShaderFile.close();fShaderFile.close();// Convert stream into GLchar arrayvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str(); }catch(std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const GLchar* vShaderCode = vertexCode.c_str();const GLchar* fShaderCode = fragmentCode.c_str();[...]接下來我們需要編譯和裝配這些shaders。需要注意的是我們需要處理編譯出錯的情況。如果出錯的話,我們打印出編譯時的錯誤方便我們的調試(你早晚會用到的):
// 2. Compile shaders GLuint vertex, fragment; GLint success; GLchar infoLog[512];// Vertex Shader vertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex, 1, &vShaderCode, NULL); glCompileShader(vertex); // Print compile errors if any glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertex, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; };// Similiar for Fragment Shader [...] // Shader Program this->Program = glCreateProgram(); glAttachShader(this->Program, vertex); glAttachShader(this->Program, fragment); glLinkProgram(this->Program); // Print linking errors if any glGetProgramiv(this->Program, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(this->Program, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; }// Delete the shaders as they're linked into our program now and no longer necessery glDeleteShader(vertex); glDeleteShader(fragment);最后我們實現use函數,它只負責對glUseProgram的調用:
void Use() { glUseProgram(this->Program); }- 1
這樣就完成了我們自己的shader類的創建。使用這個類也十分簡便,我們只要生成一個shader類的對象,然后使用這個對象就好了:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag"); ... while(...) {ourShader.Use();glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), 1.0f);DrawStuff(); }上面代碼中,假設我們將兩個shader分別存儲在shader.vs和shader.frag中。命名什么的都是無所謂的,只要存儲的是字符文件保證讀出來的是字符創就可以了。
本節最終代碼。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎
總結
以上是生活随笔為你收集整理的【Modern OpenGL】Shader的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 现实地形导入UE4全流程
- 下一篇: 人到了35岁还一事无成,最可怕的不是工资