Learn OpenGL (一):開啟視窗
原文 | Hello Window |
---|---|
作者 | JoeyDeVries |
翻譯 | Geequlim, Krasjet |
環境VS2015+OpenGL3.3
#include <glad/glad.h>
#include <GLFW/glfw3.h>
請確認是在包含GLFW的標頭檔案之前包含了GLAD的標頭檔案。GLAD的標頭檔案包含了正確的OpenGL標頭檔案(例如GL/gl.h
),所以需要在其它依賴於OpenGL的標頭檔案之前包含GLAD。
接下來我們建立main函式,在這個函式中我們將會例項化GLFW視窗:
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
glfwWindowHint是設定版本號的,其實只需要照搬即可。
接下來我們建立一個視窗物件,這個視窗物件存放了所有和視窗相關的資料,而且會被GLFW的其他函式頻繁地用到。
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);
glfwCreateWindow函式需要視窗的寬和高作為它的前兩個引數。第三個引數表示這個視窗的名稱(標題),這裡我們使用"LearnOpenGL"
GLAD
在之前的教程中已經提到過,GLAD是用來管理OpenGL的函式指標的,所以在呼叫任何OpenGL的函式之前我們需要初始化GLAD。(不用的話渲染會失敗,所以還是照搬就好了)
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
我們給GLAD傳入了用來載入系統相關的OpenGL函式指標地址的函式。GLFW給我們的是glfwGetProcAddress
,它根據我們編譯的系統定義了正確的函式。
視口
在我們開始渲染之前還有一件重要的事情要做,我們必須告訴OpenGL渲染視窗的尺寸大小,即視口(Viewport),這樣OpenGL才只能知道怎樣根據視窗大小顯示資料和座標。我們可以通過呼叫glViewport函式來設定視窗的維度(Dimension):
glViewport(0, 0, 800, 600);
glViewport函式前兩個引數控制視窗左下角的位置。第三個和第四個引數控制渲染視窗的寬度和高度(畫素)。
我們實際上也可以將視口的維度設定為比GLFW的維度小,這樣子之後所有的OpenGL渲染將會在一個更小的視窗中顯示,這樣子的話我們也可以將一些其它元素顯示在OpenGL視口之外。
OpenGL幕後使用glViewport中定義的位置和寬高進行2D座標的轉換,將OpenGL中的位置座標轉換為你的螢幕座標。例如,OpenGL中的座標(-0.5, 0.5)有可能(最終)被對映為螢幕中的座標(200,450)。注意,處理過的OpenGL座標範圍只為-1到1,因此我們事實上將(-1到1)範圍內的座標對映到(0, 800)和(0, 600)。
然而,當用戶改變視窗的大小的時候,視口也應該被調整。我們可以對視窗註冊一個回撥函式(Callback Function),它會在每次視窗大小被調整的時候被呼叫。這個回撥函式的原型如下:
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
這個幀緩衝大小函式需要一個GLFWwindow作為它的第一個引數,以及兩個整數表示視窗的新維度。每當視窗改變大小,GLFW會呼叫這個函式並填充相應的引數供你處理。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
我們還需要註冊這個函式,告訴GLFW我們希望每當視窗調整大小的時候呼叫這個函式:(framebuffer_size_callback會識別width和height???這個希望懂的人可以告訴我why)
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
當視窗被第一次顯示的時候framebuffer_size_callback也會被呼叫。對於視網膜(Retina)顯示屏,width和height都會明顯比原輸入值更高一點。
我們還可以將我們的函式註冊到其它很多的回撥函式中。比如說,我們可以建立一個回撥函式來處理手柄輸入變化,處理錯誤訊息等。我們會在建立視窗之後,渲染迴圈初始化之前註冊這些回撥函式。
我們可不希望只繪製一個影象之後我們的應用程式就立即退出並關閉視窗。我們希望程式在我們主動關閉它之前不斷繪製圖像並能夠接受使用者輸入。因此,我們需要在程式中新增一個while迴圈,我們可以把它稱之為渲染迴圈(Render Loop),它能在我們讓GLFW退出前一直保持執行。下面幾行的程式碼就實現了一個簡單的渲染迴圈:
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
- glfwWindowShouldClose函式在我們每次迴圈的開始前檢查一次GLFW是否被要求退出,如果是的話該函式返回
true
然後渲染迴圈便結束了,之後為我們就可以關閉應用程式了。 - glfwPollEvents函式檢查有沒有觸發什麼事件(比如鍵盤輸入、滑鼠移動等)、更新視窗狀態,並呼叫對應的回撥函式(可以通過回撥方法手動設定)。
- glfwSwapBuffers函式會交換顏色緩衝(它是一個儲存著GLFW視窗每一個畫素顏色值的大緩衝),它在這一迭代中被用來繪製,並且將會作為輸出顯示在螢幕上。
雙緩衝(Double Buffer)
前緩衝儲存著最終輸出的影象,它會在螢幕上顯示;而所有的的渲染指令都會在後緩衝上繪製。
當渲染迴圈結束後我們需要正確釋放/刪除之前的分配的所有資源。我們可以在main函式的最後呼叫glfwTerminate函式來完成。
glfwTerminate();
return 0;
這樣便能清理所有的資源並正確地退出應用程式。現在你可以嘗試編譯並執行你的應用程式了,如果沒做錯的話,你將會看到如下的輸出:
輸入
我們同樣也希望能夠在GLFW中實現一些輸入控制,這可以通過使用GLFW的幾個輸入函式來完成。我們將會使用GLFW的glfwGetKey函式,它需要一個視窗以及一個按鍵作為輸入。這個函式將會返回這個按鍵是否正在被按下。我們將建立一個processInput函式來讓所有的輸入程式碼保持整潔。
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
這裡我們檢查使用者是否按下了返回鍵(Esc)(如果沒有按下,glfwGetKey將會返回GLFW_RELEASE。如果使用者的確按下了返回鍵,我們將通過glfwSetwindowShouldClose使用把WindowShouldClose
屬性設定為 true
的方法關閉GLFW。下一次while迴圈的條件檢測將會失敗,程式將會關閉。
我們接下來在渲染迴圈的每一個迭代中呼叫processInput:
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
這就給我們一個非常簡單的方式來檢測特定的鍵是否被按下,並在每一幀做出處理。
渲染
我們要把所有的渲染(Rendering)操作放到渲染迴圈中,因為我們想讓這些渲染指令在每次渲染迴圈迭代的時候都能被執行。程式碼將會是這樣的:
// 渲染迴圈
while(!glfwWindowShouldClose(window))
{
// 輸入
processInput(window);
// 渲染指令
...
// 檢查並呼叫事件,交換緩衝
glfwPollEvents();
glfwSwapBuffers(window);
}
為了測試一切都正常工作,我們使用一個自定義的顏色清空螢幕。在每個新的渲染迭代開始的時候我們總是希望清屏,否則我們仍能看見上一次迭代的渲染結果(這可能是你想要的效果,但通常這不是)。我們可以通過呼叫glClear函式來清空螢幕的顏色緩衝,它接受一個緩衝位(Buffer Bit)來指定要清空的緩衝,可能的緩衝位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。由於現在我們只關心顏色值,所以我們只清空顏色緩衝。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
注意,除了glClear之外,我們還呼叫了glClearColor來設定清空螢幕所用的顏色。當呼叫glClear函式,清除顏色緩衝之後,整個顏色緩衝都會被填充為glClearColor裡所設定的顏色。在這裡,我們將螢幕設定為了類似黑板的深藍綠色。
你應該能夠回憶起來我們在 OpenGL 這節教程的內容,glClearColor函式是一個狀態設定函式,而glClear函式則是一個狀態使用的函式,它使用了當前的狀態來獲取應該清除為的顏色。
是時候放程式碼了:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
//GLFW的標頭檔案之前xu包含了GLAD的標頭檔案
#include <iostream>
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
int main()
{
// glfw: initialize and configure
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw window creation
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//framebuffer_size_callback會識別width和height
// glad: load all OpenGL function pointers
//GLAD是用來管理OpenGL的函式指標的,
//所以在呼叫任何OpenGL的函式之前我們需要初始化GLAD。
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// render loop
while (!glfwWindowShouldClose(window))
{
// input
processInput(window);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
glfwSwapBuffers(window);
glfwPollEvents();
}
// glfw: terminate, clearing all previously allocated GLFW resources.
glfwTerminate();
return 0;
}
/*
// 渲染迴圈
while(!glfwWindowShouldClose(window))
{
// 輸入
processInput(window);
// 渲染指令
...
// 檢查並呼叫事件,交換緩衝
glfwPollEvents();
glfwSwapBuffers(window);
}
*/