首先,給出這次學習的代碼原網址。------>原作者的源代碼 (黑體是源碼,注釋是寫的。) 引用的庫(預編譯):
#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)頂點數組對象: