一、著色器
? ? ? ?在 OpenGL 中,著色器(Shader)是運行在 GPU 上的程序,用于處理圖形渲染管線中的不同階段。
? ? ? ?這些小程序為圖形渲染管線的某個特定部分而運行。從基本意義上來說,著色器只是一種把輸入轉化為輸出的程序。著色器也是一種非常獨立的程序,因為它們之間不能相互通信;它們之間唯一的溝通只有通過輸入和輸出。
? ?? OpenGL 著色器通常分為幾種類型,每種類型負責處理特定的渲染任務。以下是一些常見的著色器類型及其作用:
頂點著色器(Vertex Shader):
處理每個頂點的數據。
進行頂點位置的變換,包括模型變換、視圖變換和投影變換。
計算光照、陰影和其它與頂點相關的屬性。
輸出用于片元著色器的插值(如紋理坐標、法線方向等)。
片元著色器(Fragment Shader):
處理每個片元的每個像素的數據。
計算最終的像素顏色值,包括紋理采樣、顏色混合和透明度。
應用后處理效果,如模糊、色調映射等。
幾何著色器(Geometry Shader)(可選):
處理圖元的頂點數據。
允許創建新的圖元(如將三角形轉換為線框)或修改現有圖元的頂點。
可以增加或減少渲染的頂點數量。
曲面細分著色器(Tessellation Shader)(可選):
控制曲面細分的過程,用于生成平滑的曲面。
定義細分后的頂點位置和其它相關數據。
計算著色器(Compute Shader)(可選):
執行通用計算任務,不直接參與渲染管線。
可以用于處理大量數據并行計算,如物理模擬、圖像處理等。
二、GLSL?
? ? ? 在 OpenGL 程序中,著色器通常以 GLSL(OpenGL Shading Language)編寫,這是一種類似于 C/C 的高級著色語言。著色器程序需要編譯和鏈接到著色器程序(Shader Program)中,然后才能被 OpenGL 使用。
? ? ??GLSL是為圖形計算量身定制的,它包含一些針對向量和矩陣操作的有用特性。
? ? ? 著色器的開頭總是要聲明版本,接著是輸入和輸出變量、uniform和main函數。每個著色器的入口點都是main函數,在這個函數中我們處理所有的輸入變量,并將結果輸出到輸出變量中。?
一個典型的著色器結構:?
// 指定使用的 GLSL 版本號
#version version_number// 定義一個輸入變量,其數據類型為 type,名稱為 in_variable_name
// 該變量在頂點著色器中用于接收頂點數據
in type in_variable_name;// 定義另一個輸入變量,其數據類型為 type,名稱為 in_variable_name
// 該變量在頂點著色器中用于接收頂點數據
in type in_variable_name;// 定義一個輸出變量,其數據類型為 type,名稱為 out_variable_name
// 該變量在頂點著色器中用于輸出處理后的頂點數據
out type out_variable_name;// 定義一個統一變量,其數據類型為 type,名稱為 uniform_name
// 該變量在頂點著色器中用于存儲著色器程序中全局的數據
uniform type uniform_name;// 頂點著色器的主函數
void main()
{// 在這里處理輸入變量,并進行一些圖形操作// 例如,應用變換、計算光照、紋理坐標等...// 將處理過的結果輸出到輸出變量out_variable_name = weird_stuff_we_processed;
}
應用示例:
#version 400 corelayout (location = 0) in vec3 aPos; // 頂點位置
layout (location = 1) in vec3 aColor; // 頂點顏色out vec4 outColor; // 輸出顏色uniform mat4 matModel; // 模型變換矩陣
uniform mat4 matView; // 視圖變換矩陣
uniform mat4 matProjection; // 投影變換矩陣void main()
{gl_Position = matProjection * matView * matModel * vec4(aPos, 1.0); // 應用變換矩陣outColor = aColor; // 輸出顏色
}
在這個示例中:
aPos
和aColor
是輸入變量,分別表示頂點的位置和顏色。
outColor
是輸出變量,表示處理后的顏色。
matModel
、matView
和Projection
是統一變量,分別表示模型變換矩陣、視圖變換矩陣和投影變換矩陣。在
main
函數中,我們應用變換矩陣計算變換后的頂點位置,并將頂點顏色直接輸出。
? ? ?當我們特別談論到頂點著色器的時候,每個輸入變量也叫頂點屬性(Vertex Attribute)。我們能聲明的頂點屬性是有上限的,它一般由硬件來決定。OpenGL確保至少有16個包含4分量的頂點屬性可用,但是有些硬件或許允許更多的頂點屬性,可以查詢GL_MAX_VERTEX_ATTRIBS來獲取具體的上限:?
int nrAttributes; // 定義一個整數變量,用于存儲最大頂點屬性數量
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); // 查詢 OpenGL 實現支持的最大頂點屬性數量
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl; // 輸出最大頂點屬性數量
通常情況下它至少會返回16個,大部分情況下是夠用了。?
三、數據類型?
? ? ? ?和其他編程語言一樣,GLSL有數據類型可以來指定變量的種類。GLSL中包含C等其它語言大部分的默認基礎數據類型:int
、float
、double
、uint
和bool
。GLSL也有兩種容器類型,分別是向量(Vector)和矩陣(Matrix)。
向量
? ? ? GLSL中的向量是一個可以包含有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; // 嘗試從 someVec 中提取 x 和 y 分量,并添加額外的 x 和 x 分量
vec3 anotherVec = differentVec.zyw; // 嘗試從 differentVec 中提取 z、y 和 w 分量
vec4 otherVec = someVec.xxxx + anotherVec.yxzy; // 嘗試從 someVec 中提取四個 x 分量,并與 anotherVec 的 y、z 和 y 分量相加
vec4 differentVec = someVec.xyxx;
這一行試圖從vec2
類型的someVec
中提取兩個分量,然后錯誤地添加了兩個額外的x
分量。在 GLSL 中,你只能通過.xy
、.xz
等來訪問向量的分量。如果你想要復制someVec
的x
和y
分量到differentVec
,應該是vec4 differentVec = vec4(someVec, 0.0, 0.0, 1.0);
。
vec3 anotherVec = differentVec.zyw;
這一行試圖從vec4
類型的differentVec
中提取三個分量,這是不允許的。vec3
類型只能有三個索引(.x
、.y
、.z
)。如果你想要從vec4
類型中提取z
和y
分量到vec3
類型,應該是vec3 anotherVec = vec3(differentVec.z, differentVec.y, 0.0);
。
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
這一行試圖訪問someVec
的四個x
分量,這是不正確的,因為vec2
類型只有兩個分量。另外,anotherVec.yxzy
試圖訪問vec3
類型的anotherVec
的x
、y
和z
分量,但是沒有vec4
類型,所以不能進行加法操作。如果你想要將someVec
的x
和y
分量與anotherVec
的y
、z
分量進行組合,應該是vec4 otherVec = vec4(someVec.x, someVec.y, anotherVec.y, anotherVec.z);
。?
修:?
vec2 someVec; // 聲明一個包含兩個分量的二維向量
vec4 differentVec = vec4(someVec, 0.0, 0.0, 1.0); // 復制 someVec 的 x 和 y 分量到 differentVec 的前兩個分量vec3 anotherVec = vec3(differentVec.z, differentVec.y, 0.0); // 提取 differentVec 的 z 和 y 分量到 anotherVec
vec4 otherVec = vec4(someVec.x, someVec.y, anotherVec.y, anotherVec.z); // 組合 someVec 和 anotherVec 的分量
? ? 上述代碼中的變量 someVec
、differentVec
和 anotherVec
需要在著色器中被賦予具體的值,否則它們將包含未定義的值。?
? ? 你可以使用上面4個字母任意組合來創建一個和原來向量一樣長的(同類型)新向量,只要原來向量有那些分量即可;然而,不允許在一個vec2
向量中去獲取.z
元素。我們也可以把一個向量作為一個參數傳給不同的向量構造函數,以減少需求參數的數量:
vec2 vect = vec2(0.5, 0.7); // 創建一個二維向量 vect 并初始化其 x 分量為 0.5,y 分量為 0.7
vec4 result = vec4(vect, 0.0, 0.0); // 創建一個四維向量 result,并將 vect 的 x 和 y 分量復制到 result 的前兩個分量,z 和 w 分量初始化為 0.0
vec4 otherResult = vec4(result.xyz, 1.0); // 創建一個新的四維向量 otherResult,其 x、y 和 z 分量來自 result,w 分量設置為 1.0
使用場景
? ? ? ?這種類型的操作在圖形程序中非常有用,特別是在處理頂點數據、變換和光照計算時。例如,
vec4
向量常用于表示齊次坐標(包括位置和齊次坐標w
),這在進行投影變換時非常有用。注意事項
在 GLSL 中,向量的分量訪問(如
result.xyz
)返回一個新的向量,包含原向量的指定分量。當你從一個四維向量中提取三個分量并創建一個新的四維向量時,你需要顯式地指定第四個分量(在這個例子中是
1.0
)。這種操作在頂點著色器中特別有用,因為頂點著色器可以接收位置、顏色、紋理坐標等作為輸入,然后對這些數據進行變換和計算。
向量是一種靈活的數據類型,我們可以把它用在各種輸入和輸出上。
四、輸入和輸出?
? ? ? ?雖然著色器是各自獨立的小程序,但是它們都是一個整體的一部分,出于這樣的原因,我們希望每個著色器都有輸入和輸出,這樣才能進行數據交流和傳遞。GLSL定義了in
和out
關鍵字專門來實現這個目的。每個著色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變量與下一個著色器階段的輸入匹配,它就會傳遞下去。但在頂點和片段著色器中會有點不同。
? ? ? ?頂點著色器應該接收的是一種特殊形式的輸入,否則就會效率低下。頂點著色器的輸入特殊在,它從頂點數據中直接接收輸入。為了定義頂點數據該如何管理,使用location
這一元數據指定輸入變量,這樣我們才可以在CPU上配置頂點屬性。我們已經在前面看過這個了,layout (location = 0)
。頂點著色器需要為它的輸入提供一個額外的layout
標識,這樣我們才能把它鏈接到頂點數據。
? ? 你也可以忽略
layout (location = 0)
標識符,通過在OpenGL代碼中使用glGetAttribLocation查詢屬性位置值(Location)。
// 指定使用的 GLSL 版本號和核心配置文件
#version 400 core// 定義一個輸入變量,其數據類型為 vec3(三個浮點數),名稱為 aPos
// 這個變量在頂點著色器中用于接收頂點位置數據
layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值為0// 定義一個輸出變量,其數據類型為 vec4(四個浮點數),名稱為 vertexColor
// 這個變量在頂點著色器中用于輸出顏色數據,該數據將傳遞給片元著色器
out vec4 vertexColor; // 為片段著色器指定一個顏色輸出// 頂點著色器的主函數
void main()
{// 設置 gl_Position,這是頂點的最終位置// 我們如何把一個 vec3 作為 vec4 的構造器的參數,第四個分量默認為 1.0gl_Position = vec4(aPos, 1.0); // 將頂點位置轉換為齊次坐標// 把輸出變量設置為暗紅色// 注意:顏色值的順序是 RGBA(紅、綠、藍、透明度)vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把輸出變量設置為暗紅色
}
? ? ?在 OpenGL 程序中用于頂點著色器階段,負責處理頂點位置和顏色。通過設置
gl_Position
,它將頂點位置轉換為齊次坐標,以便在視圖變換和投影變換后正確渲染頂點。同時,通過設置vertexColor
,它為每個頂點指定一個顏色值,這些顏色值將傳遞給片元著色器,用于確定每個片元的最終顏色。?
?? ? ? ?片段著色器,它需要一個vec4
顏色輸出變量,因為片段著色器需要生成一個最終輸出的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)。
? ? ? ?所以,如果我們打算從一個著色器向另一個著色器發送數據,必須在發送方著色器中聲明一個輸出,在接收方著色器中聲明一個類似的輸入。當類型和名字都一樣的時候,OpenGL就會把兩個變量鏈接到一起,它們之間就能發送數據了(這是在鏈接程序對象時完成的)。
// 指定使用的 GLSL 版本號和核心配置文件
#version 400 core// 定義一個輸出變量,其數據類型為 vec4(四個浮點數),名稱為 FragColor
// 這個變量在片段著色器中用于輸出顏色數據,該數據將用于最終的像素顏色
out vec4 FragColor;// 定義一個輸入變量,其數據類型為 vec4(四個浮點數),名稱為 vertexColor
// 這個變量在片段著色器中用于接收從頂點著色器傳遞過來的顏色數據
in vec4 vertexColor; // 從頂點著色器傳來的輸入變量(名稱相同、類型相同)// 片段著色器的主函數
void main()
{// 將從頂點著色器接收到的顏色值直接賦值給輸出變量 FragColorFragColor = vertexColor;
}
? ? ?可以看到我們在頂點著色器中聲明了一個vertexColor變量作為vec4
輸出,并在片段著色器中聲明了一個類似的vertexColor。由于它們名字相同且類型相同,片段著色器中的vertexColor就和頂點著色器中的vertexColor鏈接了。
五、Uniform
? ? ? ?Uniform是另一種從我們的應用程序在 CPU 上傳遞數據到 GPU 上的著色器的方式,但uniform和頂點屬性有些不同。
? ? ? ?首先,uniform是全局的(Global)。全局意味著uniform變量必須在每個著色器程序對象中都是獨一無二的,而且它可以被著色器程序的任意著色器在任意階段訪問;//第二,無論你把uniform值設置成什么,uniform會一直保存它們的數據,直到它們被重置或更新。
? ? ? ?要在 GLSL 中聲明 uniform,只需在著色器中使用?uniform
?關鍵字,并帶上類型和名稱。從那時起,我們就可以在著色器中使用新聲明的 uniform。
// 指定使用的 GLSL 版本號和核心配置文件
#version 400 core// 定義一個輸出變量,其數據類型為 vec4(四個浮點數),名稱為 FragColor
// 這個變量在片段著色器中用于輸出顏色數據,該數據將用于最終的片段(像素)顏色
out vec4 FragColor;// 定義一個統一變量,其數據類型為 vec4(四個浮點數),名稱為 ourColor
// 這個變量在片段著色器中用于存儲從OpenGL程序代碼中傳遞的顏色值
uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量// 片段著色器的主函數
void main()
{// 將統一變量 ourColor 的值賦給輸出變量 FragColor// 這意味著每個片段(像素)都將使用 ourColor 指定的顏色FragColor = ourColor;
}
? ? ?在片段著色器中聲明了一個uniform?vec4
的ourColor,并把片段著色器的輸出顏色設置為uniform值的內容。因為uniform是全局變量,可以在任何著色器中定義它們,而無需通過頂點著色器作為中介。頂點著色器中不需要這個uniform,所以不用在那里定義它。
? ? 如果你聲明了一個uniform卻在GLSL代碼中沒用過,編譯器會靜默移除這個變量,導致最后編譯出的版本中并不會包含它,這可能導致幾個非常麻煩的錯誤,記住這點!?
? ? ? 這個uniform現在還是空的;還沒有給它添加任何數據。首先需要找到著色器中uniform屬性的索引/位置值。當得到uniform的索引/位置值后,就可以更新它的值了。這次我們不去給像素傳遞單獨一個顏色,而是讓它隨著時間改變顏色:?
// 獲取當前時間值,通常用于動畫效果
float timeValue = glfwGetTime();// 計算綠色分量的值,使其在0.0到1.0之間變化,創建一個周期性的效果
// 這里使用了正弦函數(sin)來生成周期性變化,并將其范圍限制在0.0到1.0之間
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;// 獲取著色器程序中名為 "ourColor" 的uniform變量的位置
// 這個位置用于后續通過 glUniform 函數設置uniform變量的值
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");// 激活(使用)名為 shaderProgram 的著色器程序
// 這確保后續的 glUniform 調用更新的是正確的著色器程序中的uniform變量
glUseProgram(shaderProgram);// 更新著色器程序中 "ourColor" uniform變量的值
// 這里設置紅色和藍色分量為0.0,綠色分量為之前計算的 greenValue,透明度(Alpha)為1.0
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的。
? ? ?因為OpenGL在其核心是一個C庫,所以它不支持類型重載,在函數參數不同的時候就要為其定義新的函數;glUniform是一個典型例子。這個函數有一個特定的后綴,標識設定的uniform的類型。可能的后綴有:
后綴 含義 f
函數需要一個float作為它的值 i
函數需要一個int作為它的值 ui
函數需要一個unsigned int作為它的值 3f
函數需要3個float作為它的值 fv
函數需要一個float向量/數組作為它的值 ? ? ?每當你打算配置一個OpenGL的選項時就可以簡單地根據這些規則選擇適合你的數據類型的重載函數。在上面的例子里,我們希望分別設定uniform的4個float值,所以通過glUniform4f傳遞我們的數據。
? ? ?如果打算讓顏色慢慢變化,就要在游戲循環的每一次迭代中(所以他會逐幀改變)更新這個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();
}
while(!glfwWindowShouldClose(window)) // 當窗口沒有接收到關閉信號時持續循環
{// 處理用戶輸入,例如按鍵或鼠標移動processInput(window);// 渲染指令開始// 清除顏色緩沖區,設置清屏顏色為 RGB(0.2, 0.3, 0.3),Alpha 為 1.0(不透明)glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 清除當前激活的緩沖區(顏色緩沖區、深度緩沖區等)glClear(GL_COLOR_BUFFER_BIT);// 激活(使用)之前編譯好的著色器程序glUseProgram(shaderProgram);// 更新著色器程序中的 uniform 變量 ourColor,以動態改變顏色float timeValue = glfwGetTime(); // 獲取當前時間float greenValue = sin(timeValue) / 2.0f + 0.5f; // 計算綠色分量值int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); // 獲取 uniform 變量位置glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 設置 uniform 變量的值// 繪制三角形// 綁定頂點數組對象,它包含了頂點數據和頂點屬性的配置glBindVertexArray(VAO);// 繪制數組中的頂點,GL_TRIANGLES 表示繪制三角形glDrawArrays(GL_TRIANGLES, 0, 3);// 交換前后緩沖區,將渲染結果展示給用戶glfwSwapBuffers(window);// 處理所有待處理的事件,例如鍵盤輸入、鼠標移動等glfwPollEvents();
}
? ? ?這段代碼中,
glfwWindowShouldClose(window)
函數檢查用戶是否嘗試關閉窗口(例如,點擊窗口的關閉按鈕或按下ESC
鍵)。只要窗口沒有關閉,就繼續執行循環體內的代碼。? ? ?在循環體內,首先處理用戶輸入,然后開始渲染過程。使用
glClearColor
和glClear
函數清除顏色緩沖區。接著,激活之前編譯好的著色器程序,并更新其中的ourColor
變量,使其動態變化,從而創建一個顏色隨時間變化的效果。? ? ?然后,綁定頂點數組對象(VAO)并繪制三角形。最后,交換前后緩沖區,使渲染結果可見,并處理所有待處理的事件。
完整代碼:
#include <glad/glad.h> // 包含GLAD庫的頭文件,用于加載OpenGL函數指針
#include <GLFW/glfw3.h> // 包含GLFW庫的頭文件,用于創建窗口和管理輸入
#include <iostream> // 包含標準輸入輸出流庫
#include <cmath> // 包含數學庫,用于sin函數void framebuffer_size_callback(GLFWwindow* window, int width, int height); // 定義處理窗口大小改變事件的回調函數
void processInput(GLFWwindow* window); // 定義處理用戶輸入的函數// 設置窗口的寬度和高度
const unsigned int SCR_WIDTH = 800; // 窗口寬度
const unsigned int SCR_HEIGHT = 600; // 窗口高度// 定義頂點著色器的GLSL源碼
const char* vertexShaderSource = "#version 400 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\0";// 定義片段著色器的GLSL源碼
const char* fragmentShaderSource = "#version 400 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\n\0";int main()
{// 初始化GLFW并配置// ----------------------glfwInit(); // 初始化GLFW庫glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 設置GLFW創建OpenGL 3的上下文glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 設置次要版本號為3glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 設置OpenGL核心配置文件#ifdef __APPLE__ // 蘋果系統需要設置為向前兼容模式glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif// 創建GLFW窗口// --------------------GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);if (window == NULL) // 如果窗口創建失敗{std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate(); // 終止GLFWreturn -1;}glfwMakeContextCurrent(window); // 設置當前上下文為新創建的窗口glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // 設置窗口大小改變的回調函數// 加載OpenGL函數指針// ---------------------------------------if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // 使用GLAD加載OpenGL函數指針{std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// 構建并編譯我們的著色器程序// ------------------------------------// 頂點著色器unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);// 檢查著色器編譯錯誤int success;char infoLog[512];glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}// 片段著色器unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// 檢查著色器編譯錯誤glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;}// 鏈接著色器unsigned int shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);// 檢查鏈接錯誤glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if (!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;}glDeleteShader(vertexShader);glDeleteShader(fragmentShader);// 設置頂點數據(和緩沖區)并配置頂點屬性// ------------------------------------------------------------------float vertices[] = {0.5f, -0.5f, 0.0f, // 底部右側-0.5f, -0.5f, 0.0f, // 底部左側0.0f, 0.5f, 0.0f // 頂部 };unsigned int VBO, VAO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);// 首先綁定頂點數組對象,然后綁定并設置頂點緩沖區,然后配置頂點屬性。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)(VAOs)(或 VBOs)當它不是直接必要時。// glBindVertexArray(0);// 綁定頂點數組對象(它已經被綁定,但只是為了演示):看到我們只有一個頂點數組對象我們// 可以在渲染相應的三角形之前綁定它;這是另一種方法。glBindVertexArray(VAO);// 渲染循環// -----------while (!glfwWindowShouldClose(window)) // 當窗口沒有接收到關閉信號時持續循環{// 輸入processInput(window); // 處理用戶輸入// 渲染// ------glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 清除顏色緩沖區,設置清屏顏色為 RGB(0.2, 0.3, 0.3),Alpha 為 1.0(不透明)glClear(GL_COLOR_BUFFER_BIT); // 清除當前激活的緩沖區(顏色緩沖區、深度緩沖區等)// 確保在任何 glUniform 調用之前激活著色器glUseProgram(shaderProgram); // 激活(使用)名為 shaderProgram 的著色器程序// 更新著色器uniform顏色double timeValue = glfwGetTime(); // 獲取當前時間float greenValue = static_cast<float>(sin(timeValue) / 2.0 + 0.5); // 計算綠色分量值int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); // 獲取 uniform 變量位置glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 設置 uniform 變量的值// 繪制三角形glBindVertexArray(VAO); // 綁定頂點數組對象glDrawArrays(GL_TRIANGLES, 0, 3); // 繪制數組中的頂點,GL_TRIANGLES 表示繪制三角形// GLFW:交換緩沖區并輪詢IO事件(按鍵按下/釋放、鼠標移動等)glfwSwapBuffers(window); // 交換前后緩沖區,將渲染結果展示給用戶glfwPollEvents(); // 處理所有待處理的事件}// 可選:一旦它們的目的已經達到,就釋放所有資源:// --------------------------------------------------------------------glDeleteVertexArrays(1, &VAO); // 刪除頂點數組對象glDeleteBuffers(1, &VBO); // 刪除緩沖區對象glDeleteProgram(shaderProgram); // 刪除著色器程序// 終止 GLFW,清除所有先前分配的 GLFW 資源。// ------------------------------------------------------------------glfwTerminate(); // 終止 GLFWreturn 0;
}// 處理所有輸入:查詢 GLFW 本幀是否有相關按鍵被按下/釋放,并相應地做出反應
// ----------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window) {if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) // 如果按下了 ESC 鍵glfwSetWindowShouldClose(window, true); // 設置窗口關閉標志為真
}// GLFW:每當窗口大小改變(由操作系統或用戶調整大小時)此回調函數執行
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {// 確保視口與新窗口尺寸匹配;注意,在視網膜顯示器上寬度和高度將明顯大于指定值。glViewport(0, 0, width, height); // 設置視口大小
}
六、頂點添加顏色數據
? ? ?把顏色數據加進頂點數據中(把顏色數據添加為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 400 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
輸出變量,必須再修改一下片段著色器:?
// 指定使用的 GLSL 版本號和核心配置文件
#version 400 core// 定義一個輸出變量,其數據類型為 vec4(四個浮點數),名稱為 FragColor
// 這個變量在片段著色器中用于輸出顏色數據,該數據將用于最終的片段(像素)顏色
out vec4 FragColor; // 定義一個輸入變量,其數據類型為 vec3(三個浮點數),名稱為 ourColor
// 這個變量在片段著色器中用于接收從頂點著色器傳遞過來的顏色數據
in vec3 ourColor;// 片段著色器的主函數
void main()
{// 將從頂點著色器接收到的顏色值轉換為四維向量,并設置 alpha 分量為 1.0(不透明)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字節。
運行結果:
? ? ? 這是在片段著色器中進行的所謂片段插值(Fragment Interpolation)的結果。當渲染一個三角形時,光柵化(Rasterization)階段通常會造成比原指定頂點更多的片段。光柵會根據每個片段在三角形形狀上所處相對位置決定這些片段的位置。
? ? ? 基于這些位置,它會插值(Interpolate)所有片段著色器的輸入變量。比如,有一個線段,上面的端點是綠色的,下面的端點是藍色的。如果一個片段著色器在線段的70%的位置運行,它的顏色輸入屬性就會是一個綠色和藍色的線性結合;更精確地說就是30%藍 + 70%綠。
七、著色器類?
? ? ? 編寫、編譯、管理著色器是件麻煩事,寫一個類會輕松一點,它可以從硬盤讀取著色器,然后編譯并鏈接它們,并對它們進行錯誤檢測。
? ? ?把著色器類全部放在在頭文件里,主要是為了學習用途,當然也方便移植。先添加必要的include,并定義類結構:
#ifndef SHADER_H
#define SHADER_H// 防止頭文件重復包含#include <glad/glad.h> // 錯誤:此處多余的分號(需刪除)
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader {
public:unsigned int ID; // OpenGL著色器程序對象的ID// 構造函數:通過頂點和片段著色器文件路徑創建著色器程序Shader(const char* vertexPath, const char* fragmentPath);// 激活當前著色器程序void use();// 設置Uniform變量的工具函數(const成員函數,不修改對象狀態)void setBool(const std::string &name, bool value) const;void setInt(const std::string &name, int value) const;void setFloat(const std::string &name, float value) const;
};#endif
? ? ? 在頭文件頂部使用了幾個預處理指令(Preprocessor Directives)。這些預處理指令會告知你的編譯器只在它沒被包含過的情況下才包含和編譯這個頭文件,即使多個文件都包含了這個著色器頭文件。它是用來防止鏈接沖突的。
? ? ?著色器類儲存了著色器程序的ID。它的構造器需要頂點和片段著色器源代碼的文件路徑,這樣就可以把源碼的文本文件儲存在硬盤上了。除此之外,還加入了一些工具函數:use用來激活著色器程序,所有的set…函數能夠查詢一個unform的位置值并設置它的值。?
從文件讀取?
①?使用C++文件流讀取著色器內容,儲存到幾個string
對象里。
Shader(const char* vertexPath, const char* fragmentPath)
{// 1. 從文件路徑中獲取頂點/片段著色器std::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// 保證ifstream對象可以拋出異常:vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);try {// 打開文件vShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// 讀取文件的緩沖內容到數據流中vShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf(); // 關閉文件處理器vShaderFile.close();fShaderFile.close();// 轉換數據流到stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str(); }catch(std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char* fShaderCode = fragmentCode.c_str();[...]
②編譯和鏈接著色器。
(注意,我們也將檢查編譯/鏈接是否失敗,如果失敗則打印編譯時錯誤,調試的時候這些錯誤輸出會及其重要)
// 2. 編譯著色器
unsigned int vertex, fragment;
int success;
char infoLog[512];// 頂點著色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印編譯錯誤(如果有的話)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{glGetShaderInfoLog(vertex, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};// 片段著色器也類似
[...]// 著色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印連接錯誤(如果有的話)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{glGetProgramInfoLog(ID, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}// 刪除著色器,它們已經鏈接到我們的程序中了,已經不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
③use函數
void use()
{ glUseProgram(ID);
}
④uniform的setter函數
// 設置布爾值到著色器的統一變量中
// 參數:
// name: 統一變量的名稱(在著色器代碼中定義的變量名)
// value: 要設置的布爾值
// 注意:
// OpenGL 中布爾值通常以整數形式傳遞(true 為 1,false 為 0)
void setBool(const std::string &name, bool value) const
{// 調用 OpenGL 的 glUniform1i 函數將布爾值設置到著色器的統一變量中// value 被隱式轉換為整數(true 轉換為 1,false 轉換為 0)glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}// 設置整數值到著色器的統一變量中
// 參數:
// name: 統一變量的名稱
// value: 要設置的整數值
void setInt(const std::string &name, int value) const
{ // 調用 OpenGL 的 glUniform1i 函數將整數值設置到著色器的統一變量中glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}// 設置浮點值到著色器的統一變量中
// 參數:
// name: 統一變量的名稱
// value: 要設置的浮點值
void setFloat(const std::string &name, float value) const
{ // 調用 OpenGL 的 glUniform1f 函數將浮點值設置到著色器的統一變量中glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
⑤創建一個著色器對象(Shader
類的實例),并在渲染循環中使用它來設置統一變量并繪制圖形
// 創建一個 Shader 對象,指定頂點著色器和片段著色器的文件路徑
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...) // 假設這是一個渲染循環,例如 OpenGL 的主循環
{// 激活著色器程序ourShader.use();// 設置著色器中的統一變量 "someUniform" 的值為 1.0fourShader.setFloat("someUniform", 1.0f);// 調用繪制函數,繪制圖形DrawStuff();
}
頂點和片段著色器儲存為兩個叫做shader.vs
和shader.fs
的文件
練習?
4.0.shader.fs
#version 400 core
out vec4 FragColor; // 輸出片段顏色in vec3 ourColor; // 從頂點著色器接收的顏色void main()
{FragColor = vec4(ourColor, 1.0); // 設置片段顏色(添加透明度)
}
?4.0.shader.vs
#version 400 core
layout (location = 0) in vec3 aPos; // 頂點位置屬性
layout (location = 1) in vec3 aColor; // 頂點顏色屬性out vec3 ourColor; // 將頂點顏色傳遞給片段著色器void main()
{gl_Position = vec4(aPos, 1.0); // 設置頂點位置(齊次坐標)ourColor = aColor; // 將頂點顏色傳遞到片段著色器
}
1.修改頂點著色器讓三角形上下顛倒
#version 400 core
layout (location = 0) in vec3 aPos; // 頂點位置屬性
layout (location = 1) in vec3 aColor; // 頂點顏色屬性out vec3 ourColor; // 輸出變量,傳遞給片段著色器void main()
{gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0); // 設置頂點位置,并對 y 分量取反ourColor = aColor; // 將頂點顏色傳遞給片段著色器
}
2.使用uniform定義一個水平偏移量,在頂點著色器中使用這個偏移量把三角形移動到屏幕右側?
// In your CPP file:
// ======================
float offset = 0.5f;
ourShader.setFloat("xOffset", offset);// In your vertex shader:
// ======================
#version 400 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;uniform float xOffset;void main()
{gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); // add the xOffset to the x position of the vertex positionourColor = aColor;
}
// 設置統一變量的值float offset = 0.5f; // 定義一個偏移量ourShader.use(); // 激活著色器程序ourShader.setFloat("xOffset", offset); // 設置統一變量的值
// 指定 GLSL 的版本為 4.00,并使用核心(Core)模式。這是 OpenGL 4.0 及以上版本的標準。
#version 400 core// 定義頂點位置輸入,位于頂點屬性位置 0。
layout (location = 0) in vec3 aPos;// 定義頂點顏色輸入,位于頂點屬性位置 1。
layout (location = 1) in vec3 aColor;// 定義一個輸出變量,用于將頂點顏色傳遞給片段著色器。
out vec3 ourColor;// 定義一個統一變量,用于在運行時動態調整頂點的 x 坐標。
uniform float xOffset;void main()
{// 設置頂點的最終位置。// 將 xOffset 的值添加到頂點的 x 坐標上,從而動態調整頂點的水平位置。gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0);// 將頂點顏色傳遞給片段著色器。ourColor = aColor;
}
?
3.使用out
關鍵字把頂點位置輸出到片段著色器,并將片段的顏色設置為與頂點位置相等(來看看連頂點位置值都在三角形中被插值的結果)。
vs
#version 400 core
layout (location = 0) in vec3 aPos; // 頂點位置輸入
layout (location = 1) in vec3 aColor; // 頂點顏色輸入out vec3 ourPosition; // 輸出到片段著色器的頂點位置void main()
{gl_Position = vec4(aPos, 1.0); // 設置頂點的最終位置ourPosition = aPos; // 將頂點位置傳遞給片段著色器
}
fs
#version 400 core
out vec4 FragColor; // 輸出片段顏色
in vec3 ourPosition; // 從頂著點色器接收的頂點位置void main()
{FragColor = vec4(ourPosition, 1.0); // 將插值后的頂點位置作為顏色輸出
}
Q:為什么三角形的左下角是黑的?
A:片段顏色的輸出等于三角形頂點的坐標(插值后)。三角形左下角的坐標是
(-0.5f, -0.5f, 0.0f)
。由于 x 和 y 值是負數,它們會被截斷為0.0f
的值。這種情況會一直持續到三角形的中心部分,因為從中心部分開始,插值后的值會再次變為正值。0.0f
的值當然對應黑色。
參考:
著色器 - LearnOpenGL CN