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

推荐订阅源

量子位
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security
博客园 - 叶小钗
博客园 - 三生石上(FineUI控件)
C
Cybersecurity and Infrastructure Security Agency CISA
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园_首页
T
Threat Research - Cisco Blogs
C
Cisco Blogs
Recent Announcements
Recent Announcements
S
Securelist
N
Netflix TechBlog - Medium
The Register - Security
The Register - Security
P
Privacy & Cybersecurity Law Blog
宝玉的分享
宝玉的分享
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题
T
Tor Project blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
月光博客
月光博客
AWS News Blog
AWS News Blog
P
Proofpoint News Feed
博客园 - 司徒正美
L
LINUX DO - 最新话题
Stack Overflow Blog
Stack Overflow Blog
博客园 - 聂微东
H
Help Net Security
Spread Privacy
Spread Privacy
PCI Perspectives
PCI Perspectives
Project Zero
Project Zero
I
Intezer
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
The Last Watchdog
The Last Watchdog
C
Check Point Blog
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
MyScale Blog
MyScale Blog
V
Vulnerabilities – Threatpost
Recorded Future
Recorded Future
T
Tenable Blog
Jina AI
Jina AI
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志

机核

游戏性能旗舰最强之选,一加 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”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
一个简单的地形渲染器
凉州 · 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(); };