使用GLFW與GLAD創(chuàng)建窗口并畫出三角形

本文章內(nèi)容代碼可在這里找到,如果此代碼對(duì)您有幫助,煩請(qǐng)動(dòng)動(dòng)您的手指,點(diǎn)個(gè)Star,謝謝!歡迎訪問我的個(gè)人主頁Orient。

創(chuàng)建窗口

1、首先我們引入必要的頭文件:

#include "glad.h"
#include <GLFW/glfw3.h>

請(qǐng)確保GLAD頭文件的引入在GLFW之前,GLAD的頭文件包含了正確的OpenGL頭文件(例如GL/gl.h),所以需要在其他依賴于OpenGL的頭文件之前引入GLAD

2、實(shí)例化GLFW窗口

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // Mac必須添加此行,Windows忽略
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    return 0;
}

前兩行代碼指定了OpenGL的主版本和次版本號(hào)(4.1),第三行代表著使用核心模式(Core-profile),意味著我們只能使用OpenGL功能的一個(gè)子集(沒有我們不再需要的向后兼容特性)。

3、接下來創(chuàng)建一個(gè)窗口對(duì)象,它存放了所有和窗口相關(guān)的數(shù)據(jù),而且會(huì)被GLFW的其他函數(shù)頻繁調(diào)用

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if(window == NULL)
{
    std::cout << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
}
glfwMakeContexCurrent(window);

glfwCreateWindow函數(shù),前兩個(gè)參數(shù)是窗口的寬高,第三個(gè)參數(shù)是這個(gè)窗口的命名,后兩個(gè)暫時(shí)忽略,返回了一個(gè)GLFWwindow對(duì)象。glfwMakeContexCurrent函數(shù)告訴GLFW將窗口的上下文設(shè)置為當(dāng)前線程的主上下文。

4、GLAD是用來管理OpenGL的函數(shù)指針的,所以調(diào)用任何OpenGL函數(shù)之前需要初始化GLAD

if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
    std::cout << "Failed to initialize GLAD" << std::endl;
    return -1;
}

我們給GLAD傳入了用來加載系統(tǒng)相關(guān)的OpenGL函數(shù)指針地址的函數(shù)。GLFW給我們的是glfwGetProcAdress,它根據(jù)我們編譯的系統(tǒng)定義了正確的函數(shù)。

5、視口

在開始渲染之前必須告訴OpenGL渲染窗口(Viewport)的尺寸大小,這樣OpenGL才能知道怎樣根據(jù)窗口大小顯示數(shù)據(jù)和坐標(biāo)。

// 此函數(shù)設(shè)置窗口的維度(Dimension)
glViewport(0, 0, 800, 600);

前兩個(gè)參數(shù)控制窗口左下角位置,后兩個(gè)控制渲染窗口的寬高(像素)。也可將視口維度設(shè)置比GLFW窗口維度小,這樣子之后所有的OpenGL渲染將會(huì)在一個(gè)更小的窗口中顯示,這樣子的話我們也可以將一些其它元素顯示在OpenGL視口之外。

6、對(duì)窗口注冊(cè)回調(diào)函數(shù)(CallbackFunction)

函數(shù)注冊(cè)后會(huì)在每次窗口大小改變的時(shí)候調(diào)用,視口也會(huì)隨之調(diào)整

函數(shù)原型如下:

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

進(jìn)行注冊(cè),告訴GLFW每當(dāng)窗口調(diào)整時(shí)調(diào)用此函數(shù):

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

7、為了使圖像能夠持續(xù)顯示而不是一閃即逝,我們需要寫一個(gè)渲染循環(huán),使得GLFW在退出之前一直保持運(yùn)行

while(!glfwWindowShouldClose(window))
{
    glfwSwapBuffers(window);
    glfwPollEvents();    
}

glfwWindowShouldClose函數(shù)在每次循環(huán)開始前檢查一次GLFW是否被要求退出,是的話返回true,循環(huán)結(jié)束

glfwPollEvents函數(shù)檢查是否有觸發(fā)事件,比如鍵盤、鼠標(biāo)等信號(hào)輸入,然后更新窗口狀態(tài),調(diào)用相應(yīng)的回調(diào)函數(shù)(可通過回調(diào)方法手動(dòng)設(shè)置)。

glfwSwapBuffers函數(shù)會(huì)交換顏色緩沖

8、渲染結(jié)束后釋放所有資源

glfwTerminate();
return 0;

至此,窗口創(chuàng)建完成

接下來我們進(jìn)行一些完善工作

9、接下來我們添加一個(gè)觸發(fā)時(shí)間,當(dāng)用戶按下Esc鍵時(shí)關(guān)閉窗口。

void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

glfwGetKey函數(shù)需要一個(gè)窗口以及一個(gè)按鍵作為輸入。這個(gè)函數(shù)將會(huì)返回這個(gè)案件是否正在被按下,我們將其定義在processInput函數(shù)當(dāng)中

接下來在渲染循環(huán)的每一個(gè)迭代中調(diào)用processInput

