在画出出色的效果之前,首先要做的就是创建一个 OpenGL 上下文和一个用于显式的窗口。
一些函数库已经提供了这样的功能,可以提供给开发者一个窗口和上下文来渲染。
比较流行的有 GLUT, SDL, SFML, GLFW, 此处我们使用 GLFW.
OpenGL 相关库
OpenGL 相关库简介
- GLEW 是对底层 OpenGL 的接口的封装,可以让开发者的代码跨平台
- GLAD 与 GLEW 作用基本相同,可以看作是它的升级版本
- Freeglut 主要用于创建 OpenGL 上下文、接受一些鼠标键盘事件等等
- GLFW 作用与 Freeglut 基本相同,可以看作是它的升级版
GLAD 通常和 GLFW 配合使用; GLEW 通常和 Freeglut 配合使用。
GLFW
GLAD
由于 OpenGL 驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。然而这个过程既复杂有繁琐,而 GLAD 库可以简化此过程。
配置 GLAD & GLAF
将得到的压缩包进行解压。
为了方便可以将使用需要的文件放在指定的目录中。此处我选择将其放在文件夹 C:\Program Files\OpenGL 中。
- 将解压后的 GLFW 文件夹中的 include 和 lib-vc2019 (如果是其它版本的 VS 记得改为其它版本)复制到文件夹 C:\Program Files\OpenGL 中;
- 将解压后的 GLAD 文件夹中的 include 文件夹中的 glad 和 KHR 文件夹复制到文件夹 C:\Program Files\OpenGL\include 中;
- 将解压后的 GLAD 文件夹中的 src 文件夹复制到文件夹 C:\Program Files\OpenGL 中;
移动后的文件结构如下:
OpenGL |
include |
glad |
GLFW |
KHR |
vc2019 |
... |
src |
glad.c |
创建工程及配置
- 打开 VS2019 新建一个空白工程;
- 右键解决方案名称选择属性;
- 点击VC++项目中的包含目录,添加 OpenGL 的 include 目录,此处为 C:\Program Files\OpenGL\include ;
- 点击VC++项目中的库文件,添加 OpenGL 的工程库目录,此处为 C:\Program Files\OpenGL\lib-vc2019 ;
- 点击链接器中的输入中的附加依赖项,添加库文件 glfw.lib ;
- 在新项目中添加 C:\Program Files\OpenGL\src 文件夹下的源文件 glad.c(每次新建工程都要添加);
新建窗口
-
新建一个源文件,要包含以下头文件
#include <glad/glad.h>
#include <GLFW/glfw3.h>
-
创建 main
函数,实例化 GLFW 窗口
int main() {
// 初始化 GLFW
glfwInit();
// 配置 GLFW
// 第一个参数是 enum 类型表示选项名称
// 第二个参数是 int 类型用于设置第一个参数的值
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 设置主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 设置次版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心模式
// 仅 Mac OS X 系统需要下面的语句
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
-
创建一个窗口对象
// glfwCreateWindow 函数用于创建窗口对象 glfwCreateWindow
// 第一、二、三个参数分别是窗口的宽度、高度、窗口的标题
// 后两个参数暂时先忽略
// 返回一个 GLFWwindow 对象的指针
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// 窗口创建成功之后通知 GLFW 将窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
-
加载系统储相关的 OpenGL 函数指针地址
GLAD 是用来管理 OpenGL 的函数指针的,所以任何调用 OpenGL 的函数之前都要初始化 GLAD。
// gladLoadGLLoader: GLAD 装载 GL 装载机
// glfwGetProcAddress: GLFW 根据编译的系统定义正确的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
-
视口
在开始渲染之前必须要告诉 OpenGL 渲染矩形区域的尺寸大小,即视口(Viewport)。
视口的大小可以和窗口相同,也可以大于或者小于窗口的大小。
只有绘制在视口区域的图形才能显式,
// glViewport: 设置视口的锚点和大小
// 第一、二个参数控制视口左下角的位置,窗口左下角为(0, 0)
// 第三、四个参数控制视口的大小(单位:像素)
glViewport(0, 0, 800, 600);
当用户改变窗口的大小时,视口也应该被调整。
可以对窗口注册一个回调函数,它会在每次窗口大小被调整时被调用。
// 回调函数第一个参数是窗口对象指针
// 回调函数的后两个参数是窗口被改变之后的宽度和高度
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
另外,还需要注册这个回调函数,告诉 GLFW 开发者希望每当窗口调整大小的时候调用这个函数。
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
-
渲染循环
在程序中添加一个 while
循环用于在 GLFW 推出之前不断的绘制图像并接受用户的输入。
while(!glfwWindowShouldClose(window)) {
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose
函数在每次循环开始时检查一次 GLFW 是否被要求退出。
glfwPollEvents
函数检测是否触发了什么事件、更新窗口状态,并调用对应的回调函数。
glfwSwapBuffers
函数会交换颜色缓存,它在这一迭代中被用来绘制,并且作为输出显示在屏幕上。
双缓冲(Double buffer)
应用程序使用单缓冲绘图时会可出现图像闪烁的问题,这是因为生成的图像不是一下子被绘制出来的,而是一步步生成的,这导致渲染的结果不真实。
为了避免这一问题,我们采用双缓冲渲窗口应用程序。其中,前缓冲中保存着最终输出的图像;后换冲用于绘制所有的渲染指令。当所有的渲染指令完成之后,交换前后缓冲,图像就会立即显示出来。
-
正确释放/删除之前的分配的所有资源
// 正确释放/删除之前的分配的所有资源
glfwTerminate();
输入
以获取一个按键输入为例:当按下 Escape 按键时关闭窗口。
// 创建一个 processInput 来处理输入
void processInput(GLFWwindow *window) {
// glfwGetKey 用于检查按键 Escape 是否被按下(如果按下了返回 GLDFW_RELEASE)
// glfwSetWindowShouldClose 用于 Escape 按下时将 WindowShouldClose 属性设置为 true
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true); // 下一次循环就会关闭 GLFW
}
// 在循环渲染的每次迭代中调用 processInput
while (!glfwWindowShouldClose(window)) {
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
渲染
我们要把所有的渲染(Rendering)操作放到渲染循环中,从而保证渲染指令在每次渲染循环迭代的时候都能被执行。
// 渲染循环
while(!glfwWindowShouldClose(window)) {
// 输入
processInput(window);
// 渲染指令
...
// 检查并调用事件,交换缓冲
glfwPollEvents();
glfwSwapBuffers(window);
}