1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 12:45:29 +08:00

Irregular update

This commit is contained in:
Meow J
2016-08-23 01:14:57 +08:00
parent 07cba71890
commit d97d795cfc
39 changed files with 1019 additions and 44 deletions

View File

@@ -4,48 +4,46 @@
---|---
作者 | JoeyDeVries
翻译 | HHz(qq:1158332489)
校对 | 暂无
校对 | Meow J
看完前面的教程之后我们已适当地了解了OpenGL内部作原理,并且我们已经能够用这些知识绘制一些复杂的图。然而,我们还只能做出一些小Demo并未达到用OpenGL开发实际应用的水平。这一部分主要介绍了一个用OpenGL制作的很简单的2D游戏。本部分教程将展现OpenGL在较大较复杂程序中的强大之处
看完前面的教程之后我们已经了解了非常多的OpenGL内部作原理,并且我们已经能够用这些知识绘制一些复杂的图。然而,除了之前的几个技术演示之外,我们还没有真正利用OpenGL开发一个实际应用。这篇教程为OpenGL 2D游戏制作系列教程的入门篇。这个系列教程将展示我们该如何将OpenGL应用到更大更复杂的环境中。注意这个系列教程不一定会引入新的OpenGL概念但会或多或少地向我们展示如何将所学的概念应用到更大的程序中去
!!! Attention
由于我们希望事情能够简单一点我们会以别人开发过的一个2D街机游戏为基础开发我们自己的2D游戏。这就是<def>Breakout</def>一个于1976年发布的运行在Atari 2600主机上的经典2D游戏。游戏要求玩家通过操控一个挡板反弹小球以破坏所有的砖块而不能让小球到达屏幕底端。当玩家破坏了所有砖块的时候即为完成了游戏。
注意本部分内容不一定引入新的OpenGL概念但会或多或少地向我们展示示如何将前面所学的概念应用到更大的程序中去。
下面我们可以看到Breakout原本在Atari 2600上是什么样子的
由于我们要做到尽可能地简化我们将开发一个别人开发过的2D电脑游戏。简单介绍一下本节的主要内容本节将开发一个1976年运行在Atari2600上的经典2D游戏控制台程序。游戏要求玩家通过操控一个挡板反弹一个小球撞倒所有的砖块并且要让小球不超出挡板边界玩家消灭所有砖块即为完成了游戏。
![](../../img/06/Breakout/01/breakout2600.png)
![](http://www.learnopengl.com/img/in-practice/breakout/breakout2600.png)
游戏有以下几个机制:
游戏有以下要点:
- 玩家能够控制一个小挡板,它只能在屏幕范围内左右移动。
- 球在屏幕上运动,每一次碰撞都会使球根据碰撞位置改变运动方向。对屏幕边框,砖块,挡板都需要有碰撞判定。
- 当球运动到屏幕底部边界的时候,游戏结束或者玩家丧失一点生命值。
- 球碰到砖块的时候,砖块会被破坏。
- 当所有砖块都被破坏,玩家获胜。
- 球的方向可以通过球碰撞在挡板上的位置(即到挡板中心的距离)来操控。
- 一个由玩家操纵且只能左右不超出屏幕边界移动的小挡板
- 球在屏幕上运动,每一次碰撞会使球 在其触及的地方改变运动方向(这与屏幕范围,砖块和挡板有关)
- 当球运动到了屏幕下边界,玩家会丧失一点生命值
- 球碰到砖块,砖块即被消去
- 砖块被消完,玩家即获胜
- 通过球打在挡板上的位置(距离挡板中心的距离),球的后续方向能够被玩家操控
因为小球不时可能会遇上砖块之间的缝隙所以它会继续弹开周围砖层进而离开缝隙。它后来若又进入了某个缺口依然遵循这个原则这就是为什么本节要叫这个名字Breakout的原因 ,小球必须 “Breakout”(弹出来).
*PS这段话不好翻译大概就是介绍打砖块的游戏规则
由于小球会不时通过缝隙到达砖墙的上方,它会在顶部边缘与砖块层的上边缘之间不断弹跳。小球会一直持续这种状态直到最终再次进入缝隙掉下来。这也是这个游戏名字的来源,小球必须要从缝隙中逃脱(Break out)出来。
# OpenGL Breakout
我们将完全利用opengl实现这个经典的电脑游戏几乎是2d游戏的基础这个版本的Breakout将能够运行在一些可出特效的显卡上。
我们将把这个经典的街机游戏作为我们之后要完全使用OpenGL实现这个游戏的基础这个版本的Breakout将运行在显卡上所以我们能够改进经典的Breakout游戏给它加一些很棒的特性
除了以上这些特性我们的Breakout还将拥有
- 精灵渲染
除了以上这些经典的机制我们的Breakout还将拥有
- 你见过最棒的画质
- 粒子系统
- 文字渲染
- 高级文字渲染
- 道具
- 后期特效
-重关卡(可自定义
- 后期处理特效
-(可自定义的)关卡
为了引起大家的兴趣,让大家看看完成本教程后所制作的游戏的样子:
为了引起大家的兴趣,你可以看看游戏完成之后的样子:
![](http://www.learnopengl.com/img/in-practice/breakout/cover.png)
![](../../img/06/Breakout/01/cover.png)
这些教程将结合前面大量概念,讲述了在之前所学的功能如何放在一起形成一个游戏。因此,最重要的是你完成了前面部分的教程
这些教程将结合之前教程中的大量概念,并且会展示如何让它们以一个整体工作。所以,在你开始这个系列教程的学习之前,请至少完成[「入门」](../../01 Getting started/01 OpenGL.md)章节
同样,有些东西也会提及其他教程的一些概念(比如说帧缓冲Framebuffers所以有必要时被提及的教程会列出
而且,有些教程会需求其它一些教程的概念(比如说在[「高级OpenGL」](../../04 Advanced OpenGL/05 Framebuffers.md)章节中的帧缓冲),所以当必要的时候我会列出需求的教程
如果你已准备好开发游戏,可以开始下一节了。
如果你已准备好开发这个游戏,可以开始[下一节](02 Setting up.md)的学习了。

View File

@@ -0,0 +1,106 @@
# 准备工作
原文 | [Setting up](http://learnopengl.com/#!In-Practice/2D-Game/Setting-up)
---|---
作者 | JoeyDeVries
翻译 | [ZMANT](https://github.com/Itanq)
校对 | 暂无
## 设置
在我们开始实际构造这个游戏之前,我们首先需要设置一些简单的框架来处理这个游戏,这个游戏将会用到几个第三方库,它们大多数都已经在前面的教程中介绍过了。不管在那个地方需要用到新的库的时候,我们都会作出适当的介绍。
首先,我们定义一个叫做`Game`的类来包含所有有关的渲染和游戏设置代码。有了这个类,我们就可以用这个类把游戏代码(稍稍的)有组织的嵌入到游戏窗口代码中。用这种方式,你就可以把相同的代码迁移到完全不同的窗口库(比如 SDL或SFML)而不需要做太多的工作。
> 存在有成千上万的方式来抽象并概括游戏/图形代码然后封装到类和对象中。在这个教程中,你刚好会看到有一个方法来解决这个问题。如果你感到有一个更好的方法,尽量实现你的改进。
这个`Game`类封装了一个初始化函数,一个更新函数,一个处理输入函数以及一个渲染函数:
```C++
class Game
{
public:
// Game state
GameState State;
GLboolean Keys[1024];
GLuint Width, Height;
// Constructor/Destructor
Game(GLuint width, GLuint height);
~Game();
// Initialize game state (load all shaders/textures/levels)
void Init();
// GameLoop
void ProcessInput(GLfloat dt);
void Update(GLfloat dt);
void Render();
};
```
这个类可能就是你期望中的游戏类。我们通过一个`width`和`height`(对应于你玩游戏时的设备分辨率)来初始化一个游戏实例并且使用`Init`函数来加载着色器、纹理以及初始化游戏状态。我们可以通过调用`ProcessInput`函数来使用存储在`Keys`数组里的数据来处理输入并且在`Update`函数里面更新游戏设置状态(比如玩家/球的移动)。最后,我们还可以在`Render`函数里面渲染游戏。注意,我们在渲染逻辑里面分离出了运动逻辑。
这个`Game`类同样了封装了一个叫做`State`的变量,它的类型`GameState`如下定义:
```C++
// Represents the current state of the game
enum GameState {
GAME_ACTIVE,
GAME_MENU,
GAME_WIN
};
```
这可以使我们跟踪游戏当前的状态。这样我们就可以根据当前游戏的状态来决定渲染或者处理不同的元素(比如当我们在游戏菜单界面的时候就可能需要渲染和处理不同的元素)。
目前为止,这个`Game`类的函数还完全是空的,因为我们还没有写实际的实现代码,但这里就是`Game`类的[header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up.h)和[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up)文件。
## 通用
因为我们正在开发一个大型应用所以我们将不得不频繁的重用一些OpenGL的概念比如纹理和着色器等。因此为这两个元素(items)创建一个更加易用的接口就是情理之中的事了,就像在我们前面一个教程中创建的那个`shader`类一样。
定义一个着色器类,它接收两个或三个字符串(如果有几何着色器)然后生成一个编译好的着色器(或者如果失败的话就生成一条错误信息)。这个着色器类同样也包含大量实用的函数来快速的设置`uniform`变量。同样也定义一个纹理类它从给定的字节数组中生成一个2D纹理图像(基于它的内容)。并且这个纹理类同样也封装了许多实用的函数。
我们并不打算钻研这些类实现的细节,因为学到这里你应该可以很容易的理解它们是如何工作的了。出于这个原因你可以找到它们的头文件和实现的代码文件,有详细的注释,如下:
>* Shader : [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader)
>* Texture: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture)
注意到当前的这个纹理类仅仅设置成单一的`2D`纹理,但你可以很容易的通过可选的纹理类型来扩展。
## 资源管理器
当着色器和纹理类的函数被他们自己使用的时候,他们确实需要一个字节数组或是几个字符串来初始化他们。我们可以很容易的在他们自己的类里面嵌入文件的加载代码,但这就稍微有点违反`单一职责原则`(也就是说)在这个类里我们应该仅仅关注纹理或着色器本身而不需要关注他们的文件加载结构。
出于这个原因,通常用一个更加有组织的方法(来实现文件的加载),就是创建一个叫做`resource manager`的单独实体类来加载游戏相关的资源。这里有好几个方法来实现`resouce manager`;在这个教程里我们选择使用一个单独的静态`resouce manager`(就是给它静态属性),也就是说可以在整个工程中使用它来加载资源以及使用和她相关加载功能。
使用具有静态属性的单独的类有很多好处,但它主要的坏处就是会失去`OOP`特性以及控制结构/破坏。不过,这些对于小项目来说是很容易处理的。
就像其他的类文件一样,这个资源管理器的列表如下:
>* Resource Manager: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager.h), [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager)
使用资源管理我们可以很容易的把着色器加载到程序里面,比如:
```C++
Shader shader = ResourceManager::LoadShader("vertex.vs", "fragment.vs", nullptr, "test");
// then use it
shader.Use();
// or
ResourceManager::GetShader("test").Use();
```
通过定义的`Game`类和`resouce manager`类一起就可以很很容易管理`Shader`和`Texture2D`,基于这个基础,在以后的教程里我们将会广泛的使用这些类来实现这个`Breakout`游戏。
## 程序
对这个游戏我们仍然需要创建一个窗口并且设置OpenGL的初识状态。我们确保使用OpenGL的[面剔除](http://learnopengl.com/#!Advanced-OpenGL/Face-culling)功能和它的[混合](http://learnopengl.com/#!Advanced-OpenGL/Blending)功能。我们不需要使用深度测试;因为这个游戏完全是一个二维的,所有顶点的`z`坐标都具有相同的值。因此开启深度测试并没有什么用还有可能造成`z-fighting`现象。
这个`Breakout`游戏开始时候的代码相当的简单:我们用`GLFW`创建一个窗口,注册了一些回调函数,创建一个`Game`实例并且调用了`Game`类所有相关的函数。这个代码如下:
>* Program: [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/program)
运行这个代码,你可能得到下面的输出:
![ans](http://learnopengl.com/img/in-practice/breakout/setting-up.png)
目前为止我们已经有了一个后面的教程需要的固定框架;我们将会持续的扩展这个`Game`类来封装一些新的功能。如果你准备好了就点击[下一个](http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites)教程。

View File

@@ -0,0 +1,221 @@
# 渲染精灵
原文 | [Rendering Sprites](http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites)
---|---
作者 | JoeyDeVires
翻译 | [ZMANT](https://github.com/Itanq)
校队 | 暂无
## 渲染精灵
为了给我们当前这个黑漆漆的游戏世界带来一点生机,我们将会渲染一些精灵(Sprite)来填补一些空虚。精灵有很多种定义但主要是指一个2D图片它通常是和一些属性数据一起使用比如用一些数据来表示它在世界坐标下的位置一个旋转的角度以及一个表示二维空间的大小的变量。在2D游戏中我们主要使用精灵来渲染图片/纹理对象。
就像前面那些教程里做的那样我们可以把顶点数据传到GPU并且手动的通过一些操作来创建一些2D形状。然而在一个大型应用中就像我们正在做的这个我们还是宁愿在渲染2D形状上做一些抽象。如果我们手动的去定义并转换每一个对象那就相当的凌乱了。
在这里我们将会定义一个用最少的代码去渲染大量的精灵的渲染类。这样我们就可以从像散沙一样的OpenGL渲染代码中摘要出游戏代码这是在一个大型应用中常用的做法。尽管我们首先要做的是设置一个合适的投影矩阵。
## 2D投影矩阵
从这个[坐标系统](../../01 Getting started/08 Coordinate Systems.md)教程我们明白了投影矩阵的作用是把视图空间坐标转化为标准化设备坐标。通过生成合适的投影矩阵,我们就可以在不同的坐标系下计算,这可能比把所有的坐标指定为标准化设备坐标(再计算)要更容易处理。
我们并不需要在坐标系执行任何的透视应用,因为这个游戏完全是在二维平面,所以一个正交投影矩阵就可以很好的工作了。因为正交投影矩阵几乎是直接把整个坐标变换到裁切空间,我们可以定义如下的矩阵来把世界坐标指定为屏幕坐标:
```c++
glm::mat4 projection = glm::ortho(0.0f,800.0f,600.0f,0.0f,-1.0f,1.0f);
```
前面的四个参数依次指定了投影椎体左、右、下、上边界。这个投影矩阵把所有在`0`到`800`之间的`x`坐标变换到`-1`到`1`之间以及把所有在`0`到`600`之间的`y`坐标变换到`-1`到`1`之间。这里我们指定了视椎体上部的`y`坐标值为`0`,同时其下部的`y`坐标值为`600`。结果就是这个场景的左上角坐标为`(0,0)`,右下角坐标为`(800,600)`,就像屏幕坐标那样。视图空间坐标直接对应像素坐标。
![](../../img/06/Breakout/03/projection.png)
这允许我们指定每个坐标都等于它们在屏幕上最终的像素坐标这在2D游戏里相当直观。
## 渲染精灵
渲染一个实际的精灵不因太复杂化。我们创建一个纹理四边形,并且在使用了前面预先定义的正交投影矩阵(变换到标准化设备坐标)之后我们可以使用模型矩阵来变换它。
!!! important
由于`Breakout`游戏是一个静态游戏,这里不需要视图/摄像机矩阵,我们可以直接使用投影矩阵把世界空间坐标变换到裁切空间坐标
为了变换精灵我们使用了下面的顶点着色器:
```c++
#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 projection;
void main()
{
TexCoords = vertex.zw;
gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0);
}
```
注意,我们使用了一个单一的`vec4`变量来存储了位置和纹理坐标数据。因为位置和纹理坐标数据都只包含了两个浮点型数据,所以我们可以把他们组合在一起作为一个单一的顶点属性。
像素着色器相对比较简单。我们设置了一个纹理和颜色向量,她们两个都会对像素最后的颜色产生影响。同样也设置了一个`uniform`颜色向量,我们就可以很容易的在游戏代码里面改变精灵的颜色。
```c++
#version 330 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D image;
uniform vec3 spriteColor;
void main()
{
color = vec4(spriteColor, 1.0) * texture(image, TexCoords);
}
```
为了让渲染精灵的代码更加有条理,我们定义了一个`SpriteRenderer`类,只需要一个单一的函数就可以渲染精灵,它的定义如下:
```c++
class SpriteRenderer
{
public:
SpriteRenderer(Shader &shader);
~SpriteRenderer();
void DrawSprite(Texture2D &texture, glm::vec2 position,
glm::vec2 size = glm::vec2(10, 10), GLfloat rotate = 0.0f,
glm::vec3 color = glm::vec3(1.0f));
private:
Shader shader;
GLuint quadVAO;
void initRenderData();
};
```
`SpriteRenderer`类封装了一个着色器对象,一个顶点数组对象以及一个渲染和初始化函数。它的构造函数接受一个着色器对象用于后面的渲染。
### 初始化
首先,让我们探究一下负责配置`quadVAO`的`initRenderData`h函数
```c++
void SpriteRenderer::initRenderData()
{
// Configure VAO/VBO
GLuint VBO;
GLfloat vertices[] = {
// Pos // Tex
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 0.0f
};
glGenVertexArrays(1, &this->quadVAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindVertexArray(this->quadVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
```
在这我们首先定义了一组以四边形的左上角为`(0,0)`的顶点坐标。这意味着当我们在四边形上申请一个移动或伸缩变换的时候四边形的左上角会被变换。这在2D图形或GUI这类元素的位置是相对于元素左上角的系统而言很常见。
下面我们简单的向`GPU`传递了顶点数据并且配置了顶点属性,这里的情况是只有一个单一的顶点属性。我们仅仅需要为每一个精灵渲染器定义一个单一的`VAO`,因为他们的顶点数据都是一样的。
### 渲染
渲染精灵并不是太难;我们使用精灵渲染器的着色器对象,配置一个模型矩阵并且设置相关的`uniform`变量。这里最重要的就是变换的顺序:
```c++
void SpriteRenderer::DrawSprite(Texture2D &texture, glm::vec2 position,
glm::vec2 size, GLfloat rotate, glm::vec3 color)
{
// Prepare transformations
this->shader.Use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(position, 0.0f));
model = glm::translate(model, glm::vec3(0.5f * size.x, 0.5f * size.y, 0.0f));
model = glm::rotate(model, rotate, glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::translate(model, glm::vec3(-0.5f * size.x, -0.5f * size.y, 0.0f));
model = glm::scale(model, glm::vec3(size, 1.0f));
this->shader.SetMatrix4("model", model);
this->shader.SetVector3f("spriteColor", color);
glActiveTexture(GL_TEXTURE0);
texture.Bind();
glBindVertexArray(this->quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
```
当试图在一个场景中用旋转矩阵和伸缩矩阵放置一个对象的时候,建议首先做伸缩变换,然后旋转最后是移动变换。因为矩阵乘法是从右向左执行的,所以我们变换的矩阵顺序是相反的:移动,旋转,缩放。
旋转变换可能看起来稍微有点让人望而却步。我们从[变换](../../01 Getting started/07 Transformations/)教程里面知道旋转总是围绕原点`(0,0)`转动的。因为我们指定了四边形的左上角为`(0,0)`,所有的旋转都是围绕`(0,0)`的。主要是这个旋转的原点是四边形的左上角,这样就会造成不太友好的旋转效果。我们想要做的就是把旋转的点移到四边形的中心,这样旋转就是围绕四边形中心而不是左上角了。我们通过在旋转之前把旋转点移动到四边形中心解决了这个问题。
![](../../img/06/Breakout/03/rotation-origin.png)
因为我们首先缩放了这个四边形,我们不得不在把原点变换到精灵中心的时候输入一个`size`变量(这就是我们乘了精灵的`size`向量)。一旦应用了旋转变换之后,我们就需要做上一次变换的逆变换。
把所有变换组合起来我们就可以以我们喜欢的方式移动、缩放并且旋转每一个精灵了。在下面你可以找到完整的精灵渲染器的源代码:
>* SpriteRenderer: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/sprite_renderer.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/sprite_renderer)
## 你好,精灵
使用`SpriteRenderer`类我们有了把实际图片渲染到屏幕的能力!让我们来在游戏代码里面初始化一个精灵并且加载一个我们最喜爱的[纹理](../../img/06/Breakout/03/awesomeface.png)
```c++
SpriteRenderer *Renderer;
void Game::Init()
{
// Load shaders
ResourceManager::LoadShader("shaders/sprite.vs", "shaders/sprite.frag", nullptr, "sprite");
// Configure shaders
glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(this->Width),
static_cast<GLfloat>(this->Height), 0.0f, -1.0f, 1.0f);
ResourceManager::GetShader("sprite").Use().SetInteger("image", 0);
ResourceManager::GetShader("sprite").SetMatrix4("projection", projection);
// Set render-specific controls
Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));
// Load textures
ResourceManager::LoadTexture("textures/awesomeface.png", GL_TRUE, "face");
}
```
然后在渲染函数里面我们渲染一下我们的吉祥物来看看是否一切都按正确的方式工作了:
```c++
void Game::Render()
{
Renderer->DrawSprite(ResourceManager::GetTexture("face"),
glm::vec2(200, 200), glm::vec2(300, 400), 45.0f, glm::vec3(0.0f, 1.0f, 0.0f));
}
```
这里我们把精灵放置在靠近屏幕中心的位置,它的高度稍微有点大于宽度。我们同样也把它旋转了`45.0f`度并且给了一个绿色。注意,我们设定的精灵的位置是精灵四边形左上角的位置。
如果你一切都做对了你应该可以看到下面的输出:
![](../../img/06/Breakout/03/rendering-sprites.png)
你可以在[这里](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_rendering-sprites)找到更新后的游戏源代码。
目前我们的渲染系统正确工作了,我们在下一个教程里设置了游戏的等级,它在那里将会有更好的用处。

View File

@@ -0,0 +1,236 @@
# 粒子
原文 | [Particles](http://learnopengl.com/#!In-Practice/2D-Game/Particles)
----- | ----
作者 | JoeydeVries
翻译 | [ZMANT](https://github.com/Itanq)
校对 | 暂无
一个微粒,从OpenGL的角度看就是一个总是面向摄像机方向且(通常)包含一个大部分区域是透明的纹理的小四边形.一个微粒本身主要就是一个精灵(sprite),前面我们已经早就使用过了,但是当你把成千上万个这些微粒放在一起的时候,就可以创造出令人疯狂的效果.
当处理这些微粒的时候,通常是由一个叫做粒子发射器或粒子生成器的东西完成的,从这个地方,持续不断的产生新的微粒并且旧的微粒随着时间逐渐消亡.如果这个粒子发射器产生一个带着类似烟雾纹理的微粒的时候,它的颜色亮度同时又随着与发射器距离的增加而变暗,那么就会产生出灼热的火焰的效果:
![fire](http://www.learnopengl.com/img/in-practice/breakout/particles_example.jpg)
一个单一的微粒通常有一个生命值变量,并且从它产生开始就一直在缓慢的减少.一旦它的生命值少于某个极限值(通常是0)我们就会杀掉这个粒子,这样下一个粒子产生时就可以让它来替换那个被杀掉的粒子.一个粒子发射器控制它产生的所有粒子并且根据它们的属性来改变它们的行为.一个粒子通常有下面的属性:
```c++
struct Particle {
glm::vec2 Position, Velocity;
glm::vec4 Color;
GLfloat Life;
Particle()
: Position(0.0f), Velocity(0.0f), Color(1.0f), Life(0.0f) { }
};
```
看上面那个火焰的例子,那个粒子发射器可能在靠近发射器的地方产生每一个粒子,并且有一个向上的速度,这样每个粒子都是朝着正$y$轴方向移动.那似乎有3个不同区域,只是可能相比其他的区域,给了某个区域内的粒子更快的速度.我们也可以看到,$y$轴方向越高的粒子,它们的黄色或者说亮度就越低.一旦某个粒子到达某个高度的时候,它的生命值就会耗尽然后被杀掉;绝不可能直冲云霄.
你可以想象到用这样一个系统,我们就可以创造一些有趣的效果比如火焰,青烟,烟雾,魔法效果,炮火残渣等等.在Breakout游戏里,我们将会使用下面那个小球来创建一个简单的粒子生成器来制作一些有趣的效果,结果看起来就像这样:
<video src="http://learnopengl.com/video/in-practice/breakout/particles.mp4" controls="controls"></video>
上面那个粒子生成器在这个球的位置产生无数的粒子,根据球移动的速度给了粒子相应的速度,并且根据它们的生命值来改变他们的颜色亮度.
为了渲染这些粒子,我们将会用到有不同实现的着色器:
```c++
#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 position, vec2 texCoords>
out vec2 TexCoords;
out vec4 ParticleColor;
uniform mat4 projection;
uniform vec2 offset;
uniform vec4 color;
void main()
{
float scale = 10.0f;
TexCoords = vertex.zw;
ParticleColor = color;
gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0);
}
```
以及像素着色器:
```c++
#version 330 core
in vec2 TexCoords;
in vec4 ParticleColor;
out vec4 color;
uniform sampler2D sprite;
void main()
{
color = (texture(sprite, TexCoords) * ParticleColor);
}
```
我们获取每个粒子的位置和纹理属性并且设置两个uniform变量:$offset$和$color$来改变每个粒子的输出状态.注意到,在顶点着色器里,我们把这个四边形的粒子缩小了10倍;你也可以把这个缩放变量设置成uniform类型的变量从而控制一些个别的粒子.
首先,我们需要一个粒子数组,然后用Particle结构体的默认构造函数来实例化.
```c++
GLuint nr_particles = 500;
std::vector<Particle> particles;
for (GLuint i = 0; i < nr_particles; ++i)
particles.push_back(Particle());
```
然后在每一帧里面,我们都会用一个起始变量来产生一些新的粒子并且对每个粒子(还活着的)更新它们的值.
```c++
GLuint nr_new_particles = 2;
// Add new particles
for (GLuint i = 0; i < nr_new_particles; ++i)
{
int unusedParticle = FirstUnusedParticle();
RespawnParticle(particles[unusedParticle], object, offset);
}
// Uupdate all particles
for (GLuint i = 0; i < nr_particles; ++i)
{
Particle &p = particles[i];
p.Life -= dt; // reduce life
if (p.Life > 0.0f)
{ // particle is alive, thus update
p.Position -= p.Velocity * dt;
p.Color.a -= dt * 2.5;
}
}
```c++
第一个循环看起来可能有点吓人.因为这些粒子会随着时间消亡,我们就想在每一帧里面产生`nr_new_particles`个新粒子.但是一开始我们就知道了总的粒子数量是`nr_partiles`,所以我们不能简单的往粒子数组里面添加新的粒子.否则的话我们很快就会得到一个装满成千上万个粒子的数组,考虑到这个粒子数组里面其实只有一小部分粒子是存活的,这样就太浪费效率了.
我们要做的就是找到第一个消亡的粒子然后用一个新产生的粒子来更新它.函数`FirstUnuseParticle`就是试图找到第一个消亡的粒子并且返回它的索引值给调用者.
```c++
GLuint lastUsedParticle = 0;
GLuint FirstUnusedParticle()
{
// Search from last used particle, this will usually return almost instantly
for (GLuint i = lastUsedParticle; i < nr_particles; ++i){
if (particles[i].Life <= 0.0f){
lastUsedParticle = i;
return i;
}
}
// Otherwise, do a linear search
for (GLuint i = 0; i < lastUsedParticle; ++i){
if (particles[i].Life <= 0.0f){
lastUsedParticle = i;
return i;
}
}
// Override first particle if all others are alive
lastUsedParticle = 0;
return 0;
}
```
这个函数存储了它找到的上一个消亡的粒子的索引值,由于下一个消亡的粒子索引值总是在上一个消亡的粒子索引值的右边,所以我们首先从它存储的上一个消亡的粒子索引位置开始查找,如果我们没有任何消亡的粒子,我们就简单的做一个线性查找.如果没有粒子消亡就返回索引值`0`,结果就是第一个粒子被覆盖.需要注意的是,如果是最后一种情况,就意味着你粒子的生命值太长了,在每一帧里面需要产生更少的粒子,或者你只是没有保留足够的粒子.
之后,一旦粒子数组中第一个消亡的粒子被发现的时候,我们就通过调用`RespawnParticle`函数更新它的值,函数接受一个`Particle`对象,一个`GameObject`对象和一个`offset`向量:
```c++
void RespawnParticle(Particle &particle, GameObject &object, glm::vec2 offset)
{
GLfloat random = ((rand() % 100) - 50) / 10.0f;
GLfloat rColor = 0.5 + ((rand() % 100) / 100.0f);
particle.Position = object.Position + random + offset;
particle.Color = glm::vec4(rColor, rColor, rColor, 1.0f);
particle.Life = 1.0f;
particle.Velocity = object.Velocity * 0.1f;
}
```
这个函数简单的重置这个粒子的生命值为1.0f,随机的给一个大于`0.5`的颜色值(经过颜色向量)并且(在物体周围)分配一个位置和速度基于游戏里的物体.
对于更新函数里的第二个循环遍历了所有粒子,并且对于每个粒子的生命值都减去一个时间差;这样每个粒子的生命值就精确到了秒.然后再检查这个粒子是否是还活着的,若是,则更新它的位置和颜色属性.这里我们缓慢的减少粒子颜色值的`alpha`值,以至于它看起来就是随着时间而缓慢的消亡.
最后保留下来就是实际需要渲染的粒子:
```c++
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
particleShader.Use();
for (Particle particle : particles)
{
if (particle.Life > 0.0f)
{
particleShader.SetVector2f("offset", particle.Position);
particleShader.SetVector4f("color", particle.Color);
particleTexture.Bind();
glBindVertexArray(particleVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
```
在这,对于每个粒子,我们一一设置他们的`uniform`变量`offse`和`color`,绑定纹理,然后渲染`2D`四边形的粒子.有趣的是我们在这看到了两次调用函数`glBlendFunc`.当要渲染这些粒子的时候,我们使用`GL_ONE`替换默认的目的因子模式`GL_ONE_MINUS_SRC_ALPHA`,这样,这些粒子叠加在一起的时候就会产生一些平滑的发热效果,就像在这个教程前面那样使用混合模式来渲染出火焰的效果也是可以的,这样在有大多数粒子的中心就会产生更加灼热的效果.
因为我们(就像这个系列教程的其他部分一样)喜欢让事情变得有条理,所以我们就创建了另一个类`ParticleGenerator`来封装我们刚刚谈到的所有功能.你可以在下面的链接里找到源码:
>* [header](http://www.learnopengl.com/code_viewer.php?code=in-practice/breakout/particle_generator.h),[code](http://www.learnopengl.com/code_viewer.php?code=in-practice/breakout/particle_generator)
然后在游戏代码里,我们创建这样一个粒子发射器并且用[这个](http://www.learnopengl.com/img/in-practice/breakout/textures/particle.png)纹理初始化.
```c++
ParticleGenerator *Particles;
void Game::Init()
{
[...]
ResourceManager::LoadShader("shaders/particle.vs", "shaders/particle.frag", nullptr, "particle");
[...]
ResourceManager::LoadTexture("textures/particle.png", GL_TRUE, "particle");
[...]
Particles = new ParticleGenerator(
ResourceManager::GetShader("particle"),
ResourceManager::GetTexture("particle"),
500
);
}
```
然后我们在`Game`类的`Updata`函数里为粒子生成器添加一条更新语句:
```c++
void Game::Update(GLfloat dt)
{
[...]
// Update particles
Particles->Update(dt, *Ball, 2, glm::vec2(Ball->Radius / 2));
[...]
}
```
每个粒子都将使用球的游戏对象属性对象,每帧产生两个粒子并且他们都是偏向球得中心,最后是渲染粒子:
```c++
void Game::Render()
{
if (this->State == GAME_ACTIVE)
{
[...]
// Draw player
Player->Draw(*Renderer);
// Draw particles
Particles->Draw();
// Draw ball
Ball->Draw(*Renderer);
}
}
```
注意到,我们是在渲染球体之前且在渲染其他物体之后渲染粒子的,这样,粒子就会在所有其他物体面前,但报纸在球体之后,你可以在[这里](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_particles)找到更新的`game`类的源码.
如果你现在编译并运行你的程序,你可能会看到在球体之后有一条小尾巴.就像这个教程开始的那样,给了这个游戏更加现代化的面貌.这个系统还可以很容易的扩展到更高级效果的主体上,就用这个粒子生成器自由的去实验吧,看看你是否可以创建出你自己的特效.