1. 程式人生 > >LearnOpenGL學習筆記(一)——現有代碼理解

LearnOpenGL學習筆記(一)——現有代碼理解

屬性 pan main array 需要 setw sep 最好 uil

首先,給出這次學習的代碼原網址。------>原作者的源代碼 (黑體是源碼,註釋是寫的。) 引用的庫(預編譯):

#include <glad/glad.h>  //控制編譯時函數的具體位置的庫,GLAD是用來管理OpenGL的函數指針。
//因為各個計算機顯卡驅動版本不同,所以需要在編譯的時候現場確定位置。
#include <GLFW/glfw3.h>
//OpenGL的c語言實現庫,提供了一些必備的函數接口。

#include <iostream> //c++的輸入輸出流庫
#include <cmath>//是一個數學函數庫,等同c語言的math.h

自定義的函數聲明及全局變量:

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
//用戶改變窗口的大小的時候,視口也應該被調整,這個函數會在每次幀緩沖中,
//傳入窗口的大小來設置視口(也即是渲染的大小),這就是一個回調函數
void processInput(GLFWwindow *window);
//這個函數名的翻譯是:加工輸入對象。
//這個函數是用來容納輸入控制,在程序中,我們想做出一些諸如按鍵,點擊,來讓
程序作出一些反應。(這裏例子設置為按esc鍵,窗口渲染結束,退出。)
const
unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
//設置窗口寬800,高600

著色器源代碼:

//這是一個頂點著色器(Vertex Shader)的代碼,這個著色器允許我們自己編寫,
它的功能是將數學的坐標數據(輸入),轉換為點並可以進行額外的處理,變成點
給下面的形狀圖元裝配,幾何著色器。
const char *vertexShaderSource =
"#version 330 core\n" 
"layout (location = 0) in vec3 aPos;\n" 
"void main()\n"
"{\n" " gl_Position = vec4(aPos, 1.0);\n" "}\0";
//這段代碼的寫法很奇怪,這段代碼對應的機器碼是運行在GPU,存儲GPU附近的 寄存器裏。語言是一種叫GLSL的著色器語言本身這段代碼的編譯的時候只是一段 字符串常量,在運行的時候再進行編譯進入GPU中運行。 //首先需要有一個版本聲明,這個與你用的opengl版本一致,還有聲明使用“核心模式” 然後是一個關鍵字in,用來聲明所有的輸入頂點屬性。現在我們只關心位置數據, 所以我們只需要一個頂點屬性,也就是一個(x,y,z)。這裏我們用vec3這個向量 類型作為輸入。用gl_Position(vec4)作為輸出.這裏有一個 layout (location = 0)這個設置了輸入變量的位置值,主要是用來確定 aPos這個值的位置在哪裏,類似一個句柄(?暫時還不理解這個) //這是一個片段著色器,用處是將光柵化的一個個空白像素,填上顏色 const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "uniform vec4 ourColor;\n" "void main()\n" "{\n" " FragColor = ourColor;\n" "}\n\0";

關於兩個自定義函數的實現:

void processInput(GLFWwindow *window) //處理輸入函數
{ if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); 
}
//首先傳入我們的窗口對象,明確是針對哪個窗口而言
//glfwGetKey函數檢查esc是否被按下,被按下返回GLFW_RELEASE,執行條件體。
將該窗口的WindowShouldClose屬性設置成ture,來關閉窗口。

void framebuffer_size_callback(GLFWwindow* window, int width, int height) 
{   glViewport(0, 0, width, height); 
}
//窗口大小的回調函數,每次渲染時都會,調用傳入窗口的寬,高,來重新設置視口的大小


int main()函數內部各個部分分析: (1)初始化glfw:

glfwInit(); //初始化glfw庫,這在調用大部分其它GLFW函數前,都需要初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//設置版本,設置核心模式。(Hint直接翻譯成中文是“線索”,這裏大致指“選項”。)

