OpenGL入门第二课:来左边跟我一起画个”龙“

创建窗口
在画出出色的效果之前,首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。然而,这些操作在每个系统上都是不一样的,OpenGL有意将这些操作抽象(Abstract)出去。这意味着我们不得不自己处理创建窗口,定义OpenGL上下文以及处理用户输入。
有一些库已经提供了我们所需的功能,其中一部分是特别针对OpenGL的。这些库节省了我们书写操作系统相关代码的时间,提供给我们一个窗口和一个OpenGL上下文用来渲染。最流行的几个库有GLUT,SDL,SFML和GLFW。
GLAD和GLFW
- GLFW:创建窗口、处理输入(键盘鼠标)、管理 OpenGL 上下文的库。
- GLAD:加载 OpenGL 的函数指针(加载器/绑定器),能调用 OpenGL 的函数。。
GLFW
GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入,
CMake
CMake是一个工程文件生成工具。我们从GLFW源码创建一个Visual Studio 2019工程文件,之后进行编译。

在设置完源代码目录和目标目录之后,点击Configure(设置)按钮,让CMake读取设置和源代码。这里我们使用默认设置,并再次点击Configure(设置)按钮保存设置。保存之后,点击Generate(生成)按钮,生成的工程文件会在你的build文件夹中。
编译
在build文件夹里可以找到GLFW.sln文件,用Visual Studio 2019打开。因为CMake已经配置好了项目,并按照默认配置将其编译为64位的库,所以我们直接点击Build Solution(生成解决方案)按钮,然后在build/src/Debug文件夹内就会出现我们编译出的库文件glfw3.lib。
库生成完毕之后,我们需要让IDE知道库和头文件的位置。有两种方法:
- 找到IDE或者编译器的**/lib和/include文件夹,添加GLFW的include文件夹里的文件到IDE的/include文件夹里去。用类似的方法,将glfw3.lib添加到/lib**文件夹里去。虽然这样能工作,但这不是推荐的方式,因为这样会让你很难去管理库和include文件,而且重新安装IDE或编译器可能会导致这些文件丢失。
- 推荐的方式是建立一个新的目录包含所有的第三方库文件和头文件,并且在你的IDE或编译器中指定这些文件夹。我个人会使用一个单独的文件夹,里面包含Libs和Include文件夹,在这里存放OpenGL工程用到的所有第三方库和头文件。这样我的所有第三方库都在同一个位置(并且可以共享至多台电脑)。然而这要求你每次新建一个工程时都需要告诉IDE/编译器在哪能找到这些目录。
完成上面步骤后,我们就可以使用GLFW创建我们的第一个OpenGL工程了!
第一个工程
首先,打开Visual Studio,创建一个新的项目。如果VS提供了多个选项,选择Visual C++,然后选择**Empty Project(空项目)**(别忘了给你的项目起一个合适的名字)。由于我们将在64位模式中执行所有操作,而新项目默认是32位的,因此我们需要将Debug旁边顶部的下拉列表从x86更改为x64:

现在我们终于有一个空的工作空间了,开始创建我们第一个OpenGL程序吧!
链接
为了使我们的程序使用GLFW,我们需要把GLFW库
要添加这些目录(需要VS搜索库和include文件的地方),我们首先进入Project Properties(工程属性,在解决方案窗口里右键项目),然后选择**VC++ Directories(VC++ 目录)**选项卡(如下图)。在下面的两栏添加目录:

这里你可以把自己的目录加进去,让工程知道到哪去搜索。你需要手动把目录加在后面,也可以点击需要的位置字符串,选择**<Edit..>选项,之后会出现类似下面这幅图的界面,图是选择Include Directories(包含目录)**时的界面:

这里可以添加任意多个目录,IDE会从这些目录里寻找头文件。所以只要你将GLFW的Include文件夹加进路径中,你就可以使用<GLFW/..>来引用头文件。库文件夹也是一样的。
现在VS可以找到所需的所有文件了。最后需要在Linker(链接器)选项卡里的Input(输入)选项卡里添加glfw3.lib这个文件:

