• 文章
  • 一个简单的 OpenGL 动画,使用 glfw,逐步讲解
发布者:
2014 年 12 月 2 日 (最后更新:2014 年 12 月 2 日)

一个简单的 OpenGL 动画,使用 glfw,逐步讲解

评分:4.1/5 (294 票)
*****
作者:Manu Sánchez

glfw 是一个用于 OpenGL 应用程序的 C 语言窗口管理库,它是旧的、广为人知的 GLUT 和 freeGLUT 库的替代品。该库得到了积极维护,并附带了一系列优秀的示例和文档。

在本文中,我们将学习如何轻松设置一个 OpenGL 应用程序,这得益于 glfw,我们将通过一个简单的动画来模拟一个弹跳的小球。

glfw API 概述


glfw 是一个 C API,它依赖回调来处理 OpenGL 应用程序所需的各种配置、事件、错误等。
此外,您可能使用的多个资源,例如窗口、OpenGL 上下文等,都由库内部管理,它只提供句柄作为这些资源的标识符。

 
GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);


这里的 `window` 变量只是一个窗口的句柄,您通过调用 `glfwCreateWindow()` 函数请求该窗口。您无需手动释放窗口资源,因为它由库管理。当然,如果您出于任何原因想删除该窗口,也可以这样做。

 
glfwDestroyWindow(window);


在该调用之后,`window` 句柄将失效,它所代表的窗口将关闭。

这种设计的要点是:库管理资源,您只使用它们。因此没有资源泄露。您可以通过 API 提供的回调来自定义与这些资源的交互。

例如:当我的窗口大小调整时会发生什么?我需要重新排列我的 OpenGL 渲染的视口!不用担心,您可以告诉 glfw 在这种情况下该怎么做,只需设置一个回调。

1
2
3
4
5
6
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);


我们的目标:一个小巧、有趣、有弹性、非常可爱的小球


让我们编写一个简单的白色弹跳球动画。我不是游戏设计师,这里的目标是仅用几行代码就能让动画工作。

提前向任何看了这张图片眼睛会不适的人道歉

正如我所说,我是一名程序员……

一个 C++11 的 glfw 应用程序

glfw 有一个 C API。这很好,但我是一名 C++ 程序员。让我们将这个 API 包装在一个简单的基于继承的小框架中。

`glfw_app` 基类


我提议的是一个简单的设计,将所有重复性任务委托给基类,然后通过继承和多态性自定义您需要的内容,从而以简单的方式创建自定义的基于 glfw 的 OpenGL 应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class glfw_app 
{
public:
    glfw_app(const std::string& window_title, int window_width, int window_height);
    virtual ~glfw_app();
    
    void start();

    virtual void on_keydown(GLFWwindow* window, int key, int scancode, int action, int mods);
    virtual void on_error(int error, const char* desc);
    virtual void on_resize(GLFWwindow* window, int width, int height);
    virtual void glloop() = 0;
    
    GLFWwindow* window() const;
};


这个基类很简单:它为我们管理一个 glfw 窗口及其 OpenGL 上下文,封装(并当前隐藏)事件和渲染循环,最后提供了一些多态函数来告诉我们在按下按键时、窗口大小调整时等应该做什么。

以最简单的 glfw 示例——一个简单的三角形(摘自 glfw 文档)。借助我们的 `glfw_class` 类,只需几行代码就可以写出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void triangle::on_keydown(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
		glfwSetWindowShouldClose(window, GL_TRUE);
}

void triangle::glloop()
{
	float ratio = glfw_app::framebuffer_width() / (float)glfw_app::framebuffer_height();

	glClear(GL_COLOR_BUFFER_BIT);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f);
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glRotatef((float)glfwGetTime() * 50.f, 0.f, 0.f, 1.f);

	glBegin(GL_TRIANGLES);
	glColor3f(1.f, 0.f, 0.f);
	glVertex3f(-0.6f, -0.4f, 0.f);
	glColor3f(0.f, 1.f, 0.f);
	glVertex3f(0.6f, -0.4f, 0.f);
	glColor3f(0.f, 0.f, 1.f);
	glVertex3f(0.f, 0.6f, 0.f);
	glEnd();
}


就这样!所有其他事情(缓冲交换、窗口和 gl 上下文管理等)都由基类完成。怎么做?我们一步一步来看。

资源管理


如上所述,`glfw_app` 类旨在管理一个 glfw 窗口及其相应的 OpenGl 设置。因此,所有 glfw/OpenGL 设置都在类的构造函数中完成,所有清理工作都在析构函数中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
glfw_app::glfw_app(const std::string& window_title , int window_width , int window_height)
{
    if( !glfwInit() )
        throw std::runtime_error
    {
        "Unable to initialize glfw runtime"
    };

    _window = glfwCreateWindow(window_width , window_height , window_title.c_str() , nullptr , nullptr);

    if( !_window )
        throw std::runtime_error
    {
        "Unable to initialize glfw window"
    };

    glfwMakeContextCurrent(_window);
    glfwSwapInterval(1);
}

