OpenGL | 通過繪制一個三角形來入門 OpenGL 圖形渲染管線

文章目錄

  • 前言
    • 什么是 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 只是一種規范,嚴格意義上來講不能視為庫,不同的顯卡生產商在 OpenGLAPI 提供上有著細微的差距,而 OpenGL 的核心代碼和顯卡核心技術綁定因此是非開源的,使用時通常僅能對廠商提供的 API 進行操作。
  • OpenGL 優勢在于它是跨平臺的,一份代碼可以在 MacWindowsLinux,甚至移動端的 iOSAndroid 上運行。(比為不同平臺專門編寫不同 APIDirect3D 更適合懶人,當然在 iOS 上可能更多還是選擇蘋果專用的 Metal)。
  • 眾所周知,用編程語言(C++、Java、C#)實現的程序都是運行在 CPU 上,但實現圖形處理的時候,為了精確控制 GPU,因此需要將代碼從 CPU 上移植到 GPU 上(代碼在 GPU 上的運行速度會更快),而著色器允許我們在 GPU 上寫代碼,是否有 可編程著色器(programmable shaders)modern OpenGLlegacy OpenGL 的主要區別,當然,本質上是 現代OpenGL老OpenGL 讓渡了更多的 控制權 給程序員。
  • OpenGL Context(OpenGL 上下文環境) 的創建需要借助一些工具,比如輕量級的庫 GLFW(Graphics Library Framework,圖形庫框架),GLFW 的主要功能是 創建并管理窗口 和 OpenGL 上下文,同時還提供了基礎權限操作——處理手柄、鍵盤、鼠標輸入的功能。

回顧

在上一篇博客中,最后檢測配置 OpenGL 環境是否成功是通過一段代碼來實現的,代碼中有關 GLFW 和 GLAD 的內容比較簡單,在此不做贅述。可以通過代碼中的注釋進行理解,覺得注釋沒有講清楚的也可以通過 LearnOpenGL CN 中的 你好,窗口 一文進行學習。

本篇博客全部代碼。代碼中有關 GLFWGLAD 的內容如下,這些代碼類似于模塊一般,幾乎是我們要渲染圖像并顯示在窗口時必須編寫的:

#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像素 的處理過程。該過程可以分為兩個階段:

  1. 3D坐標轉換為2D坐標;
  2. 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像素,它只會處理標準化設備坐標【在頂點著色器中處理過的頂點坐標就是標準化設備坐標】 ,標準化設備坐標是一個 xyz 值在 -1.0 ~ 1.0 的一小段空間。任何落在范圍外的坐標都會被丟棄/裁剪,不會顯示在屏幕上。下面是一個定義在標準化設備坐標中的三角形(忽略z軸):

在這里插入圖片描述

繪制三角形的第一步是以數組的形式傳遞 33D坐標 作為圖形渲染管線的輸入,用來表示一個三角形。

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

  1. 頂點數據會被作為輸入發送給圖形渲染管線的第一個處理階段:頂點著色器。
  2. 頂點著色器會在GPU上創建內存用于儲存頂點數據。
  3. 頂點緩沖對象(VBO)負責管理這個GPU內存(通常被稱為顯存),他在顯存中儲存大量頂點,配置OpenGL如何解釋這些內存,并且指定顯存中的數據如何發送給顯卡。

CPU把數據發送到顯卡相對較慢,但VBO可以一次性發送一大批數據,而不是每個頂點發送一次。當數據發送至顯卡的內存中后,頂點著色器幾乎能立即訪問頂點。

接下來,執行如下流程:

glGenBuffers

  1. 使用 glGenBuffers 函數和一個緩沖 ID 生成一個 VBO 對象:
unsigned int VBO;
glGenBuffers(1, &VBO);

函數原型:

void glGenBuffers(GLsizei n,GLuint * buffers);
  • n:生成的緩沖對象的數量;
  • buffers:用來存儲緩沖對象名稱的數組。
  • 此時僅生成了一個緩沖對象,但是緩沖對象的類型還不確定。

glBindBuffer

  1. 使用 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)綁定了多個緩沖對象(VBOVBO1VBO2),此時將數據傳入哪個緩沖對象就成了問題。

glBufferData

  1. 調用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_DRAWGL_STREAM_DRAW 將導致顯卡把數據放在能夠高速寫入的內存部分,而內存空間是十分寶貴的,不要隨便使用,因此合理填寫 usage 對應的值,不會/幾乎不會改變的數據一定要填寫為 GL_STATIC_DRAW


建立了一個頂點和一個片段著色器

著色器是什么?

