在上一节把环境搭建完毕后,今天就来学习怎么创建一个窗口,并在这个窗口实现一些基本功能。这里面涉及的函数很多,我尽力将逐个函数的功能以及怎么调用讲述清楚。
首先,导入GLFW和GLEW两个库的头文件,验证一下环境是否搭建成功:
#include<iostream>
#include<GL/glew.h>
#include<GLFW/glfw3.h>
1.初始化和配置GLFW
先创建一个main
函数,在这个函数里面初始化GLFW:
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //使用的OpenGL版本号3.3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //使用流水线配置模式
return 0;
}
glfwInit()
:初始化GLFW
glfwWindowHint(int hint, int value)
:在初始化完GLFW后,要调用这个函数来配置GLFW,告知GLFW我们要使用OpenGL的版本号是多少,且用的是什么模式。
GLFW_CONTEXT_VERSION_MAJOR
:告知GLFW使用OpenGL的主版本号,这里为3。
GLFW_CONTEXT_VERSION_MINOR
:告知GLFW使用OpenGL的次版本号,这里也是3,即使用的OpenGL版本是3.3。
GLFW_OPENGL_PROFILE
:告知GLFW要使用的模式是什么。
GLFW_OPENGL_CORE_PROFILE
:告知GLFW使用的核心模式(Core-profile)
核心模式意味着我们只能使用OpenGL功能的一个子集(没有了我们已不再需要的向后兼容特性)
2.创建一个窗口对象
在完成对GLFW的初始化后,我们就可以创建一个窗口对象,这个窗口对象存放的所有与窗口相关的数据和信息,且会被其他函数频繁用到。
//也是在main函数里,紧接上述代码
GLFWwindow* window = glfwCreateWindow(800, 600, "Learn OpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwCreateWindow(int width, int height, const char *title, GLFWmonitor *monitor, GLFWwindow *share)
:这个函数创建一个窗口对象,它需要窗口的宽和高作为它的前两个参数,需要一个窗口的名称(字符串)作为它的第三个参数,最后两个参数暂时忽略,这里并用不上。然后返回一个GLFWwindow
对象,这个就是创建好的窗口对象。
创建完成以后,并在后面跟了一个失败检查,如果失败就告知用户并执行glfwTerminate()
。
glfwTerminate()
:清理所有资源并正确地退出应用程序。
glfwMakeContextCurrent(GLFWwindow *window);
这个函数通知GLFW将指定窗口的上下文(context)设置为当前线程的主上下文。我的理解是可能接下来某些函数的操作,虽然并没有指定哪个window,但由于使用的这个函数,所以相应的操作是作用于这个主窗口。(可能有误)
3.初始化GLEW
GLEW是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLEW。在OpenGL环境没有完全建立时初始化GLEW有可能会失败,所以要在GLFW初始化之后再来初始化GLEW。
//同样在main函数里,紧接其上
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
std::cout << "Failed to initial GLEW." << std::endl;
glfwTerminate();
return -1;
}
glewExperimental
:这个布尔变量意味着是否开启一些GLEW实验性质的功能。在使用核心模式的时候,这个布尔变量要设为ture,否则OpenGL程序有可能会崩溃。
glewInit()
:初始化GLEW,如果初始化成功会返回GLEW_OK
这个枚举值。
4.渲染窗口
虽然我们使用了glfwCreateWindow()
创建了一个窗口对象,但其仅仅只是一个对象,并不能真正显示出来,要另外使用一个函数把这个窗口渲染出来:
//同样在main函数里,紧接其上
glViewport(0, 0, 800, 600);//渲染窗口大小
glViewport(GLint x, GLint y, GLsizei witdh, GLsizei height)
:渲染一个窗口,前两个参数控制窗口左下角的位置,后两个参数控制渲染窗口的宽度和高度(pixel)。而对于GLint
和GLsizei
是什么类型,这其实是int
类型的另外一种名称,在glew.h里使用了typedef
换了皮而已,实际上还是int
类型。
实际上也可以将视口的维度设置为比GLFW的维度小,这样子之后所有的OpenGL渲染将会在一个更小的窗口中显示,这样子的话我们也可以将一些其它元素显示在OpenGL窗口之外。至此这个窗口就能显示出来,但由于执行到这就已经到了
main
函数的末尾,main
函数一旦结束这个窗口就会关闭。所以呈现的效果就是窗口在屏幕上一闪而过。
5.渲染循环
我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口。我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此我们需要一个循环不断接收外界的输入并作出相应的反应,直到GLFW主动退出。这个循环称为渲染循环(Render Loop):
//同样在main函数里,紧接其上
while (!glfwWindowShouldClose(window))
{
//接收输入,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose(GLFWwindow *window)
:这个函数会检查一次GLFW是否被要求退出(会有相应的函数要求GLFW退出),如果是该函数返回true,然后这个while循环就会结束。
glfwPollEvents()
:这个函数会检查是否有触发事件(会接收来自外部设备的输入)、更新窗口状态,并调用对应的回调函数。
glfwSwapBuffers(GLFWwindow *window)
:交换颜色缓冲区(保存着GLFW窗口每一像素颜色值的缓冲区),在这一迭代(iteration)中被用来绘制,并且将会作为输出显示在屏幕上。
因为OpenGL采用双缓冲来渲染窗口,所以存在交换缓冲区这么一个说法,那么何为双缓冲,这值得深究:
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁(flicking)的问题。 这是因为生成的图像不是一下子被绘制出来的(not drawn in an instant),而是按照从左到右,由上而下(left to right and top to bottom)逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实(result may contain artifacts)。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像(front buffer contains the final output image),它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制(all the rendering commands draw to the back buffer)。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
在有了渲染循环之后,我们就能看到这个窗口了,效果如图:
6.实现输入退出控制
我想能够在GLFW中实现退出程序控制(键入某个按钮退出程序),这可以通过glfwGetKey()
实现,这与Unity里的Input.GetKey()
函数有几分相似。我在main()函数以外的地方创建了一个新的函数processInput
去完成这个功能,但要注意的是,你必须要在调用这个函数之前定义好这个函数亦或是有这个函数的声明式,不然会报错说这个函数不存在或未定义。
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window,GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
int glfwGetKey(GLFWwindow *window, int key)
:这个函数检查指定窗口是否有指定按键的输入,如果有就返回GLFW_PRESS
否则返回GLFW_RELEASE
。而GLFW_KEY_ESCAPE
对应的按键就是Esc。
glfwSetWindowShouldClose(GLFWwindow *window, int value)
:这个函数设置是否关闭指定的GLFW窗口对象,如果设为true,就是关闭。
我们在这个函数里面检查用户是否键入了Esc,如果是关闭窗口对象。
定义好这个函数,就要在渲染循环的每一个迭代上调用这个函数:
while (!glfwWindowShouldClose(window))
{
//处理输入
processInput(window);
//接收输入,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
这就给我们一个非常简单的方式来检测特定的键是否被按下,并在每一帧做出处理。
7.清屏指令
除了输入控制要放在渲染循环,所有的渲染操作也要放在渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行。这些操作的循环的顺序应该是这样的:
while (!glfwWindowShouldClose(window))
{
//处理输入
processInput(window);
//渲染指令
...
//接收输入,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
就是在每一帧的最后才接收输入,在每一帧开始时处理上一帧接收的输入,处理完在执行渲染指令。这与一般的想法不太一样,认为接收完输入就可以立刻处理输入。这是因为这个glfwPollEvents()
执行起来是颇费时间的,放在前面有可能会造成帧率不稳定。
在这里我想做的一个渲染操作就是使用一个自定义颜色清空屏幕。在每一个渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果,这一般不是我们想要的。
while (!glfwWindowShouldClose(window))
{
//处理输入
processInput(window);
//渲染指令
glClearColor(1.0f, 0, 0, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//接收输入,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
glClear(GLBitfield mask)
:这个函数就是用来清空屏幕的颜色缓冲,它接收一个缓存位(GLBitfield)来指定要清空的缓冲,可能的缓冲有GL_COLOR_BUFFER_BIT
,GL_DEPTH_BUFFER_BIT
和GL_STENCIL_BUFFER_BIT
,我们现在只想清空颜色,所以指定GL_COLOR_BUFFER_BIT
。
glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha)
:这个函数设置清空屏幕所用的颜色,它接收RGB值以及一个透明度值。这里我设置成了大红色。
当渲染循环结束后我们需要正确释放之前分配的(allocated)的资源,我们可以在main
函数的最后使用glfwTerminate()
函数来完成,这个函数刚有提到,不再赘述。
//同样在main函数里,紧接其上
glfwTerminate();
return 0;