这是一个由C++编写的,基于OpenGL图形库等实现的一个简单地形渲染器。可以实现自定义地形类型,通过修正不同地块类型之间的硬边角,实现不同地形之间的平滑过渡,并且将其渲染出来。
引擎运行依靠的是Application脚本,这个脚本负责引擎运行的各个环节:初始化、Update、Render等
main函数创建Application对象,调用初始化函数和Run函数来启动和运行引擎
int main(void){
Application::set_data_path("../data/");//设置资源目录
Application::InitOpengl();//初始化引擎
GameObject* go=new GameObject("LoginSceneGo");
go->AddComponent("Transform");
go->AddComponent("LoginScene");
Application::Run();//开始引擎主循环
}
通过添加LoginScene脚本作为组件,添加到游戏物体上,来加载游戏场景
游戏场景内存放着所有游戏物体,相当于Unity里的Scene,是引擎运行的基础结构:
class Application {
public:
static const std::string& data_path(){return data_path_;}
static void set_data_path(std::string data_path){data_path_=data_path;}
/// 初始化OpenGL
static void InitOpengl();
static void Run();
static void UpdateScreenSize();
/// 每一帧内逻辑代码。
static void Update();
/// 逻辑代码执行后,应用到渲染。
static void Render();
private:
static std::string data_path_;//资源目录
static GLFWwindow* glfw_window_;
};
GameObject是组件的载体,同时可以设定为其他物体的父物体或者子物体,就这么多
class GameObject {
public:
GameObject(std::string name);
~GameObject();
std::string& name(){return name_;}
void set_name(std::string name){name_=name;}
/// 添加组件
Component* AddComponent(std::string component_type_name);
/// 添加父物体
GameObject* SetParent(GameObject* parent);
/// 添加子物体
GameObject* AddChildObject(std::string child_type_name, GameObject* child);
/// 获取组件
Component* GetComponent(std::string component_type_name);
/// 获取所有同名组件
std::vector<Component*>& GetComponents(std::string component_type_name);
/// 获取子物体
GameObject* GetChildObject(std::string child_type_name);
/// 遍历自身所有Component
void ForeachComponent(std::function<void(Component* component)> func);
/// 遍历所有GameObject
static void Foreach(std::function<void(GameObject* game_object)> func);
unsigned char layer(){return layer_;}
void set_layer(unsigned char layer){layer_=layer;}
private:
std::string name_;
unsigned char layer_;//将物体分不同的层,用于相机分层、物理碰撞分层等。
GameObject* parent_;
std::unordered_map<std::string,std::vector<Component*>> component_type_instance_map_;
std::unordered_map<std::string,GameObject*> childrenObjects_instance_map;
static std::list<GameObject*> game_object_list_;//存储所有的GameObject。
};
引擎的基础,所有组件脚本都要继承自Component,通过Awake和Update函数来发挥作用
class Component {
public:
Component(bool active = true);
virtual ~Component();
GameObject* game_object(){return game_object_;}
void set_game_object(GameObject* game_object){game_object_=game_object;}
void set_isActive(bool active);
virtual void Awake();
virtual void Update();
protected:
bool isActive;
private:
GameObject* game_object_;
};
顶点:存储单个顶点的位置、颜色、uv
顶点数组:存储模型所有的顶点
顶点索引数组:根据顶点数组组成面的指导
顶点数组长度,顶点索引数组长度
struct Vertex
{
glm::vec3 pos_;
glm::vec4 color_;
glm::vec2 uv_;
};
//Mesh文件头
struct MeshFileHead{
char type_[4];
unsigned short vertex_num_;//顶点个数
unsigned short vertex_index_num_;//索引个数
};
//Mesh数据
struct Mesh{
unsigned short vertex_num_;//顶点个数
unsigned short vertex_index_num_;//顶点索引个数
Vertex* vertex_data_;//顶点数据
unsigned short* vertex_index_data_;//顶点索引数据
};
而MeshFilter类,继承自Component组件类,负责从.mesh文件中读取顶点数据
创建Mesh结构体对象
先读取顶点数和索引数,再依次读取顶点数组和索引数组的内容,并存入Mesh
class MeshFilter:public Component{
public:
MeshFilter(bool active = true);
~MeshFilter();
public:
void LoadMesh(string mesh_file_path);//加载Mesh文件
Mesh* mesh(){return mesh_;};//Mesh对象
private:
Mesh* mesh_;//Mesh对象
};
接收mvp矩阵和顶点位置,赋值给gl_Position
#version 330 core
uniform mat4 u_mvp;
layout(location = 0) in vec3 a_pos;
layout(location = 1) in vec4 a_color;
layout(location = 2) in vec2 a_uv;
out vec4 v_color;
out vec2 v_uv;
void main()
{
gl_Position = u_mvp * vec4(a_pos, 1.0);
v_color = a_color;
v_uv = a_uv;
}
由于这次项目并未使用到贴图,所以只需传递颜色,而无需处理uv
#version 330 core
uniform sampler2D u_diffuse_texture;
in vec4 v_color;
in vec2 v_uv;
layout(location = 0) out vec4 o_fragColor;
void main()
{
o_fragColor = v_color;
}
.mat格式的文件保存着着色器程序、贴图文件的保存地址,通过rapidxml来解析文件
这次的渲染流程只通过OpenGL对顶点颜色数据的自动插值来渲染颜色,无需用到贴图
//tile.mat
<material shader="shader/tile">
<texture name="None" image="None"/>
</material>
Material类的实例都保存了一个.mat文件,并且会将其解析获得shader文件和材质文件,同样这里只放上shader部分和使用过程
//Material.cpp
void Material::Parse(string material_path)
{
//解析xml
rapidxml::file<> xml_file((Application::data_path()+material_path).c_str());
rapidxml::xml_document<> document;
document.parse<0>(xml_file.data());
//根节点
rapidxml::xml_node<>* material_node=document.first_node("material");
if(material_node == nullptr){
return;
}
rapidxml::xml_attribute<>* material_shader_attribute=material_node->first_attribute("shader");
if(material_shader_attribute == nullptr){
return;
}
shader_=Shader::Find(material_shader_attribute->value());
...
}
Render()方法,这是MeshRenderer的主体,也是渲染的主要流程,基本上分为这么几步:
MeshRenderer会保存Material类的引用,使用其解析.mat文件的功能
之后获取自身MeshFilter组件的.mesh文件,使用其导入顶点数据的功能
最后上传这些数据,完成渲染
class MeshRenderer:public Component{
public:
MeshRenderer(bool active = true);
~MeshRenderer();
void SetMaterial(Material* material);//设置Material
Material* material(){return material_;}
void Render();//渲染
private:
Material* material_;
unsigned int vertex_buffer_object_=0;//顶点缓冲区对象
unsigned int element_buffer_object_=0;//索引缓冲区对象
unsigned int vertex_array_object_=0;//顶点数组对象
};
在Appication类中,Render()函数被每帧调用,并且获取所有物体的MeshRenderer组件,执行一次Render
这里值得一提的是,可以把深度缓冲的清除也放在这个函数里边,这样每帧只被执行一次即可
深度缓冲必须要打开,否则物体的渲染顺序会影响前后遮挡关系
//Application.cpp
void Application::Render(){
//每帧清除颜色和深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//遍历所有相机,每个相机的View Projection,都用来做一次渲染。
Camera::Foreach([&](){
GameObject::Foreach([](GameObject* game_object){
auto component=game_object->GetComponent("MeshRenderer");
if (!component){
return;
}
auto mesh_renderer=dynamic_cast<MeshRenderer*>(component);
if(!mesh_renderer){
return;
}
mesh_renderer->Render();
});
});
}
这里引用B站的一位Up主的视频视频,我从此视频中了解到的这个系统:
[给种田游戏添加程序化生成] https://www.bilibili.com/video/BV1WaH9eTEPU/?spm_id_from=333.999.0.0&amp;vd_source=4a26dbc01fe9994125e4362834171eb2
用普通的TileMap来来创建地形时,我们会做这几件事:
为了实现更好的展现效果,我们就需要绘制出多种地形边缘的瓦片,在地形分界处按照周围地形贴上去
而在对于每一个需要绘制的砖块,则根据它周围四个砖块的类型,决定这个砖块如何被渲染出来
Tile边角的模型
去掉一个边角的砖块模型
去掉两个相对边角的砖块模型
四分之一大小的砖块模型
而这些模型,就可以组成不同砖块,解决不同地形之间的过渡效果
所有Tile的建模均在Blender中完成,并且通过脚本创建文件导出顶点数据
通过AI生成的脚本,就可以实现将选定的物体导出为自定义的.mesh格式文件,具体代码有点长,这里只放一部分
这个类不是组件,而是只用来存储导入的TileMap数据:
Map的长宽
所有Tile的Tile类型
所有Tile组件的引用
class TileMap {
public:
TileMap(int width, int height);
~TileMap();
unsigned int GetMapHeight() const {return height_data;}
unsigned int GetMapWidth() const {return width_data;}
const vector<vector<unsigned short>>& GetMap_Data() const {return tileMap_data;}
const vector<vector<Tile*>>& GetMap_Instance() const {return tileMap_instance;}
unordered_map<unsigned short,int> TileMap::GetTile_Data(pair<int,int> position);
void SetMap_Data(const vector<vector<unsigned short>>& map);
void SetMap_Instance(const vector<vector<Tile*>>& map);
private:
vector<vector<unsigned short>> tileMap_data;
vector<vector<Tile*>> tileMap_instance;
unsigned int width_data;
unsigned int height_data;
unsigned int width_instance;
unsigned int height_instance;
};
class TileCrater: public Component {
public:
TileCrater(){};
~TileCrater(){};
void LoadTileMap(const std::string& filename);
void CreateTiles();
void InitTiles() const;
private:
TileMap* tileMap;
};
创建子瓦片,添加渲染组件
获得渲染类型,确定渲染方式
开启子瓦片渲染
class Tile: public Component {
public:
Tile(){};
~Tile(){};
void SetMap(TileMap* map){tileMap = map;};
void TileCreate(float x, float z);
void TileInitialize(pair<int,int> tilePos);
private:
TileMap* tileMap;
/// Tile在TileMap中的相对位置
pair<int,int> tile_orientation;
/// 存储Tile子块的Tile类型
std::unordered_map<unsigned short,int> childTiles_data_map;
/// 存储子块位置和引用的map
std::unordered_map<unsigned short,GameObject*> childTiles_instance_map;
GameObject* SetChildTile(unsigned short childPos, GameObject* childTile);
vector<pair<unsigned short,unsigned short>> GetTileRenderLine();
};
评论区
共 条评论热门最新