说明:跟着learnopengl的内容学习,不是纯翻译,只是自己整理记录。
强烈推荐原文,无论是内容还是排版。 原文链接
本文地址: http://blog.csdn.net/aganlengzi/article/details/50354192
在这一次中让我们首先测试上次配置的GLFW和GLEW是否能够正常的运行。上次已经新建了一个空工程,但是其中并没有任何源程序文件。现在,我们向其中添加一个文件,我添加的是main.cpp。在这个文件中我们通过使用GLFW和GLEW相关的功能函数来测试搭建的环境是否能够提供正确的功能。
测试的文件的各部分内容如下所示:
#include
// GLEW
#define GLEW_STATIC
#include
// GLFW
#include
这是main.cpp中的头文件部分。上次说过,我们使用GLEW的静态库方式,所以需要在原文件的开头定义GLEW_STATIC宏来显式地声明。而且需要在GLFW之前包含GLEW,因为包含的GLEW头文件中包含了正确的OpenGL头文件(比如说GL/gl.h)。换句话说,我们也就不需要再在这个工程中包含任何含有OpenGL函数声明的头文件,因为glew.h中已经包含了。
接下来,我们创建main函数,在main函数中,我们初始化GLFW窗口:
using namespace std;
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_RESIZABLE, GL_FALSE);
cout<<"Success!"<system("pause");
return 0;
}
如上面的代码显示,在main函数中,我们首先通过glfwInit函数来初始化GLFW,然后我们通过glfwWindowHint函数来配置GLFW相关的属性。glfwWindowHint的第一个参数是需要配置的属性,他们是以GLFW_开头的大量枚举类型。第二个参数是一个用于设置这个属性值的整数。所有可能的属性和相关的可能的属性值可以在GLFW的窗口处理文档中找到。现在你运行这个程序,如果你得到的是一些未定义的引用错误而不是屏幕输出成功,那么意味着你没有成功让你的工程链接到GLFW库。如果是这样,建议你仔细检查上述的配置情况。
关于代码细节,因为本教程使用的是OpenGL3.3版本,所以我们想要告诉GLFW我们使用的是3.3版本,这样的话GLFW就能够根据这个版本创建正确的OpenGL上下文环境。这确保了我们不会因为用户没有OpenGL3.3版本而运行失败。所以在代码中,我们可以看到,我们将主版本号和次版本号都设置为3.我们还告诉GLFW我们明确地使用core-profile模式并且创建的窗口不能够被用户改变大小。而一旦我们设置了我们将要使用core-profile模式,当我们使用旧的功能函数的时候就会导致无效的操作错误,这在我们无意中用到我们不想用的旧版本的函数时是一个很好的提醒。需要注意的是,在Mac OSX系统中你还需要在上面的初始化代码中添加一行:
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
确保你的系统和硬件中使或者支持OpenGL3.3或者更高的OpenGL版本,否则程序将会崩溃或者显示未定义的行为。确定系统中安装的OpenGL版本的方法是,在Linux中调用glxinfo或者在Windows平台使用OpenGL Extension Viewer,我通过这个工具检测到我的平台是OpenGL4.2版本的。一般只要不是太老的显卡应该都是能够达到这个教程的要求的。
接下来,我们需要创建一个窗口对象,这个窗口对象包含所有窗口相关的数据并且会被其他GLFW的其他功能函数频繁使用。
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
上述代码中,glfwCreateWindow函数需要为创建的窗口设定宽度值和高度值,这是它的前两个参数。第三个参数是创建窗口的名称,这里设置为LearnOpenGL。我们目前可以忽略后两个参数。这个函数返回一个GLFWwindow类型的对象,我们后面会在其他的GLFW操作中用到这个对象作为参数。之后,我们设置创建的这个窗口对象作为我们当前进程的主窗口上下文。
此时运行上面的程序(注意这部分创建窗口的代码放置在上面代码的设置属性和输出成功信息之间),我们会得到一个窗口。
在上一篇教程中,我们讲到GLEW帮助我们管理OpenGL管理函数指针,我们在调用OpenGL函数之前需要先初始化GLEW。初始化的代码如下所示:
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
注意在初始化GLEW之前,我们设定glewExperimental为GL_TRUE。这个设置保证GLEW使用更现代的技术来管理OpenGL的函数。如果让其保持其默认值(GL_FALSE),在使用OpenGL的core-profile模式时可能产生问题。
测试GLEW需要十分注意的是,上面的代码一定要在窗口对象创建之后。否则会报错。
在我们开始渲染我们想要的图形之前,我们还需要做一件事:我们需要告诉OpenGL渲染窗口(或者叫画布更合适)的尺寸以便于OpenGL知道我们想要怎样在窗口上显示数据和坐标。我们可以通过glViewport函数(注意这里我们已经开始使用OpenGL标准中的函数了)来设置这些维度:
glViewport(0, 0, 800, 600);
前两个参数设置了画布左下角的位置,后两个参数设置了画布的宽度和高度。在本例中,这个“画布”被设置成了和GLFW窗口一样的大小。实际上我们可以将这些值设置为比GLEW相关维度小的值。这样的话所有的OpenGL渲染效果都将会显示在一个更小的“视口”上,我们可以在视口外和窗口内显示其他的元素。
在后台,OpenGL使用glViewport设置的数据来将它处理的2D坐标转换成你屏幕上的坐标(注意这两个坐标并不是一个坐标,OpenGL使用的是标准化设备坐标系,显示在屏幕上的是屏幕坐标系)。例如,一个被OpenGL处理的点的位置是(-0.5,0.5)将会显示在我们创建的屏幕上的(200,450)的位置。OpenGL使用的是标准化设备坐标系,是一个三维,但是每一维度上的坐标值都在-1到1之间的立方空间。所以实际上(-1~1)被映射到(0~800)和(0~600)。
我们不希望这个程序简单地绘制一张图像(只显示一次的意思)就立即退出关闭窗口。我们希望这个程序能够不断地绘制图像并且能够处理用户的输入,直到这个程序被显式地告知结束。为了达到这个效果,我们需要创建一个while循环——就是我们称之为最简化的游戏循环,它一直运行直到我们告知GLFW来关闭。下面的代码就定义了这么一个简单的游戏循环:
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
glfwSwapBuffers(window);
}
glfwWindowShouldClose函数负责在每次循环迭代的时候检查是否被指定关闭,如果被指定关闭,这个函数返回true,整个游戏循环结束运行。之后我们可以关闭这个程序。
glfwPollEvents函数负责检查是否有事件(比如键盘输入或者鼠标移动事件)发生,如果有事件发生就调用相关联的函数(这些函数是我们应该通过回调函数设置的)。我们通常在循环迭代开始的时候调用事件处理函数。
glfwSwapBuffers函数交换前后颜色缓冲区,显示最新绘制的内容。颜色缓冲区是一块很大的缓冲区,其中包含了要显示在GLFW窗口上的每个像素点的颜色值。
双缓冲机制
当一个程序只是在单一的缓冲区绘制要显示的结果图像的时候,可能会产生闪烁等问题。这是因为绘制图像是需要花时间的,所以当绘制图像花费的时间能够被人眼察觉,那么就会产生问题。为了避免这个问题,窗口绘制程序使用双缓冲区来绘制。前缓冲区包含了最终要显示在屏幕上的图像,而所有的绘制命令只在后缓冲区进行绘制。当后缓冲区中的所有绘制指令都执行完成的时候,我们就交换前后缓冲区,此时刚刚绘制完成所有图像的缓冲区成为前缓冲区,立即显示完整的图像。而包含上一次显示内容的缓冲区变为后缓冲区,在这个缓冲区中可以进行下一帧图像的绘制。这样做的效果就是消除了所有的前面说到的问题。
最后,当我们退出游戏循环的时候,我们需要正确地清除/删除掉我们分配的所有资源。我们通过glfwTerminate函数来完成这个功能。这个函数在main函数的最后调用。
glfwTerminate();
return 0;
这样所有分配的资源会为清除干净并且正确地退出程序。现在试着编译完整的程序并运行,我们将会得到如下图所示的效果图:
如果最终显示的是上面这个黑底的图像窗口,我们就可以确信我们配置成功了!如果你没有得到这个结果,可能你需要仔细检查前面配置中,或者你的代码中出现了什么小问题。完成main.cpp文件。
我们还想利用GLFW的来处理某种形式的输入,我们可以利用GLFW的回调函数来实现。一个回调函数基本上就是一个函数指针,这个函数指针由开发者设置并且在函数运行的适当时候(触发时机到达)由GLFW来调用。KeyCallback是其中我们可以设置的一个回调函数,这个函数会在每次用户用键盘交互的时候被调用。这个函数的原型为:
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
按键输入处理函数将一个GLFWwindow对象作为它的第一个参数,第二个参数是一个整数值,来指定被操作的键值,第四个参数指定了这个键是被按下还是被释放,还有一个整型参数指定是否shift,alt或者超级键被同时按下。每当用户按下一个按键,GLFW都会以合适的参数调用这个函数进行处理。
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
// When a user presses the escape key, we set the WindowShouldClose property to true,
// closing the application
if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
在上述key_callback函数中,我们检查是否按下的键为ESC,当这个键被按下(而不是被释放)时,我们通过glfwSetwindowShouldClose设置WindowShouldClose值为true来关闭GLFW窗口。while循环将会在下一次条件判断时失败,程序就会关闭。
最后需要做的是将这个函数通过合适的回调函数进行设置,如下所示:
glfwSetKeyCallback(window, key_callback);
我们可以注册另外很多事件的回调函数为我们自己的函数。例如,我们可以设置回调函数来处理窗口尺寸的改变,来处理错误信息等等。需要注意的是,我们应该在创建窗口之后和游戏循环初始化之前注册这些回调函数。
我们想要将所有的渲染命令都放到游戏循环体中执行,因为我们想在每次while循环迭代的时候都执行一遍这些渲染指令。实现的方式大致如下:
// Program loop
while(!glfwWindowShouldClose(window))
{
// Check and call events
glfwPollEvents();
// Rendering commands here
...
// Swap the buffers
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函数调用时指定相应的缓冲区被设置(清空)成什么颜色值。一旦设置了这个颜色值,每次我们调用glClear函数的时候,相应的整个缓冲区都会被清空并填充为glClearColor配置的颜色。上面的代码将会得到如下图所示的效果,并且在按下ESC键的时候窗口会关闭:
这是完整的main.cpp。
正如你可能想起来前面教程中提到的,glClearColor就是一个状态设置函数,而glClear正是一个状态使用函数,它使用当前设置的状态(颜色)来执行后面的动作。
至此,我们已经准备好了所有应该准备的内容,后面可以在上面讲到的游戏循环中填入我们的渲染指令了。具体怎么使用OpenGL进行绘制和渲染才是这个教程中的重点内容啊。