
























项目简介
这是一个由C++编写的,基于OpenGL图形库等实现的一个简单地形渲染器。可以实现自定义地形类型,通过修正不同地块类型之间的硬边角,实现不同地形之间的平滑过渡,并且将其渲染出来。
渲染器展示
1 / 2
实现流程
一、引擎部分
1. 组件结构
简单来说:
这个引擎的框架实现依靠于C++
借助Opengl等图形库得以渲染图像
编写游戏物体和组件来创作内容
游戏的进行依托于每个组件的Awake、Update等函数的运行
组件就是脚本,脚本就是组件
2. 引擎运行
引擎运行依靠的是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,是引擎运行的基础结构:
Scene组件执行Awake函数时,将需要的游戏物体创建出来
Scene组件也会执行Update函数,负责移动相机,接收输入等
3. 具体实现
Application类
负责引擎运行的底层驱动,包括:
初始化OpenGL服务,设置数据下载地址,更新窗口大小
在初始化时调用所有组件的Awake函数,每帧调用所有物体的Update函数,来进行逻辑运算
每帧调用所有物体的Render函数来实现渲染
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类
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类
引擎的基础,所有组件脚本都要继承自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_; };
二、渲染部分
1. MeshFilt=er&模型文件.mesh
mesh格式的文件存储模型数据,主要包含:
顶点:存储单个顶点的位置、颜色、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文件中读取顶点数据
读取顶点数据使用C++的IO系统:
创建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对象 };
2. Shader
顶点着色器
接收mvp矩阵和顶点位置,赋值给gl_Position
接收顶点颜色和uv坐标并传递给片元着色器
#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; }
3. Material类&材质文件.mat
.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()); ... }
4. 单个物体渲染&MeshRenderer
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;//顶点数组对象 };
5. 启用渲染
在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(); }); }); }
三、TileMap系统
这里引用B站的一位Up主的视频视频,我从此视频中了解到的这个系统:
[给种田游戏添加程序化生成] https://www.bilibili.com/video/BV1WaH9eTEPU/?spm_id_from=333.999.0.0&amp;vd_source=4a26dbc01fe9994125e4362834171eb2
1. 原理
用普通的TileMap来来创建地形时,我们会做这几件事:
将需要绘制的地形切割成瓦片
将瓦片贴到地图上
这样做的结果就是瓦片之间出现硬边角,非常不真实
为了实现更好的展现效果,我们就需要绘制出多种地形边缘的瓦片,在地形分界处按照周围地形贴上去
不仅费时费力,而且很难通过程序进行程序化生成
因此,不同于之前用单个网格数组来存储地形,像这样
我们可以用双网格数组,像这样
世界网格用于存储地形数据,另一层网格用于地形绘制
而在对于每一个需要绘制的砖块,则根据它周围四个砖块的类型,决定这个砖块如何被渲染出来
而最终,我们需要的模型只有如下四种
Tile边角的模型
去掉一个边角的砖块模型
去掉两个相对边角的砖块模型
四分之一大小的砖块模型
而这些模型,就可以组成不同砖块,解决不同地形之间的过渡效果
2. 模型创建及导入
所有Tile的建模均在Blender中完成,并且通过脚本创建文件导出顶点数据
这就是上文提到的四种模型
通过AI生成的脚本,就可以实现将选定的物体导出为自定义的.mesh格式文件,具体代码有点长,这里只放一部分
3. 实现
TileMap类
这个类不是组件,而是只用来存储导入的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; };
TileCreater组件
加载地图文件,并存储在二维数组中
根据地图文件,创建Tile对象,为它们添加Tile组件
开启每个Tile的初始化,开启渲染
class TileCrater: public Component { public: TileCrater(){}; ~TileCrater(){}; void LoadTileMap(const std::string& filename); void CreateTiles(); void InitTiles() const; private: TileMap* tileMap; };
Tile组件
创建子瓦片,添加渲染组件
获得渲染类型,确定渲染方式
开启子瓦片渲染
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(); };
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。