我們發現上圖中某些階段會用到著色器(Shader),它是運行在 GPU 上的可編程的小程序,在圖形渲染管線某個特定部分快速處理數據。著色器有以下特點:

  • 運行在 GPU 上,節省了寶貴的 CPU 時間。
  • 著色器只是一種把 輸入 處理后 輸出 的程序。除輸入/輸出之外不能相互通信。

為什么需要使用著色器?

實際上,圖像處理完全可以在 CPU 中進行,通過串行多核計算實現圖形渲染。但渲染工作都十分單一,僅僅是多點計算,對于處理復雜工作的 CPU 而言,將大量時間花在處理簡單的渲染工作上無疑是種資源的浪費。

于是 GPU 這個專注于圖像處理的硬件誕生了,它是一個允許并行計算的超多核處理器,GPU 擁有成百上千個核心,意味著在圖形處理方面 GPU 能帶來更快的處理速度和更好的圖形效果。(常見 CPU 可能是4核的,但是兩者核心能處理的工作復雜度是不可相提并論的)

綜上,OpenGL 實現了一種可以讓點和像素的計算在 GPU 中進行的規范,這就是著色器。

這里僅對著色器做簡單的認知介紹,更多關于著色器的知識可詳見該文。


著色器的結構

著色器通常具有以下結構:

  1. 聲明版本
  2. 輸入和輸出變量
  3. Uniform
  4. 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坐標,但 xyz 的取值范圍不再是整個空間,而是 -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:包含 3float 分量的三維向量。
  • aPos 是一個 vec3 輸入變量。
  • gl_Position:預定義的變量,類型為 vec4,這里通過 vec3 變量 aPos 的數據來充當 vec4 構造器的參數,把 w 分量設置為 1.0fvec.w 分量不是用作表達空間中的位置的(我們處理的是 3D 不是 4D),而是用在 透視除法(Perspective Division) 上。

為了能夠讓 OpenGL 使用頂點著色器,必須在運行時動態編譯它的源代碼。

  1. 將頂點著色器的源代碼硬編碼在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";
  1. glCreateShader 創建這個著色器對象,通過 unsigned int 存儲 glCreateShader 返回的 ID ,以便于引用該著色器對象所在的內存空間。
// glCreateShader函數參數:要創建的著色器類型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
  1. 將著色器源碼附加到著色器對象上,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource 的參數:

  • GLuint shader:指定要被編譯源代碼的著色器對象的句柄(ID)
  • GLsizei count:指定傳遞的源碼字符串數量,這里只有一個
  • const GLchar **string:指向包含源代碼的字符串的指針數組,這也就是為什么上面的代碼在調用時傳入的是 vertexShaderSource 指針本身的地址,而不是指針指向的字符串的地址。(因為該參數會被二次解引用)
  • const GLint *length:為 NULL 則將整個字符串進行拷貝替換;不為 NULL 則將替換指定長度部分。
  1. 通過 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,就會得到混合色——黃色。

  1. GLSL 片段著色器源代碼,聲明輸出變量:
#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 
  • out :關鍵字,聲明輸出變量到 FragColor 中。
  1. 硬編碼
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";
  1. 創建著色器對象并記錄ID、附加源碼、編譯:
// 與頂點著色器的最大區別 glCreateShader 的參數 —— GL_FRAGMENT_SHADER
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
  1. 檢查編譯是否成功,代碼與頂點著色器檢查部分相同。
  2. 把兩個著色器對象鏈接到一個用來渲染的著色器程序(Shader Program)中。

著色器程序對象

著色器程序對象(Shader Program Object)是多個著色器合并之后并最終完成鏈接的版本。

當鏈接著色器至一個程序的時候,程序會把每個著色器的輸出鏈接到下個著色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連接錯誤。

  1. 創建一個程序對象,shaderProgram 接收新創建程序對象的ID引用:
unsigned int shaderProgram = glCreateProgram();
  1. 把之前編譯完成頂點/片段著色器附加到著色器程序對象上,然后用 glLinkProgram 鏈接:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
  1. 如檢測編譯時是否成功那樣,檢測鏈接是否成功:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
  1. 在把著色器對象鏈接到程序對象以后,刪除著色器對象,不再需要它們了,釋放占用的內存:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
  1. 激活著色器程序對象
// 將這段函數加入while循環函數的渲染部分,就可以激活這個著色器程序對象了。
glUseProgram(shaderProgram);

把頂點數據鏈接到頂點著色器的頂點屬性上

頂點緩沖數據會被解析為下面這樣子:
在這里插入圖片描述

  • 位置數據被儲存為32位(4字節)浮點值。
  • 每個位置包含3個這樣的值。
  • 在這3個值之間沒有空隙(或其他值)。這幾個值在數組中緊密排列。
  • 數據中第一個值在緩沖開始的位置。