while (!glfwWindowShouldClose(window))
{
    processInput(window);

    // 這里是渲染指令
    ...

    glfwSwapBuffers(window);
    glfwPollEvents();
}

10、我們使用一個(gè)自定義的顏色清空屏幕,使得在每個(gè)新的渲染迭代開始后清除上一次渲染結(jié)果,并顯示我們自定義的顏色

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

渲染一個(gè)三角形

開始繪制之前,我們需要給OpenGL輸入一些頂點(diǎn)數(shù)據(jù)(范圍在[-1, 1],需要自行進(jìn)行坐標(biāo)變換)。我們需要渲染一個(gè)三角形,因此我們需要三個(gè)頂點(diǎn)位置,將它定義為一個(gè)float數(shù)組:

// 由于我們繪制的是一個(gè)2D三角形,因此,將其頂點(diǎn)的z坐標(biāo)都設(shè)置為0
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

接下來使用glGenBuffers函數(shù)和一個(gè)緩沖ID生成一個(gè)VBO對(duì)象,并使用glBindBuffer函數(shù)把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER目標(biāo)上:

unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);  

從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER目標(biāo)上的)緩沖調(diào)用都會(huì)用來配置當(dāng)前綁定的緩沖(VBO)。然后我們可以調(diào)用glBufferData函數(shù),它會(huì)把之前定義的頂點(diǎn)數(shù)據(jù)復(fù)制到緩沖的內(nèi)存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

第一個(gè)參數(shù)是目標(biāo)緩沖的類型,頂點(diǎn)緩沖對(duì)象當(dāng)前綁定到GL_ARRAY_BUFFER目標(biāo)上。

第二個(gè)參數(shù)指定傳輸數(shù)據(jù)大小。

第三個(gè)參數(shù)是我們實(shí)際發(fā)送的數(shù)據(jù)。

第四個(gè)參數(shù)指定了顯卡管理數(shù)據(jù)的方式,有一下三種形式:
GL_STATIC_DRAW:數(shù)據(jù)不會(huì)或幾乎不改變。
GL_DYNAMIC_DRAW:數(shù)據(jù)會(huì)改變很多。
GL_STREAM_DRAW:數(shù)據(jù)每次繪制都會(huì)改變。

頂點(diǎn)著色器

首先用GLSL(OoenGL Shading Language)編寫頂點(diǎn)著色器,然后編譯這個(gè)著色器。下面給出一個(gè)非?;A(chǔ)的頂點(diǎn)著色器源代碼:

#version 410 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

首先申明OpenGL版本4.1(對(duì)應(yīng)410)。
接下來使用in關(guān)鍵字,在頂點(diǎn)著色器中聲明所有的輸入頂點(diǎn)屬性(Input Vertex Attribute)?,F(xiàn)在我們只關(guān)心位置(Position)數(shù)據(jù),所以我們只需要一個(gè)頂點(diǎn)屬性。GLSL有一個(gè)向量數(shù)據(jù)類型,它包含1到4個(gè)float分量,包含的數(shù)量可以從它的后綴數(shù)字看出來。由于每個(gè)頂點(diǎn)都有一個(gè)3D坐標(biāo),我們就創(chuàng)建一個(gè)vec3輸入變量aPos。我們同樣也通過layout (location = 0)設(shè)定了輸入變量的位置值(Location)你后面會(huì)看到為什么我們會(huì)需要這個(gè)位置值。

編譯著色器

先創(chuàng)建一個(gè)著色器對(duì)象,注意還是用ID來引用。所以我們儲(chǔ)存這個(gè)頂點(diǎn)著色器為unsigned int,然后用glCreateShader創(chuàng)建這個(gè)著色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我們把需要?jiǎng)?chuàng)建的著色器類型以參數(shù)形式提供給glCreateShader。由于我們正在創(chuàng)建一個(gè)頂點(diǎn)著色器,傳遞的參數(shù)是GL_VERTEX_SHADER。

接下來把著色器源碼附加到著色器對(duì)象上,并編譯它:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource函數(shù)把要編譯的著色器對(duì)象作為第一個(gè)參數(shù)。第二參數(shù)指定了傳遞的源碼字符串?dāng)?shù)量,這里只有一個(gè)。第三個(gè)參數(shù)是頂點(diǎn)著色器真正的源碼,第四個(gè)參數(shù)我們先設(shè)置為NULL。

片段著色器

先給出片段著色器源碼:

#version 410 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

片段著色器只需要一個(gè)輸出變量,這個(gè)變量是一個(gè)4分量向量,它表示的是最終的輸出顏色,我們應(yīng)該自己將其計(jì)算出來。我們可以用out關(guān)鍵字聲明輸出變量,這里我們命名為FragColor。下面,我們將一個(gè)alpha值為1.0(1.0代表完全不透明)的橘黃色的vec4賦值給顏色輸出。

編譯片段著色器的過程與頂點(diǎn)著色器類似,只不過我們使用GL_FRAGMENT_SHADER常量作為著色器類型:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

著色器程序

