diff --git a/docs/06 In Practice/2D-Game/07 Postprocessing.md b/docs/06 In Practice/2D-Game/07 Postprocessing.md index 230f4e5..dfa6472 100644 --- a/docs/06 In Practice/2D-Game/07 Postprocessing.md +++ b/docs/06 In Practice/2D-Game/07 Postprocessing.md @@ -1,7 +1,227 @@ # 后期处理 -**暂无翻译** +原文 | [Postprocessing](https://learnopengl.com/#!In-Practice/2D-Game/Postprocessing) +----- | ---- +作者 | JoeydeVries +翻译 | [包纸](https://github.com/ShirokoSama) +校对 | 暂无 -这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!In-Practice/2D-Game/Postprocessing),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。 +如果我们可以通过几个后期处理(Postprocess)特效丰富Breakout游戏的视觉效果的话,会不会是一件很有趣的事情?利用OpenGL的帧缓冲,我们可以相对容易地创造出模糊的抖动效果、反转场景里的所有颜色、做一些“疯狂”的顶点运动、或是使用一些其他有趣的特效。 - \ No newline at end of file +!!! important + + 这章教程广泛运用了之前[帧缓冲](../../04 Advanced OpenGL/05 Framebuffers.md)与[抗锯齿](../../04 Advanced OpenGL/11 Anti Aliasing.md)章节的概念。 + +在教程的帧缓冲章节里,我们演示了如何使用单个纹理,通过后期处理特效实现有趣的效果(反相、灰度、模糊、锐化、边缘检测)。在Breakout中我们将做一些类似的事情:我们会创建一个帧缓冲对象,并附带一个多重采样的渲染缓冲对象作为其颜色附件。游戏中所有的渲染相关代码都应该渲染至这个多重采样的帧缓冲,然后将其内容传输([Bit blit](https://en.wikipedia.org/wiki/Bit_blit))至一个不同的帧缓冲中,该帧缓冲用一个纹理作为其颜色附件。这个纹理会包含游戏的渲染后的抗锯齿图像,我们对它应用零或多个后期处理特效后渲染至一个大的2D四边形。(译注:这段表述的复杂流程与教程帧缓冲章节的内容相似,原文中包含大量易混淆的名词与代词,建议读者先仔细理解帧缓冲章节的内容与流程)。 + +总结一下这些渲染步骤: + +1. 绑定至多重采样的帧缓冲 +2. 和往常一样渲染游戏 +3. 将多重采样的帧缓冲内容传输至一个普通的帧缓冲中(这个帧缓冲使用了一个纹理作为其颜色缓冲附件) +4. 解除绑定(绑定回默认的帧缓冲) +5. 在后期处理着色器中使用来自普通帧缓冲的颜色缓冲纹理 +6. 渲染屏幕大小的四边形作为后期处理着色器的输出 + +我们的后期处理着色器允许使用三种特效:**shake**, **confuse**和**chaos**。 + +- **shake**:轻微晃动场景并附加一个微小的模糊效果。 +- **shake**:反转场景中的颜色并颠倒x轴和y轴。 +- **chaos**: 利用边缘检测卷积核创造有趣的视觉效果,并以圆形旋转动画的形式移动纹理图片,实现“混沌”特效。 + +以下是这些效果的示例: + +![](../../img/06/Breakout/07/postprocessing_effects.png) + +在2D四边形上操作的顶点着色器如下所示: + +```c++ +#version 330 core +layout (location = 0) in vec4 vertex; // + +out vec2 TexCoords; + +uniform bool chaos; +uniform bool confuse; +uniform bool shake; +uniform float time; + +void main() +{ + gl_Position = vec4(vertex.xy, 0.0f, 1.0f); + vec2 texture = vertex.zw; + if(chaos) + { + float strength = 0.3; + vec2 pos = vec2(texture.x + sin(time) * strength, texture.y + cos(time) * strength); + TexCoords = pos; + } + else if(confuse) + { + TexCoords = vec2(1.0 - texture.x, 1.0 - texture.y); + } + else + { + TexCoords = texture; + } + if (shake) + { + float strength = 0.01; + gl_Position.x += cos(time * 10) * strength; + gl_Position.y += cos(time * 15) * strength; + } +} +``` + +基于uniform是否被设置为true,顶点着色器可以执行不同的分支。如果chaosconfuse被设置为true,顶点着色器将操纵纹理坐标来移动场景(以圆形动画变换纹理坐标或反转纹理坐标)。因为我们将纹理环绕方式设置为了GL_REPEAT,所以**chaos**特效会导致场景在四边形的各个部分重复。除此之外,如果**shake**被设置为true,它将微量移动顶点位置。需要注意的是,chaosconfuse不应同时为true,而shake则可以与其他特效一起生效。 + +当任意特效被激活时,除了偏移顶点的位置和纹理坐标,我们也希望创造显著的视觉效果。我们可以在片段着色器中实现这一点: + +```c++ +#version 330 core +in vec2 TexCoords; +out vec4 color; + +uniform sampler2D scene; +uniform vec2 offsets[9]; +uniform int edge_kernel[9]; +uniform float blur_kernel[9]; + +uniform bool chaos; +uniform bool confuse; +uniform bool shake; + +void main() +{ + color = vec4(0.0f); + vec3 sample[9]; + // 如果使用卷积矩阵,则对纹理的偏移像素进行采样 + if(chaos || shake) + for(int i = 0; i < 9; i++) + sample[i] = vec3(texture(scene, TexCoords.st + offsets[i])); + + // 处理特效 + if(chaos) + { + for(int i = 0; i < 9; i++) + color += vec4(sample[i] * edge_kernel[i], 0.0f); + color.a = 1.0f; + } + else if(confuse) + { + color = vec4(1.0 - texture(scene, TexCoords).rgb, 1.0); + } + else if(shake) + { + for(int i = 0; i < 9; i++) + color += vec4(sample[i] * blur_kernel[i], 0.0f); + color.a = 1.0f; + } + else + { + color = texture(scene, TexCoords); + } +} +``` + +这个着色器几乎直接构建自帧缓冲教程的片段着色器,并根据被激活的特效类型进行相应的后期处理。这一次,偏移矩阵(offset matrix)和卷积核作为uniform变量,由应用程序中的代码定义。好处是我们只需要设置这些内容一次,而不必在每个片段着色器执行时重新计算这些矩阵。例如,偏移矩阵的配置如下所示: + +```c++ +GLfloat offset = 1.0f / 300.0f; +GLfloat offsets[9][2] = { + { -offset, offset }, // 左上 + { 0.0f, offset }, // 中上 + { offset, offset }, // 右上 + { -offset, 0.0f }, // 左中 + { 0.0f, 0.0f }, // 正中 + { offset, 0.0f }, // 右中 + { -offset, -offset }, // 左下 + { 0.0f, -offset }, // 中下 + { offset, -offset } // 右下 +}; +glUniform2fv(glGetUniformLocation(shader.ID, "offsets"), 9, (GLfloat*)offsets); +``` + +由于所有管理帧缓冲器的概念已经在之前的教程中有过广泛的讨论,所以这次我不会深入其细节。下面是PostProcessor类的代码,它管理初始化,读写帧缓冲并将一个四边形渲染至屏幕。如果你理解了帧缓冲与反锯齿章节的教程,你应该可以完全它的代码。 + +- **PostProcessor**:[头文件](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/post_processor.h),[代码](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/post_processor) + +有趣的是BeginRenderEndRender函数。由于我们必须将整个游戏场景渲染至帧缓冲中,因此我们可以在场景的渲染代码之前和之后分别调用BeginRenderEndRender。接着,这个类将处理幕后的帧缓冲操作。在游戏的渲染函数中使用PostProcessor类如下所示: + +```c++ +PostProcessor *Effects; + +void Game::Render() +{ + if (this->State == GAME_ACTIVE) + { + Effects->BeginRender(); + // 绘制背景 + // 绘制关卡 + // 绘制挡板 + // 绘制粒子 + // 绘制小球 + Effects->EndRender(); + Effects->Render(glfwGetTime()); + } +} +``` + +无论我们需要什么,我们只需要将需要的PostProcessor类中的特效属性设置为true,其效果就可以立即可见。 + +## Shake it + +作为这些效果的演示,我们将模拟球击中坚固的混凝土块时的视觉冲击。无论在哪里发生碰撞,只要在短时间内实现晃动(shake)效果,便能增强撞击的冲击感。 + +我们只想允许晃动效果持续一小段时间。我们可以通过声明一个持有晃动效果持续时间的变量ShakeTime来实现这个功能。无论碰撞何时发生,我们将这个变量重置为一个特定的持续时间: + +```c++ +GLfloat ShakeTime = 0.0f; + +void Game::DoCollisions() +{ + for (GameObject &box : this->Levels[this->Level].Bricks) + { + if (!box.Destroyed) + { + Collision collision = CheckCollision(*Ball, box); + if (std::get<0>(collision)) // 如果发生了碰撞 + { + // 如果不是实心的砖块则摧毁 + if (!box.IsSolid) + box.Destroyed = GL_TRUE; + else + { // 如果是实心的砖块则激活shake特效 + ShakeTime = 0.05f; + Effects->Shake = true; + } + [...] + } + } + } + [...] +} +``` + +然后在游戏的Update函数中,我们减少ShakeTime变量的值直到其为0.0,并停用shake特效。 + +```c++ +void Game::Update(GLfloat dt) +{ + [...] + if (ShakeTime > 0.0f) + { + ShakeTime -= dt; + if (ShakeTime <= 0.0f) + Effects->Shake = false; + } +} +``` + +这样,每当我们碰到一个实心砖块时,屏幕会短暂地抖动与模糊,给玩家一些小球与坚固物体碰撞的视觉反馈。 + + + +你可以在[这里](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_postprocessing)找到更新后的Game类。 + +在[下一章](./08 Powerups.md)关于“道具”的教程中我们将带来另外两种的特效的使用。 \ No newline at end of file diff --git a/docs/06 In Practice/2D-Game/08 Powerups.md b/docs/06 In Practice/2D-Game/08 Powerups.md index 347e824..38a0c5f 100644 --- a/docs/06 In Practice/2D-Game/08 Powerups.md +++ b/docs/06 In Practice/2D-Game/08 Powerups.md @@ -1,7 +1,333 @@ # 道具 -**暂无翻译** +原文 | [Powerups](https://learnopengl.com/#!In-Practice/2D-Game/Powerups) +----- | ---- +作者 | JoeydeVries +翻译 | [包纸](https://github.com/ShirokoSama) +校对 | 暂无 -这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!In-Practice/2D-Game/Powerups),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。 +Breakout已经接近完成了,但我们可以至少再增加一种游戏机制让它变得更酷。“充电”(译注:Powerups,很多游戏中都会用这个单词指代可以提升能力的道具,本文之后也会用道具一词作为其翻译)怎么样? - \ No newline at end of file +这个想法的含义是,无论一个砖块何时被摧毁,它都有一定几率产生一个道具块。这样的道具快会缓慢降落,而且当它与玩家挡板发生接触时,会发生基于道具类型的有趣效果。例如,某一种道具可以让玩家挡板变长,另一种道具则可以让小球穿过物体。我们还可以添加一些可以给玩家造成负面影响的负面道具。 + +我们可以将道具建模为具有一些额外属性的GameObject,这也是为什么我们定义一个继承自GameObjectPowerUp类并在其中增添了一些额外的成员属性。 + +```c++ +const glm::vec2 SIZE(60, 20); +const glm::vec2 VELOCITY(0.0f, 150.0f); + +class PowerUp : public GameObject +{ +public: + // 道具类型 + std::string Type; + GLfloat Duration; + GLboolean Activated; + // 构造函数 + PowerUp(std::string type, glm::vec3 color, GLfloat duration, + glm::vec2 position, Texture2D texture) + : GameObject(position, SIZE, texture, color, VELOCITY), + Type(type), Duration(duration), Activated() + { } +}; +``` + +PowerUp类仅仅是一个有一些额外状态的GameObject, 所以我们简单地将它定义为一个头文件,你可以在[这里](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/power_up.h)找到它。 + +每个道具以字符串的形式定义它的类型,持有表示它有效时长的持续时间与表示当前是否被激活的属性。在Breakout中,我们将添加4种增益道具与2种负面道具: + +![](../../img/06/Breakout/08/powerups.png) + +- **Speed**: 增加小球20%的速度 +- **Sticky**: 当小球与玩家挡板接触时,小球会保持粘在挡板上的状态直到再次按下空格键,这可以让玩家在释放小球前找到更合适的位置 +- **Pass-Through**: 非实心砖块的碰撞处理被禁用,使小球可以穿过并摧毁多个砖块 +- **Pad-Size-Increase**: 增加玩家挡板50像素的宽度 +- **Confuse**: 短时间内激活confuse后期特效,迷惑玩家 +- **Chaos**: 短时间内激活chaos后期特效,使玩家迷失方向 + +你可以在下面找到道具的高质量纹理: + +- **Texture**: [Speed](https://learnopengl.com/img/in-practice/breakout/textures/powerup_speed.png),[Sticky](https://learnopengl.com/img/in-practice/breakout/textures/powerup_sticky.png),[Pass-Through](https://learnopengl.com/img/in-practice/breakout/textures/powerup_passthrough.png),[Pad-Size-Increase](https://learnopengl.com/img/in-practice/breakout/textures/powerup_increase.png),[Confuse](https://learnopengl.com/img/in-practice/breakout/textures/powerup_confuse.png),[Chaos](https://learnopengl.com/img/in-practice/breakout/textures/powerup_chaos.png). + +与关卡中的砖块纹理类似,每个道具纹理都是完全灰度的,这使得我们在将其与颜色向量相乘时可以保持色彩的平衡。 + +因为我们需要跟踪游戏中被激活的道具的类型、持续时间、相关效果等状态,所以我们将它们存储在一个容器内: + +```c++ +class Game { + public: + [...] + std::vector PowerUps; + [...] + void SpawnPowerUps(GameObject &block); + void UpdatePowerUps(GLfloat dt); +}; +``` + +我们还定义了两个管理道具的函数,SpawnPowerUps在给定的砖块位置生成一个道具,UpdatePowerUps管理所有当前被激活的道具。 + +## SpawnPowerUps + +每次砖块被摧毁时我们希望以一定几率生成一个道具,这个功能可以在GameSpawnPowerUps函数中找到: + +```c++ +GLboolean ShouldSpawn(GLuint chance) +{ + GLuint random = rand() % chance; + return random == 0; +} +void Game::SpawnPowerUps(GameObject &block) +{ + if (ShouldSpawn(75)) // 1/75的几率 + this->PowerUps.push_back( + PowerUp("speed", glm::vec3(0.5f, 0.5f, 1.0f), 0.0f, block.Position, tex_speed + )); + if (ShouldSpawn(75)) + this->PowerUps.push_back( + PowerUp("sticky", glm::vec3(1.0f, 0.5f, 1.0f), 20.0f, block.Position, tex_sticky + ); + if (ShouldSpawn(75)) + this->PowerUps.push_back( + PowerUp("pass-through", glm::vec3(0.5f, 1.0f, 0.5f), 10.0f, block.Position, tex_pass + )); + if (ShouldSpawn(75)) + this->PowerUps.push_back( + PowerUp("pad-size-increase", glm::vec3(1.0f, 0.6f, 0.4), 0.0f, block.Position, tex_size + )); + if (ShouldSpawn(15)) // 负面道具被更频繁地生成 + this->PowerUps.push_back( + PowerUp("confuse", glm::vec3(1.0f, 0.3f, 0.3f), 15.0f, block.Position, tex_confuse + )); + if (ShouldSpawn(15)) + this->PowerUps.push_back( + PowerUp("chaos", glm::vec3(0.9f, 0.25f, 0.25f), 15.0f, block.Position, tex_chaos + )); +} +``` + +这样的SpawnPowerUps函数以一定几率(1/75普通道具,1/15负面道具)生成一个新的PowerUp对象,并设置其属性。每种道具有特殊的颜色使它们更具有辨识度,同时根据类型决定其持续时间的秒数,若值为0.0f则表示它持续无限长的时间。除此之外,每个道具初始化时传入被摧毁砖块的位置与上一小节给出的对应纹理。 + +## 激活道具 + +接下来我们更新游戏的DoCollisions函数使它不只检查小球与砖块和挡板的碰撞,还检查挡板与所有未被销毁的道具的碰撞。注意我们在砖块被摧毁的同时调用SpawnPowerUps函数。 + +```c++ +void Game::DoCollisions() +{ + for (GameObject &box : this->Levels[this->Level].Bricks) + { + if (!box.Destroyed) + { + Collision collision = CheckCollision(*Ball, box); + if (std::get<0>(collision)) + { + if (!box.IsSolid) + { + box.Destroyed = GL_TRUE; + this->SpawnPowerUps(box); + } + [...] + } + } + } + [...] + for (PowerUp &powerUp : this->PowerUps) + { + if (!powerUp.Destroyed) + { + if (powerUp.Position.y >= this->Height) + powerUp.Destroyed = GL_TRUE; + if (CheckCollision(*Player, powerUp)) + { // 道具与挡板接触,激活它! + ActivatePowerUp(powerUp); + powerUp.Destroyed = GL_TRUE; + powerUp.Activated = GL_TRUE; + } + } + } +} +``` + +对所有未被销毁的道具,我们检查它是否接触到了屏幕底部或玩家挡板,无论哪种情况我们都销毁它,但当道具与玩家挡板接触时,激活这个道具。 + +激活道具的操作可以通过将其Activated属性设为true来完成,实现其效果则需要将它传给ActivatePowerUp函数: + +```c++ +void ActivatePowerUp(PowerUp &powerUp) +{ + // 根据道具类型发动道具 + if (powerUp.Type == "speed") + { + Ball->Velocity *= 1.2; + } + else if (powerUp.Type == "sticky") + { + Ball->Sticky = GL_TRUE; + Player->Color = glm::vec3(1.0f, 0.5f, 1.0f); + } + else if (powerUp.Type == "pass-through") + { + Ball->PassThrough = GL_TRUE; + Ball->Color = glm::vec3(1.0f, 0.5f, 0.5f); + } + else if (powerUp.Type == "pad-size-increase") + { + Player->Size.x += 50; + } + else if (powerUp.Type == "confuse") + { + if (!Effects->Chaos) + Effects->Confuse = GL_TRUE; // 只在chaos未激活时生效,chaos同理 + } + else if (powerUp.Type == "chaos") + { + if (!Effects->Confuse) + Effects->Chaos = GL_TRUE; + } +} +``` + +ActivatePowerUp的目的正如其名称,它按本章教程之前所预设的那样激活了一个道具的效果。我们检查道具的类型并相应地改变游戏状态。对于**Sticky**和**Pass-through**效果,我们也相应地改变了挡板和小球的颜色来给玩家一些当前被激活了哪种效果的反馈。 + +因为**Sticky**和**Pass-through**效果稍微改变了一些原有的游戏逻辑,所以我们将这些效果作为属性存储在小球对象中,这样我们可以根据小球当前激活了什么效果而改变游戏逻辑。我们只在BallObject的头文件中增加了两个属性,但为了完整性下面给出了更新后的代码: + +- **GameObject**: [头文件](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/ball_object.h),[代码](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/ball_object) + +这样我们可以通过改动DoCollisions函数中小球与挡板碰撞的代码便捷地实现**Sticky**效果。 + +```c++ +if (!Ball->Stuck && std::get<0>(result)) +{ + [...] + Ball->Stuck = Ball->Sticky; +} +``` + +在这里我们将小球的Stuck属性设置为它自己的Sticky属性,若**Stikcy**效果被激活,那么小球则会在与挡板接触时粘在上面,玩家不得不再次按下空格键才能释放它。 + +在同样的DoCollisions函数中还有个为了实现**Pass-through**效果的类似小改动。当小球的PassThrough属性被设置为true时,我们不对非实习砖块做碰撞处理操作。 + +```c++ +Direction dir = std::get<1>(collision); +glm::vec2 diff_vector = std::get<2>(collision); +if (!(Ball->PassThrough && !box.IsSolid)) +{ + if (dir == LEFT || dir == RIGHT) // 水平碰撞 + { + [...] + } + else + { + [...] + } +} +``` + +其他效果可以通过简单的更改游戏的状态来实现,如小球的速度、挡板的尺寸、PostProcessor对象的效果。 + +## 更新道具 + +现在剩下要做的就是保证道具生成后可以移动,并且在它们的持续时间用尽后失效,否则道具将永远保持激活状态。 + +在游戏的UpdatePowerUps函数中,我们根据道具的速度移动它,并减少已激活道具的持续时间,每当时间减少至小于0时,我们令其失效,并恢复相关变量的状态。 + +```c++ +void Game::UpdatePowerUps(GLfloat dt) +{ + for (PowerUp &powerUp : this->PowerUps) + { + powerUp.Position += powerUp.Velocity * dt; + if (powerUp.Activated) + { + powerUp.Duration -= dt; + + if (powerUp.Duration <= 0.0f) + { + // 之后会将这个道具移除 + powerUp.Activated = GL_FALSE; + // 停用效果 + if (powerUp.Type == "sticky") + { + if (!isOtherPowerUpActive(this->PowerUps, "sticky")) + { // 仅当没有其他sticky效果处于激活状态时重置,以下同理 + Ball->Sticky = GL_FALSE; + Player->Color = glm::vec3(1.0f); + } + } + else if (powerUp.Type == "pass-through") + { + if (!isOtherPowerUpActive(this->PowerUps, "pass-through")) + { + Ball->PassThrough = GL_FALSE; + Ball->Color = glm::vec3(1.0f); + } + } + else if (powerUp.Type == "confuse") + { + if (!isOtherPowerUpActive(this->PowerUps, "confuse")) + { + Effects->Confuse = GL_FALSE; + } + } + else if (powerUp.Type == "chaos") + { + if (!isOtherPowerUpActive(this->PowerUps, "chaos")) + { + Effects->Chaos = GL_FALSE; + } + } + } + } + } + this->PowerUps.erase(std::remove_if(this->PowerUps.begin(), this->PowerUps.end(), + [](const PowerUp &powerUp) { return powerUp.Destroyed && !powerUp.Activated; } + ), this->PowerUps.end()); +} +``` + +你可以看到对于每个效果,我们通过将相关元素重置来停用它。我们还将PowerUpActivated属性设为false,在UpdatePowerUps结束时,我们通过循环PowerUps容器,若一个道具被销毁切被停用,则移除它。我们在算法开头使用remove_if函数,通过给定的lamda表达式消除这些对象。 + +!!! important + + remove_if函数将lamda表达式为true的元素移动至容器的末尾并返回一个迭代器指向应被移除的元素范围的开始部分。容器的erase函数接着擦除这个迭代器指向的元素与容器末尾元素之间的所有元素。 + +可能会发生这样的情况:当一个道具在激活状态时,另一个道具与挡板发生了接触。在这种情况下我们有超过1个在当前PowerUps容器中处于激活状态的道具。然后,当这些道具中的一个被停用时,我们不应使其效果失效因为另一个相同类型的道具仍处于激活状态。出于这个原因,我们使用isOtherPowerUpActive检查是否有同类道具处于激活状态。只有当它返回false时,我们才停用这个道具的效果。这样,给定类型的道具的持续时间就可以延长至最近一次被激活后的持续时间。 + +```c++ +GLboolean IsOtherPowerUpActive(std::vector &powerUps, std::string type) +{ + for (const PowerUp &powerUp : powerUps) + { + if (powerUp.Activated) + if (powerUp.Type == type) + return GL_TRUE; + } + return GL_FALSE; +} +``` + +这个函数简单地检查是否有同类道具处于激活状态,如果有则返回GL_TRUE。 + +最后剩下的一件事便是渲染道具: + +```c++ +void Game::Render() +{ + if (this->State == GAME_ACTIVE) + { + [...] + for (PowerUp &powerUp : this->PowerUps) + if (!powerUp.Destroyed) + powerUp.Draw(*Renderer); + [...] + } +} +``` + +结合所有的这些功能,我们有了一个可以运作的道具系统,它不仅使游戏更有趣,还使游戏更具有挑战性。它看上去会像这样: + + + +你可以在下面找到所有更新后的代码(当关卡重置时我们同时重置所有道具效果): + +- **Game**: [头文件](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_powerups.h),[代码](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_powerups) \ No newline at end of file diff --git a/docs/06 In Practice/2D-Game/09 Audio.md b/docs/06 In Practice/2D-Game/09 Audio.md index 3d5b660..a64a99c 100644 --- a/docs/06 In Practice/2D-Game/09 Audio.md +++ b/docs/06 In Practice/2D-Game/09 Audio.md @@ -1,7 +1,83 @@ # 音效 -**暂无翻译** +原文 | [Audio](https://learnopengl.com/#!In-Practice/2D-Game/Audio) +----- | ---- +作者 | JoeydeVries +翻译 | [包纸](https://github.com/ShirokoSama) +校对 | 暂无 -这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!In-Practice/2D-Game/Audio),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。 +无论我们将游戏音量调到多大,我们都不会听到来自游戏的任何音效。我们已经展示了这么多内容,但没有任何音频,游戏仍显得有些空洞。在本节教程中,我们将解决这个问题。 + +OpenGL不提供关于音频的任何支持。我们不得不手动将音频加载为字节格式,处理并将其转化为音频流,并适当地管理多个音频流以供我们的游戏使用。然而这有一些复杂,并且需要一些底层的音频工程知识。 + +如果你乐意,你可以手动加载来自多种扩展名的音频文件的音频流。然而,我们将使用被称为irrKlang的音频管理库。 + +## irrKlang + +GLM Logo + +IrrKlang是一个可以播放WAV,MP3,OGG和FLAC文件的高级二维和三维(Windows,Mac OS X,Linux)声音引擎和音频库。它还有一些可以自由调整的音频效果,如混响、延迟和失真。 + +!!! important + + 3D音频意味着音频源可以有一个3D位置,然后根据相机到音频源的位置衰减音量,使其在一个3D世界里显得自然(想想3D世界中的枪声,通常你可以从音效中听出它来自什么方向/位置)。 + +IrrKlang是一个易于使用的音频库,只需几行代码便可播放大多数音频文件,这使它成为我们Breakout游戏的完美选择。请注意,irrKlang有一个有一定限制的证书:允许你将irrKlang用于非商业目的,但是如果你想使用irrKlang商业版,就必须支付购买他们的专业版。由于Breakout和本教程系列是非商业性的,所以我们可以自由地使用他们的标准库。 + +你可以从他们的[下载页面](http://www.ambiera.com/irrklang/downloads.html)下载irrKlang,我们将使用1.5版本。由于irrKlang是非开源的代码,因此我们不得不使用irrKlang为我们提供的任何东西。幸运的是,他们有大量的预编译库文件,所以你们大多数人应该可以很好地使用它。 + +你需要引入了irrKlang的头文件,将他们的库文件(irrKlang.lib)添加到链接器设置中,并将他们的dll文件复制到适当的目录下(通常和.exe在同一目录下)。需要注意的是,如果你想要加载MP3文件,则还需要引入ikpMP3.dll文件。 + +## 添加音乐 + +为了这个游戏我特制了一个小小的音轨,让游戏更富有活力。在[这里](https://learnopengl.com/audio/in-practice/breakout/breakout.mp3)你可以找到我们将要用作游戏背景音乐的音轨。这个音轨会在游戏开始时播放并不断循环直到游戏结束。你可以随意用自己的音频替换它,或者用喜欢的方式使用它。 + + + +利用irrKlang库将其添加到Breakout游戏里非常简单。我们引入相应的头文件,创建irrKlang::ISoundEngine,用createIrrKlangDevice初始化它并使用这个引擎加载、播放音频: + +```c++ +#include +using namespace irrklang; + +ISoundEngine *SoundEngine = createIrrKlangDevice(); + +void Game::Init() +{ + [...] + SoundEngine->play2D("audio/breakout.mp3", GL_TRUE); +} +``` + +在这里,我们创建了一个SoundEngine,用于管理所有与音频相关的代码。一旦我们初始化了引擎,便可以调用play2D函数播放音频。第一个参数为文件名,第二个参数为是否循环播放。 + +这就是全部了!现在运行游戏会使你的耳机或扬声器迸发出声波。 + +## 添加音效 + +我们还没有结束,因为音乐本身并不能使游戏完全充满活力。我们希望在游戏发生一些有趣事件时播放音效,作为给玩家的额外反馈,如我们撞击砖块、获得道具时。下面你可以找到我们需要的所有音效(来自freesound.org): + +[bleep.mp3](https://learnopengl.com/audio/in-practice/breakout/bleep.mp3): 小球撞击非实心砖块时的音效 + + + +[solid.wav](https://learnopengl.com/audio/in-practice/breakout/solid.wav):小球撞击实心砖块时的音效 + + + +[powerup.wav](https://learnopengl.com/audio/in-practice/breakout/powerup.wav): 获得道具时的音效 + + + +[bleep.wav](https://learnopengl.com/audio/in-practice/breakout/bleep.wav): 小球在挡板上反弹时的音效 + + + +无论在哪里发生碰撞,我们都会播放相应的音效。我不会详细阐述每一行的代码,并把更新后的代码放在了[这里](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_audio),你应该可以轻松地找到相应的添加音效的地方。 + +把这些集成在一起后我们的游戏显得更完整了,就像这样: + + + +IrrKlang允许一些更精细的音频管理功能,如内存管理、声音特效和声音事件回调。看看他们的C++[教程](http://www.ambiera.com/irrklang/tutorials.html)并尝试一些功能吧。 - \ No newline at end of file diff --git a/docs/06 In Practice/2D-Game/11 Final thoughts.md b/docs/06 In Practice/2D-Game/11 Final thoughts.md index c8d7ba5..12adb87 100644 --- a/docs/06 In Practice/2D-Game/11 Final thoughts.md +++ b/docs/06 In Practice/2D-Game/11 Final thoughts.md @@ -1,7 +1,29 @@ # 结语 -**暂无翻译** +原文 | [Final thoughts](https://learnopengl.com/#!In-Practice/2D-Game/Final-thoughts) +----- | ---- +作者 | JoeydeVries +翻译 | [包纸](https://github.com/ShirokoSama) +校对 | 暂无 -这篇教程暂时还没有进行翻译,您可以先阅读[原文](https://learnopengl.com/#!In-Practice/2D-Game/Final-thoughts),或经常来刷新看看是否有更新的进展。当然,我们更欢迎您在[GitHub上](https://github.com/LearnOpenGL-CN/LearnOpenGL-CN)认领翻译这篇文章,帮助我们完善这个教程系列。 +与仅仅是用OpenGL创建一个技术演示相比,这一整章的教程给我们了一次体验在此之上的更多内容的机会。我们从零开始制作了一个2D游戏,并学习了如何对特定的底层图形学概念进行抽象、使用基础的碰撞检测技术、创建粒子、展示基于正射投影矩阵的场景。所有的这些都使用了之前教程中讨论过的概念。我们并没有真正地学习和使用OpenGL中新的、令人兴奋的图形技术,更多的是在将所有知识整合至一个更大的整体中。 - \ No newline at end of file +Breakout这样的一个简单游戏的制作可以被数千种方法完成,而我们的做法也只是其中之一。随着游戏越来越庞大,你开始应用的抽象思想与设计模式就会越多。如果希望进行更深入的学习与阅读,你可以在[game programming patterns](http://gameprogrammingpatterns.com/)找到大部分的抽象思想与设计模式。(译注:《游戏编程模式》一书国内已有中文翻译版,GPP翻译组译,人民邮电出版社) + +请记住,编写出一个有着非常干净、考虑周全的代码的游戏是一件很困难的任务(几乎不可能)。你只需要在编写游戏时使用在当时你认为正确的方法。随着你对视频游戏开发的实践越来越多,你学习的新的、更好地解决问题的方法就越多。不必因为编写“完美”代码的困难感到挫败,坚持编程吧! + +## 优化 + +这些教程的内容和目前已完成的游戏代码的关注点都在于如何尽可能简单地阐述概念,而没有深入地优化细节。因此,很多性能相关的考虑都被忽略了。为了在游戏的帧率开始下降时可以提高性能,我们将列出一些现代的2D OpenGL游戏中常见的改进方案。 + +- **渲染精灵表单/纹理图谱(Sprite sheet / Texture atlas)**:代替使用单个渲染精灵渲染单个纹理的渲染方式,我们将所有需要用到的纹理组合到单个大纹理中(如同位图字体),并用纹理坐标来选择合适的精灵与纹理。切换纹理状态是非常昂贵的操作,而使用这种方法让我们几乎可以不用在纹理间进行切换。除此之外,这样做还可以让GPU更有效率地缓存纹理,获得更快的查找速度。(译注:cache的局部性原理) +- **实例化渲染**:代替一次只渲染一个四边形的渲染方式,我们可以将想要渲染的所有四边形批量化,并使用[实例化渲染](../../04 Advanced OpenGL/10 Instancing.md)在一次<>draw call中成批地渲染四边形。这很容易实现,因为每个精灵都由相同的顶点组成,不同之处只有一个模型矩阵(Model Matrix),我们可以很容易地将其包含在一个实例化数组中。这样可以使OpenGL每帧渲染更多的精灵。实例化渲染也可以用来渲染粒子和字符字形。 +- **三角形带(Triangle Strips)**:代替每次渲染两个三角形的渲染方式,我们可以用OpenGL的TRIANGLE_STRIP渲染图元渲染它们,只需4个顶点而非6个。这节约了三分之一需要传递给GPU的数据量。 +- **空间划分(Space partition)算法**:当检查可能发生的碰撞时,我们将小球与当前关卡中的每一个砖块进行比较,这么做有些浪费CPU资源,因为我们可以很容易得知在这一帧中,大多数砖块都不会与小球很接近。使用BSP,八叉树(Octress)或k-d(imension)树等空间划分算法,我们可以将可见的空间划分成许多较小的区域,并判断小球是否在这个区域中,从而为我们省去大量的碰撞检查。对于Breakout这样的简单游戏来说,这可能是过度的,但对于有着更复杂的碰撞检测算法的复杂游戏,这些算法可以显著地提高性能。 +- **最小化状态间的转换**:状态间的变化(如绑定纹理或切换着色器)在OpenGL中非常昂贵,因此你需要避免大量的状态变化。一种最小化状态间变化的方法是创建自己的状态管理器来存储OpenGL状态的当前值(比如绑定了哪个纹理),并且只在需要改变时进行切换,这可以避免不必要的状态变化。另外一种方式是基于状态切换对所有需要渲染的物体进行排序。首先渲染使用着色器A的所有对象,然后渲染使用着色器B的所有对象,以此类推。当然这可以扩展到着色器、纹理绑定、帧缓冲切换等。 + +这些应该可以给你一些关于,我们可以用什么样的的高级技巧进一步提高2D游戏性能地提示。这也让你感受到了OpenGL的强大功能。通过亲手完成大部分的渲染,我们对整个渲染过程有了完整的掌握,从而可以实现对过程的优化。如果你对Breakout的性能并不满意,你可以把这些当做练习。 + +## 开始创作! + +你已经看到了如何在OpenGL中创建一个简单的游戏,现在轮到你来创作属于自己的渲染/游戏程序了。到目前为止我们讨论的许多技术都可以应用于大部分2D游戏中,如渲染精灵、基础的碰撞检测、后期处理、文本渲染和粒子系统。现在你可以将这些技术以你认为合理的方式进行组合与修改,并开发你自己的手制游戏吧! diff --git a/docs/img/06/Breakout/07/postprocessing_effects.png b/docs/img/06/Breakout/07/postprocessing_effects.png new file mode 100644 index 0000000..344eb8c Binary files /dev/null and b/docs/img/06/Breakout/07/postprocessing_effects.png differ diff --git a/docs/img/06/Breakout/08/powerups.png b/docs/img/06/Breakout/08/powerups.png new file mode 100644 index 0000000..ff56be0 Binary files /dev/null and b/docs/img/06/Breakout/08/powerups.png differ diff --git a/docs/img/06/Breakout/09/irrklang.png b/docs/img/06/Breakout/09/irrklang.png new file mode 100644 index 0000000..125d124 Binary files /dev/null and b/docs/img/06/Breakout/09/irrklang.png differ