GLFW 入门

971 阅读10分钟

GLFW: Getting started

开始

目录表

  • 一步一步地
    • 包括GLFW头文件
    • 初始化和终止GLFW
    • 设置错误回调
    • 创建窗口和上下文
    • 使当前是OpenGL上下文
    • 检查窗口关闭标志
    • 接收输入事件
    • 使用OpenGL渲染
    • 读取定时器
    • 交换缓冲区
    • 处理事件
  • 把它们放在一起

本指南将介绍如何使用GLFW 3编写一个简单的应用程序。应用程序将创建一个窗口和OpenGL上下文,呈现一个旋转三角形,并在用户关闭窗口或按Escape键时退出。本指南将介绍一些最常用的函数,但还有更多。

本指南假定没有使用早期版本GLFW的经验。如果您过去使用过GLFW 2,请阅读从GLFW 2迁移到GLFW 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 ())  
{  
    //初始化失败  
}

请注意,GLFW_TRUE和GLFW_FALSE现在和将来都是1和0。
当您使用完GLFW后,通常在应用程序退出之前,您需要终止GLFW。

glfwTerminate ();  

这将破坏所有剩余的窗口并释放由GLFW分配的任何其他资源。调用之后,必须再次初始化GLFW。

设置错误回调

大多数事件都是通过回调报告的,无论是按下的键、移动的GLFW窗口还是发生的错误。回调是由GLFW调用的带有描述事件参数的C函数(或c++静态方法)。
如果GLFW函数失败,则会向GLFW错误回调函数报告一个错误。您可以通过错误回调接收这些报告。此函数必须具有下面的签名,但可以执行其他回调中允许的任何操作。

void error_callback(int error, const char* description)
{
    fprintf(stderr, "Error: %s\n", description);
}

必须设置回调函数,这样GLFW才知道调用它们。设置错误回调的函数是少数几个可以在初始化之前调用的GLFW函数之一,它可以在初始化期间和之后通知您错误。

glfwSetErrorCallback(error_callback);

创建窗口和上下文

窗口和它的OpenGL上下文是通过调用glfwSetWindowCloseCallback.来创建的,它返回一个句柄到创建的窗口和上下文对象的组合

GLFWwindow* window = glfwCreateWindow(640,480,"My Title", NULL, NULL);
if(!window)
{
    // 窗口或OpenGL上下文创建失败
}

这将创建一个带有OpenGL上下文的640 * 480窗口模式窗口。如果窗口或OpenGL上下文创建失败,将返回NULL。您应该始终检查返回值。虽然窗口创建很少失败,但上下文创建取决于正确安装的驱动程序,甚至在具有必要硬件的机器上也可能失败。

默认情况下,GLFW创建的OpenGL上下文可以有任何版本。您可以通过在创建之前设置GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR提示来要求最小OpenGL版本。如果机器上不支持所需的最低版本,则上下文(和窗口)创建失败。

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINJOR,0);
GLFWwindow* window = glfwCreateWindow(640,480,"My Title",NULL,NULL);
if(!window)
{
    // 窗口或上下文创建失败
}

窗口句柄传递给所有与窗口相关的函数,并提供给所有与窗口相关的回调函数,因此它们可以告诉哪个窗口接收了事件。

当不再需要一个窗口和上下文时,销毁它。

glfwDestroyWindow(window);

一旦调用这个函数,就不会再为该窗口传递事件,并且它的句柄无效。

将OpenGL上下文置为当前

在你可以使用OpenGL API之前,你必须有一个当前的OpenGL上下文。

glfwMakeContextCurrent(window);

该上下文将保持当前状态,直到您将另一个上下文设置为当前状态,或者直到拥有当前上下文的窗口被销毁。

如果你正在使用扩展加载器库来访问现代OpenGL,那么这就是初始化它的时候,因为加载器需要一个当前上下文来加载。这个例子使用了glad,但是同样的规则适用于所有这样的库。

gladLoadGL (glfwGetProcAddress);

检查窗口关闭标志

每个窗口都有一个标志,指示该窗口是否应该关闭。

当用户试图关闭窗口时,通过按标题栏中的关闭小部件或使用像Alt+F4这样的组合键,该标志被设置为1。请注意,该窗口实际上并没有关闭,因此您应该监视该标志,并销毁该窗口或向用户提供某种反馈。

while(!glfwWindowShouldClose(window))
{
    //保持运行
}

当用户试图关闭窗口时,你可以通过使用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(窗口,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的优秀资源。以下是其中的一些:

这些碰巧都使用GLFW,但OpenGL本身的工作原理是相同的,无论你使用什么API来创建窗口和上下文。

读取定时器

为了创建流畅的动画,需要一个时间源。GLFW提供了一个计时器,返回自初始化以来的秒数。所使用的时间源在每个平台上都是最精确的,通常具有微秒或纳秒分辨率。

double time = glfwGetTime();

交换缓冲区

默认情况下,GLFW窗口使用双缓冲。这意味着每个窗口都有两个渲染缓冲区;一个前缓冲,一个后缓冲。前缓冲区是要显示的缓冲区,后缓冲区是要渲染的缓冲区。

当整个帧被渲染后,缓冲区需要相互交换,所以后缓冲区变成前缓冲区,反之亦然。

glfwSwapBuffers(window);

交换间隔表示在交换缓冲区之前需要等待多少帧,通常称为vsync。默认情况下,交换间隔为零,这意味着缓冲区交换将立即发生。在快速的机器上,许多这些帧永远不会被看到,因为屏幕通常每秒只更新60-75次,所以这浪费了大量的CPU和GPU周期。

另外,由于缓冲区会在屏幕更新的中间进行交换,导致屏幕撕裂。

由于这些原因,应用程序通常希望将交换间隔设置为1。它可以设置为更高的值,但通常不建议这样做,因为它会导致输入延迟。

glfwSwapInterval (1);

此函数作用于当前上下文,除非上下文是当前的,否则将失败。

处理事件

GLFW需要定期与窗口系统通信,以便接收事件并显示应用程序尚未锁定。事件处理必须在有可见窗口时定期执行,通常在缓冲区交换后的每一帧执行。

处理挂起事件有两种方法;轮询和等待。这个例子将使用事件轮询,它只处理那些已经接收到的事件,然后立即返回。

glfwPollEvents ();

当像大多数游戏一样持续渲染时,这是最好的选择。如果您只需要在接收到新输入后更新呈现,那么glfwWaitEvents是更好的选择。它将等待,直到至少接收到一个事件,在此期间将线程置于睡眠状态,然后处理所有接收到的事件。这节省了大量的CPU周期,并且对于许多类型的编辑工具都很有用。

把它们放在一起

现在您已经知道了如何初始化GLFW、创建窗口和轮询键盘输入,那么就可以创建一个简单的程序了。

该程序创建一个640 * 480的窗口模式窗口,并启动一个循环,清除屏幕,呈现三角形并处理事件,直到用户按下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);

    // 注意:为简洁起见,省略了OpenGL错误检查

    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);
}

上面的程序可以在源代码包的examples/simple.c中找到,并且在构建GLFW时与所有其他示例一起编译。如果您从源包构建GLFW,那么您已经在Windows上有simple.exe,在Linux上有simple或simple.app在macOS。

本教程只使用了GLFW提供的许多函数中的几个。GLFW涵盖的每个领域都有指南。每个指南将介绍该类别的所有功能。

本教程到此结束。一旦你编写了一个使用GLFW的程序,你就需要编译和链接它。如何做到这一点取决于您正在使用的开发环境,该环境的文档最好地解释了这一点。要了解特定于GLFW的详细信息,请参阅构建应用程序.。