OpenGL是什么
OpenGL是一个规范而非一个API,也就是说它是一系列与图形、图像相关的函数的功能规定,而非实现。OpenGL自3.3版本开始废弃了立即渲染模式(固定的渲染管线),并开始采用核心模式。
OpenGL是一个巨大的状态机,其状态被称为上下文,开发者可以通过状态设置函数、状态使用函数等设置OpenGL上下文并进行操作。OpenGL中的对象可以被视为一个C风格结构体,它是一些选项的集合,代表OpenGL状态的一个子集。
准备工作
要想画出图形,首先要创建OpenGL上下文,并且需要一个窗口来显示图形。然而OpenGL并不提供创建窗口等的方法,因此为了省下写代码操作操作系统的精力,可以引入GLFW库来获取这些功能。此外,OpenGL的函数位置需要在运行时获取,因此需要在运行时获取函数地址并保存在函数指针中。GLAD可以方便地实现这一过程。在Visual Studio中新建项目,链接好OpenGL库、GLFW库和GLAD库,并将glad.c添加到项目中后,便可以在程序中引用glad、glfw等库了。
由于本文并不把重点放在具体如何下载和链接这些库上,建议参考创建窗口 – LearnOpenGL CN进行上述操作。
第一个窗口——支起一块黑板
必要的库准备好后,就可以通过代码来创建窗口了。当然,要先初始化OpenGL。
// 因为实际opengl的头文件由GLAD引用,所以此处的两个头文件不能交换顺序
#include <glad/glad.h>
#include <GLFW/glfw3.h>
int main()
{
glfwInit();
// 指定opengl版本(此处是4.6)
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
}
关于第三个glfwWindowHint
中的选项,可以在GLFW: Window guide中看到注解,前一个参数是选项名称,后一个参数是选项的值:
GLFW_OPENGL_PROFILE specifies which OpenGL profile to create the context for. Possible values are one of
GLFW_OPENGL_CORE_PROFILE
orGLFW_OPENGL_COMPAT_PROFILE
, orGLFW_OPENGL_ANY_PROFILE
to not request a specific profile. If requesting an OpenGL version below 3.2,GLFW_OPENGL_ANY_PROFILE
must be used. If OpenGL ES is requested, this hint is ignored.
设置好窗口属性后就可以创建窗口对象,用于存放所有和窗口有关的数据
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);
注:此部分内容仅作补充,便于理解,实际并不需要过于关心
在GLFW的库中,GLFWwindow结构体的内容如下方所示(该内容是被封装的因此除非是库开发者否则不应在任何情况下进行修改)。这是一个十分庞大的结构体,而在创建窗口对象时,开发者可以通过提供的API来设置窗口的属性,API的内部实现如右侧所示。可以粗略地看出,这一过程就是将尺寸、标题等内容传入API中,赋给一系列config并最终创建GLFWwindow对象。
struct _GLFWwindow
{
struct _GLFWwindow* next;
// Window settings and state
// ...(此处省略部分成员)
GLFWvidmode videoMode;
_GLFWmonitor* monitor;
_GLFWcursor* cursor;
char* title;
// ...(此处省略部分成员)
_GLFWcontext context;
struct {
GLFWwindowposfun pos;
GLFWwindowsizefun size;
// ...(此处省略部分成员)
} callbacks;
// This is defined in platform.h
GLFW_PLATFORM_WINDOW_STATE
};
GLFWAPI GLFWwindow* glfwCreateWindow(
int width, int height,
const char* title,
GLFWmonitor* monitor,
GLFWwindow* share)
{
_GLFWfbconfig fbconfig;
_GLFWctxconfig ctxconfig;
_GLFWwndconfig wndconfig;
_GLFWwindow* window;
// ...(此处省略部分实现)
fbconfig = _glfw.hints.framebuffer;
ctxconfig = _glfw.hints.context;
wndconfig = _glfw.hints.window;
wndconfig.width = width;
wndconfig.height = height;
wndconfig.title = title;
ctxconfig.share = (_GLFWwindow*) share;
// ...(此处省略部分实现)
window->monitor = (_GLFWmonitor*) monitor;
// ...(此处省略部分实现)
window->title = _glfw_strdup(title);
if (!_glfw.platform.createWindow(window, &wndconfig, &ctxconfig, &fbconfig))
{
glfwDestroyWindow((GLFWwindow*) window);
return NULL;
}
return (GLFWwindow*) window;
}
在调用openGL的函数之前,我们还需要初始化GLAD来管理函数指针。
// GLAD用于管理opengl的函数指针,在调用opengl的函数之前要先初始化GLAD,
// 此处将一个glfw的函数传入了GLAD中
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
有了GLAD,我们就可以调用OpenGL的函数来设置视口尺寸了。注意,窗口尺寸和视口尺寸是两个概念。窗口尺寸是指实际的窗口像素数,而视口则表示渲染场景的区域。OpenGL 渲染是基于一种坐标空间的,窗口的尺寸并不直接影响 OpenGL 渲染的内容。OpenGL 使用标准化设备坐标(NDC),然后通过视口变换将其映射到窗口的像素坐标。如果窗口尺寸变化,视口映射就需要重新设置,才能确保渲染的内容正确显示,否则可能出现渲染内容失真或超出窗口范围等问题。
// 设置视口尺寸
glViewport(0, 0, 800, 600);
// 设置窗口尺寸的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
为了不让程序在渲染出一帧后立刻结束,我们需要进行一个while循环,使得窗口只有在被认为应该关闭时关闭。而对于每一帧,我们检测用户是否按下了ESC,如果按下了就认为窗口应该关闭,否则就渲染一帧。我们暂时让OpenGL每一帧都把窗口涂满像黑板的颜色,清屏操作通过使用状态使用函数glClear()
实现,而状态设置函数glClearColor()
则可以设置清屏后的颜色。
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
// 设置窗口应当关闭,将在渲染循环中关闭窗口
glfwSetWindowShouldClose(window, true);
}
int main()
{
...
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;
}
注意到过程中还使用了glfwSwapBuffers(window)
。这是因为我们使用了双缓冲来防止闪烁等问题。以下补充引用自你好,窗口 – LearnOpenGL CN:
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
在交换双缓冲后,glfw还需要检查一下事件的触发,例如窗口尺寸的变化等。最后调用glfwTerminate来释放使用的资源。
到此为止,我们已经可以生成第一个窗口,之后我们将用它来渲染第一个三角形: