本文轉載於拉Box小能手知乎作者
原文連結:https://zhuanlan.zhihu.com/p/72497359
本文為學習LearnOpenGL的學習筆記,如有書寫和理解錯誤還請大佬扶正;
教程連結:
https://link.zhihu.com/?target=https%3A//learnopengl-cn.github.io/01%2520Getting%2520started/04%2520Hello%2520Triangle/
一,基礎概念
1,圖形渲染管線
在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關於把3D坐標轉變為適應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline)管理的。圖形渲染管線可以被劃分為兩個主要部分:
第一部分把你的3D坐標轉換為2D坐標;
第二部分是把2D坐標轉變為實際的有顏色的像素;
2,著色器
圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(都有一個特定的函數),並且很容易並行執行。正是由於它們具有並行執行的特性,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做著色器(Shader)。
3,渲染管線的幾個階段
渲染管線首先,我們以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);頂點數據是一系列頂點的集合。一個頂點(Vertex)是一個3D坐標的數據的集合。而頂點數據是用頂點屬性(Vertex Attribute)表示的;
頂點著色器(Vertex Shader),把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D坐標轉為另一種3D坐標(後面會學到),同時頂點著色器允許我們對頂點屬性進行一些基本處理;
圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入並所有的點裝配成指定圖元的形狀;
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什麼,OpenGL需要你去指定這些數據所表示的渲染類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪製指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據;片段著色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段著色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色;
Alpha測試和混合(Blending)階段。這個階段檢測片段的對應的深度(和模板(Stencil))值,用它們來判斷這個像素是其它物體的前面還是後面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend);
在現代OpenGL中,我們必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有默認的頂點/片段著色器),幾何著色器是可選的,其他使用默認著色器;
二,繪製三角形步驟
1,定義頂點數據數組
2,創建,綁定VAO (Vertex Array Object)頂點數組對象 管理頂點屬性
1,創建VAO;glGenVertexArrays(1, &VAO);
2,綁定VAO;glBindVertexArray(VAO);
3,如需多個VAO 創建VAO數組 VAOs[n],glGenVertexArrays(n,VAOs),並分別進行綁定 glBindVertexArray(VAOs[1]) ;
3,創建,綁定VBO(Vertex Buffer Objects)頂點緩衝對象 管理頂點數據儲存的內存
1,創建VBO; glGenBuffer(1,&VBO);
2,綁定VBO; glBindBuffer(GL_ARRAY_BUFFER,VBO);
(OpenGL允許我們同時綁定多個緩衝,只要它們是不同的緩衝類型。我們可以使用glBindBuffer函數把新創建的緩衝綁定到GL_ARRAY_BUFFER目標上)3,把頂點數據拷貝到VBO緩衝內存中; glBufferData(...);
4,如需多個VBO 創建VBO數組 VBOs[n];glGenVertexArrays(n,VBOs),並分別進行綁定 glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]) ;
只有VBO時VAO存儲3(1)創建,綁定EBO(ElementBuffer Objects/Index Buffer Object)索引緩衝對象 管理頂點數據儲存的內存
1,創建EBO;glGenBuffers(1, &EBO);
2,綁定EBO;glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
3,如需多個EBO 創建EBO數組 EBOs[n];glGenBuffers(n,EBOs),並分別進行綁定 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[1]) ;
VBO與EBO同時存儲VAO中4,連結頂點屬性 設置頂點屬性指針
5,定義頂點著色器
1,聲明所用的OpenGL版本號以及模塊;#Version 330 core;
2,聲明用來接收頂點數據的變量以及位置;in vec3 aPos/Location;
3,簡單的賦值;預定義變量gl_Position;
6,編譯頂點著色器
1,創建頂點著色器對象;glCreateShader(CL_VERTEX_SHADER);
2,把定義的著色器源碼附加到頂點著色器對象上;glShaderSource(...);
3,編譯著色器;glCompileShader(VertexShader);
4,檢查是否編譯成功,並列印信息;glGetShaderriv();
7,定義片源著色器
8,編譯片源著色器
1,創建片源著色器對象;glCreateShader(GL_FRAGMENT_SHADER);
2,把定義片源著色器源碼附加到片源著色器對象上;glShaderSource(...);
3,編譯著色器;glCompileShader(fragmentShader);
4,檢查是否編譯成功,並列印信息;glGetShaderriv();
9,連結著色器程序
1,創建連結著色程序;glCreateProgram();
2,把之前的頂點與片源綁定到連結程序上;glAttachShader(..);
3,連結編譯;glLinkProgram(shaderProgram);
4,檢查連結編譯是否成功,並列印信息;glGetProgramInfoLog(...);
5,激活程序對象(一般放在渲染循環裡);glUseProgram(shaderProgram);
6,渲染繪製;glDrawArrays(...);;
7,刪除創建的著色器;glDeleteShader(vertexShader/fragmentShader);
效果預覽
效果預覽完整的代碼及注釋:
後續學習會把著色器代碼進行封裝,優化代碼;
此時的頂點輸入均在標準化坐標系之內(範圍xyz均在[-1,1]),因為OpenGl頂點數據僅在此範圍坐標才會顯示在屏幕上;(正常坐標要經過變換到達標準化坐標系)
//GLAD的頭文件包含了正確的OpenGL頭文件(例如GL/gl.h),所以需要在其它依賴於OpenGL的頭文件之前包含GLAD。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
/*******************************************定義常量************************************************/
//設置窗口的寬和高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
/*******************************************函數************************************************/
//響應鍵盤輸入事件
//ESC推出窗口
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
//當用戶改變窗口的大小的時候,視口也應該被調整。
//對窗口註冊一個回調函數(Callback Function),它會在每次窗口大小被調整的時候被調用
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
//glViewport函數前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)
glViewport(0, 0, width, height);
}
/*******************************************頂點與片源著色器************************************************/
//定義頂點與片源著色器的函數字符串
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";
//第一個片源著色器代碼
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(0.0f, 0.0f, 1.0f, 1.0f);\n"
"}\n\0";
//第二個片源著色器代碼
//賦予不同的顏色
const char *fragmentShaderSource01 = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n"
"}\n\0";
/*******************************************主函數************************************************/
//主函數
int main()
{
//初始化GLFW
glfwInit();
//聲明版本與核心
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本號
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本號
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//創建窗口並設置其大小,名稱,與檢測是否創建成功
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
{
cout << "Failed to create GLFW window" << endl;
glfwTerminate();
return -1;
}
//創建完畢之後,需要讓當前窗口的環境在當前線程上成為當前環境,就是接下來的畫圖都會畫在我們剛剛創建的窗口上
glfwMakeContextCurrent(window);
//告訴GLFW我們希望每當窗口調整大小的時候調用這個函數
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//glad尋找opengl的函數地址,調用opengl的函數前需要初始化glad
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//構建和編譯Shader
//頂點Shader
//創建頂點著色器對象
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
//著色器源碼附加到對象上,然後編譯
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 檢查是否編譯成功
int success; //int 類型標識是否成功
char infoLog[512];//儲存錯誤消息的容器
//glGetShaderiv()檢查是否編譯成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
//glGetShaderInfoLog()獲取錯誤消息,然後列印它。
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//片源Shader
//創建第一個片源著色器對象
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;
}
//創建第二個片源著色器對象
int fragmentShader01 = glCreateShader(GL_FRAGMENT_SHADER);
////著色器源碼附加到對象上,然後編譯
glShaderSource(fragmentShader01, 1, &fragmentShaderSource01, NULL);
glCompileShader(fragmentShader01);
// 檢查是否編譯成功
glGetShaderiv(fragmentShader01, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader01, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//連結Shader,連結頂點與片源
//創建第一個連結對象
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);
//創建連結第二個著色器
int shaderProgram01 = glCreateProgram();
//著色器附加到了程序上,然後用glLinkProgram連結
glAttachShader(shaderProgram01, vertexShader);
glAttachShader(shaderProgram01, fragmentShader01);
glLinkProgram(shaderProgram01);
//檢查是否連結出錯
glGetProgramiv(shaderProgram01, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram01, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
//把著色器對象連結到程序對象以後,刪除著色器對象,不再需要它們
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader01);
//設置頂點數據和頂點屬性和索引
float vertices01[] = {
// 第一個四邊形
0.1f, 0.9f, 0.0f,
0.9f, 0.9f, 0.0f,
0.9f, 0.1f, 0.0f,
0.1f, 0.1f, 0.0f,
};
//索引
unsigned int indices[] = {
0, 1, 3,
1, 2, 3
};
float vertices02[] = {
// 第二個三角形
-0.5f, 0.9f, 0.0f,
-0.1f, 0.1f, 0.0f,
-0.9f, 0.1f, 0.0f,
};
//創建 VBO 頂點緩衝對象 VAO頂點數組對象 EBO索引緩衝對象
//多個對象
unsigned int VBOs[2], VAOs[2], EBO;
glGenVertexArrays(2, VAOs);
glGenBuffers(2, VBOs);
glGenBuffers(1, &EBO);
//第一個
//綁定VAO,VBO與EBO對象
glBindVertexArray(VAOs[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//複製頂點數據到緩衝內存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices01), vertices01, GL_STATIC_DRAW);
// 賦值頂點索引到緩衝內存中
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//連結頂點屬性,設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//以頂點屬性位置值作為參數,啟用頂點屬性;頂點屬性默認是禁用的。
glEnableVertexAttribArray(0);
//第二個
//綁定第二個VAO,VBO與EBO對象
glBindVertexArray(VAOs[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]);
//複製頂點數據到緩衝內存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices02), vertices02, GL_STATIC_DRAW);
//連結頂點屬性,設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//以頂點屬性位置值作為參數,啟用頂點屬性;頂點屬性默認是禁用的。
glEnableVertexAttribArray(0);
/*******************************************渲染循環************************************************/
//glfwWindowShouldClose()檢查窗口是否需要關閉。如果是,遊戲循環就結束了,接下來我們將會清理資源,結束程序
while (!glfwWindowShouldClose(window))
{
//響應鍵盤輸入
processInput(window);
//設置清除顏色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//清除當前窗口,把顏色設置為清除顏色
glClear(GL_COLOR_BUFFER_BIT);
//激活連結程序,激活著色器,開始渲染
glUseProgram(shaderProgram);
//綁定VAO
glBindVertexArray(VAOs[0]);
//繪製四邊形
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//繪製第二個三角形
glUseProgram(shaderProgram01);
glBindVertexArray(VAOs[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);
//線框繪製
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//交換顏色緩衝
glfwSwapBuffers(window);
//處理事件
glfwPollEvents();
}
//解除綁定
glDeleteVertexArrays(2, VAOs);
glDeleteBuffers(2, VBOs);
glDeleteBuffers(1, &EBO);
//釋放前面所申請的資源
glfwTerminate();
return 0;
}
一些函數注釋:
函數glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//glBufferData函數,它會把之前定義的頂點數據複製到緩衝的內存中
//glBufferData是一個專門用來把用戶定義的數據複製到當前綁定緩衝的函數。
//第一個參數是目標緩衝的類型:頂點緩衝對象當前綁定到GL_ARRAY_BUFFER目標上。
//第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。
//第三個參數是我們希望發送的實際數據。
//第四個參數指定了我們希望顯卡如何管理給定的數據。它有三種形式:
//GL_STATIC_DRAW :數據不會或幾乎不會改變。
//GL_DYNAMIC_DRAW:數據會被改變很多。
//GL_STREAM_DRAW :數據每次繪製時都會改變
//三角形的位置數據不會改變,每次渲染調用時都保持原樣,所以它的使用類型最好是GL_STATIC_DRAW。如果,比如說一個緩衝中的數據將頻繁被改變,那麼使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯卡把數據放在能夠高速寫入的內存部分
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
函數glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//連結頂點屬性手,動指定輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性。
//解析頂點數據(應用到逐個頂點屬性上)設置頂點屬性指針
//glVertexAttribPointer函數的參數非常多,逐一介紹它們
//第一個參數指定我們要配置的頂點屬性。還記得我們在頂點著色器中使用layout(location = 0)定義了position頂點屬性的位置值(Location)嗎?它可以把頂點屬性的位置值設置為0。因為我們希望把數據傳遞到這一個頂點屬性中,所以這裡我們傳入0。
//第二個參數指定頂點屬性的大小。頂點屬性是一個vec3,它由3個值組成,所以大小是3。
//第三個參數指定數據的類型,這裡是GL_FLOAT(GLSL中vec*都是由浮點數值組成的)。
//下個參數定義我們是否希望數據被標準化(Normalize)。如果我們設置為GL_TRUE,所有數據都會被映射到0(對於有符號型signed數據是 - 1)到1之間。我們把它設置為GL_FALSE。
//第五個參數叫做步長(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由於下個組位置數據在3個float之後,我們把步長設置為3 * sizeof(float)。
//最後一個參數的類型是void*,所以需要我們進行這個奇怪的強制類型轉換。它表示位置數據在緩衝中起始位置的偏移量(Offset)。由於位置數據在數組的開頭,所以這裡是0。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
函數glShaderSource(vertexShader, 1, &vertexShaderSource, NULL)
//第一個參數為要編譯的著色器對象。
//第二參數指定了傳遞的源碼字符串數量,這裡只有一個。
//第三個參數是頂點著色器真正的源碼,
//第四個參數我們先設置為NULL。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
函數glDrawArrays(GL_TRIANGLES, 0, 3)
//函數第一個參數是我們打算繪製的OpenGL圖元的類型。
//第二個參數指定了頂點數組的起始索引,我們這裡填0
//最後一個參數指定我們打算繪製多少個頂點,這裡是3
glDrawArrays(GL_TRIANGLES, 0, 3);
函數glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
//第一個參數指定了我們繪製的模式
//第二個參數是我們打算繪製頂點的個數
//第三個參數是索引的類型,這裡是GL_UNSIGNED_INT
//最後一個參數裡我們可以指定EBO中的偏移量
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
線框模式(Wireframe Mode)
要想用線框模式繪製你的三角形,你可以通過
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函數配置OpenGL如何繪製圖元。
第一個參數表示我們打算將其應用到所有的三角形的正面和背面,
第二個參數告訴我們用線來繪製。之後的繪製調用會一直以線框模式繪製三角形,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)將其設置回默認模式。