glfw_app::~glfw_app()
{
    glfwDestroyWindow(_window);
    glfwTerminate();
}


该类充当单例:每个应用程序只有一个 `glfw_app` 实例,因为只有一个 glfw 应用程序(应用程序本身)。

主循环


主循环是封装的。这使得编写自定义 OpenGL 应用程序更加简单,因为在大多数情况下,这个循环几乎是相同的(获取事件、渲染、交换缓冲)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void glfw_app::start()
{
    glfwloop();
}

void glfw_app::glfwloop()
{
    while( !glfwWindowShouldClose(_window) )
    {
	    //Here we call our custom loop body
        this->glloop(); 

        glfwSwapBuffers(_window);
        glfwPollEvents();
    }
}


事件处理


`glfw_app` 有一些形式为 `on_EVENT()` 的多态函数用于事件处理。它们只是包装了原始的 glfw 回调,但通过多态性进行自定义对于 OOP 程序员来说更自然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void glfw_app::on_keydown(GLFWwindow* window , int key , int scancode , int action , int mods) 
{
    //Does nothing by default. Override to customize
}

void glfw_app::on_error(int error , const char* desc) 
{
    //Does nothing by default
}

void glfw_app::on_resize(GLFWwindow* window , int width , int height)
{
    //By defualt rearranges OpenGL viewport to the current framebuffer size.

    glViewport(0 , 0 , width , height);
}


回调 API 与 OOP


这并不容易。我们不能仅仅将多态函数传递给 C 回调,因为它们不能转换为纯函数对象。这是有道理的,因为(即使忽略动态分派部分)它们需要一个对象来调用。

为了能够将这些多态函数作为回调注入 glfw API,我们需要在 C 和 C++ 世界之间架起一座桥梁。`static` 成员函数!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class glfw_app_manager
{
    static glfw_app* _app;
    
    static void on_keydown(GLFWwindow* window, int key, int scancode, int action, int mods)
    {
        if(_app) _app->on_keydown(window,key,scancode,action,mods);
    }
    
    static void on_error(int error, const char* desc)
    {
        if(_app) _app->on_error(error,desc);
    }
    
    static void on_resize(GLFWwindow* window, int width, int height)
    {
        if(_app) _app->on_resize(window,width,height);
    }
    
public:
    static void start_app(glfw_app* app)
    {
        _app = app;
        
        glfwSetKeyCallback(app->window() , on_keydown);
        glfwSetFramebufferSizeCallback(app->window() , on_resize);
        glfwSetErrorCallback(on_error);
    }
};


如前所述,我们的应用程序类实际上是一个单例。`glfw_app_manager` 类负责管理它。它存储当前应用程序实例,注册我们的桥梁作为回调,然后通过它们调用我们的应用程序函数。

最后,通过编写一个函数模板来使 glfw 应用程序的实例化更加容易,为我们的小框架添加一些装饰。

1
2
3
4
5
6
7
8
9
template<typename T , typename... ARGS , typename = typename std::enable_if<std::is_base_of<glfw_app,T>::value>::type>
std::unique_ptr<T> make_app(ARGS&&... args)
{
    std::unique_ptr<T> app{ new T{ std::forward<ARGS>(args)...} };
    
    glfw_app_manager::start_app(app.get());
    
    return app;
}


使用它,设置一个 glfw 应用程序可以像这样简单:

1
2
3
4
5
6
7
8
9
#include "glfw_app.hpp"
#include "your_glfw_app.hpp"

int main()
{
    auto app = make_app<your_glfw_app>("glfw!" , 800 , 600);
    
    app->start();
}


长话短说。给我看球!


这是弹跳球 glfw 应用程序的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ball : public glfw_app
{
public:
	template<typename... ARGS>
	ball(ARGS&&... args) : glfw_app{ std::forward<ARGS>(args)... } , 
		x_ball{ 0.0f },
		y_ball{ 0.8f },
		vx_ball{ 0.0f },
		vy_ball{ 0.0f }
	{}

	virtual void on_keydown(GLFWwindow* window, int key, int scancode, int action, int mods) override;

	virtual void glloop() override;

private:
	float x_ball, y_ball;
	float vx_ball, vy_ball;
	const float gravity = 0.01;
	const float radius = 0.05f;

	void draw_ball();
};


我们有球的坐标、球的速度和它的半径。还有一个 `gravity` 常量,因为我们希望我们的球弹跳。
构造函数中的模板部分是一个带有完美转发的可变参数模板,只是为了将所有参数传递给基类构造函数。

