OpenGL学习笔记(一)
参考资料:LearnOpenGL中文翻译
一、OpenGL是什么
OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范。
OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(注:这里开发者是指编写OpenGL库的人)。
实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。
OpenGL3.3规范文档
二、OpenGL自身是一个状态机
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。
假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。
当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态应用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。
三、对象(Object)
在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct)。
当我们使用一个对象时,通常看起来像如下一样(把OpenGL上下文看作一个大的结构体):
// OpenGL的状态
struct OpenGL_Context
{
...
object* object_Window_Target;
...
};
// 创建对象
GLuint objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);
这一小段代码展现了你以后使用OpenGL时常见的工作流。我们首先创建一个对象,然后用一个id保存它的引用(实际数据被储存在后台)。然后我们将对象绑定至上下文的目标位置(例子中窗口对象目标的位置被定义成GL_WINDOW_TARGET)。接下来我们设置窗口的选项。最后我们将目标位置的对象id设回0,解绑这个对象。设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效。
过程:
① 创建对象,id保存到GLunit变量中
② 绑定对象到Context
③ 通过Context设置对象的参数
④ 解绑对象
四、开始
创建窗口
在我们画出出色的效果之前,首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。然而,这些操作在每个系统上都是不一样的,OpenGL有目的地从这些操作抽象(Abstract)出去。这意味着我们不得不自己处理创建窗口,定义OpenGL上下文以及处理用户输入。
幸运的是,有一些库已经提供了我们所需的功能,其中一部分是特别针对OpenGL的。这些库节省了我们书写操作系统相关代码的时间,提供给我们一个窗口和上下文用来渲染。最流行的几个库有GLUT,SDL,SFML和GLFW。在教程里我们将使用GLFW。
使用到的库
GLFW
和 GLAD
GLFW
:主要用于创建窗口、OpenGL上下文、接收一些鼠标键盘事件等等。
GLAD
:是对底层OpenGL接口的封装,可以让你的代码跨平台。
开始代码
准备GLFW
void initGLFW() {
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
}
在main函数中调用glfwInit函数来初始化GLFW,然后我们可以使用glfwWindowHint函数来配置GLFW。前面两个glfwWindowHint是为了使用与3.3版本的OpenGL所对应的上下文对象而进行设置。第三个glfwWindowHint是设置所使用的是OpenGL的核心模式(Core-profile)。另外可以设置以下代码禁止用户调整窗口大小:
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
创建窗口对象
GLFWwindow* initWindow() {
// 调用glfwCreateWindow函数
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return nullptr;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
return window;
}
调用glfwCreateWindow
函数,前面两个参数是规定窗口的宽和长,第三个参数是窗口的名称,最后两个参数我们暂时忽略,它的返回值是一个窗口对象的指针。
然后调用glfwMakeContextCurrent
函数设置window
对象为当前的上下文对象。并设置当窗口被用户拖动改变大小时的回调函数。
初始化GLAD
bool initGLAD() {
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return false;
}
return true;
}
当我们初始化了GLAD
之后,才能使用其封装的一些OpenGL
的API
。
渲染循环
我们希望程序在我们明确地关闭它之前不断绘制图像并能够接受用户输入。因此,我们需要在程序中添加一个while循环,我们可以把它称之为游戏循环(Game Loop),它能在我们让GLFW退出前一直保持运行。下面几行的代码就实现了一个简单的游戏循环:
while(!glfwWindowShouldClose(window))
{
glfwPollEvents();
glfwSwapBuffers(window);
}
glfwWindowShouldClose
函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后游戏循环便结束了,之后为我们就可以关闭应用程序了。glfwPollEvents
函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。glfwSwapBuffers
函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。
双缓冲机制:
前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
循环后结束程序
glfwTerminate();
return 0;
增加输入响应
我们希望点击键盘Esc键时跳出渲染循环并结束程序:
void processInput(GLFWwindow *window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
即如果接收到Esc键按下,则设置window
对象的windowShouldClose属性为true。
在渲染循环中添加:
while(!glfwWindowShouldClose(window)) {
// input
// -----
processInput(window); <---
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwPollEvents();
glfwSwapBuffers(window);
}
添加渲染事件
在每个新的渲染迭代开始的时候我们总是希望清屏,否则我们仍能看见上一次迭代的渲染结果(这可能是你想要的效果,但通常这不是)。我们可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BIT
,GL_DEPTH_BUFFER_BIT
和GL_STENCIL_BUFFER_BIT
。由于现在我们只关心颜色值,所以我们只清空颜色缓冲。
除了glClear
之外,我们还调用了glClearColor
来设置清空屏幕所用的颜色。当调用glClear
函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor
里所设置的颜色。在这里,我们将屏幕设置为了类似黑板的墨绿色。
void renderLoop(GLFWwindow *window) {
// 设置清屏时填充的颜色(墨绿色)
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// render loop
// -----------
while (!glfwWindowShouldClose(window)) {
// input
// -----
processInput(window);
// render
// ------
glClear(GL_COLOR_BUFFER_BIT); <--清屏
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
}
运行结果
运行成功!