Learn OpenGL (三):着色器
GLSL
著色器是使用一種叫GLSL的類C語言寫成的。GLSL是為圖形計算量身定制的,它包含一些針對向量和矩陣操作的有用特性。
著色器的開頭總是要聲明版本,接著是輸入和輸出變量、uniform和main函數。每個著色器的入口點都是main函數,在這個函數中我們處理所有的輸入變量,并將結果輸出到輸出變量中。如果你不知道什么是uniform也不用擔心,我們后面會進行講解。
一個典型的著色器有下面的結構:
#version version_number
in type in_variable_name;
in type in_variable_name;out type out_variable_name;uniform type uniform_name;int main()
{// 處理輸入并進行一些圖形操作...// 輸出處理過的結果到輸出變量out_variable_name = weird_stuff_we_processed;
}
數據類型
和其他編程語言一樣,GLSL有數據類型可以來指定變量的種類。GLSL中包含C等其它語言大部分的默認基礎數據類型:int、float、double、uint和bool。GLSL也有兩種容器類型,它們會在這個教程中使用很多,分別是向量(Vector)和矩陣(Matrix),其中矩陣我們會在之后的教程里再討論。
向量
GLSL中的向量是一個可以包含有1、2、3或者4個分量的容器,分量的類型可以是前面默認基礎類型的任意一個。它們可以是下面的形式(n代表分量的數量):
| 類型 | 含義 |
|---|---|
vecn | 包含n個float分量的默認向量 |
bvecn | 包含n個bool分量的向量 |
ivecn | 包含n個int分量的向量 |
uvecn | 包含n個unsigned int分量的向量 |
dvecn | 包含n個double分量的向量 |
大多數時候我們使用vecn,因為float足夠滿足大多數要求了。
一個向量的分量可以通過vec.x這種方式獲取,這里x是指這個向量的第一個分量。你可以分別使用.x、.y、.z和.w來獲取它們的第1、2、3、4個分量。GLSL也允許你對顏色使用rgba,或是對紋理坐標使用stpq訪問相同的分量。
向量這一數據類型也允許一些有趣而靈活的分量選擇方式,叫做重組(Swizzling)。重組允許這樣的語法:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
你可以使用上面4個字母任意組合來創建一個和原來向量一樣長的(同類型)新向量,只要原來向量有那些分量即可;然而,你不允許在一個vec2向量中去獲取.z元素。我們也可以把一個向量作為一個參數傳給不同的向量構造函數,以減少需求參數的數量:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
向量是一種靈活的數據類型,我們可以把用在各種輸入和輸出上。學完教程你會看到很多新穎的管理向量的例子。
輸入與輸出
雖然著色器是各自獨立的小程序,但是它們都是一個整體的一部分,出于這樣的原因,我們希望每個著色器都有輸入和輸出,這樣才能進行數據交流和傳遞。GLSL定義了in和out關鍵字專門來實現這個目的。每個著色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變量與下一個著色器階段的輸入匹配,它就會傳遞下去。但在頂點和片段著色器中會有點不同。
頂點著色器應該接收的是一種特殊形式的輸入,否則就會效率低下。頂點著色器的輸入特殊在,它從頂點數據中直接接收輸入。為了定義頂點數據該如何管理,我們使用location這一元數據指定輸入變量,這樣我們才可以在CPU上配置頂點屬性。我們已經在前面的教程看過這個了,layout (location = 0)。頂點著色器需要為它的輸入提供一個額外的layout標識,這樣我們才能把它鏈接到頂點數據。
你也可以忽略layout (location = 0)標識符,通過在OpenGL代碼中使用glGetAttribLocation查詢屬性位置值(Location),但是我更喜歡在著色器中設置它們,這樣會更容易理解而且節省你(和OpenGL)的工作量。
另一個例外是片段著色器,它需要一個vec4顏色輸出變量,因為片段著色器需要生成一個最終輸出的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)。
所以,如果我們打算從一個著色器向另一個著色器發送數據,我們必須在發送方著色器中聲明一個輸出,在接收方著色器中聲明一個類似的輸入。當類型和名字都一樣的時候,OpenGL就會把兩個變量鏈接到一起,它們之間就能發送數據了(這是在鏈接程序對象時完成的)。為了展示這是如何工作的,我們會稍微改動一下之前教程里的那個著色器,讓頂點著色器為片段著色器決定顏色。
頂點著色器
#version 330 core
layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值為0out vec4 vertexColor; // 為片段著色器指定一個顏色輸出void main()
{gl_Position = vec4(aPos, 1.0); // 注意我們如何把一個vec3作為vec4的構造器的參數vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把輸出變量設置為暗紅色
}
片段著色器
#version 330 core
out vec4 FragColor;in vec4 vertexColor; // 從頂點著色器傳來的輸入變量(名稱相同、類型相同)void main()
{FragColor = vertexColor;
}
你可以看到我們在頂點著色器中聲明了一個vertexColor變量作為vec4輸出,并在片段著色器中聲明了一個類似的vertexColor。由于它們名字相同且類型相同,片段著色器中的vertexColor就和頂點著色器中的vertexColor鏈接了。由于我們在頂點著色器中將顏色設置為深紅色,最終的片段也是深紅色的。下面的圖片展示了輸出結果:
Uniform
Uniform是一種從CPU中的應用向GPU中的著色器發送數據的方式,但uniform和頂點屬性有些不同。首先,uniform是全局的(Global)。全局意味著uniform變量必須在每個著色器程序對象中都是獨一無二的,而且它可以被著色器程序的任意著色器在任意階段訪問。第二,無論你把uniform值設置成什么,uniform會一直保存它們的數據,直到它們被重置或更新。
我們可以在一個著色器中添加uniform關鍵字至類型和變量名前來聲明一個GLSL的uniform。從此處開始我們就可以在著色器中使用新聲明的uniform了。我們來看看這次是否能通過uniform設置三角形的顏色:
#version 330 core
out vec4 FragColor;uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量void main()
{FragColor = ourColor;
}
我們在片段著色器中聲明了一個uniform?vec4的ourColor,并把片段著色器的輸出顏色設置為uniform值的內容。因為uniform是全局變量,我們可以在任何著色器中定義它們,而無需通過頂點著色器作為中介。頂點著色器中不需要這個uniform,所以我們不用在那里定義它。
如果你聲明了一個uniform卻在GLSL代碼中沒用過,編譯器會靜默移除這個變量,導致最后編譯出的版本中并不會包含它,這可能導致幾個非常麻煩的錯誤,記住這點!
這個uniform現在還是空的;我們還沒有給它添加任何數據,所以下面我們就做這件事。我們首先需要找到著色器中uniform屬性的索引/位置值。當我們得到uniform的索引/位置值后,我們就可以更新它的值了。這次我們不去給像素傳遞單獨一個顏色,而是讓它隨著時間改變顏色:
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
首先我們通過glfwGetTime()獲取運行的秒數。然后我們使用sin函數讓顏色在0.0到1.0之間改變,最后將結果儲存到greenValue里。
接著,我們用glGetUniformLocation查詢uniform?ourColor的位置值。我們為查詢函數提供著色器程序和uniform的名字(這是我們希望獲得的位置值的來源)。如果glGetUniformLocation返回-1就代表沒有找到這個位置值。最后,我們可以通過glUniform4f函數設置uniform值。注意,查詢uniform地址不要求你之前使用過著色器程序,但是更新一個uniform之前你必須先使用程序(調用glUseProgram),因為它是在當前激活的著色器程序中設置uniform的。
現在你知道如何設置uniform變量的值了,我們可以使用它們來渲染了。如果我們打算讓顏色慢慢變化,我們就要在游戲循環的每一次迭代中(所以他會逐幀改變)更新這個uniform,否則三角形就不會改變顏色。下面我們就計算greenValue然后每個渲染迭代都更新這個uniform:
while(!glfwWindowShouldClose(window))
{// 輸入processInput(window);// 渲染// 清除顏色緩沖glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// 記得激活著色器glUseProgram(shaderProgram);// 更新uniform顏色float timeValue = glfwGetTime();float greenValue = sin(timeValue) / 2.0f + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);// 繪制三角形glBindVertexArray(VAO);glDrawArrays(GL_TRIANGLES, 0, 3);// 交換緩沖并查詢IO事件glfwSwapBuffers(window);glfwPollEvents();
}
這里的代碼對之前代碼是一次非常直接的修改。這次,我們在每次迭代繪制三角形前先更新uniform值。如果你正確更新了uniform,你會看到你的三角形逐漸由綠變黑再變回綠色。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;const char *vertexShaderSource ="#version 330 core\n""layout (location = 0) in vec3 aPos;\n""void main()\n""{\n"" gl_Position = vec4(aPos, 1.0);\n""}\0";const char *fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""uniform vec4 ourColor;\n""void main()\n""{\n"" FragColor = ourColor;\n""}\n\0";// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
void processInput(GLFWwindow *window)
{if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);
}// glfw: whenever the window size changed (by OS or user resize) this callback function executes
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// make sure the viewport matches the new window dimensions; note that width and // height will be significantly larger than specified on retina displays.glViewport(0, 0, width, height);
}int main()
{// glfw: initialize and configureglfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfw window creationGLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);glfwMakeContextCurrent(window);glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glad: load all OpenGL function pointersif (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// build and compile our shader program// vertex shaderint vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);// check for shader compile errors// fragment shaderint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// check for shader compile errors// link shadersint shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);glDeleteShader(vertexShader);glDeleteShader(fragmentShader);// set up vertex data (and buffer(s)) and configure vertex attributesfloat vertices[] = {0.5f, -0.5f, 0.0f, // bottom right-0.5f, -0.5f, 0.0f, // bottom left0.0f, 0.5f, 0.0f // top };unsigned int VBO, VAO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);glBindVertexArray(VAO);// render loopwhile (!glfwWindowShouldClose(window)){// inputprocessInput(window);// renderglClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// be sure to activate the shader before any calls to glUniformglUseProgram(shaderProgram);// update shader uniformfloat timeValue = glfwGetTime();float greenValue = sin(timeValue) / 2.0f + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);// render the triangleglDrawArrays(GL_TRIANGLES, 0, 3);// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)glfwSwapBuffers(window);glfwPollEvents();}// optional: de-allocate all resources once they've outlived their purpose:glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);// glfw: terminate, clearing all previously allocated GLFW resources.glfwTerminate();return 0;
}
顏色插值
在前面的教程中,我們了解了如何填充VBO、配置頂點屬性指針以及如何把它們都儲存到一個VAO里。這次,我們同樣打算把顏色數據加進頂點數據中。我們將把顏色數據添加為3個float值至vertices數組。我們將把三角形的三個角分別指定為紅色、綠色和藍色:
float vertices[] = {// 位置 // 顏色0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 頂部
};
由于現在有更多的數據要發送到頂點著色器,我們有必要去調整一下頂點著色器,使它能夠接收顏色值作為一個頂點屬性輸入。需要注意的是我們用layout標識符來把aColor屬性的位置值設置為1:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值為 0
layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值為 1out vec3 ourColor; // 向片段著色器輸出一個顏色void main()
{gl_Position = vec4(aPos, 1.0);ourColor = aColor; // 將ourColor設置為我們從頂點數據那里得到的輸入顏色
}
由于我們不再使用uniform來傳遞片段的顏色了,現在使用ourColor輸出變量,我們必須再修改一下片段著色器:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;void main()
{FragColor = vec4(ourColor, 1.0);
}
因為我們添加了另一個頂點屬性,并且更新了VBO的內存,我們就必須重新配置頂點屬性指針。更新后的VBO內存中的數據現在看起來像這樣:
知道了現在使用的布局,我們就可以使用glVertexAttribPointer函數更新頂點格式,
// 位置屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 顏色屬性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer函數的前幾個參數比較明了。這次我們配置屬性位置值為1的頂點屬性。顏色值有3個float那么大,我們不去標準化這些值。
由于我們現在有了兩個頂點屬性,我們不得不重新計算步長值。為獲得數據隊列中下一個屬性值(比如位置向量的下個x分量)我們必須向右移動6個float,其中3個是位置值,另外3個是顏色值。這使我們的步長值為6乘以float的字節數(=24字節)。
同樣,這次我們必須指定一個偏移量。對于每個頂點來說,位置頂點屬性在前,所以它的偏移量是0。顏色屬性緊隨位置數據之后,所以偏移量就是3 * sizeof(float),用字節來計算就是12字節。
運行程序你應該會看到如下結果:
如果你在哪卡住了,可以在這里查看源碼。
這個圖片可能不是你所期望的那種,因為我們只提供了3個顏色,而不是我們現在看到的大調色板。這是在片段著色器中進行的所謂片段插值(Fragment Interpolation)的結果。當渲染一個三角形時,光柵化(Rasterization)階段通常會造成比原指定頂點更多的片段。光柵會根據每個片段在三角形形狀上所處相對位置決定這些片段的位置。
基于這些位置,它會插值(Interpolate)所有片段著色器的輸入變量。比如說,我們有一個線段,上面的端點是綠色的,下面的端點是藍色的。如果一個片段著色器在線段的70%的位置運行,它的顏色輸入屬性就會是一個綠色和藍色的線性結合;更精確地說就是30%藍 + 70%綠。
這正是在這個三角形中發生了什么。我們有3個頂點,和相應的3個顏色,從這個三角形的像素來看它可能包含50000左右的片段,片段著色器為這些像素進行插值顏色。如果你仔細看這些顏色就應該能明白了:紅首先變成到紫再變為藍色。片段插值會被應用到片段著色器的所有輸入屬性上。
Color = vec4(clamp(Position, A, B), 1.0);函數返回的value介于A、B之間,若value小于min,返回min,若大于max,返回max
#include <learnopengl/shader_s.h>是將著色器封裝的頭文件
總結
以上是生活随笔為你收集整理的Learn OpenGL (三):着色器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Learn OpenGL (二):三角形
- 下一篇: Learn OpenGL (四):纹理