版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.01.20 |
前言
OpenGL 图形库项目中一直也没用过,最近也想学着使用这个图形库,感觉还是很有意思,也就自然想着好好的总结一下,希望对大家能有所帮助。下面内容来自欢迎来到OpenGL的世界。
1. OpenGL 图形库使用(一) —— 概念基础
2. OpenGL 图形库使用(二) —— 渲染模式、对象、扩展和状态机
3. OpenGL 图形库使用(三) —— 着色器、数据类型与输入输出
4. OpenGL 图形库使用(四) —— Uniform及更多属性
5. OpenGL 图形库使用(五) —— 纹理
6. OpenGL 图形库使用(六) —— 变换
7. OpenGL 图形库的使用(七)—— 坐标系统之五种不同的坐标系统(一)
8. OpenGL 图形库的使用(八)—— 坐标系统之3D效果(二)
9. OpenGL 图形库的使用(九)—— 摄像机(一)
10. OpenGL 图形库的使用(十)—— 摄像机(二)
11. OpenGL 图形库的使用(十一)—— 光照之颜色
12. OpenGL 图形库的使用(十二)—— 光照之基础光照
13. OpenGL 图形库的使用(十三)—— 光照之材质
14. OpenGL 图形库的使用(十四)—— 光照之光照贴图
15. OpenGL 图形库的使用(十五)—— 光照之投光物
16. OpenGL 图形库的使用(十六)—— 光照之多光源
17. OpenGL 图形库的使用(十七)—— 光照之复习总结
18. OpenGL 图形库的使用(十八)—— 模型加载之Assimp
19. OpenGL 图形库的使用(十九)—— 模型加载之网格
20. OpenGL 图形库的使用(二十)—— 模型加载之模型
21. OpenGL 图形库的使用(二十一)—— 高级OpenGL之深度测试
22. OpenGL 图形库的使用(二十二)—— 高级OpenGL之模板测试Stencil testing
23. OpenGL 图形库的使用(二十三)—— 高级OpenGL之混合Blending
24. OpenGL 图形库的使用(二十四)—— 高级OpenGL之面剔除Face culling
25. OpenGL 图形库的使用(二十五)—— 高级OpenGL之帧缓冲Framebuffers
26. OpenGL 图形库的使用(二十六)—— 高级OpenGL之立方体贴图Cubemaps
27. OpenGL 图形库的使用(二十七)—— 高级OpenGL之高级数据Advanced Data
28. OpenGL 图形库的使用(二十八)—— 高级OpenGL之高级GLSL Advanced GLSL
29. OpenGL 图形库的使用(二十九)—— 高级OpenGL之几何着色器Geometry Shader
30. OpenGL 图形库的使用(三十)—— 高级OpenGL之实例化Instancing
31. OpenGL 图形库的使用(三十一)—— 高级OpenGL之抗锯齿Anti Aliasing
32. OpenGL 图形库的使用(三十二)—— 高级光照之高级光照Advanced Lighting
33. OpenGL 图形库的使用(三十三)—— 高级光照之Gamma校正Gamma Correction
34. OpenGL 图形库的使用(三十四)—— 高级光照之阴影 - 阴影映射Shadow Mapping
35. OpenGL 图形库的使用(三十五)—— 高级光照之阴影 - 点阴影Point Shadows
36. OpenGL 图形库的使用(三十六)—— 高级光照之法线贴图Normal Mapping
37. OpenGL 图形库的使用(三十七)—— 高级光照之视差贴图Parallax Mapping
38. OpenGL 图形库的使用(三十八)—— 高级光照之HDR
39. OpenGL 图形库的使用(三十九)—— 高级光照之泛光
40. OpenGL 图形库的使用(四十)—— 高级光照之延迟着色法Deferred Shading
41. OpenGL 图形库的使用(四十一)—— 高级光照之SSAO
42. OpenGL 图形库的使用(四十二)—— PBR之理论Theory
43. OpenGL 图形库的使用(四十三)—— PBR之光照Lighting
44. OpenGL 图形库的使用(四十四)—— PBR之几篇没有翻译的英文原稿
45. OpenGL 图形库的使用(四十五)—— 实战之调试Debugging
46. OpenGL 图形库的使用(四十六)—— 实战之文本渲染Text Rendering
47. OpenGL 图形库的使用(四十七)—— 实战之2D游戏 - Breakout
48. OpenGL 图形库的使用(四十八)—— 实战之2D游戏 - 准备工作
49. OpenGL 图形库的使用(四十九)—— 实战之2D游戏 - 渲染精灵
关卡
Breakout不会只是一个单一的绿色笑脸,而是一些由许多彩色砖块组成的完整关卡。我们希望这些关卡有以下特性:他们足够灵活以便于支持任意数量的行或列、可以拥有不可摧毁的坚固砖块、支持多种类型的砖块且这些信息被存储在外部文件中。
在本教程中,我们将简要介绍用于管理大量砖块的游戏关卡对象的代码,首先我们需要先定义什么是一个砖块。
我们创建一个被称为游戏对象的组件作为一个游戏内物体的基本表示。这样的游戏对象持有一些状态数据,如其位置、大小与速率。它还持有颜色、旋转、是否坚硬(不可被摧毁)、是否被摧毁的属性,除此之外,它还存储了一个Texture2D
变量作为其精灵(Sprite)。
游戏中的每个物体都可以被表示为GameObject
或这个类的派生类,你可以在下面找到GameObject的代码:
/*******************************************************************
** This code is part of Breakout.
**
** Breakout is free software: you can redistribute it and/or modify
** it under the terms of the CC BY 4.0 license as published by
** Creative Commons, either version 4 of the License, or (at your
** option) any later version.
******************************************************************/
#include "game_object.h"
GameObject::GameObject()
: Position(0, 0), Size(1, 1), Velocity(0.0f), Color(1.0f), Rotation(0.0f), Sprite(), IsSolid(false), Destroyed(false) { }
GameObject::GameObject(glm::vec2 pos, glm::vec2 size, Texture2D sprite, glm::vec3 color, glm::vec2 velocity)
: Position(pos), Size(size), Velocity(velocity), Color(color), Rotation(0.0f), Sprite(sprite), IsSolid(false), Destroyed(false) { }
void GameObject::Draw(SpriteRenderer &renderer)
{
renderer.DrawSprite(this->Sprite, this->Position, this->Size, this->Rotation, this->Color);
}
Breakout
中的关卡基本由砖块组成,因此我们可以用一个砖块的集合表示一个关卡。因为砖块需要和游戏对象几乎相同的状态,所以我们将关卡中的每个砖块表示为GameObject
。GameLevel类的布局如下所示:
class GameLevel
{
public:
std::vector<GameObject> Bricks;
GameLevel() { }
// 从文件中加载关卡
void Load(const GLchar *file, GLuint levelWidth, GLuint levelHeight);
// 渲染关卡
void Draw(SpriteRenderer &renderer);
// 检查一个关卡是否已完成 (所有非坚硬的瓷砖均被摧毁)
GLboolean IsCompleted();
private:
// 由砖块数据初始化关卡
void init(std::vector<std::vector<GLuint>> tileData, GLuint levelWidth, GLuint levelHeight);
};
由于关卡数据从外部文本中加载,所以我们需要提出某种关卡的数据结构,以下是关卡数据在文本文件中可能的表示形式的一个例子:
1 1 1 1 1 1
2 2 0 0 2 2
3 3 4 4 3 3
在这里一个关卡被存储在一个矩阵结构中,每个数字代表一种类型的砖块,并以空格分隔。在关卡代码中我们可以假定每个数字代表什么:
- 数字0:无砖块,表示关卡中空的区域
- 数字1:一个坚硬的砖块,不可被摧毁
- 大于1的数字:一个可被摧毁的砖块,不同的数字区分砖块的颜色
上面的示例关卡在被GameLevel
处理后,看起来会像这样:
GameLevel
类使用两个函数从文件中生成一个关卡。它首先将所有数字在Load函数中加载到二维容器(vector)里,然后在init函数中处理这些数字,以创建所有的游戏对象。
void GameLevel::Load(const GLchar *file, GLuint levelWidth, GLuint levelHeight)
{
// 清空过期数据
this->Bricks.clear();
// 从文件中加载
GLuint tileCode;
GameLevel level;
std::string line;
std::ifstream fstream(file);
std::vector<std::vector<GLuint>> tileData;
if (fstream)
{
while (std::getline(fstream, line)) // 读取关卡文件的每一行
{
std::istringstream sstream(line);
std::vector<GLuint> row;
while (sstream >> tileCode) // 读取被空格分隔的每个数字
row.push_back(tileCode);
tileData.push_back(row);
}
if (tileData.size() > 0)
this->init(tileData, levelWidth, levelHeight);
}
}
被加载后的tileData
数据被传递到GameLevel
的init函数:
void GameLevel::init(std::vector<std::vector<GLuint>> tileData, GLuint lvlWidth, GLuint lvlHeight)
{
// 计算每个维度的大小
GLuint height = tileData.size();
GLuint width = tileData[0].size();
GLfloat unit_width = lvlWidth / static_cast<GLfloat>(width);
GLfloat unit_height = lvlHeight / height;
// 基于tileDataC初始化关卡
for (GLuint y = 0; y < height; ++y)
{
for (GLuint x = 0; x < width; ++x)
{
// 检查砖块类型
if (tileData[y][x] == 1)
{
glm::vec2 pos(unit_width * x, unit_height * y);
glm::vec2 size(unit_width, unit_height);
GameObject obj(pos, size,
ResourceManager::GetTexture("block_solid"),
glm::vec3(0.8f, 0.8f, 0.7f)
);
obj.IsSolid = GL_TRUE;
this->Bricks.push_back(obj);
}
else if (tileData[y][x] > 1)
{
glm::vec3 color = glm::vec3(1.0f); // 默认为白色
if (tileData[y][x] == 2)
color = glm::vec3(0.2f, 0.6f, 1.0f);
else if (tileData[y][x] == 3)
color = glm::vec3(0.0f, 0.7f, 0.0f);
else if (tileData[y][x] == 4)
color = glm::vec3(0.8f, 0.8f, 0.4f);
else if (tileData[y][x] == 5)
color = glm::vec3(1.0f, 0.5f, 0.0f);
glm::vec2 pos(unit_width * x, unit_height * y);
glm::vec2 size(unit_width, unit_height);
this->Bricks.push_back(
GameObject(pos, size, ResourceManager::GetTexture("block"), color)
);
}
}
}
}
init函数遍历每个被加载的数字,处理后将一个相应的GameObject添加到关卡的容器中。每个砖块的尺寸(unit_width
和unit_height
)根据砖块的总数被自动计算以便于每块砖可以完美地适合屏幕边界。
在这里我们用两个新的纹理加载游戏对象,分别为block纹理与solid block纹理。
这里有一个很好的小窍门,即这些纹理是完全灰度的。其效果是,我们可以在游戏代码中,通过将灰度值与定义好的颜色矢量相乘来巧妙地操纵它们的颜色,就如同我们在SpriteRenderer
中所做的那样。这样一来,自定义的颜色/外观就不会显得怪异或不平衡。
GameLevel
类还包含一些其他的功能,比如渲染所有未被破坏的砖块,或验证是否所有的可破坏砖块均被摧毁。你可以在下面找到GameLevel类的源码:
/*******************************************************************
** This code is part of Breakout.
**
** Breakout is free software: you can redistribute it and/or modify
** it under the terms of the CC BY 4.0 license as published by
** Creative Commons, either version 4 of the License, or (at your
** option) any later version.
******************************************************************/
#include "game_level.h"
#include <fstream>
#include <sstream>
void GameLevel::Load(const GLchar *file, GLuint levelWidth, GLuint levelHeight)
{
// Clear old data
this->Bricks.clear();
// Load from file
GLuint tileCode;
GameLevel level;
std::string line;
std::ifstream fstream(file);
std::vector<std::vector<GLuint>> tileData;
if (fstream)
{
while (std::getline(fstream, line)) // Read each line from level file
{
std::istringstream sstream(line);
std::vector<GLuint> row;
while (sstream >> tileCode) // Read each word seperated by spaces
row.push_back(tileCode);
tileData.push_back(row);
}
if (tileData.size() > 0)
this->init(tileData, levelWidth, levelHeight);
}
}
void GameLevel::Draw(SpriteRenderer &renderer)
{
for (GameObject &tile : this->Bricks)
if (!tile.Destroyed)
tile.Draw(renderer);
}
GLboolean GameLevel::IsCompleted()
{
for (GameObject &tile : this->Bricks)
if (!tile.IsSolid && !tile.Destroyed)
return GL_FALSE;
return GL_TRUE;
}
void GameLevel::init(std::vector<std::vector<GLuint>> tileData, GLuint levelWidth, GLuint levelHeight)
{
// Calculate dimensions
GLuint height = tileData.size();
GLuint width = tileData[0].size(); // Note we can index vector at [0] since this function is only called if height > 0
GLfloat unit_width = levelWidth / static_cast<GLfloat>(width), unit_height = levelHeight / height;
// Initialize level tiles based on tileData
for (GLuint y = 0; y < height; ++y)
{
for (GLuint x = 0; x < width; ++x)
{
// Check block type from level data (2D level array)
if (tileData[y][x] == 1) // Solid
{
glm::vec2 pos(unit_width * x, unit_height * y);
glm::vec2 size(unit_width, unit_height);
GameObject obj(pos, size, ResourceManager::GetTexture("block_solid"), glm::vec3(0.8f, 0.8f, 0.7f));
obj.IsSolid = GL_TRUE;
this->Bricks.push_back(obj);
}
else if (tileData[y][x] > 1) // Non-solid; now determine its color based on level data
{
glm::vec3 color = glm::vec3(1.0f); // original: white
if (tileData[y][x] == 2)
color = glm::vec3(0.2f, 0.6f, 1.0f);
else if (tileData[y][x] == 3)
color = glm::vec3(0.0f, 0.7f, 0.0f);
else if (tileData[y][x] == 4)
color = glm::vec3(0.8f, 0.8f, 0.4f);
else if (tileData[y][x] == 5)
color = glm::vec3(1.0f, 0.5f, 0.0f);
glm::vec2 pos(unit_width * x, unit_height * y);
glm::vec2 size(unit_width, unit_height);
this->Bricks.push_back(GameObject(pos, size, ResourceManager::GetTexture("block"), color));
}
}
}
}
因为支持任意数量的行和列,这个游戏关卡类给我们带来了很大的灵活性,用户可以通过修改关卡文件轻松创建自己的关卡。
在游戏中
我们希望在Breakout游戏中支持多个关卡,因此我们将在Game类中添加一个持有GameLevel
变量的容器。同时我们还将存储当前的游戏关卡。
class Game
{
[...]
std::vector<GameLevel> Levels;
GLuint Level;
[...]
};
这个教程的Breakout版本共有4个游戏关卡:
然后Game类的init函数初始化每个纹理和关卡:
void Game::Init()
{
[...]
// 加载纹理
ResourceManager::LoadTexture("textures/background.jpg", GL_FALSE, "background");
ResourceManager::LoadTexture("textures/awesomeface.png", GL_TRUE, "face");
ResourceManager::LoadTexture("textures/block.png", GL_FALSE, "block");
ResourceManager::LoadTexture("textures/block_solid.png", GL_FALSE, "block_solid");
// 加载关卡
GameLevel one; one.Load("levels/one.lvl", this->Width, this->Height * 0.5);
GameLevel two; two.Load("levels/two.lvl", this->Width, this->Height * 0.5);
GameLevel three; three.Load("levels/three.lvl", this->Width, this->Height * 0.5);
GameLevel four; four.Load("levels/four.lvl", this->Width, this->Height * 0.5);
this->Levels.push_back(one);
this->Levels.push_back(two);
this->Levels.push_back(three);
this->Levels.push_back(four);
this->Level = 1;
}
现在剩下要做的就是通过调用当前关卡的Draw函数来渲染我们完成的关卡,然后使用给定的sprite渲染器调用每个GameObject
的Draw
函数。除了关卡之外,我们还会用一个很好的背景图片来渲染这个场景:
void Game::Render()
{
if(this->State == GAME_ACTIVE)
{
// 绘制背景
Renderer->DrawSprite(ResourceManager::GetTexture("background"),
glm::vec2(0, 0), glm::vec2(this->Width, this->Height), 0.0f
);
// 绘制关卡
this->Levels[this->Level].Draw(*Renderer);
}
}
结果便是如下这个被呈现的关卡,它使我们的游戏变得开始生动起来:
玩家挡板
此时我们在场景底部引入一个由玩家控制的挡板,挡板只允许水平移动,并且在它接触任意场景边缘时停止。对于玩家挡板,我们将使用以下纹理:
一个挡板对象拥有位置、大小、渲染纹理等属性,所以我们理所当然地将其定义为一个GameObject
。
// 初始化挡板的大小
const glm::vec2 PLAYER_SIZE(100, 20);
// 初始化当班的速率
const GLfloat PLAYER_VELOCITY(500.0f);
GameObject *Player;
void Game::Init()
{
[...]
ResourceManager::LoadTexture("textures/paddle.png", true, "paddle");
[...]
glm::vec2 playerPos = glm::vec2(
this->Width / 2 - PLAYER_SIZE.x / 2,
this->Height - PLAYER_SIZE.y
);
Player = new GameObject(playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle"));
}
这里我们定义了几个常量来初始化挡板的大小与速率。在Game的Init函数中我们计算挡板的初始位置,使其中心与场景的水平中心对齐。
除此之外我们还需要在Game的Render函数中添加:
Player->Draw(*Renderer);
如果你现在启动游戏,你不仅会看到关卡画面,还会有一个在场景底部边缘的奇特的挡板。到目前为止,它除了静态地放置在那以外不会发生任何事情,因此我们需要进入游戏的ProcessInput
函数,使得当玩家按下A和D时,挡板可以水平移动。
void Game::ProcessInput(GLfloat dt)
{
if (this->State == GAME_ACTIVE)
{
GLfloat velocity = PLAYER_VELOCITY * dt;
// 移动挡板
if (this->Keys[GLFW_KEY_A])
{
if (Player->Position.x >= 0)
Player->Position.x -= velocity;
}
if (this->Keys[GLFW_KEY_D])
{
if (Player->Position.x <= this->Width - Player->Size.x)
Player->Position.x += velocity;
}
}
}
在这里,我们根据用户按下的键,向左或向右移动挡板(注意我们将速率与deltaTime
相乘)。当挡板的x值小于0,它将移动出游戏场景的最左侧,所以我们只允许挡板的x值大于0时向左移动。对于右侧边缘我们做相同的处理,但我们必须比较场景的右侧边缘与挡板的右侧边缘,即场景宽度减去挡板宽度。
现在启动游戏,将呈现一个玩家可控制在整个场景底部自由移动的挡板。
你可以在下面找到更新后的Game类代码:
/*******************************************************************
** This code is part of Breakout.
**
** Breakout is free software: you can redistribute it and/or modify
** it under the terms of the CC BY 4.0 license as published by
** Creative Commons, either version 4 of the License, or (at your
** option) any later version.
******************************************************************/
#include "game.h"
#include "resource_manager.h"
#include "sprite_renderer.h"
#include "game_object.h"
// Game-related State data
SpriteRenderer *Renderer;
GameObject *Player;
Game::Game(GLuint width, GLuint height)
: State(GAME_ACTIVE), Keys(), Width(width), Height(height)
{
}
Game::~Game()
{
delete Renderer;
delete Player;
}
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("sprite", 0);
ResourceManager::GetShader("sprite").SetMatrix4("projection", projection);
// Load textures
ResourceManager::LoadTexture("textures/background.jpg", GL_FALSE, "background");
ResourceManager::LoadTexture("textures/awesomeface.png", GL_TRUE, "face");
ResourceManager::LoadTexture("textures/block.png", GL_FALSE, "block");
ResourceManager::LoadTexture("textures/block_solid.png", GL_FALSE, "block_solid");
ResourceManager::LoadTexture("textures/paddle.png", true, "paddle");
// Set render-specific controls
Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));
// Load levels
GameLevel one; one.Load("levels/one.lvl", this->Width, this->Height * 0.5);
GameLevel two; two.Load("levels/two.lvl", this->Width, this->Height * 0.5);
GameLevel three; three.Load("levels/three.lvl", this->Width, this->Height * 0.5);
GameLevel four; four.Load("levels/four.lvl", this->Width, this->Height * 0.5);
this->Levels.push_back(one);
this->Levels.push_back(two);
this->Levels.push_back(three);
this->Levels.push_back(four);
this->Level = 0;
// Configure game objects
glm::vec2 playerPos = glm::vec2(this->Width / 2 - PLAYER_SIZE.x / 2, this->Height - PLAYER_SIZE.y);
Player = new GameObject(playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle"));
}
void Game::Update(GLfloat dt)
{
}
void Game::ProcessInput(GLfloat dt)
{
if (this->State == GAME_ACTIVE)
{
GLfloat velocity = PLAYER_VELOCITY * dt;
// Move playerboard
if (this->Keys[GLFW_KEY_A])
{
if (Player->Position.x >= 0)
Player->Position.x -= velocity;
}
if (this->Keys[GLFW_KEY_D])
{
if (Player->Position.x <= this->Width - Player->Size.x)
Player->Position.x += velocity;
}
}
}
void Game::Render()
{
if (this->State == GAME_ACTIVE)
{
// Draw background
Renderer->DrawSprite(ResourceManager::GetTexture("background"), glm::vec2(0, 0), glm::vec2(this->Width, this->Height), 0.0f);
// Draw level
this->Levels[this->Level].Draw(*Renderer);
// Draw player
Player->Draw(*Renderer);
}
}
后记
本篇已结束,后面更精彩~~~