著色器程序?qū)ο?Shader Program Object)是多個(gè)著色器合并之后并最終鏈接完成的版本。如果要使用剛才編譯的著色器我們必須把它們鏈接(Link)為一個(gè)著色器程序?qū)ο?,然后在渲染?duì)象的時(shí)候激活這個(gè)著色器程序。已激活著色器程序的著色器將在我們發(fā)送渲染調(diào)用的時(shí)候被使用。

創(chuàng)建程序?qū)ο螅?/p>

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram函數(shù)創(chuàng)建一個(gè)程序,并返回新創(chuàng)建程序?qū)ο蟮腎D引用。現(xiàn)在我們需要把之前編譯的著色器附加到程序?qū)ο笊?,然后?code>glLinkProgram鏈接它們:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

最后調(diào)用glUseProgram函數(shù),激活程序:

glUseProgram(shaderProgram);

著色器對(duì)象鏈接到程序?qū)ο笠院?,需要?jiǎng)h除著色器對(duì)象:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

鏈接頂點(diǎn)屬性

頂點(diǎn)著色器允許我們指定任何以頂點(diǎn)屬性為形式的輸入。這使其具有很強(qiáng)的靈活性的同時(shí),它還的確意味著我們必須手動(dòng)指定輸入數(shù)據(jù)的哪一個(gè)部分對(duì)應(yīng)頂點(diǎn)著色器的哪一個(gè)頂點(diǎn)屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點(diǎn)數(shù)據(jù)。

我們的頂點(diǎn)緩沖數(shù)據(jù)會(huì)被解析為下面這樣子:


image

因此使用glVertexAttribPointer函數(shù)告訴OpenGL該如何解析頂點(diǎn)數(shù)據(jù):

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

第一個(gè)參數(shù)指定我們要配置的頂點(diǎn)屬性

第二個(gè)參數(shù)指定頂點(diǎn)屬性的大小。

第三個(gè)參數(shù)指定數(shù)據(jù)的類型

第四個(gè)參數(shù)定義我們是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化(Normalize)。如果我們?cè)O(shè)置為GL_TRUE,所有數(shù)據(jù)都會(huì)被映射到0(對(duì)于有符號(hào)型signed數(shù)據(jù)是-1)到1之間。

第五個(gè)參數(shù)是步長(zhǎng),它告訴我們?cè)谶B續(xù)的頂點(diǎn)屬性組之間的間隔。

最后一個(gè)參數(shù)表示位置數(shù)據(jù)在緩沖中起始位置的偏移量(Offset)。

接下來使用glEnableVertexAttribArray函數(shù),以頂點(diǎn)屬性位置值作為參數(shù),啟用頂點(diǎn)屬性。

代碼最終大概長(zhǎng)這樣:

// 0. 復(fù)制頂點(diǎn)數(shù)組到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 當(dāng)我們渲染一個(gè)物體時(shí)要使用著色器程序
glUseProgram(shaderProgram);
// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();

頂點(diǎn)數(shù)組對(duì)象

Vertex Array Object(VAO)可以像頂點(diǎn)緩沖對(duì)象那樣被綁定,任何隨后的頂點(diǎn)屬性調(diào)用都會(huì)儲(chǔ)存在這個(gè)VAO中。

OpenGL的核心模式要求我們使用VAO,所以它知道該如何處理我們的頂點(diǎn)輸入。如果我們綁定VAO失敗,OpenGL會(huì)拒絕繪制任何東西。

一個(gè)頂點(diǎn)數(shù)組對(duì)象會(huì)儲(chǔ)存以下這些內(nèi)容:

glEnableVertexAttribArrayglDisableVertexAttribArray的調(diào)用。
通過glVertexAttribPointer設(shè)置的頂點(diǎn)屬性配置。
通過glVertexAttribPointer調(diào)用與頂點(diǎn)屬性關(guān)聯(lián)的頂點(diǎn)緩沖對(duì)象。

VAO的創(chuàng)建類似VBO:

unsigned int VAO;
glGenVertexArrays(1, &VAO);

要使用VAO,只需使用glBindVertexArray綁定VAO。從綁定之后起,我們應(yīng)該綁定和配置對(duì)應(yīng)的VBO和屬性指針,之后解綁VAO供之后使用。當(dāng)我們打算繪制一個(gè)物體的時(shí)候,我們只要在繪制物體前簡(jiǎn)單地把VAO綁定到希望使用的設(shè)定上就行了。

代碼大概是這樣的:

// ..:: 初始化代碼(只運(yùn)行一次 (除非你的物體頻繁改變)) :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把頂點(diǎn)數(shù)組復(fù)制到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 繪制代(渲染循環(huán)中) :: ..
// 4. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

繪制三角形

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays函數(shù)第一個(gè)參數(shù)是打算繪制的圖元的類型。第二個(gè)參數(shù)制訂了頂點(diǎn)數(shù)組的起始索引,第三個(gè)參數(shù)指定我們打算繪制的頂點(diǎn)個(gè)數(shù)。

最終三角形是長(zhǎng)這樣的:

triangle.png

你可以在這里找到源碼:三角形、矩形。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容