OpenGL入门1.1:窗口
OpenGL入门1.1:第一个窗口
每一个小步骤的源码都放在了Github
的内容为插入注释,可以先跳过
在上一篇文章的末尾我们测试了 GLFW 是否有正常连接进来,如果如下的代码可以顺利通过编译,那么我们就可以正式开始 OpenGL的学习了。
上下文
下面的内容可能会引起初学者的不适,看不懂其实没有关系,不断的实践就会逐渐理解一切,量变引起质变。
首先我们要知道 OpenGL 只是图形 API ,它没有窗口的支持,我们一般使用 GLUT 或者 GLFW 这种库来创建一个窗口,然后在这个窗口中使用 OpenGL 的 API 进行绘制。既然要进行绘制,我们就需要传给这个窗口大量的渲染信息,包括当前使用的颜色、是否有光照计算、开启的光源等等。这些信息就保存在了 context 里,你可以把它理解为一个很大的结构体。
并且 OpenGL 是一个状态机,上面提到的渲染信息就是状态机中的状态变量,并且每个状态变量都有默认值。在程序中,我们设置的各种状态和默认状态,在直到我们再次修改它们之前,都会一直生效(状态机并不是一个好的设计,多个绘制算法同时跑,容易被前面的状态所影响,OpenGL为了向前兼容,状态机的形式就被一直延续下来了)。
为了让你更好的理解,我们先回到代码。
GLFW 创建窗口
之前我们在 main 函数里写了:
int main()
{
glfwInit();//初始化GLFW
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);//for Mac
return 0;
}
这些函数都是用于配置 GLFW 环境的,先调用 glfwInit() 函数来初始化 GLFW,然后通过调用 glfwWindowHint() 函数来配 GLFW 的参数。
glfwWindowHint()函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整型,用来设置这个选项的值
该函数的所有的选项以及对应的值都可以在 GLFW’s window handling 这篇文档中找到
如果你现在编译你的cpp文件会得到大量的如 LNK2019 等 undefined reference (未定义的引用) 错误,也就是说你并未顺利地链接GLFW库,请回到上一篇博客仔细检查步骤。
由于本教程是基于 OpenGL 3.3 版本展开讨论的,所以我们需要告诉 GLFW 我们要使用的 OpenGL 版本是 3.3 ,所以我们将主版本号 Major 和次版本号 Minor 都设为 3,这样 GLFW 会在创建 OpenGL 上下文时做出适当的调整,这也可以确保用户在没有适当的 OpenGL 版本支持的情况下无法运行。
然后我们同样明确告诉 GLFW 我们使用的是核心模式 Core-profile 。明确告诉 GLFW 我们需要使用核心模式意味着我们只能使用 OpenGL 功能的一个子集。
从 OpenGL 3.0 开始,OpenGL 为了摆脱历史的包袱,想要彻底的废弃掉之前的许多特性,但是无奈市面上已经有大量依赖 OpenGL 之前版本的代码,导致 OpenGL 维护小组的这一想法难以付诸实施,于是在 OpenGL 3.1 开始引入了 OpenGL Context 的一些分类,比如引入了 CoreProfile 等概念,之后随着版本发展到 3.3,一切算是确定下来。
简单的来说,到了 OpenGL3.3 之后,OpenGL 的 context profile 分为了两个版本,core pfofile 和 compatibility profile,前者表示删除任何标记为deprecated(弃用)的功能,后者则表示不删除任何功能。context 除了core profile(核心渲染模式)和 compatibility profile(立即渲染模式)外,还有一种模式:foward compatibility,这个表示所有标记为 deprecated 的函数都禁用,这个模式只对 opengl3.0 及以上的版本有效。但这个选项对 OpenGL 3.2+ compatibility Profile Context 没有任何作用。
接下来我们创建一个窗口对象 window,这个对象存放了所有和窗口相关的数据,而且会被 GLFW 的其他函数频繁地用到。
glfwInit();//glfw初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);//(宽,高,窗口名)返回一个GLFWwindow类的实例:window
if (window == NULL)
{
// 生成错误则输出错误信息
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwCreateWindow() 函数需要窗口的宽和高作为它的前两个参数,第三个参数表示这个窗口的名称(标题),这里我使用 "LearnOpenGL",当然你也可以使用你喜欢的名称,最后两个参数我们暂时忽略。这个函数将会返回一个 GLFWwindow 类型的对象,我们会在其它的 GLFW 操作中使用到。
创建完窗口我们就通过 glfwMakeContextCurrent(window)
通知 GLFW 将我们窗口的上下文设置为当前线程的主上下文。
GLAD 初始化
GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数,GLFW给我们的是glfwGetProcAddress,它根据我们编译的系统定义了正确的函数。
glViewport
在我们开始渲染之前,我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用glViewport函数来设置窗口的维度(Dimension):
glViewport(0, 0, 800, 600);
glViewport函数前两个参数控制 窗口左下角的位置,第三个和第四个参数控制渲染窗口的宽度和高度(像素)。
我们实际上也可以将视口的维度设置为比 GLFW 窗口的维度小,这样子之后所有的 OpenGL 渲染将会在一个更小的窗口中显示,通过这样的设置我们也可以将一些其它元素显示在 OpenGL 视口之外。
OpenGL 幕后使用 glViewport 中定义的位置和宽高进行2D坐标的转换,将 OpenGL 中的位置坐标转换为你的屏幕坐标。例如,OpenGL 中的坐标 (-0.5, 0.5) 有可能最终被映射为屏幕中的坐标 (200,450) 。注意,处理过的 OpenGL 坐标范围只为 -1 到 1,因此我们事实上是将 (-1到1) 范围内的坐标映射到了 (0, 800) 和 (0, 600) 的范围内。
当用户改变窗口的大小的时候,视口也应该被调整,我们可以对窗口注册一个 回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用,这个回调函数的原型如下:
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
也会被调用,对于高DPI的显示屏,width和height可能会明显比原输入值更高一点。
我们还可以将我们的函数注册到其它很多的回调函数中,比如可以创建一个回调函数来处理手柄输入变化,处理错误消息等,我们一般会在 创建窗口之后,渲染循环初始化之前 注册这些回调函数。
渲染循环
我们不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口,而是希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入,因此,我们需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop)。
如果你还是不理解为什么需要一个循环,可以看看这篇博客。渲染循环能在我们让GLFW退出前一直保持运行,下面几行的代码就实现了一个简单的渲染循环:
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwWindowShouldClose()
函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回 true 然后渲染循环便结束了,之后为我们就可以关闭应用程序了。glfwPollEvents()
函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。glfwSwapBuffers()
函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
最后一件事
当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源,我们可以在main函数的最后调用glfwTerminate()来完成。
glfwTerminate();
return 0;
这样便能清理所有的资源并正确地退出应用程序。
现在我们完整的test.cpp代码如下:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
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);
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);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
while (!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
试编译并运行,如果没做错的话,你将会看到如下的输出:
一个非常无聊的黑色窗口,这就对了。
输入
我们同样也希望能够在GLFW中实现一些输入控制,这可以通过使用GLFW的几个输入函数来完成。我们将会使用GLFW的glfwGetKey函数,它需要一个窗口以及一个按键作为输入,这个函数将会返回这个按键是否正在被按下,我们将创建一个processInput函数来让所有的输入代码保持整洁。
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回键
glfwSetWindowShouldClose(window, true);
}
这里我们检查用户是否按下了返回键(Esc)(如果没有按下,glfwGetKey 将会返回 GLFW_RELEASE,如果用户的确按下了返回键,我们将通过 glfwSetwindowShouldClose 使用把 WindowShouldClose 属性设置为 true 的方法关闭 GLFW,下一次 while 循环的条件检测将会失败,程序就会进入 return 0
关闭。
我们接下来在渲染循环的每一个迭代中加入 processInput 的调用:
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
这就给我们一个非常简单的方式来检测特定的键是否被按下,并在每一帧做出处理。你也可尝试在 processInput()
中添加多个不同的按键。
渲染
我们要把所有的渲染 (Rendering) 操作放到渲染循环中,因为我们想让这些渲染指令在每次渲染循环迭代的时候都能被执行,代码将会是这样的:
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染指令
......
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
// 检查触发什么事件,更新窗口状态
glfwPollEvents();
}
为了测试一切都正常工作,我们使用一个自定义的颜色清空屏幕。
在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)
我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位 (Buffer Bit) 来指定要清空的缓冲,可能的缓冲位有 GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT 和 GL_STENCIL_BUFFER_BIT
由于现在我们只关心颜色值,所以我们只清空颜色缓冲 GL_COLOR_BUFFER_BIT,将下述代码加入到渲染循环的渲染指令部分
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
注意,除了 glClear()
之外,我们还调用了 glClearColor()
来设置清空屏幕所用的颜色,当调用 glClear()
函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为 glClearColor()
里所设置的颜色。
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
// 检查触发什么事件,更新窗口状态
glfwPollEvents();
}
编译运行:
现在,我们将屏幕设置为了类似黑板的深蓝绿色(当然你可以改为任意你喜欢的颜色),并且按Esc才会结束运行。
glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色。
至此,现在我们已经做好开始在渲染循环中添加许多渲染调用的准备了。
最后,是参考源代码:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
int main()
{
//实例化GLFW窗口
glfwInit();//glfw初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
//(宽,高,窗口名)返回一个GLFWwindow类的实例:window
if (window == NULL)
{
// 生成错误则输出错误信息
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 告诉GLFW我们希望每当窗口调整大小的时候调用改变窗口大小的函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad管理opengl函数指针,初始化glad
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
// 生成错误则输出错误信息
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
// 检查触发什么事件,更新窗口状态
glfwPollEvents();
}
// 释放之前的分配的所有资源
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 每当窗口改变大小,GLFW会调用这个函数并填充相应的参数供你处理
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
// 返回这个按键是否正在被按下
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回键
glfwSetWindowShouldClose(window, true);
}