使用GLFW与GLAD创建窗口并画出三角形

本文章内容代码可在这里找到,如果此代码对您有帮助,烦请动动您的手指,点个Star,谢谢!欢迎访问我的个人主页Orient

创建窗口

1、首先我们引入必要的头文件:

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

请确保GLAD头文件的引入在GLFW之前,GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其他依赖于OpenGL的头文件之前引入GLAD

2、实例化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的主版本和次版本号(4.1),第三行代表着使用核心模式(Core-profile),意味着我们只能使用OpenGL功能的一个子集(没有我们不再需要的向后兼容特性)。

3、接下来创建一个窗口对象,它存放了所有和窗口相关的数据,而且会被GLFW的其他函数频繁调用

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函数,前两个参数是窗口的宽高,第三个参数是这个窗口的命名,后两个暂时忽略,返回了一个GLFWwindow对象。glfwMakeContexCurrent函数告诉GLFW将窗口的上下文设置为当前线程的主上下文。

4、GLAD是用来管理OpenGL的函数指针的,所以调用任何OpenGL函数之前需要初始化GLAD

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

我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAdress,它根据我们编译的系统定义了正确的函数。

5、视口

在开始渲染之前必须告诉OpenGL渲染窗口(Viewport)的尺寸大小,这样OpenGL才能知道怎样根据窗口大小显示数据和坐标。

// 此函数设置窗口的维度(Dimension)
glViewport(0, 0, 800, 600);

前两个参数控制窗口左下角位置,后两个控制渲染窗口的宽高(像素)。也可将视口维度设置比GLFW窗口维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL视口之外。

6、对窗口注册回调函数(CallbackFunction)

函数注册后会在每次窗口大小改变的时候调用,视口也会随之调整

函数原型如下:

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

进行注册,告诉GLFW每当窗口调整时调用此函数:

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

7、为了使图像能够持续显示而不是一闪即逝,我们需要写一个渲染循环,使得GLFW在退出之前一直保持运行

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

glfwWindowShouldClose函数在每次循环开始前检查一次GLFW是否被要求退出,是的话返回true,循环结束

glfwPollEvents函数检查是否有触发事件,比如键盘、鼠标等信号输入,然后更新窗口状态,调用相应的回调函数(可通过回调方法手动设置)。

glfwSwapBuffers函数会交换颜色缓冲

8、渲染结束后释放所有资源

glfwTerminate();
return 0;

至此,窗口创建完成

接下来我们进行一些完善工作

9、接下来我们添加一个触发时间,当用户按下Esc键时关闭窗口。

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

glfwGetKey函数需要一个窗口以及一个按键作为输入。这个函数将会返回这个案件是否正在被按下,我们将其定义在processInput函数当中

接下来在渲染循环的每一个迭代中调用processInput

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

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

    glfwSwapBuffers(window);
    glfwPollEvents();
}

10、我们使用一个自定义的颜色清空屏幕,使得在每个新的渲染迭代开始后清除上一次渲染结果,并显示我们自定义的颜色

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

渲染一个三角形

开始绘制之前,我们需要给OpenGL输入一些顶点数据(范围在[-1, 1],需要自行进行坐标变换)。我们需要渲染一个三角形,因此我们需要三个顶点位置,将它定义为一个float数组:

// 由于我们绘制的是一个2D三角形,因此,将其顶点的z坐标都设置为0
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

接下来使用glGenBuffers函数和一个缓冲ID生成一个VBO对象,并使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

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

从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

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

第一个参数是目标缓冲的类型,顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。

第二个参数指定传输数据大小。

第三个参数是我们实际发送的数据。

第四个参数指定了显卡管理数据的方式,有一下三种形式:
GL_STATIC_DRAW:数据不会或几乎不改变。
GL_DYNAMIC_DRAW:数据会改变很多。
GL_STREAM_DRAW:数据每次绘制都会改变。

顶点着色器

首先用GLSL(OoenGL Shading Language)编写顶点着色器,然后编译这个着色器。下面给出一个非常基础的顶点着色器源代码:

#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(对应410)。
接下来使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量aPos。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。

编译着色器

先创建一个着色器对象,注意还是用ID来引用。所以我们储存这个顶点着色器为unsigned int,然后用glCreateShader创建这个着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER

接下来把着色器源码附加到着色器对象上,并编译它:

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

glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL

片段着色器

先给出片段着色器源码:

#version 410 core
out vec4 FragColor;

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

片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为FragColor。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。

编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型:

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

着色器程序

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

创建程序对象:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:

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

最后调用glUseProgram函数,激活程序:

glUseProgram(shaderProgram);

着色器对象链接到程序对象以后,需要删除着色器对象:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

链接顶点属性

顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。

我们的顶点缓冲数据会被解析为下面这样子:


image

因此使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据:

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

第一个参数指定我们要配置的顶点属性

第二个参数指定顶点属性的大小。

第三个参数指定数据的类型

第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。

第五个参数是步长,它告诉我们在连续的顶点属性组之间的间隔。

最后一个参数表示位置数据在缓冲中起始位置的偏移量(Offset)。

接下来使用glEnableVertexAttribArray函数,以顶点属性位置值作为参数,启用顶点属性。

代码最终大概长这样:

// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();

顶点数组对象

Vertex Array Object(VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。

OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。

一个顶点数组对象会储存以下这些内容:

glEnableVertexAttribArrayglDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

VAO的创建类似VBO:

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

要使用VAO,只需使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

代码大概是这样的:

// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 绘制代(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

绘制三角形

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

glDrawArrays函数第一个参数是打算绘制的图元的类型。第二个参数制订了顶点数组的起始索引,第三个参数指定我们打算绘制的顶点个数。

最终三角形是长这样的:

triangle.png

你可以在这里找到源码:三角形矩形

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容