(2)創建窗口對象(窗口對象存放了所有和窗口相關的數據,會不斷被調用):

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); if (window == NULL) //檢測防止失敗的函數。
{ 
std::cout << "Failed to create GLFW window" << std::endl; 
glfwTerminate();//釋放空間,防止內存溢出
return -1;
}  
glfwMakeContextCurrent(window); 
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//glfwCreateWindow函數,傳入寬,高,還有標題,還有兩個參數暫時不用。
//glfwMakeContextCurrent通知GLFW將窗口的上下文設置為當前線程的主上下文,
這樣下一個渲染時刻,這個就被渲染出來了。
//glfwSetFramebufferSizeCallback是註冊回調函數,後面參數是想要回調的函數名,來讓每次渲染該函數都被調用。

(3)初始化GLAD(設定正確的函數指針):

if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{ 
 std::cout << "Failed to initialize GLAD" << std::endl;
 return -1;
}
//glfwGetProcAddress是glfw提供的系統相關的OpenGL函數指針地址的函數,做了一個
(GLADloadproc)類型轉換,用gladLoadGLLoader函數根據每個人的系統定義了正確的函數

(4)渲染循環while:

while(!glfwWindowShouldClose(window)) { 
glfwSwapBuffers(window); 
glfwPollEvents(); 
}
//glfwWindowShouldClose函數在我們每次循環的開始前檢查一次GLFW是否被要求退出
//glfwSwapBuffers函數會交換顏色緩沖,它在這一渲染叠代中被繪制,作為輸出顯示在屏幕上。
//glfwPollEvents函數檢查有沒有觸發什麽事件,並調用對應的回調函數(需要註冊在window對象上)

渲染循環簡單可以分為三步: 1.輸入 2.渲染指令3.檢查並調用事件,交換緩沖 (5)正確釋放之前的分配的所有資源:

glfwTerminate();//該函數釋放所以分配內存空間
return 0;

(6)每次渲染的清屏函數:

glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT);
//glClear函數來清空屏幕的顏色緩沖,它接受一個緩沖位(Buffer Bit)來指定要清空的緩沖,我們這裏只清空顏色,不清空其他的。
//glClearColor來設置清空屏幕所用的顏色,註意glClearColor函數是一個狀態設置函數,本身不做清除,而是設置glClear函數,而glClear函數則是一個狀態使用的函數,它使用了當前的狀態來獲取應該用什麽顏色替換之前的顏色。

(7)運行時動態編譯著色器:

//我們現在已經寫了一個著色器源碼(放在一個字符串,用一個指針指向了它),現在我們創建著色器對象(用一個int變量作為引用來指向著色器所在的內存空間)。再將源碼傳進去,在創建對象之後,在運行時進行動態編譯。
int vertexShader = glCreateShader(GL_VERTEX_SHADER);  
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);  
glCompileShader(vertexShader);
//glCreateShader函數用來創建對象。參數GL_VERTEX_SHADER用來告訴函數創建的時一個頂點著色器。
//glShaderSource函數,用來將源代碼綁定在創建對象上(註意這裏用的是引用),1表示傳入的源代碼字符串數量,第三個參數是頂點著色器真正的源碼所在的全局變量指針,(?這裏為什麽要傳入指針本身的地址,而不是指針指向的字符串的地址呢)
//glCompileShader(),被調用的時候,就會按照先前綁定的情況進行綁定,這裏也是設置函數與使用函數的區別。
----------------檢測glCompileShader編譯是否成功,錯誤返回錯誤原因-------------
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檢查是否編譯成功。如果編譯失敗則調用glGetShaderInfoLog獲取錯誤消息,並且打印
//片段著色器編譯過程類似。

(8)連接著色器制作著色器程序對象:

//之前我們定義了著色器源碼,並且設定了如何去動態編譯,但是著色器工作是六個著色器按照固定順序共同完成的,單獨一個著色器並沒有辦法使用,所以我們需要定義一個著色器對象,用來連接並容納我們自己定義的著色器,當然glfw庫本身也提供剩下的著色器,來完成著色工作
int shaderProgram = glCreateProgram();  //依舊是用一個句柄來引用著色器程序對象
glAttachShader(shaderProgram, vertexShader);  
glAttachShader(shaderProgram, fragmentShader);  //附加
glLinkProgram(shaderProgram);//特定順序鏈接
//glAttachShader函數把之前編譯的著色器附加到程序對象上,之後glLinkProgram函數會把他們鏈接起來,這是按照一個特定的順序鏈接的,這一步程序會自動幫助我們完成。
------------------檢測glLinkProgram鏈接是否成功,並返回錯誤原因-------------------
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); 
if (!success) { 
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); 
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; 
} //這裏使用的是glGetProgramiv,glGetProgramInfoLog這兩個函數它們跟上面兩個類似,就是傳入的不是shader而是program。

