• 文章
  • 一個簡單的 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 示例中的冗餘。