解析頂點數據對應的代碼實現:

  1. 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_BYTEGL_UNSIGNED_BYTEGL_SHORTGL_UNSIGNED_SHORTGL_FIXEDGL_FLOAT,初始值為 GL_FLOAT。此外,GLSLvec* 都是由浮點數值組成的。
  • normalized:指定當被訪問時,固定點數據值是否應該被歸一化【Normalize】(GL_TRUE)或者直接轉換為固定點值(GL_FALSE)。如果為GL_TRUE,所有數據都會被映射到0(對于有符號型signed數據是-1)到1之間。
  • stride:指定連續頂點屬性之間的偏移量。初始值為0,意為頂點屬性是緊密排列在一起的。由于下個組位置數據在 3float 之后,我們把步長設置為3 * sizeof(float)。在此例中兩個頂點屬性之間沒有空隙,因此也可以設置為 0 來讓 OpenGL 決定具體步長是多少(只有當數值是緊密排列時才可用)。
  • pointer:指定第一個組件在數組的第一個頂點屬性中的偏移量。該數組與GL_ARRAY_BUFFER綁定,儲存于緩沖區中。初始值為0;由于位置數據在數組的開頭,所以偏移量是0

每個頂點屬性從一個頂點緩沖對象管理的內存中獲得它的數據,而具體是從哪個頂點緩沖對象(程序中可以有多個頂點緩沖對象)獲取則是通過在調用glVertexAttribPointer時綁定到GL_ARRAY_BUFFER的緩沖對象決定的。同一時刻只能有一個緩沖對象綁定到GL_ARRAY_BUFFER,此時綁定到GL_ARRAY_BUFFER的是先前定義的VBO,頂點屬性0會鏈接到它的頂點數據。

  1. glEnableVertexAttribArray 啟用頂點屬性。頂點屬性默認是禁用的。
glEnableVertexAttribArray(0);

函數原型:

