原文链接
译者:ktxiaok
开始入门
本指南将会带你熟悉使用GLFW 3编写一个简单的应用。这个应用会创建一个窗口、OpenGL上下文,渲染一个旋转的三角形,并且实现当用户关闭窗口或者按下Escape键时程序退出的功能。本指南会介绍一些最常用的函数,但是实际上有更多。
本指南假定你没有使用GLFW早期版本的经验。如果你在过去使用过GLFW 2, 请阅读GLFW 2到3, 因为有些函数在GLFW 3相对于以前会表现得不同。
一步步来
包含GLFW头文件
在你应用的源文件使用GLFW的地方,你需要包含它的头文件。
#include <GLFW/glfw3.h>
这个头文件提供了GLFW API的所有常数、类型和函数原型。
默认情况下它也会包含来自你开发环境的OpenGL头文件。在一些平台上这个头文件只支持一些更老版本的OpenGL。最极端的情况是Windows,它典型地只支持OpenGL 1.2。
大多数程序会反而使用一个扩展加载程序库且包含它的头文件。本例将会使用通过glad生成的文件。GLFW头文件会检测大多数这样的头文件,如果它们被第一个包含进来,那么就不会再从你的开发环境中包含。
#include <glad/gl.h>
#include <GLFW/glfw3.h>
为了确保没有头文件冲突,你需要在GLFW的头文件之前定义 GLFW_INCLUDE_NONE 来显式禁用包含开发环境的头文件。这将会允许这两个头文件以任意的顺序来包含。
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <glad/gl.h>
初始化和终止GLFW
在你能使用大多数GLFW函数之前,必须要初始化程序库。对于成功的初始化,将会返回 GLFW_TRUE。如果发生错误,将会返回GLFW_FALSE。
if (!glfwInit()) { // Initialization failed }
注意到 GLFW_TRUE 和 GLFW_FALSE 通常会是1和0。
在你完成对GLFW的使用后,特别是在应用退出之前,你需要终止GLFW。
glfwTerminate();
这会销毁任何残留的窗口以及释放由GLFW分配的其他任何资源。在此调用之后,你必须在使用任何需要的GLFW函数之前再一次初始化GLFW。
设置一个错误回调
大多数事件都会通过回调来报告, 不管它是否一个按键被按下、一个GLFW窗口被移动或者一个发生一个错误。回调是被GLFW以描述事件所用的参数来调用的C函数(或者C++静态函数)。
如果有一个GLFW函数崩溃,一个错误会被报告给GLFW的错误回调函数。你可以通过错误回调来接受这些报告。这个函数必须拥有下面这样的签名(signature),但也可以在其他回调中做任何事情。
void error_callback(int error, const char* description)
{
fprintf(stderr, "Error: %s\n", description);
}
回调函数必须被设置,这样GLFW才知道去调用它们。这个设置错误回调的函数是少数的能够在初始化之前调用的GLFW函数之一,它能让你注意到在初始化中和之后发生的错误。
glfwSetErrorCallback(error_callback);
创建一个窗口和上下文
窗口和它的OpenGL上下文将会通过一个对 glfwCreateWindow 函数的调用来创建, 它会返回一个创建的组合窗口和上下文对象的句柄。
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
if (!window)
{
// Window or OpenGL context creation failed
}
这创建了一个带有OpenGL上下文的,分辨率为640x480的窗口化模式窗口。如果窗口或者OpenGL上下文创建过程失败,将会返回 NULL。
你应该总是检查返回的值。如果窗口创建罕见地失败了,那么依赖正确安装的驱动程序的上下文创建在即使安装了必要的硬件的机器上也会失败。
默认情况下,GLFW创建的OpenGL上下文可能拥有任意的版本。你可以请求最小的OpenGL版本,通过在创建之前设置 GLFW_CONTEXT_VERSION_MAJOR 和 GLFW_CONTEXT_VERSION_MINOR 提示(hints)。
如何请求的最小版本在机器上不受到支持,上下文(和窗口)创建将会失败。
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
if (!window)
{
// Window or context creation failed
}
窗口句柄会被传递所有窗口相关的函数,并会被提供给所有窗口相关的回调函数,所以它们可以分辨哪个窗口接收到了事件。
当一个窗口和上下文不再需要时,销毁它。
glfwDestroyWindow(window);
一旦这个函数被调用,不会再有其他事件发送给这个窗口,并且它的句柄会无效化。
设置当前OpenGL上下文
在你能够使用OpenGL API之前,你必须要拥有一个当前的OpenGL上下文。
glfwMakeContextCurrent(window);
这个上下文会持续保持在当前直到你设置其他的上下文在当前或者拥有这个上下文的窗口被销毁。
如果你使用一个扩展加载器程序库(extension loader library)来访问现代OpenGL,然后当需要初始化它时,加载器需要一个当前的上下文来加载。下面这个例子使用了glad,但是对于所有这样的程序库应用的规则都是一样的。
gladLoadGL(glfwGetProcAddress);
检查窗口关闭标示
每个窗口都有一个窗口需要被关闭的标示。
当用户尝试去关闭窗口时,不论是通过点击在标题栏上的叉号部件,还是使用一个组合键如Alt+F4,这个标示都会被设置成1。注意到窗口不是实际上被关闭了,所以你需要去监视这个标示来销毁窗口或者给用户某种反馈。
while (!glfwWindowShouldClose(window))
{
// Keep running
}
通过调用 glfwSetWindowCloseCallback 设置一个关闭回调你可以被告知用户何时尝试去关闭窗户。这个回调在关闭标示被设置时会被立即调用。
你也可以通过 glfwSetWindowShouldClose 来自己设置。如果你想要解释其他类型的关闭窗口的输入,像按下Escape键这样的例子,这将会十分有用。
接受输入事件
每个窗口都有许许多多的可以被设置的回调函数来接受所有各种各样的事件。为了接收按键按下和释放事件,需要创建一个按键回调函数。
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
按键回调像其他相关的窗口回调一样,是需要为每一个窗口设置的。
glfwSetKeyCallback(window, key_callback);
便于在事件发生时事件回调被调用,你需要按照下述说明处理事件。
使用OpenGL渲染
一旦你有了一个当前的OpenGL上下文,你就可以正常地使用OpenGL了。在本教程中,将会渲染一个多颜色的旋转的三角形。需要为了 glViewport 检索帧缓存大小。
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
你也能使用 glfwSetFramebufferSizeCallback 来设置一个帧缓存大小回调,这样你会在大小被更改时被告知。
如何使用OpenGL来渲染的细节超出了本教程的范围,但是这里有许多杰出的资源来学习现代OpenGL。这里将会列举一些:
- Anton’s OpenGL 4 Tutorials
- Learn OpenGL
- Open.GL
这些都使用了GLFW,但是无论你使用的创建窗口和上下文的API是什么,OpenGL它自身的工作方式都是相同的。
读取计时器
为了创建一个平滑的动画,需要一个时间源。GLFW提供了一个可以返回从初始化开始的秒数的计时器。这个使用的时间源在任何平台上大部分是精确的并且一般有着微秒级或者纳秒级的精度。
double time = glfwGetTime();
交换缓冲
GLFW默认使用了双缓冲技术。这意味着每个窗口会有两个渲染缓冲区,一个前置缓冲区和一个后置缓冲区。前置缓冲区会在屏幕上显示而后置缓冲区是你渲染的目标。
当整个帧已经渲染完毕时,两个缓冲区需要进行交换,所以后置缓冲区会变成前置缓冲区,反之亦然。
glfwSwapBuffers(window);
交换间隔指示了直到交换缓冲区前需要等待多少帧,通常被理解为垂直同步。默认情况下,交换间隔为0,意味着缓冲区交换会立即发生。在一些快速的机器上,因为屏幕保持以典型的60-75次每秒的速度更新,许多帧会永远看不到,所以这会浪费许多CPU和GPU周期。
而且,因为缓冲区可能会在屏幕更新的中途被交换,导致画面撕裂。
因此,应用需要代表性地设置交换间隔为1。也可以设置成更高的值,但是通常情况下这不会被推荐,因为这会导致输入延迟。
glfwSwapInterval(1);
这个函数会在当前上下文生效并且会在没有当前上下文的情况下失败。
处理事件
GLFW需要定期地与窗口系统进行交流,不仅是为了接收事件,还是为了让整个应用看起来没有卡住。当你有着可见的窗口时,事件处理必须定期执行,正常情况下它会在每帧的缓冲区交换之后进行。
这里有两种方式来处理挂起的事件。轮询(polling)和等待(waiting)。这个例子将会使用事件轮询,它只会处理已经接收到的事件并且会立即返回。
glfwPollEvents();
当连续渲染时这是最好的选择,像大多数的游戏那样。如果你只需要在收到新的输入后更新一次渲染,glfwWaitEvents 会是更好的选择。
它会一直等待直到收到至少一个事件,与此同时它会将线程进入休眠状态, 然后处理所有收到的事件。这将会节省大量的CPU周期,比如对于一些修改器界面来说是非常有用的。
把它们放在一起
现在你已经直到怎么去初始化GLFW,创建一个窗口和轮询(poll)键盘输入了。这可以让我们创建一个简单的程序。
这个程序会创建一个640x480分辨率的窗口化模式窗口,开启一个清理屏幕、渲染三角形和处理事件的渲染,直到用户按下Escape或者关闭窗口。
#include <glad/gl.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include "linmath.h"
#include <stdlib.h>
#include <stdio.h>
static const struct
{
float x, y;
float r, g, b;
} vertices[3] =
{
{ -0.6f, -0.4f, 1.f, 0.f, 0.f },
{ 0.6f, -0.4f, 0.f, 1.f, 0.f },
{ 0.f, 0.6f, 0.f, 0.f, 1.f }
};
static const char* vertex_shader_text =
"#version 110\n"
"uniform mat4 MVP;\n"
"attribute vec3 vCol;\n"
"attribute vec2 vPos;\n"
"varying vec3 color;\n"
"void main()\n"
"{\n"
" gl_Position = MVP * vec4(vPos, 0.0, 1.0);\n"
" color = vCol;\n"
"}\n";
static const char* fragment_shader_text =
"#version 110\n"
"varying vec3 color;\n"
"void main()\n"
"{\n"
" gl_FragColor = vec4(color, 1.0);\n"
"}\n";
static void error_callback(int error, const char* description)
{
fprintf(stderr, "Error: %s\n", description);
}
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
int main(void)
{
GLFWwindow* window;
GLuint vertex_buffer, vertex_shader, fragment_shader, program;
GLint mvp_location, vpos_location, vcol_location;
glfwSetErrorCallback(error_callback);
if (!glfwInit())
exit(EXIT_FAILURE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
window = glfwCreateWindow(640, 480, "Simple example", NULL, NULL);
if (!window)
{
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwSetKeyCallback(window, key_callback);
glfwMakeContextCurrent(window);
gladLoadGL(glfwGetProcAddress);
glfwSwapInterval(1);
// NOTE: OpenGL error checks have been omitted for brevity
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
glCompileShader(vertex_shader);
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
glCompileShader(fragment_shader);
program = glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
mvp_location = glGetUniformLocation(program, "MVP");
vpos_location = glGetAttribLocation(program, "vPos");
vcol_location = glGetAttribLocation(program, "vCol");
glEnableVertexAttribArray(vpos_location);
glVertexAttribPointer(vpos_location, 2, GL_FLOAT, GL_FALSE,
sizeof(vertices[0]), (void*) 0);
glEnableVertexAttribArray(vcol_location);
glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,
sizeof(vertices[0]), (void*) (sizeof(float) * 2));
while (!glfwWindowShouldClose(window))
{
float ratio;
int width, height;
mat4x4 m, p, mvp;
glfwGetFramebufferSize(window, &width, &height);
ratio = width / (float) height;
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
mat4x4_identity(m);
mat4x4_rotate_Z(m, m, (float) glfwGetTime());
mat4x4_ortho(p, -ratio, ratio, -1.f, 1.f, 1.f, -1.f);
mat4x4_mul(mvp, p, m);
glUseProgram(program);
glUniformMatrix4fv(mvp_location, 1, GL_FALSE, (const GLfloat*) mvp);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
上面的程序可以在source package的examples/simple.c处找到,并且会在你构建GLFW时连同其他所有例子一起被编译。如果你从source package构建GLFW,你就已经拥有了在Windows上的simple.exe,在Linux上的simple,或者在macOS上的simple.app。
本教程只使用了其中少数的GLFW提供的大量函数。