这个系列文章我们来介绍一位海外工程师如何探索 OpenGL 音视频渲染技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,这是第 3 篇:OpenGL 窗口入门。
—— 来自公众号“关键帧Keyframe”的分享
让我们看看能否让 GLFW 正常运行。首先,创建一个 .cpp文件,并在新创建的文件顶部添加以下包含指令:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
确保在 GLFW 之前包含 GLAD。GLAD 的包含文件会在后台包含所需的 OpenGL 头文件(如 GL/gl.h),因此请确保在需要 OpenGL 的其他头文件(如 GLFW)之前包含 GLAD。
接下来,我们创建主函数,在其中实例化 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;
}
在主函数中,我们首先通过 glfwInit初始化 GLFW,之后可以通过 glfwWindowHint配置 GLFW。glfwWindowHint的第一个参数告诉我们想要配置的选项,我们可以从一个以 GLFW_开头的大枚举中选择选项。第二个参数是一个整数,用于设置选项的值。所有可能的选项及其对应值可以在 GLFW 的窗口处理文档中找到。如果你现在尝试运行应用程序并出现大量 未定义引用错误,这意味着你没有成功链接 GLFW 库。
由于本书的重点是 OpenGL 3.3 版本,我们希望告诉 GLFW 使用 3.3 版本。这样 GLFW 在创建 OpenGL 上下文时可以做出适当的安排,确保当用户没有正确的 OpenGL 版本时,GLFW 无法运行。我们将主版本和次版本都设置为 3。我们还告诉 GLFW 明确使用核心模式。告诉 GLFW 使用核心模式意味着我们将获得一个更小的 OpenGL 功能子集,而不包含我们不再需要的向后兼容功能。注意,在 Mac OS X 上,你需要添加 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);到初始化代码中才能正常工作。
确保你的系统/硬件上安装了 3.3 或更高版本的 OpenGL,否则应用程序可能会崩溃或表现出未定义的行为。要查找机器上的 OpenGL 版本,你可以在 Linux 上调用 glxinfo,或者在 Windows 上使用类似 OpenGL Extension Viewer 的工具。如果支持的版本较低,请尝试检查你的显卡是否支持 OpenGL 3.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;
}
glfwMakeContextCurrent(window);
glfwCreateWindow函数需要窗口的宽度和高度作为前两个参数。第三个参数允许我们为窗口创建一个名称;目前我们将其命名为 "LearnOpenGL",但你可以随意命名。我们可以忽略最后两个参数。该函数返回一个 GLFWwindow 对象,我们稍后需要它来执行其他 GLFW 操作。之后,我们告诉 GLFW 将我们窗口的上下文设置为当前线程的主上下文。
1、GLAD
在上一章中,我们提到 GLAD 管理 OpenGL 的函数指针,因此在调用任何 OpenGL 函数之前,我们需要初始化 GLAD:
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
我们向 GLAD 传递一个加载 OpenGL 函数指针地址的函数,这取决于操作系统。GLFW 提供了 glfwGetProcAddress,它根据我们编译的操作系统定义正确的函数。
2、视口
在我们开始渲染之前,我们还需要做一件事。我们必须告诉 OpenGL 渲染窗口的大小,以便 OpenGL 知道如何将数据和坐标显示到窗口中。我们可以通过 glViewport函数设置这些 尺寸:
glViewport(0, 0, 800, 600);
glViewport的前两个参数设置窗口左下角的位置。第三和第四个参数设置渲染窗口的宽度和高度(以像素为单位),我们将其设置为与 GLFW 窗口大小相同。
实际上,我们可以将视口尺寸设置为小于 GLFW 的尺寸;然后所有 OpenGL 渲染将显示在一个较小的窗口中,我们可以在 OpenGL 视口之外显示其他元素。
在后台,OpenGL 使用通过 glViewport指定的数据将处理过的 2D 坐标转换为屏幕坐标。例如,处理过的点 (-0.5, 0.5)在最终转换后将映射到屏幕坐标 (200, 450)。注意,OpenGL 中处理过的坐标范围在 -1 到 1 之间,因此我们实际上是从范围 (-1 到 1) 映射到 (0, 800) 和 (0, 600)。
然而,当用户调整窗口大小时,视口也应该相应调整。我们可以在窗口上注册一个回调函数,每当窗口大小发生变化时调用该函数。这个调整大小的回调函数具有以下原型:
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 我们希望在每次窗口调整大小时调用此函数,通过注册它:
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
当窗口首次显示时,framebuffer_size_callback也会被调用,并返回窗口的实际尺寸。对于视网膜显示屏,宽度和高度会显著高于原始输入值。
我们可以设置许多回调函数来注册我们自己的函数。例如,我们可以创建一个回调函数来处理游戏手柄输入变化、错误消息等。我们在创建窗口后和启动渲染循环之前注册这些回调函数。
3、启动引擎
我们不希望应用程序绘制一张图片后立即退出并关闭窗口。我们希望应用程序持续绘制图片并处理用户输入,直到程序被明确告知停止。为此,我们必须创建一个 while循环,我们称之为渲染循环,它会一直运行,直到我们告诉 GLFW 停止。以下代码展示了一个非常简单的渲染循环:
while (!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose函数在每次循环迭代开始时检查是否已指示 GLFW 关闭。如果是,该函数返回 true,渲染循环停止,之后我们可以关闭应用程序。
glfwPollEvents函数检查是否触发了任何事件(如键盘输入或鼠标移动事件),更新窗口状态,并调用相应的函数(我们可以通过回调方法注册这些函数)。 glfwSwapBuffers将在此渲染迭代期间使用的颜色缓冲区(一个包含 GLFW 窗口中每个像素颜色值的大型 2D 缓冲区)与显示输出的屏幕交换。
双缓冲
当应用程序在单缓冲区中绘制时,生成的图像可能会出现闪烁问题。这是因为最终输出图像不是瞬间生成的,而是逐像素绘制的,通常从左到右、从上到下。由于用户在图像仍在渲染时看到它,结果可能会出现伪影。为避免这些问题,窗口应用程序采用双缓冲进行渲染。前台缓冲区包含显示在屏幕上的最终输出图像,而所有渲染命令都绘制到 后台缓冲区。一旦所有渲染命令完成,我们 交换后台缓冲区和前台缓冲区,以便图像可以显示而不再被渲染,从而消除了上述所有伪影。
4、最后一件事
一旦我们退出渲染循环,我们希望正确清理/删除 GLFW 分配的所有资源。我们可以通过在主函数末尾调用 glfwTerminate函数来实现这一点。
glfwTerminate();
return 0;
这将清理所有资源并正确退出应用程序。现在尝试编译你的应用程序,如果一切顺利,你应该看到以下输出:

如果它是一个非常单调和无聊的黑色图像,那你就做对了!如果你没有得到正确的图像,或者对所有内容如何组合在一起感到困惑,请查看完整的源代码(如果它开始闪烁不同的颜色,请继续阅读)。
如果你在编译应用程序时遇到问题,首先确保所有链接器选项都设置正确,并且你在 IDE 中正确包含了目录(如上一章所述)。还要确保你的代码是正确的;你可以通过与完整源代码进行比较来验证它。
5、输入
我们还希望在 GLFW 中实现某种形式的输入控制,我们可以通过 GLFW 的几个输入函数来实现。我们将使用 GLFW 的 glfwGetKey函数,它接受窗口和按键作为输入。该函数返回该按键当前是否被按下。我们创建一个 processInput函数来组织所有输入代码:
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
在这里,我们检查用户是否按下了 ESC 键(如果未按下,glfwGetKey返回 GLFW_RELEASE)。如果用户按下了 ESC 键,我们通过将 WindowShouldClose属性设置为 true来关闭 GLFW,使用 glfwSetWindowShouldClose。主 while循环的下一次条件检查将失败,应用程序关闭。
然后我们在渲染循环的每次迭代中调用 processInput:
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
这为我们提供了一种简单的方法,每帧检查特定的按键并相应地做出反应。渲染循环的一次迭代通常被称为一帧。
6、渲染
我们希望将所有渲染命令放在渲染循环中,因为我们希望在循环的每次迭代或帧中执行所有渲染命令。这可能看起来像这样:
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染命令放在这里
...
// 检查并调用事件并交换缓冲区
glfwPollEvents();
glfwSwapBuffers(window);
}
为了测试是否一切正常,我们希望用我们选择的颜色清除屏幕。在帧开始时,我们希望清除屏幕,否则我们仍然会看到上一帧的结果(这可能是你想要的效果,但通常不是)。我们可以通过 glClear清除屏幕的颜色缓冲区,其中我们传入缓冲区位来指定要清除的缓冲区。我们可以设置的位包括 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);
注意,我们还使用 glClearColor指定用于清除屏幕的颜色。每当我们调用 glClear并清除颜色缓冲区时,整个颜色缓冲区将被 glClearColor配置的颜色填充。这将导致一种深绿色调。
如你可能从《OpenGL》一章中回忆起的,glClearColor函数是一个 状态设置函数,而 glClear是一个 状态使用函数,因为它使用当前状态来检索清除颜色。

音视频方向学习、求职,欢迎加入我们的星球
丰富的音视频知识、面试题、技术方案干货分享,还可以进行面试辅导

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。