惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
博客园 - 【当耐特】
量子位
Engineering at Meta
Engineering at Meta
博客园_首页
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
V
Visual Studio Blog
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Jina AI
Jina AI
The Cloudflare Blog
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
博客园 - 聂微东

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测
一个简单的地形渲染器
凉州 · 2025-11-28 · via 机核

项目简介

这是一个由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系统:

  1. 创建Mesh结构体对象

  2. 先读取顶点数和索引数,再依次读取顶点数组和索引数组的内容,并存入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的主体,也是渲染的主要流程,基本上分为这么几步:

  1. MeshRenderer会保存Material类的引用,使用其解析.mat文件的功能

  2. 之后获取自身MeshFilter组件的.mesh文件,使用其导入顶点数据的功能

  3. 最后上传这些数据,完成渲染

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;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(); };