文章目錄
- 前言
- 什么是 OpenGl ?
- 回顧
- openGL 的 Object
- 顯存結構
- 工作階段
- 通過頂點緩沖對象將頂點數據初始化至緩沖中
- 標準化設備坐標
- 頂點緩沖對象 VBO
- glGenBuffers
- glBindBuffer
- glBufferData
- 建立了一個頂點和一個片段著色器
- 著色器是什么?
- 為什么需要使用著色器?
- 著色器的結構
- 頂點著色器
- 片段著色器
- 著色器程序對象
- 把頂點數據鏈接到頂點著色器的頂點屬性上
- 繪制單個物體
- 頂點數組對象 VAO
- 為什么要使用VAO?
- VAO儲存的內容
- 使用 VAO 的流程:
- 繪制三角形(glDrawArrays函數)
- 元素緩沖對象 EBO / 索引緩沖對象 IBO
- vertices 頂點數據
- glGenBuffers
- glBufferData
- glPolygonMode
- glDrawElements
- VAO 與 EBO
- 繪制矩形
- uniform
- glGetUniformLocation
- glUniform4f
- 向VAO加入顏色數據
- 顏色數據存入VAO
- 頂點著色器和片段著色器的聯動
- 繪制三色三角形
- 封裝一個著色器類
- 構造函數
- use函數
- uniform的set函數
- 使用
- 拓展
- 讓三角形顛倒
- 移動三角形
前言
什么是 OpenGl ?
- OpenGL 只是一種規范,嚴格意義上來講不能視為庫,不同的顯卡生產商在 OpenGL 的 API 提供上有著細微的差距,而 OpenGL 的核心代碼和顯卡核心技術綁定因此是非開源的,使用時通常僅能對廠商提供的 API 進行操作。
- OpenGL 優勢在于它是跨平臺的,一份代碼可以在 Mac、Windows、Linux,甚至移動端的 iOS、Android 上運行。(比為不同平臺專門編寫不同 API 的 Direct3D 更適合懶人,當然在 iOS 上可能更多還是選擇蘋果專用的 Metal)。
- 眾所周知,用編程語言(C++、Java、C#)實現的程序都是運行在 CPU 上,但實現圖形處理的時候,為了精確控制 GPU,因此需要將代碼從 CPU 上移植到 GPU 上(代碼在 GPU 上的運行速度會更快),而著色器允許我們在 GPU 上寫代碼,是否有 可編程著色器(programmable shaders) 是
modern OpenGL
和legacy OpenGL
的主要區別,當然,本質上是 現代OpenGL 比 老OpenGL 讓渡了更多的 控制權 給程序員。 - OpenGL Context(OpenGL 上下文環境) 的創建需要借助一些工具,比如輕量級的庫 GLFW(Graphics Library Framework,圖形庫框架),GLFW 的主要功能是 創建并管理窗口 和 OpenGL 上下文,同時還提供了基礎權限操作——處理手柄、鍵盤、鼠標輸入的功能。
回顧
在上一篇博客中,最后檢測配置 OpenGL 環境是否成功是通過一段代碼來實現的,代碼中有關 GLFW 和 GLAD 的內容比較簡單,在此不做贅述。可以通過代碼中的注釋進行理解,覺得注釋沒有講清楚的也可以通過 LearnOpenGL CN 中的 你好,窗口 一文進行學習。
本篇博客全部代碼。代碼中有關 GLFW 和 GLAD 的內容如下,這些代碼類似于模塊一般,幾乎是我們要渲染圖像并顯示在窗口時必須編寫的:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>// 對窗口注冊一個回調函數(Callback Function),它會在每次窗口大小被調整的時候被調用。
// 參數:window - 被改變大小的窗口,width、height-窗口的新維度。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// 告訴OpenGL渲染窗口的尺寸大小,即視口(Viewport)// 這樣OpenGL才只能知道怎樣根據窗口大小顯示數據和坐標// 調用glViewport函數來設置窗口的維度(Dimension)// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)glViewport(0, 0, width, height);
}// 實現輸入控制的函數:查詢GLFW是否在此幀中按下/釋放相關鍵,并做出相應反應
void processInput(GLFWwindow *window)
{// glfwGetKey兩個參數:窗口,按鍵// 沒有被按下返回 GLFW_PRESSstd::cout << "是否點擊ESC?" << std::endl;std::cout << glfwGetKey(window, GLFW_KEY_ESCAPE) << std::endl;if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)// 被按下則將 WindowShouldClose 屬性置為 true// 以便于在關閉 渲染循環glfwSetWindowShouldClose(window, true);
}const unsigned int SCR_WIDTH = 800; // 創建窗口的寬
const unsigned int SCR_HEIGHT = 600; // 創建窗口的高int main()
{glfwInit(); // 初始化GLFW// glfwWindowHint函數的第一個參數代表選項的名稱// 第二個參數接受一個整型,用來設置這個選項的值// 將主版本號(Major)和次版本號(Minor)都設為3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);// 使用的是核心模式(Core-profile)glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__// macOS需要本語句生效 glfwWindow 的相關配置glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif// 參數依次為:寬、高、窗口的名稱,顯示器用于全屏模式,設為NULL是為窗口// 窗口的上下文為共享資源,NULL為不共享資源GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "FirstWindow", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 釋放空間,防止內存溢出glfwTerminate();return -1;}// 創建完畢之后,需要讓window的context成為當前線程的current contextglfwMakeContextCurrent(window);// 窗口大小改變時視口也要隨之改變,這通過對窗口注冊 framebuffer_size_callback 實現。// 它會在每次窗口大小被調整時調用glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glfwGetProcAddress是glfw提供的用來 加載系統相關的OpenGL函數指針地址 的函數// gladLoadGLLoader函數根據使用者的系統定義了正確的函數if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}/* 渲染循環(Render Loop) */// glfwWindowShouldClose 檢查一次GLFW是否被要求退出// 為true時渲染循環結束while(!glfwWindowShouldClose(window)){// 監測鍵盤輸入processInput(window);/* 渲染 */// glfwSwapBuffers 交換顏色緩沖,用來繪制并作為輸出顯示在屏幕glfwSwapBuffers(window);// glfwPollEvents 檢查是否有觸發事件glfwPollEvents();}glfwTerminate();return 0;
}
openGL 的 Object
為什么 OpenGL 會有這么多的 Object?
因為 OpenGL 的改進目標就是把以前所有直接從客戶端傳值到服務端的操作,都變成對(服務端中)顯存的Object的更新,之后客戶端只要綁定(Bind)
一下,服務端就能直接在顯存讀取Object
的數據了,如此一來就避免了大量低效的數據傳輸。而每個Object
就對應一個顯存結構。
顯存結構
- 頂點數組對象:Vertex Array Object【VAO】
- 頂點緩沖對象:Vertex Buffer Object【VBO】
- 元素緩沖對象:Element Buffer Object【EBO】
- 索引緩沖對象:Index Buffer Object【IBO】
工作階段
圖形渲染管線(Graphics Pipeline),指的是一堆原始圖形數據途經一個輸送管道,將 OpenGL 中的 3D坐標 轉為 適配屏幕 的 2D像素 的處理過程。該過程可以分為兩個階段:
- 3D坐標轉換為2D坐標;
- 2D坐標轉變為有顏色的2D像素。
2D坐標和像素不同,2D坐標精確表示一個點在2D空間中的位置,而2D像素是這個點的近似值,2D像素受到屏幕/窗口分辨率的限制。
下圖是圖形渲染管線的每個階段的抽象展示,藍色部分表示該階段可以注入自定義著色器:
圖形渲染管線本質上是一個狀態機,每個階段將會把前一個階段的輸出作為輸入,且這些階段都允許并行執行。
- 頂點著色器見下文。
圖元裝配(Primitive Assembly)
階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS
,那么就是一個頂點),并所有的點裝配成指定圖元的形狀;上圖例子中是一個三角形。- 幾何著色器把圖元裝配階段輸出的一系列頂點的集合作為輸入,可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。上圖例子中生成了另一個三角形。
光柵化階段(Rasterization Stage)
把圖元映射為最終屏幕上相應的像素,生成供片段著色器(Fragment Shader)
使用的片段(Fragment)
(一個片段是 OpenGL 渲染一個像素所需的所有數據)。在片段著色器運行之前會執行裁切(Clipping)
。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。- 片段著色器見下文。
Alpha測試
和混合(Blending)
階段檢測片段的對應的深度
和模板(Stencil))值
,用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(物體的透明度)
并對物體進行混合(Blend)
。所以,即使在片段著色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
圖形渲染管線非常復雜,包含很多可配置的部分。然而,對于大多數場合,我們只需要配置頂點和片段著色器就行了。幾何著色器通常使用它默認的著色器就行了。
通過頂點緩沖對象將頂點數據初始化至緩沖中
標準化設備坐標
OpenGL 不是簡單地把所有的 3D坐標
變換為屏幕上的 2D像素
,它只會處理標準化設備坐標【在頂點著色器中處理過的頂點坐標就是標準化設備坐標】 ,標準化設備坐標是一個 x
、y
和 z
值在 -1.0 ~ 1.0
的一小段空間。任何落在范圍外的坐標都會被丟棄/裁剪,不會顯示在屏幕上。下面是一個定義在標準化設備坐標中的三角形(忽略z軸):
繪制三角形的第一步是以數組的形式傳遞 3
個 3D坐標
作為圖形渲染管線的輸入,用來表示一個三角形。
PS:在真實的程序里輸入數據通常都不是標準化設備坐標,而是頂點坐標,將頂點坐標轉換為標準化設備坐標是在頂點著色器中完成的,但是本篇博客旨在盡可能簡潔的闡述完渲染流程,因此直接向頂點著色器傳入標準化設備坐標。
以標準化設備坐標的形式(OpenGL的可見區域)定義一個頂點數據數組:
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f
};
vertices
中每一行就是一個頂點(Vertex):一個 3D坐標 的數據的集合。vertices
叫做頂點數據(Vertex Data):是一系列頂點的集合。
由于 OpenGL 是在3D空間中工作的,而渲染的是一個2D三角形,因此將頂點的z
坐標設置為 0.0
。這樣子的話三角形每一點的 深度(Depth) 都是一樣的,從而使它看上去像是2D的。
深度: 代表一個像素在空間中和屏幕的距離,如果離屏幕遠就可能被別的像素遮擋而變得不可見,因此會被丟棄以節省資源。
頂點緩沖對象 VBO
- 頂點數據會被作為輸入發送給圖形渲染管線的第一個處理階段:頂點著色器。
- 頂點著色器會在
GPU
上創建內存用于儲存頂點數據。 - 頂點緩沖對象(
VBO
)負責管理這個GPU
內存(通常被稱為顯存),他在顯存中儲存大量頂點,配置OpenGL如何解釋這些內存,并且指定顯存中的數據如何發送給顯卡。
從
CPU
把數據發送到顯卡相對較慢,但VBO
可以一次性發送一大批數據,而不是每個頂點發送一次。當數據發送至顯卡的內存中后,頂點著色器幾乎能立即訪問頂點。
接下來,執行如下流程:
glGenBuffers
- 使用
glGenBuffers
函數和一個緩沖ID
生成一個VBO
對象:
unsigned int VBO;
glGenBuffers(1, &VBO);
函數原型:
void glGenBuffers(GLsizei n,GLuint * buffers);
n
:生成的緩沖對象的數量;buffers
:用來存儲緩沖對象名稱的數組。- 此時僅生成了一個緩沖對象,但是緩沖對象的類型還不確定。
glBindBuffer
- 使用
glBindBuffer()
來確定生成的緩沖對象的類型,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFER
:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
函數原型:
void glBindBuffer(GLenum target,GLuint buffer);
target
:緩沖對象的類型;buffer
:要綁定的緩沖對象的名稱。
官方文檔指出:GL_INVALID_VALUE is generated if buffer is not a name previously returned form a call to glGenBuffers。
換句話說,buffer
雖然是 GLuint
類型的,但是不能直接指定個常量比如說 2
,如果這樣做了,就會出現 GL_INVALID_VALUE
的錯誤:
OpenGL允許我們同時綁定多個緩沖類型,但要求這些緩沖類型是不同的。舉個簡單例子:
我要把數據存入頂點緩沖區,但是頂點緩沖區(GL_ARRAY_BUFFER
)綁定了多個緩沖對象(VBO
、VBO1
、VBO2
),此時將數據傳入哪個緩沖對象就成了問題。
glBufferData
- 調用
glBufferData
函數,把之前定義的頂點數據復制到緩沖的內存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
函數原型:
void glBufferData(GLenum target,GLsizeiptr size,const GLvoid * data,GLenum usage);
target
:目標緩沖的類型。size
:指定傳輸數據的大小(以字節為單位);用sizeof
計算出頂點數據大小就行。data
:指定要復制數據的內存地址,如果不復制數據,則為NULL
。usage
:指定顯卡管理給定的數據的形式。有三種:GL_STATIC_DRAW
:數據不會或幾乎不會改變。GL_DYNAMIC_DRAW
:數據會被改變很多。GL_STREAM_DRAW
:數據每次繪制時都會改變。
GL_DYNAMIC_DRAW
和 GL_STREAM_DRAW
將導致顯卡把數據放在能夠高速寫入的內存部分,而內存空間是十分寶貴的,不要隨便使用,因此合理填寫 usage
對應的值,不會/幾乎不會改變的數據一定要填寫為 GL_STATIC_DRAW
。
建立了一個頂點和一個片段著色器
著色器是什么?
我們發現上圖中某些階段會用到著色器(Shader),它是運行在 GPU
上的可編程的小程序,在圖形渲染管線某個特定部分快速處理數據。著色器有以下特點:
- 運行在
GPU
上,節省了寶貴的CPU
時間。 - 著色器只是一種把 輸入 處理后 輸出 的程序。除輸入/輸出之外不能相互通信。
為什么需要使用著色器?
實際上,圖像處理完全可以在 CPU
中進行,通過串行多核計算實現圖形渲染。但渲染工作都十分單一,僅僅是多點計算,對于處理復雜工作的 CPU
而言,將大量時間花在處理簡單的渲染工作上無疑是種資源的浪費。
于是 GPU
這個專注于圖像處理的硬件誕生了,它是一個允許并行計算的超多核處理器,GPU
擁有成百上千個核心,意味著在圖形處理方面 GPU
能帶來更快的處理速度和更好的圖形效果。(常見 CPU
可能是4核的,但是兩者核心能處理的工作復雜度是不可相提并論的)
綜上,OpenGL 實現了一種可以讓點和像素的計算在 GPU
中進行的規范,這就是著色器。
這里僅對著色器做簡單的認知介紹,更多關于著色器的知識可詳見該文。
著色器的結構
著色器通常具有以下結構:
- 聲明版本
- 輸入和輸出變量
- Uniform
- main函數。每個著色器的入口點都是main函數,在這里處理所有的輸入變量,并將結果輸出到輸出變量中。
// 聲明版本
#version version_number
// 輸入變量
in type in_variable_name;
in type in_variable_name;
// 輸出變量
out type out_variable_name;
// uniform
uniform type uniform_name;
// main 函數
int main(){// 處理輸入并進行一些圖形操作...// 輸出處理過的結果到輸出變量out_variable_name = weird_stuff_we_processed;
}
頂點著色器
因為 GPU
中沒有默認的頂點/片段著色器,所以現代 OpenGL
要求至少設置一個頂點著色器和一個片段著色器才能實現渲染。
頂點著色器主要功能是把 3D坐標 轉換為 標準化設備坐標,后者依然是 3D坐標,但 x
、y
、z
的取值范圍不再是整個空間,而是 -1.0 ~ 1.0
。同時允許我們對頂點屬性進行一些基本處理。
用 著色器語言GLSL(OpenGL Shading Language) 編寫頂點著色器:
# version 330 core
layout (location = 0) in vec3 aPos;void main()
{gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
version 330
:GLSL 版本聲明,OpenGL 3.3 以及更高版本中,GLSL版本號和OpenGL的版本是匹配的(比如說GLSL 420版本對應于OpenGL 4.2)。core
:核心模式。layout (location = 0)
:設定了輸入變量的位置值(Location),位置數據是一種頂點屬性。in
:關鍵字,在著色器中聲明所有的輸入頂點屬性(Input Vertex Attribute) 到aPos
中。vec3
:包含3
個float
分量的三維向量。aPos
是一個vec3
輸入變量。gl_Position
:預定義的變量,類型為vec4
,這里通過vec3
變量aPos
的數據來充當vec4
構造器的參數,把w
分量設置為1.0f
,vec.w
分量不是用作表達空間中的位置的(我們處理的是 3D 不是 4D),而是用在 透視除法(Perspective Division) 上。
為了能夠讓 OpenGL
使用頂點著色器,必須在運行時動態編譯它的源代碼。
- 將頂點著色器的源代碼硬編碼在C風格字符串中。
const char *vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 aPos;\n""void main()\n""{\n"" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n""}\0";
- 用
glCreateShader
創建這個著色器對象,通過unsigned int
存儲glCreateShader
返回的ID
,以便于引用該著色器對象所在的內存空間。
// glCreateShader函數參數:要創建的著色器類型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 將著色器源碼附加到著色器對象上,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource
的參數:
GLuint shader
:指定要被編譯源代碼的著色器對象的句柄(ID)GLsizei count
:指定傳遞的源碼字符串數量,這里只有一個const GLchar **string
:指向包含源代碼的字符串的指針數組,這也就是為什么上面的代碼在調用時傳入的是vertexShaderSource
指針本身的地址,而不是指針指向的字符串的地址。(因為該參數會被二次解引用)const GLint *length
:為 NULL 則將整個字符串進行拷貝替換;不為 NULL 則將替換指定長度部分。
- 通過
glGetShaderiv
檢查著色器是否編譯成功,如果編譯失敗則調用glGetShaderInfoLog
獲取錯誤消息,并且打印。
// 檢查著色器編譯錯誤
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;
}
glGetShaderiv
的原型:
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
shader
:指定要查詢的著色器對象ID。pname
:指定檢查內容,可接受的符號名稱有:GL_SHADER_TYPE
:用來判斷并返回著色器類型,頂點著色器返回GL_VERTEX_SHADER
,片元著色器返回GL_FRAGMENT_SHADER
。GL_DELETE_STATUS
:判斷著色器是否被刪除,是返回GL_TRUE
,否則返回GL_FALSE
。GL_COMPILE_STATUS
:用于檢測編譯是否成功,成功為GL_TRUE
,否則為GL_FALSE
。GL_INFO_LOG_LENGTH
:用于返回著色器的信息日志的長度,包括空終止字符(即存儲信息日志所需的字符緩沖區的大小)。 如果著色器沒有信息日志,則返回0
。GL_SHADER_SOURCE_LENGTH
:返回著色器源碼長度,不存在則返回0。
params
:因為根據第二個參數值的不同,返回的結果會有很多種,所以單獨存儲在輸入的第三個參數中。這也是為什么函數返回值是void
而不是GLuint
。
片段著色器
片段著色器(Fragment Shader)
可以接收由光柵化階段生成的每個片段數據、紋理數據、3D場景的數據(比如光照、陰影、光的顏色等),用來計算出每個光柵化空白像素的最終顏色。是所有OpenGL高級效果產生的地方。
RGBA:紅色、綠色、藍色和 alpha(透明度) 分量,當在 OpenGL 或 GLSL 中定義一個顏色的時候,把顏色每個分量的強度設置在 0.0 到 1.0 之間。比如設置紅為 1.0f,綠為 1.0f,就會得到混合色——黃色。
- GLSL 片段著色器源代碼,聲明輸出變量:
#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
out
:關鍵字,聲明輸出變量到FragColor
中。
- 硬編碼
const char *fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""void main()\n""{\n"" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n""}\n\0";
- 創建著色器對象并記錄ID、附加源碼、編譯:
// 與頂點著色器的最大區別 glCreateShader 的參數 —— GL_FRAGMENT_SHADER
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
- 檢查編譯是否成功,代碼與頂點著色器檢查部分相同。
- 把兩個著色器對象鏈接到一個用來渲染的著色器程序(
Shader Program
)中。
著色器程序對象
著色器程序對象(Shader Program Object
)是多個著色器合并之后并最終完成鏈接的版本。
當鏈接著色器至一個程序的時候,程序會把每個著色器的輸出鏈接到下個著色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連接錯誤。
- 創建一個程序對象,
shaderProgram
接收新創建程序對象的ID引用:
unsigned int shaderProgram = glCreateProgram();
- 把之前編譯完成的頂點/片段著色器附加到著色器程序對象上,然后用
glLinkProgram
鏈接:
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);
- 激活著色器程序對象:
// 將這段函數加入while循環函數的渲染部分,就可以激活這個著色器程序對象了。
glUseProgram(shaderProgram);
把頂點數據鏈接到頂點著色器的頂點屬性上
頂點緩沖數據會被解析為下面這樣子:
- 位置數據被儲存為32位(4字節)浮點值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙(或其他值)。這幾個值在數組中緊密排列。
- 數據中第一個值在緩沖開始的位置。
解析頂點數據對應的代碼實現:
glVertexAttribPointer
指定了渲染時索引值為index
的頂點屬性數組的數據:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
函數原型:
void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);
index
:要配置的頂點屬性的索引值。在頂點著色器中,曾使用layout(location = 0)
定義了position
頂點屬性的位置值(Location
)。location
的值0
就是索引值。size
:指定每個頂點屬性的組件數量,必須為1、2、3或者4。初始值為4。(如position
是由3個組件[x,y,z]組成,而顏色是4個組件[r,g,b,a])。type
:指定數組中每個組件的數據類型。可用的符號常量有:GL_BYTE
、GL_UNSIGNED_BYTE
、GL_SHORT
、GL_UNSIGNED_SHORT
、GL_FIXED
和GL_FLOAT
,初始值為GL_FLOAT
。此外,GLSL
中vec*
都是由浮點數值組成的。normalized
:指定當被訪問時,固定點數據值是否應該被歸一化【Normalize】(GL_TRUE
)或者直接轉換為固定點值(GL_FALSE
)。如果為GL_TRUE
,所有數據都會被映射到0
(對于有符號型signed
數據是-1
)到1
之間。stride
:指定連續頂點屬性之間的偏移量。初始值為0
,意為頂點屬性是緊密排列在一起的。由于下個組位置數據在3
個float
之后,我們把步長設置為3 * sizeof(float)
。在此例中兩個頂點屬性之間沒有空隙,因此也可以設置為0
來讓OpenGL
決定具體步長是多少(只有當數值是緊密排列時才可用)。pointer
:指定第一個組件在數組的第一個頂點屬性中的偏移量。該數組與GL_ARRAY_BUFFER
綁定,儲存于緩沖區中。初始值為0
;由于位置數據在數組的開頭,所以偏移量是0
。
每個頂點屬性從一個頂點緩沖對象管理的內存中獲得它的數據,而具體是從哪個頂點緩沖對象(程序中可以有多個頂點緩沖對象)獲取則是通過在調用glVertexAttribPointer時綁定到GL_ARRAY_BUFFER的緩沖對象決定的。同一時刻只能有一個緩沖對象綁定到GL_ARRAY_BUFFER,此時綁定到GL_ARRAY_BUFFER的是先前定義的VBO,頂點屬性0會鏈接到它的頂點數據。
glEnableVertexAttribArray
啟用頂點屬性。頂點屬性默認是禁用的。
glEnableVertexAttribArray(0);
函數原型:
void glEnableVertexAttribArray(GLuint index);void glDisableVertexAttribArray(GLuint index);void glEnableVertexArrayAttrib( GLuint vaobj, GLuint index);void glDisableVertexArrayAttrib(GLuint vaobj, GLuint index);
vaobj
:指定glDisableVertexArrayAttrib
和glEnableVertexArrayAttrib
函數的頂點數組對象(VAO)的名稱。index
:指定 啟用/禁用 的索引(頂點屬性位置值)。
繪制單個物體
到此所有流程就結束了,如果想在OpenGL中繪制一個物體,代碼會像是這樣:
// 0. 復制頂點數組到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 1. 設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);// 2. 當我們渲染一個物體時要使用著色器程序
glUseProgram(shaderProgram);// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
當頂點屬性個數不再是 1
,而是很多的時候,就不得不多次調用 glBufferData
和 glVertexAttribPointer
、 glEnableVertexAttribArray
。綁定正確的緩沖對象、為每個物體配置所有頂點屬性很快就變成一件麻煩事。
這就需要一個能夠存儲狀態配置的對象,然后通過綁定這個對象來恢復狀態。這就要靠VAO
了。
頂點數組對象 VAO
為什么要使用VAO?
VBO
大幅提升了繪制效率,但是頂點的位置坐標、法向量、紋理坐標等不同方面的數據每次使用時需要單獨指定,重復了一些不必要的工作。
而頂點數組對象(VAO)可以像VBO
那樣被綁定,當配置頂點屬性指針時,你只需要調用將glVertexAttribPointer
、glEnableVertexAttribArray
一次,之后再繪制物體的時候只需要綁定頂點屬性指針相應的VAO
就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO
就行了。
OpenGL的核心模式要求我們使用VAO。如果綁定VAO失敗,OpenGL會拒絕繪制任何東西。
VAO儲存的內容
glEnableVertexAttribArray
和glDisableVertexAttribArray
的調用。- 通過
glVertexAttribPointer
設置的頂點屬性配置。 - 通過
glVertexAttribPointer
調用與頂點屬性關聯的VBO
。
- VAO 中的
attribute pointer
(屬性指針)指向 VBO 中的某個屬性(pos
【位置】或者col
【顏色】),如上圖就是attribute pointer 0
來管理位置屬性,attribute pointer 1
來管理顏色屬性。 - 對于 VBO 來講,每個頂點 的 所有屬性 都相鄰存儲,頂點0的位置(
pos[0]
)、顏色(col[0]
),因此每一種attribute pointer
都會有 步長(stride
)。
使用 VAO 的流程:
- 創建一個VAO,在VAO后創建的VBO都屬于該VAO。
unsigned int VAO;
glGenVertexArrays(1, &VAO);
- 綁定VAO,綁定成功后應該綁定和配置對應的VBO和屬性指針,之后解綁VAO供再次使用。
// 綁定VAO
glBindVertexArray(VAO);
打算繪制多個物體時,首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然后儲存它們供后面使用。繪制其中一個物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再解綁VAO。
繪制三角形(glDrawArrays函數)
使用 glDrawArrays
函數,通過當前激活的著色器、之前定義的頂點屬性配置和VBO的頂點數據(通過VAO間接綁定)來繪制圖元:
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
函數原型:
GL_APICALL void GL_APIENTRY glDrawArrays (GLenum mode, GLint first, GLsizei count);
mode
:繪制方式,可選值有:GL_POINTS
:把每一個頂點作為一個點進行處理。GL_LINES
:連接每兩個頂點作為一個獨立的線段,N個頂點總共繪制N/2條線段。GL_LINE_STRIP
:繪制從第一個頂點到最后一個頂點依次相連的一組線段。GL_LINE_LOOP
:在GL_LINE_STRIP
的基礎上,最后一個頂點和第一個頂點相連。GL_TRIANGLES
:把每三個頂點作為一個獨立的三角形。GL_TRIANGLE_STRIP
:繪制一組相連的三角形。GL_TRIANGLE_FAN
:圍繞第一個點繪制相連的三角形,第一個頂點作為所有三角形的頂點。
first
:從數組緩存中的哪一位開始繪制,一般為0。count
:數組中頂點的數量。
繪制三角形的全部代碼詳見。
元素緩沖對象 EBO / 索引緩沖對象 IBO
元素緩沖對象 EBO / 索引緩沖對象 IBO 是同一個東西。假設想要繪制一個矩形,可以通過繪制兩個三角形來組成一個矩形(OpenGL主要處理三角形)。頂點集合如下:
float vertices[] = {// 第一個三角形0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, 0.5f, 0.0f, // 左上角// 第二個三角形0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角
};
上面指定了右下角
和左上角
兩次,但是一個矩形只有4
個而不是6
個頂點,這樣就產生50%
的額外開銷。好的解決方案是只儲存不同的頂點,并設定繪制這些頂點的順序。這樣只要儲存4
個頂點就能繪制矩形了,之后只要指定繪制的順序就行了。這便是 元素緩沖區對象(EBO) 的工作方式。
EBO
存儲要繪制的頂點的索引,即索引繪制(Indexed Drawing)。使用EBO
的流程如下:
vertices 頂點數據
- 首先,要定義(不重復的)頂點,和繪制出矩形所需的索引:
float vertices[] = {0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角
};unsigned int indices[] = {// 注意索引從0開始! // 此例的索引(0,1,2,3)就是頂點數組vertices的下標,// 這樣可以由下標代表頂點組合成矩形0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形
};
glGenBuffers
- 創建元素緩沖對象:
unsigned int EBO;
glGenBuffers(1, &EBO);
glBufferData
- 先綁定EBO然后用glBufferData把索引復制到緩沖:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glPolygonMode
- 控制多邊形的顯示方式,GL_LINE以線框模式繪制。
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
函數原型:
void glPolygonMode(GLenum face,GLenum mode);
- face:確定顯示模式將適用于物體的哪些部分,控制多邊形的正面和背面的繪圖模式:
- GL_FRONT表示顯示模式將適用于物體的前向面(也就是物體能看到的面)
- GL_BACK表示顯示模式將適用于物體的后向面(也就是物體上不能看到的面)
- GL_FRONT_AND_BACK表示顯示模式將適用于物體的所有面
- mode:確定選中的物體的面以何種方式顯示(顯示模式):
- GL_POINT表示顯示頂點,多邊形用點顯示
- GL_LINE表示顯示線段,多邊形用輪廓顯示
- GL_FILL表示顯示面,多邊形采用填充形式
glDrawElements
- 用glDrawElements來替換glDrawArrays函數,表示從索引緩沖區使用當前綁定的索引緩沖對象中的索引進行繪制:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
函數原型:
void glDrawElements(GLenum mode,GLsizei count,GLenum type,const GLvoid *indices);
mode
:繪制方式,同glDrawArrays
中的mode
參數。count
:打算繪制頂點的個數,vertices中有兩個頂點被復用了,因此這里填6
。type
:索引的類型,一般都是GL_UNSIGNED_INT
。*indices
:EBO
的偏移量。不再使用索引緩沖對象的時候可以傳遞一個索引數組。
VAO 與 EBO
glDrawElements
函數從當前綁定到GL_ELEMENT_ARRAY_BUFFER
目標的EBO
中獲取其索引。這意味著每次使用索引渲染對象時都必須綁定相應的EBO
,這有點麻煩。有沒有感覺到這個問題很眼熟?這不就是沒有VAO
只有VBO
時碰到的問題么。更巧的是VAO
也跟蹤EBO
綁定。在綁定VAO
時,之前綁定的最后一個EBO
自動存儲為VAO
的EBO
。
上圖有一個小細節:VAO
只可以綁定一個EBO
,但是可以綁定多個VBO
。所以確保先解綁EBO
再解綁VAO
,否則VAO
就沒有EBO
配置了,也就無法成功繪制了。
OpenGL VAO VBO EBO(IBO)的綁定、解綁問題值得一看。
繪制矩形
最后的初始化和繪制代碼現在看起來像這樣:
// ..:: 初始化代碼 :: ..
// 1. 綁定頂點數組對象
glBindVertexArray(VAO);
// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 設定頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);[...]glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);// ..:: 繪制代碼(渲染循環中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // 如果繪制多個對象,在這里切換綁定VAO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
繪制矩形的全部代碼詳見。
uniform
Uniform
是除頂點屬性外另一種從CPU
中的應用向GPU
中的著色器發送數據的方式。它的特性如下:
uniform
是全局的,意味著uniform
變量必須在每個著色器程序對象中都是獨一無二的,而且它可以被著色器程序的任意著色器在任意階段訪問。- 無論把
uniform
值設置成什么,uniform
會一直保存這些數據,直到舊有數據被重置或更新。
舉個例子,通過uniform
設置三角形的顏色:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量
void main(){FragColor = ourColor;
}
如果你聲明了一個 uniform
卻在 GLSL
代碼中沒用過,編譯器會靜默移除這個變量,導致最后編譯出的版本中并不會包含它,這可能導致幾個非常麻煩的錯誤!
在給uniform
添加數據之前,首先需要找到著色器中uniform
屬性的索引/位置值。uniform
是種類似于in
的輸入數據,之前layout (location = 0) in vec3 aPos
是通過索引值location = 0
將外部數據綁定的,而uniform
完全不需要layout
,而是通過著色器程序對象和uniform
的名字:
/* 在循環渲染的代碼塊中加入下列代碼 */
// 獲取運行的秒數
float timeValue = glfwGetTime();
// 通過sin函數讓顏色在0.0到1.0之間改變,最后將結果儲存到greenValue里。
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
// 通過glGetUniformLocation查詢uniform ourColor的位置值,返回-1代表沒有找到。
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
// 通過glUniform4f函數設置uniform值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
注意,查詢uniform
地址不要求之前使用過著色器程序(調用glUseProgram
),但是更新一個uniform
(調用glUniform4f
)之前必須先使用程序,因為設置uniform
是在當前激活的著色器程序中進行的。
glGetUniformLocation
GLint glGetUniformLocation(GLuint program, const GLchar *name);
program
:指定要查詢的著色器程序對象。name
:指向一個沒有終止符的字符串,其中包含要查詢位置的uniform
變量的名稱。
glUniform4f
OpenGL
其核心是一個C
庫,所以不支持類型重載,在函數參數不同的時候就要為其定義新的函數;glUniform
是一個典型例子。這個函數有一個特定的后綴,標識設定的uniform
的類型。可能的后綴有:
在上面的例子中,由于需要分別設定uniform
的4
個float
值,所以通過glUniform4f
傳遞數據(也可以使用glUniformfv
版本)。
繪制變色三角形的代碼可以參考這里。
向VAO加入顏色數據
顏色數據存入VAO
顏色屬于頂點屬性的一種,因此它也可以存入VAO。試試將顏色數據存入VAO然后傳給頂點著色器,而不是傳給片段著色器。
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
:
const char *vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 point;\n""layout (location = 1) in vec3 color;\n""out vec3 ourColor;\n""void main()\n""{\n"" gl_Position = vec4(point.x, point.y, point.z, 1.0);\n"" ourColor = color;\n""}\0";
添加了新的頂點屬性(顏色),就需要更新VBO
的內存并重新配置頂點屬性指針。更新后的VBO
內存中的數據如下圖所示:
知道了現在使用的布局,就可以使用glVertexAttribPointer
函數更新頂點格式:
// 位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 啟用layout 0// 顏色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); // 啟用layout 1
頂點著色器和片段著色器的聯動
此時不再使用uniform
來傳遞片段的顏色了,而是以頂點著色器的輸出ourColor
作為片段著色器的輸入:
const char *fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""in vec3 ourColor;\n""void main()\n""{\n"" FragColor = vec4(ourColor, 1.0f);\n""}\n\0";
繪制三色三角形
程序運行結果如下:
咦?運行結果貌似和設計預期有所區別。按理來說應該是一個紅綠藍純三色的三角形,怎么在三個角顏色還蠻純正的,越接近三角形中心顏色越混雜呢?這是因為片段著色器中進行了片段插值。
當渲染一個三角形時,光柵化階段通常會將幾何著色器劃分好的區域細分成更多的片段。光柵會根據每個片段在三角形上所處位置進行插值。比如有一個線段,上面的端點是綠色的,下面的端點是藍色的。如果一個片段著色器在線段的70%
(靠近綠色端)的位置運行,它的顏色輸入屬性就是30%
藍 + 70%
綠。
上圖有3個頂點和相應的3個顏色,從這個三角形的像素來看它可能包含50000左右的片段,片段著色器為這些像素進行插值顏色。如果你仔細看這些顏色就應該能明白了:紅先變成紫再變為藍色。片段插值會被應用到片段著色器的所有輸入屬性上。
封裝一個著色器類
編寫、編譯、管理著色器是件麻煩事。不妨寫一個類從硬盤讀取著色器,然后編譯并鏈接它們,并對它們進行錯誤檢測。來吧,將目前所學知識封裝到一個抽象對象中!
把著色器類全部放在在頭文件里,以方便移植。先添加必要的include,并定義類結構:
#ifndef SHADER_H
#define SHADER_H#include <glad/glad.h>; // 包含glad來獲取所有的必須OpenGL頭文件#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader{
public:// 著色器程序IDunsigned int ID;// 構造函數從文件路徑讀取頂點/片段著色器源代碼以構建著色器Shader(const char* vertexPath, const char* fragmentPath);// 使用/激活程序void use();// uniform工具函數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;
private:// 用于檢查著色器編譯/鏈接錯誤的實用程序函數void checkCompileErrors(unsigned int index, std::string type){int success;char infoLog[1024];if (type != "PROGRAM"){glGetShaderiv(index, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(index, 1024, NULL, infoLog);std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << std::endl;}}else{glGetProgramiv(index, GL_LINK_STATUS, &success);if (!success){glGetProgramInfoLog(index, 1024, NULL, infoLog);std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << std::endl;}}}
};#endif
構造函數
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;// 頂點著色器vertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);checkCompileErrors(vertex, "VERTEX");// 片段著色器也類似fragment = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment, 1, &fShaderCode, NULL);glCompileShader(fragment);checkCompileErrors(fragment, "FRAGMENT");// 著色器程序ID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);checkCompileErrors(ID, "PROGRAM");// 刪除著色器,它們已經鏈接到我們的程序中了,已經不再需要了glDeleteShader(vertex);glDeleteShader(fragment);
}
use函數
void use()
{ glUseProgram(ID);
}
uniform的set函數
void setBool(const std::string &name, bool value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
使用
引入Shader.h
頭文件來簡化代碼,以三色三角形的代碼為例:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>
#include "Shader.h"using namespace std;// 對窗口注冊一個回調函數(Callback Function),它會在每次窗口大小被調整的時候被調用。
// 參數:window - 被改變大小的窗口,width、height-窗口的新維度。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// 告訴OpenGL渲染窗口的尺寸大小,即視口(Viewport)// 這樣OpenGL才只能知道怎樣根據窗口大小顯示數據和坐標// 調用glViewport函數來設置窗口的維度(Dimension)// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)glViewport(0, 0, width, height);
}// 實現輸入控制的函數:查詢GLFW是否在此幀中按下/釋放相關鍵,并做出相應反應
void processInput(GLFWwindow *window)
{// glfwGetKey兩個參數:窗口,按鍵// 沒有被按下返回 GLFW_PRESSif(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)// 被按下則將 WindowShouldClose 屬性置為 true// 以便于在關閉 渲染循環glfwSetWindowShouldClose(window, true);
}const unsigned int SCR_WIDTH = 800; // 創建窗口的寬
const unsigned int SCR_HEIGHT = 600; // 創建窗口的高int main(){glfwInit(); // 初始化GLFW// glfwWindowHint函數的第一個參數代表選項的名稱// 第二個參數接受一個整型,用來設置這個選項的值// 將主版本號(Major)和次版本號(Minor)都設為3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);// 使用的是核心模式(Core-profile)glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__// macOS需要本語句生效 glfwWindow 的相關配置glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif// 參數依次為:寬、高、窗口的名稱,顯示器用于全屏模式,設為NULL是為窗口// 窗口的上下文為共享資源,NULL為不共享資源GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "FirstWindow", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 釋放空間,防止內存溢出glfwTerminate();return -1;}// 創建完畢之后,需要讓window的context成為當前線程的current contextglfwMakeContextCurrent(window);// 窗口大小改變時視口也要隨之改變,這通過對窗口注冊 framebuffer_size_callback 實現。// 它會在每次窗口大小被調整時調用glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glfwGetProcAddress是glfw提供的用來 加載系統相關的OpenGL函數指針地址 的函數// gladLoadGLLoader函數根據使用者的系統定義了正確的函數if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}/* 設置頂點數據(和緩沖區)并配置頂點屬性 */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 // 頂部 藍色};// build and compile our shader programShader ourShader("3.3.shader.vs", "3.3.shader.fs"); // you can name your shader files however you likeunsigned int VBOs[2], VAOs[2];glGenVertexArrays(2, VAOs);glGenBuffers(2, VBOs); // 生成2個 VBO 對象/* 首先綁定頂點數組對象,然后綁定并設置頂點緩沖區,然后配置頂點屬性。 */glBindVertexArray(VAOs[0]);glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]); // 確定生成的緩沖對象的類型// 把頂點數據復制到緩沖的內存中glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// layout(location=0), 每個頂點的pos屬性(vec*)由3個組件構成,//(vec*)中的值的類型為GL_FLOAT, 轉換為固定點值, 第一個組件的偏移量為0// 位置glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0); // 啟用layout 0// 顏色glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1); // 啟用layout 1/* 渲染循環(Render Loop) */// glfwWindowShouldClose 檢查一次GLFW是否被要求退出// 為true時渲染循環結束while(!glfwWindowShouldClose(window)){// 監測鍵盤輸入processInput(window);/* 渲染 */// 狀態設置函數,設置清空屏幕所用的顏色glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 狀態使用函數,使用設定好的顏色來清除舊的顏色緩沖glClear(GL_COLOR_BUFFER_BIT);// 上面兩種函數起到的作用也可以用 glClearBufferfv 來現實/*GLfloat color[] = {0.2, 0.3, 0.3, 1.0};glClearBufferfv(GL_COLOR, 0, color);*/ourShader.use();glBindVertexArray(VAOs[0]);glDrawArrays(GL_TRIANGLES, 0, 3);// glfwSwapBuffers 交換顏色緩沖,用來繪制并作為輸出顯示在屏幕glfwSwapBuffers(window);// glfwPollEvents 檢查是否有觸發事件glfwPollEvents();}// 可選:一旦所有資源超出其用途,則取消分配:glDeleteVertexArrays(2, VAOs);glDeleteBuffers(2, VBOs);glfwTerminate();return 0;
}
拓展
讓三角形顛倒
讓一個三角形顛倒,除了修改頂點數組,還能想到什么辦法?修改頂點著色器源代碼!
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;void main(){// just add a - to the y positiongl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0); ourColor = aColor;
}
移動三角形
使用uniform
定義一個水平偏移量,在頂點著色器中使用該偏移量就可以實現三角形的移動。
// In Render Loop of your CPP file :
float offset = 0.5f;
ourShader.setFloat("xOffset", offset);// In your vertex shader code file:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;uniform float xOffset;void main()
{// add the xOffset to the x position of the vertex positiongl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); ourColor = aColor;
}