`on_keydon()` 回调并不复杂:当用户按下 ESC 键时,它只是关闭窗口。

1
2
3
4
5
void ball::on_keydown(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
		glfwSetWindowShouldClose(window, GL_TRUE);
}


现在让我们看看渲染循环的主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ball::glloop()
{
	float ratio = framebuffer_width() / (float)framebuffer_height();

	glClear(GL_COLOR_BUFFER_BIT);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f);
	glMatrixMode(GL_MODELVIEW);

	//Bounce on window bottom
	if (y_ball + radious <= radious)
		vy_ball = std::abs(vy_ball);
	else
		vy_ball -= gravity; //Apply gravity

	//Update ball coordinates
	x_ball += vx_ball;
	y_ball += vy_ball;

	//Lets draw the ball!
	draw_ball();
}


注意球是如何投影的。我们的 OpenGL 场景的可视区域(与视口匹配的区域)在两个轴上都从 -1 到 1,其中 -1 是我们窗口的左下角,1 是左上角。
使用坐标 [-1,1] 可以轻松处理窗口边界,因为它们与窗口大小无关。

看看动画是如何工作的

1
2
3
4
5
6
7
8
9
10
11
12
	//Bounce on window bottom
	if (y_ball - radious <= - 1)
		vy_ball = std::abs(vy_ball);
	else
		vy_ball -= gravity; //Apply gravity

	//Update ball coordinates
	x_ball += vx_ball;
	y_ball += vy_ball;

	//Lets draw the ball!
	draw_ball();


球的位置和速度根据方程 `v' = v + a*t` 和 `p' = p + v * t` 更新,其中 `v` 是速度,`a` 是加速度(`gravity` 常量),`t` 是时间。

时间以帧为单位,所以在所有方程中 `t` 都为一。这就是为什么我们的代码中没有 `t` 的原因。如果您想要稳定的模拟(与帧率无关),您应该使用更复杂的技术,例如 这篇文章中描述的技术。
如果球超出窗口边界,即 `y_ball - radious` 小于 -1,我们应该让球向上运动:将垂直速度设置为正值。

1
2
if (y_ball - radious <= - 1)
    vy_ball = std::abs(vy_ball);


同时施加重力。球弹跳时不要施加加速度。

最后一步是绘制球:使用 `GL_POLYGON` 绘制一个白色的“圆”(一个正多边形)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ball::draw_ball()
{
	const float full_angle = 2.0f*3.141592654f;
	float x, y;

	glBegin(GL_POLYGON);
	glColor3f(1.0f, 1.0f, 1.0f);

	for (std::size_t i = 0; i < 20; ++i)
	{
		x = x_ball + radious*(std::cos(i*full_angle / 20.0f));
		y = y_ball + radious*(std::sin(i*full_angle / 20.0f));

		glVertex2f(x, y);
	}

	glEnd();
}


就这样!现在启动我们的球应用程序。

1
2
3
4
5
6
7
8
9
#include "glfw_app.hpp"
#include "ball.hpp"

int main()
{
    auto app = make_app<ball>("bouncing ball!" , 800 , 600);
    
    app->start();
}


构建并运行示例


biicode 是 C 和 C++ 的依赖管理器,类似于 Python 的 pip 或 Java 的 Maven。它们提供了包含 glfw 库的块(包),因此跨多个平台运行我们的示例非常容易。
我们的弹跳球示例已发布为 manu343726/glfw-example 块。打开并运行它就像这样简单:


$ bii init biicode_project
$ cd biicode_project
$ bii open manu343726/glfw_example
$ bii cpp:configure
$ bii cpp:build
$ ./bin/manu343726_glfw-example_main

如果 Linux 平台缺少 glfw 所需的一些 X11 库,构建可能会失败。它们在 `bii cpp:configure` 期间进行检查,如果出现问题,请关注其输出。

另请注意,本文的代码片段针对 C++11,因此您应该使用符合 C++11 标准的编译器,例如 GCC 4.8.1(Ubuntu 14.04 和最新的 MinGW for Windows 默认提供)、Clang 3.3 或 Visual Studio 2013。

最后,如果您想尝试更多 glfw 示例,biicode 的人员有一个 examples/glfw 块,其中包含从原始 glfw 发行版中提取的一整套示例。


$ bii open examples/glfw
$ bii cpp:configure
$ bii cpp:build
$ ./bin/examples_glfw_particles

摘要


glfw 是编写 OpenGL 应用程序的绝佳库。它的 C API 清晰简洁,并且只需稍加努力就可以使其以 C++ 的方式工作。
我们在本文中学习了如何创建一个小框架来以面向对象的方式编写简单的 OpenGL 应用程序。将最常见的任务封装在基类中可以减少简单 OpenGL 示例中的冗余。