glUseProgram(shaderProgram);//將這段函數加入while循環函數的渲染部分,就可以激活這個著色器程序對象了。
glDeleteShader(vertexShader); 
glDeleteShader(fragmentShader);
//把著色器對象鏈接到程序對象以後,可以刪除著色器對象,因為程序對象已經有了,著色器已經沒有使用的機會了,占用的內存空間可以釋放了.

(9)輸入頂點數據:

//我們的著色器程序對象需要一個輸入對象(這裏我們只給它位置坐標),然後經過六個著色器處理輸出成像素數據,在傳給硬件(顯示器),變成我們看到的圖案。
float vertices[] = { 
            0.5f, -0.5f, 0.0f, // bottom right 
           -0.5f, -0.5f, 0.0f, // bottom left 
            0.0f,  0.5f, 0.0f // top  
};
//我們這裏定義一個float的數組,f表示是單精度浮點數,註意本質上這就是九個浮點數連續排列,我們還需要告訴程序,按照三個三個去讀取,每三個中第一個是x,第二個是y,第三個是z。

//這三個點的坐標數據,會被頂點著色器放在GPU的內存(也叫做顯存)中,接下來我們要管理這個內存,因為很多時候我們需要從CPU往GPU發送大量的數據,並且不斷的訪問這些頂點的內存位置,這些內存不一定在物理上是連續的,而且CPU往GPU發送速度太慢,最好一次發送足夠的數據。所以我們需要一個對象,一個頂點緩沖對象來管理,容納這些內存位置,這樣一方面我們可以利用它往GPU發送數據,一方面我們將很多的頂點的內存位置轉為了一個頂點緩沖對象,方便去管理,使用。
//生成頂點緩沖對象,用來管理所以頂點數據所在顯存
unsigned int VBO; 
glGenBuffers(1, &VBO);//這是生成緩沖區對象
glBindBuffer(GL_ARRAY_BUFFER, VBO);//這是設置緩沖區對象的類型,這裏設置為頂點緩沖對象GL_ARRAY_BUFFER
//"從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER目標上的)緩沖調用都會用來配置當前綁定的緩沖(VBO)。"
關於這兩個函數,有一個博文提到緩沖區函數的區別--->glGenBuffers與glBindBuffer理解

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//把之前定義的頂點數據復制到緩沖的內存,第四個參數指定了我們希望顯卡如何管理給定的數據,GL_STATIC_DRAW :數據不會或幾乎不會改變。GL_DYNAMIC_DRAW:數據會被改變很多。GL_STREAM_DRAW :數據每次繪制時都會改變。

(10)鏈接頂點屬性:

//我們必須手動指定輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性,opengl不會自己替我們做這件事。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  glEnableVertexAttribArray(0);
//使用glVertexAttribPointer函數告訴OpenGL該如何解析頂點數據(應用到逐個頂點屬性上)。第一個參數頂點屬性,0是layout(location = 0)中對aPos的位置,這裏傳入0,表示我們想讓數據傳遞到0代表的頂點屬性中。第二個是頂點屬性的大小,vec3的大小是3,第三個是指定數據類型,這裏是浮點數

。第四個參數是是否希望數據被標準化,我們傳入的已經標準了不需要。第五個參數是步長,這個已經指定了3 * sizeof(float),這裏也可以是0,讓程序自己決定是多少,這個只有在數據緊密排列才可以使用。第六個參數是位置數據在緩沖中起始位置的偏移量,這裏是0.
//glEnableVertexAttribArray函數是用來啟用頂點屬性,也就是將頂點數據鏈接到著色器的頂點屬性上,因為這一項默認是禁用的,我們需要開啟。

(11)頂點數組對象:

LearnOpenGL學習筆記(一)——現有代碼理解