void glEnableVertexAttribArray(GLuint index);void glDisableVertexAttribArray(GLuint index);void glEnableVertexArrayAttrib(	GLuint vaobj, GLuint index);void glDisableVertexArrayAttrib(GLuint vaobj, GLuint index);
  • vaobj:指定 glDisableVertexArrayAttribglEnableVertexArrayAttrib 函數的頂點數組對象(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,而是很多的時候,就不得不多次調用 glBufferDataglVertexAttribPointerglEnableVertexAttribArray。綁定正確的緩沖對象、為每個物體配置所有頂點屬性很快就變成一件麻煩事。

這就需要一個能夠存儲狀態配置的對象,然后通過綁定這個對象來恢復狀態。這就要靠VAO了。


頂點數組對象 VAO

為什么要使用VAO?

VBO大幅提升了繪制效率,但是頂點的位置坐標、法向量、紋理坐標等不同方面的數據每次使用時需要單獨指定,重復了一些不必要的工作。

而頂點數組對象(VAO)可以像VBO那樣被綁定,當配置頂點屬性指針時,你只需要調用將glVertexAttribPointerglEnableVertexAttribArray一次,之后再繪制物體的時候只需要綁定頂點屬性指針相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。

OpenGL的核心模式要求我們使用VAO。如果綁定VAO失敗,OpenGL會拒絕繪制任何東西。

VAO儲存的內容

  • glEnableVertexAttribArrayglDisableVertexAttribArray 的調用。
  • 通過 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 的流程:

  1. 創建一個VAO,在VAO后創建的VBO都屬于該VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
  1. 綁定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 頂點數據

  1. 首先,要定義(不重復的)頂點,和繪制出矩形所需的索引:
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

  1. 創建元素緩沖對象:
unsigned int EBO;
glGenBuffers(1, &EBO);

glBufferData

  1. 先綁定EBO然后用glBufferData把索引復制到緩沖:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glPolygonMode

  1. 控制多邊形的顯示方式,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

  1. 用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
  • *indicesEBO的偏移量。不再使用索引緩沖對象的時候可以傳遞一個索引數組。

VAO 與 EBO

glDrawElements函數從當前綁定到GL_ELEMENT_ARRAY_BUFFER目標的EBO中獲取其索引。這意味著每次使用索引渲染對象時都必須綁定相應的EBO,這有點麻煩。有沒有感覺到這個問題很眼熟?這不就是沒有VAO只有VBO時碰到的問題么。更巧的是VAO也跟蹤EBO綁定。在綁定VAO時,之前綁定的最后一個EBO自動存儲為VAOEBO

在這里插入圖片描述

上圖有一個小細節: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中的著色器發送數據的方式。它的特性如下:

  1. uniform全局的,意味著uniform變量必須在每個著色器程序對象中都是獨一無二的,而且它可以被著色器程序的任意著色器在任意階段訪問。
  2. 無論把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的類型。可能的后綴有:
在這里插入圖片描述

在上面的例子中,由于需要分別設定uniform4float值,所以通過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;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443670.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443670.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443670.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

javascript特效_如何在網頁添加鼠標點擊特效

經常有同學問我怎么做到的&#xff0c;本論壇屬于DZ當然用的是插件啦。偶然在網上找到一個關于wordpress的特效代碼&#xff0c;分享給大家。WordPress 添加鼠標點擊特效實際上這種教程在網上搜索一下有一大堆&#xff0c;已經是各大博主玩爛的東西了。不過既然給自己的博客加上…

Android |雙鎖單例模式中使用Context如何避免內存泄露的 Warning 提示

文章目錄問題解決方法拓展問題 在Android開發中&#xff0c;經常會將工具類以單例模式的方法實現&#xff0c;而工具類中又總不可避免的用到 Context &#xff0c;例如&#xff1a; public class MySingleton {private static volatile MySingleton instance;private final C…

android nio debug模式正常 release包crash_Flutter包大小治理上的探索與實踐

Flutter作為一種全新的響應式、跨平臺、高性能的移動開發框架&#xff0c;在性能、穩定性和多端體驗一致上都有著較好的表現&#xff0c;自開源以來&#xff0c;已經受到越來越多開發者的喜愛。但是&#xff0c;Flutter的引入往往帶來包體積的增大&#xff0c;給很多研發團隊帶…

Linux學習:第三章-Linux常用命令-1

提示符&#xff1a; [rootlocalhost ~]# [登錄用戶名主機名 當前所在目錄]提示符 ~家目錄 /root /home/aa 提示符&#xff1a; #超級用戶 $普通用戶 一 linux命令的格式1、命令 [選項] [參數]lslist顯示目錄下內容 ①命令名稱&#xff1a;ls 命令英文原意&#xff1a;list 命令…

Leetcode每日一題:使括號有效的最少添加(括號匹配)

文章目錄題目解析貪心趣解題目 只有滿足下面幾點之一&#xff0c;括號字符串才是有效的&#xff1a; 它是一個空字符串&#xff0c;或者它可以被寫成 AB &#xff08;A 與 B 連接&#xff09;, 其中 A 和 B 都是有效字符串&#xff0c;或者它可以被寫作 (A)&#xff0c;其中 …

yolov5搭建環境_Yolov5環境配置和訓練私有數據,YOLOv5,以及,私人

1.使用anaconda安裝python3.8的環境conda create -n yolo5 python3.8#anaconda下載地址: https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/2.安裝pytorch1.6 torchvision0.7conda install pytorch1.6 torchvision0.7 cudatoolkit10.13.安裝相關包pip install opencv-py…

sam格式的結構和意義_各種格式的練字本,對寫字真有幫助嗎

圖片來源于筆勢通各種格式的練字本現在越來越多&#xff0c;目的主要是便于學生把握好筆畫的位置和布局&#xff0c;從而把整個字的結構處理好&#xff0c;常見的有米字格&#xff0c;回宮格等。這些練字本對于初學者來說肯定是有幫助的&#xff0c;特別是低年級學生。當然隨著…

硬件結構圖_那曲地表水電子除垢儀結構圖

那曲地表水電子除垢儀結構圖水處理設備也應斷電停止使用&#xff0c;系統長期停止運行或季節性停止運行&#xff0c;在系統停止運行前&#xff0c;在水中投加適量緩蝕劑&#xff0c;并采取滿水濕保護的措施&#xff0c;以減小腐蝕&#xff0c;保護系統。開啟進水閥檢查無誤后電…

dtm文件生成等高線 lisp_南方cass如何用圖面高程點生成等高線

展開全部1、首先點擊cass菜單欄中的等高線菜單下的建立DTM&#xff0c;彈出對話框。可以有兩種方式建32313133353236313431303231363533e58685e5aeb931333431356665立DTM&#xff0c;由數據文件生成或者由圖面高程點生成&#xff0c;第一種直接在對話框中選擇相應的數據文件&am…

postgresql興建用戶_PostgreSQL 12.2, 11.7, 10.12, 9.6.17, 9.5.21, 和 9.

### **譯者&#xff1a;朱君鵬**### **發表于2020年2月13日 作者&#xff1a;PostgreSQL全球開發小組**#### 全球開發小組已發布的更新涵蓋所有支持的版本&#xff0c;包括12.2&#xff0c;11.7&#xff0c;10.12&#xff0c;9.6.17&#xff0c;9.5.21和9.4.26。該版本修正了一…

Linux學習:第三章-Linux常用命令-2

一文件操作命令 1&#xff09;創建空文件或修改文件時間 touch文件名 命令名稱&#xff1a;touch 命令所在路徑&#xff1a;/bin/touch 執行權限&#xff1a;所有用戶 2&#xff09;刪除 rm-rf文件名 -r刪除目錄 -f強制 命令名稱&#xff1a;rm 命令英文原意&#xff1a;remove…

雙屏怎么快速切換鼠標_在筆記本上實現雙屏設計,怎么做到的?

如果給你的筆記本電腦安裝兩個屏幕&#xff0c;你會用來干什么&#xff1f;是上班時間主屏幕放著PPT&#xff0c;副屏幕偷摸玩游戲&#xff1b;還是主屏幕玩游戲&#xff0c;副屏幕刷刷B站視頻&#xff1b;亦或是主屏幕P著圖&#xff0c;副屏幕在網上找找能用的素材&#xff1f…

劍指offer之隊列的最大值

題目描述&#xff1a; 請定義一個隊列并實現函數 max_value 得到隊列里的最大值&#xff0c;要求函數max_value、push_back 和 pop_front 的均攤時間復雜度都是O(1)。 若隊列為空&#xff0c;pop_front 和 max_value 需要返回 -1 示例 1&#xff1a; 輸入: [“MaxQueue”,“pu…

信元模式mpls 避免環路_【基礎】交換機堆疊模式

堆疊是指將一臺以上的交換機組合起來共同工作&#xff0c;以便在有限的空間內提供盡可能多的端口。多臺交換機經過堆疊形成一個堆疊單元。可堆疊的交換機性能指標中有一個"最大可堆疊數"的參數&#xff0c;它是指一個堆疊單元中所能堆疊的最大交換機數&#xff0c;代…

ckeditor5自定義 vue_vue中的富文本編輯器CKEditor5

image.pngimage.png1、安裝官網已經四種版本&#xff0c;也給出了下載安裝的方法&#xff0c;參考官網安裝https://ckeditor.com/ckeditor-5/download/2、引用在組件中引用import CKEditor from ckeditor/ckeditor5-build-classicimport ckeditor/ckeditor5-build-classic/buil…

劍指offer之禮物的最大值

題目描述&#xff1a; 在一個 m*n 的棋盤的每一格都放有一個禮物&#xff0c;每個禮物都有一定的價值&#xff08;價值大于 0&#xff09;。你可以從棋盤的左上角開始拿格子里的禮物&#xff0c;并每次向右或者向下移動一格、直到到達棋盤的右下角。給定一個棋盤及其上面的禮物…

為什么叫日上_古雷150萬噸乙烯,為啥叫芒果項目?

古雷150萬噸乙烯&#xff0c;為啥叫芒果項目&#xff1f;福建石油化工集團有限責任公司9月1日在福州舉行的一場新聞通氣會上透露&#xff0c;石化基地引進世界化工巨頭——沙特基礎工業公司(簡稱SABIC)&#xff0c;合資合作共建中沙古雷乙烯項目。中沙古雷乙烯項目將在福建古雷…

Linux學習:第四章-vi編輯器

一vi編輯器簡介vim全屏幕純文本編輯器別名alias命令‘命令別名’ aliasvi’vim’ alias lsls --colorttyls正常顯示顏色 alias lsls --colornever 環境變量配置文件/root/.bashrc 二vim使用 1vi模式 vi文件名 命令模式 輸入模式 末行模式 命令----》輸入a&#xff1a;追加i&…

劍指offer之矩陣中的路徑

題目描述&#xff1a; 請設計一個函數&#xff0c;用來判斷在一個矩陣中是否存在一條包含某字符串所有字符的路徑。路徑可以從矩陣中的任意一格開始&#xff0c;每一步可以在矩陣中向左、右、上、下移動一格。如果一條路徑經過了矩陣的某一格&#xff0c;那么該路徑不能再次進入…

gradient設置上下漸變_PaintCode Mac使用教程:如何使用漸變色

Mac平臺上一款強大的iOS矢量繪圖編程軟件PaintCode Mac&#xff0c;無論您是程序員還是設計師&#xff0c;paintcode3能夠讓你像在PS中畫圖一樣繪制各種UI圖形&#xff0c;而且paintcode3會自動幫你生成針對MacOS X或iOS平臺Objective-C或C#代碼&#xff0c;能夠節約大量的編程…