要链接一个库我们必须告诉链接器它的文件名。库名字是glfw3.lib,我们把它加到**Additional Dependencies(附加依赖项)字段中(手动或者使用<Edit..>**选项都可以)。这样GLFW在编译的时候就会被链接进来了。除了GLFW之外,你还需要添加一个链接条目链接到OpenGL的库,但是这个库可能因为系统的不同而有一些差别。
GLAD
到这里还没有结束,我们仍然还有一件事要做。因为OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,在Windows上会是类似这样:
1 | // 定义函数原型 |
你可以看到代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。
配置GLAD
GLAD是一个开源的库,它能解决我们上面提到的那个繁琐的问题。GLAD的配置与大多数的开源库有些许的不同,GLAD使用了一个在线服务。在这里我们能够告诉GLAD需要定义的OpenGL版本,并且根据这个版本加载所有相关的OpenGL函数。
打开GLAD的在线服务,将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本(我们的教程中将使用3.3版本,但更新的版本也能用)。之后将模式(Profile)设置为Core,并且保证选中了生成加载器(Generate a loader)选项。现在可以先(暂时)忽略扩展(Extensions)中的内容。都选择完之后,点击生成(Generate)按钮来生成库文件。
GLAD现在应该提供给你了一个zip压缩文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。
经过前面的这些步骤之后,你就应该可以将以下的指令加到你的文件顶部了:
1 |
实例化GLFW窗口
初始化库并设置使用OpenGL的版本,并告诉GLFW使用的是OpenGL中的核心渲染模式
1 | int main() |
接下来我们创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据,而且会被GLFW的其他函数频繁地用到。
1 | GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); |
使用GLAD管理OpenGL函数指针
在之前的教程中已经提到过,GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。
1 | if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) |
给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给的是glfwGetProcAddress,它根据编译的系统定义了正确的函数。
视口
设置OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用
1 | glViewport(0, 0, 800, 600); |
glfwCreateWindow和glViewport的类比记忆
| 作用阶段 | 函数 | 类比 |
|---|---|---|
| 创建窗口+上下文 | glfwCreateWindow |
打开一个画框 |
| 设置绘图区域 | glViewport |
告诉画家“只画这块区域” |
添加一个while循环,可以把它称之为
1 | while(!glfwWindowShouldClose(window)) |
glfwWindowShouldClose 函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话,该函数返回true,渲染循环将停止运行,之后我们就可以关闭应用程序。glfwPollEvents 函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。glfwSwapBuffers 函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。
!!! Important
**双缓冲(Double Buffer)**
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。**前**缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在**后**缓冲上绘制。当所有的渲染指令执行完毕后,我们**交换**(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
渲染
我们要把所有的渲染(Rendering)操作放到渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。代码将会是这样的:
1 | // 渲染循环 |
为了测试一切都正常工作,我们使用一个自定义的颜色清空屏幕。在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)。我们可以通过调用
1 | glClearColor(0.2f, 0.3f, 0.3f, 1.0f); |
注意,除了
glfwPollEvents()函数
作用:轮询并处理输入/窗口事件,如果不调用它,窗口就会“假死”,你按什么键都没反应。
- GLFW 会把操作系统收到的事件(如:键盘输入、鼠标点击、窗口大小变化)放入一个事件队列中。
glfwPollEvents()会从这个队列中取出事件并处理,让你注册的回调函数(如key_callback)被调用。
glfwSwapBuffers(window)函数
作用:把你在后缓冲画好的图像“交换”到前缓冲中去,呈现在屏幕上,如果不调用它,永远也看不到画的东西。
OpenGL 默认使用 双缓冲机制:
- 后缓冲区(back buffer):我们用 OpenGL 渲染图像的地方。
- 前缓冲区(front buffer):显示在屏幕上的图像。
最后一件事
当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在
1 | glfwTerminate(); |


