ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

游戏引擎原理与实践:从3A渲染技术到性能优化

游戏引擎原理与实践:从3A渲染技术到性能优化 前阵子有个做独立游戏的朋友问我同样是引擎写的为什么3A游戏的画面观感和普通小游戏差出一个次元我说差的不是某个按钮而是一整套渲染、资产、性能调优的工程体系。这篇《游戏引擎原理与实践》第二篇我打算从引擎原理的角度把3A游戏背后的技术面纱掀开一点从显卡怎么把三角形变成画面到引擎怎么组织几十万行代码再到同屏几十万个物体如何不卡最后聊聊开源引擎Godot在实际项目里一个让我印象深刻的坑。这篇文章适合谁看如果你写过一些小游戏想搞懂商业引擎和开源引擎的本质差异如果你刚开始学图形学想知道那些术语到底在项目中干什么甚至你只是好奇为什么3A游戏画面能做到这种程度——这篇文章应该能给你一个比较完整的坐标系。我会尽量不堆名词先把每个技术点到底解决了什么问题讲清楚再给一些可以在自己项目里直接使用的思路。1. 为什么3A游戏的画面能“骗过”你的眼睛渲染技术的地基先说结论3A游戏画面的本质是显卡每秒钟在上亿次计算里把一堆三角形变成你屏幕上那一帧像素。所有真实感都建立在这个基础流程之上只是每一环都做到了极致。1.1 光栅化显卡到底在做什么模型在引擎里不是一个整体而是由大量三角形或者说多边形网格拼出来的。3A游戏里一个角色常模经常是十几万甚至几十万个三角形一个开放世界场景里的道具、植被、墙体加起来可能上亿。千万不要小看这个数字这就是画面细腻程度的物理基础。显卡处理三角形的流程可以概括成四步顶点输入引擎把模型的顶点坐标、法线、UV、颜色等数据提交给显卡。顶点变换通过MVP矩阵模型、视图、投影把三角形从局部坐标一步步变换到屏幕空间。光栅化把三角形内部填满像素生成一个个需要着色的像素点。像素着色对这些像素执行着色器程序算出最终颜色。你可以把光栅化理解成马赛克拼图远处看是一张完整的画走近了发现全是小色块。显卡的工作就是决定每个色块该是什么颜色然后以每秒几十帧的速度刷新整张拼图。3A画面和普通小游戏的差距很大程度上就是三角形数量和每个像素的计算精度的差距。这里有个经常被新手忽略的点光栅化阶段只处理被相机拍到的三角形。哪怕场景里有十亿个三角形只要不在视锥体内理论上就不该进渲染管线。这个不该进的剔除逻辑是后面第三章的重头戏。1.2 光照不是画上去的是算出来的很多人以为游戏里的光照是美术画出来的阴影和高光这是最大的误解。3A游戏的光照是引擎实时算出来的或者提前烘焙好存起来的总之不是手绘。光照分两个维度直接光照太阳、灯、手电筒这类光源直接照到物体表面产生的亮面和阴影。间接光照光打到地面、墙面后反弹再照亮旁边物体的效果。这是真实感最关键的部分也是最贵的部分。间接光照怎么算如果全场景都实时计算光反弹性能会直接爆炸。3A项目的常规做法是烘焙GI在编辑器里先用一个离线全局光照算法把静态场景的间接光照算完烘焙进光照贴图Lightmap或光照探针Light Probe。玩家运行时读取这些预计算结果表现是光反弹过的效果开销却只有采样一张贴图。动态物体玩家、NPC、可开合的箱子没法用静态烘焙就要靠实时GI方案。比如UE的Lumen、Unity的Enlighten或Progressive GPU光照还有各种探针方案。但即便是3A项目也不会让所有光照都在实时算而是烘焙为主、实时光源控制动态、探针补全局的组合。我在项目里踩过一个记忆深刻的坑一栋建筑的烘焙光照花了两小时结果因为窗户旁边有一个没封严的几何洞天光漏进来墙面出现一大片假亮斑。查了大半天才发现是遮挡体漏了。烘焙光照对场景网格的封闭性极度敏感任何一条缝都会变成光漏点。这属于只有实际做过才知道的破事。1.3 PBR材质为什么金属和石头看起来不一样光算完还得决定物体表面怎么反光。5年前很多游戏还在用漫反射贴图高光贴图的旧式模型美术画成什么样就显什么样。现在3A项目基本全面转向PBRPhysically Based Rendering基于物理的渲染。PBR的核心是几张贴图组合基础色Albedo/Base Color表面本身的颜色。金属度Metallic这个位置是金属还是非金属。粗糙度Roughness表面有多糙决定高光是被聚拢成镜面还是散开成磨砂。法线贴图Normal在低模表面伪造细节起伏让平面看起来有凹凸。PBR里有个底层物理常识金属没有漫反射它直接反射环境光所以金属度贴图决定了反射范围而粗糙度控制高光扩散——刷过清漆的木桌和没刷清漆的木头差的就是这个参数。表面反应为什么用PBR而不是老式的手绘高光因为PBR遵循能量守恒粗糙度越高反射越小但散射范围越大整体亮度不会虚假溢出。这对美术流水线意味着天大的好事同一个材质贴在金属枪身和塑料枪托上光照表现是物理一致的不会再因为美术调错参数导致金属看起来像塑料。而且PBR材质标准跨引擎通用——你在Substance里做好的材质拿到Unity、UE、Godot里理论上表现差异很小。1.4 后处理让你觉得真实的最后一公里有了几何、光照、材质画面还缺最后一步后处理。你如果直接把游戏引擎输出的原始颜色缓冲显示到屏幕上会发现画面往往偏暗、生硬、细节刺眼。真正让你觉得有电影感的是后处理这层滤镜。3A项目里常见的后处理环节泛光Bloom亮部溢出辉光让霓虹灯、太阳边缘看起来更亮瞎。色调映射Tone Mapping把HDR计算出的高动态范围颜色压到显示器能显示的LDR范围这一步直接决定画面的明暗情绪。景深Depth of Field模拟镜头焦点前景背景虚化。环境光遮蔽SSAO/GTAO在物体交汇的缝隙处加深阴影让角落更扎实。没有它画面会显得飘、浮。抗锯齿TAA/MSAA/DLSS/FSR把三角形边缘的锯齿抹平。业内有个不太好听但真实的说法画面好不好看一半靠渲染一半靠后处理。同一个场景关闭所有后处理再截图和开了全套后处理再截图观感差一个次元。这就是为什么不建议新手一开始就追求炫酷特效——先把色调映射和抗锯齿调对画面观感立刻上一个台阶。2. 一个引擎是如何把几十万行代码拧成一股绳的架构视角玩家看到的只是画面但3A游戏的背后是一套庞大系统在同时运转物理、动画、AI、音频、网络、资产加载、脚本逻辑……游戏引擎的价值就是把它们组合成一个帧循环稳定的整体。2.1 引擎不是一个程序是一套系统如果你打开Unity或者UE的安装目录看到的是一堆编辑器程序、运行时库、工具链。这些组合在一起才叫引擎。我习惯把它分层理解平台层操作系统接口、图形API封装Vulkan/DX12/Metal、硬件抽象。运行时层渲染系统、物理系统、动画系统、音频系统、资源管理、脚本虚拟机、网络模块。游戏逻辑层你自己写的玩法代码、关卡逻辑、AI行为、角色控制。工具层关卡编辑器、动画状态机、材质编辑器、性能剖析器、资源导入管线。3A工作室通常不会拿着商业引擎原样开工而是深度改造。你会发现很多3A游戏的制作名单里写着内部引擎或魔改版本就是这个原因。商业引擎给的是完整工作流但3A项目需要的是满足特定品类的专用工作流——射击游戏需要弹道同步格斗游戏需要帧同步开放世界需要大场景流送。引擎架构从一开始就决定了项目的天花板。这里有个实用经验小团队选引擎别只看渲染画面先看你需要哪些工具链。画面可以靠技术和美术堆但工具链缺失会导致量产崩溃。比如你要做开放世界引擎没有地形流送和Prefab批量管理你后面会在加载和场景管理上痛苦到怀疑人生。2.2 游戏循环所有3A游戏的心跳每个游戏引擎里都有一个游戏循环Game Loop它负责每帧做三件事处理输入、更新逻辑、渲染画面。60帧就是每秒循环60次120帧就是120次。听起来简单但这里藏着游戏开发最重要的一个细节逻辑更新不能依赖帧率。写代码时如果直接写position speed在60帧和144帧的机器上角色跑动速度会完全不同。正确写法是乘以帧时间# 错误与帧率绑定144Hz下角色会飞 position speed # 正确乘以deltaTime后无论帧率多少每秒位移一致 position speed * deltaTime3A游戏在多人同步上还有一个更严格的要求固定时间步长Fixed Timestep。物理模拟、网络同步、动画根运动都需要在一个确定的步长里计算否则两台不同帧率的客户端模拟出的物理结果会对不上。我的经验是凡是跟网络或物理相关的逻辑一律走固定步长纯表现层的特效、UI抖动可以走可变步长。这个“固定/可变”的边界新手特别容易搞混。2.3 ECS与数据导向设计从Unity/UE4的架构选择说起这几年聊引擎原理绕不开ECSEntity Component System实体组件系统。老式OOP习惯把游戏对象做成继承链怪物继承自敌人敌人继承自角色角色继承自Actor……层级越深越僵硬。ECS反着来实体只是一个ID组件是纯数据系统是处理数据的逻辑函数。生活化类比OOP像每台iPhone是一个对象封装了外壳、屏幕、芯片而ECS像把1000块屏幕放成一摞1000颗芯片放成一摞生产线统一处理所有屏幕的组装。后者对CPU缓存极度友好——处理一批连续内存的数据比到处跳指针快几个数量级。Unity的DOTSData-Oriented Technology Stack就是在推ECSFrostbite引擎内部也大量使用数据导向架构。但我要给一个清醒的判断ECS对于中型以下的玩法项目不是灵丹妙药它真正的优势在同屏大量同构物体的场景。如果你写一个塔防有几千个敌人共享同一套逻辑ECS的收益非常明显如果你做一个叙事驱动的小关卡传统组件模式反而开发效率更高。2.4 脚本、C和热更新3A团队怎么写游戏逻辑3A项目里语言分层是明确的性能关键的底层渲染、物理、资源用C中等性能的玩法逻辑用脚本C#/Lua或可视化脚本配置数据则直接铺数据表。这里有个经常被新手误解的点UE的蓝图很强大但系列文章里我见过太多人全蓝图写玩法之后性能崩了。蓝图的每个节点连线都有调度开销复杂逻辑跑在蓝图里比C慢几十倍是常见的。3A团队的普遍做法是原型期用蓝图快速验证正式版本把热点逻辑用C重写。Fortnite很早就公开说过他们在代码规范上强制要求上线前把蓝图重写为C。脚本层的另一个痛点是热更新。单机3A主机平台一般不做热更——因为商店审查流程不允许你动态加载代码长线运营的网游则需要。业界常用方案是C框架Lua热更或者Unity的ILRuntime/HybridCLR。但记住一个铁律脚本层千万别写每帧循环的高频计算热更代码的性能边界比原生代码窄得多。你在脚本里写一个全屏粒子的逐像素算法等着你的就是掉帧和发热。3. 同屏几十万物体不卡渲染性能优化的真实手段渲染画面漂亮只是第一步3A游戏真正难的是在有限帧预算内把漂亮画面跑出来。开放世界里随便一片森林可能有几十万棵树、草、石头想做到不卡靠的不是显卡无限强而是尽量少画。3.1 Draw Call是第一个敌人Draw Call是CPU向GPU下达绘制这个物体的一次命令。传统图形API下GPU执行一次Draw Call需要CPU准备好一大堆状态顶点缓冲、纹理绑定、着色器参数……如果一帧要提交一万个Draw CallCPU光是准备命令就会超时显卡反而处在没事可干等饭的状态。可以这么理解把显卡想成一个餐厅厨房CPU是点单服务员Draw Call是服务员递进来的菜单。服务员手写一万张菜单厨师再快也得等菜单送进来。所以优化渲染性能首要目标永远是减少Draw Call数量而不是盲目堆GPU规格。3.2 合批、实例化与遮挡剔除三个耳熟能详的高频词到底怎么用减少Draw Call的主流手段有三个合批Batching、实例化Instancing、剔除Culling。合批的思路是把多个小网格合并成一个大网格一个Draw Call全部画完。引擎里的静态合批会把场景里相同材质的静态物体合并成一张大网格动态合批则是在CPU侧把动态物体的顶点当场拼起来。动态合批有严格限制顶点数过多、UV属性过多、或者每个物体有差异都会导致合批失败。很多新人以为勾上Dynamic Batching就能万事大吉结果合批率只有15%就是因为没读这个限制。实例化GPU Instancing是3A项目里处理大量重复物体的核心方案写一次命令告诉显卡这个草模型在以下一百个位置各画一份。植被、石头、粒子、大量小道具全靠它。一张地形上铺十万棵草如果实例化做得好GPU开销升幅很小如果用一万个独立网格帧率早就掉没了。遮挡剔除则是看不见的就不画。入门时大家都会做视锥剔除——只画相机框住的物体。但视锥内还有大量被墙挡住的物体3A项目必须做遮挡查询Occlusion Query或者在关卡设计阶段就摆好遮挡体。开放世界游戏里门后有场景这种设计不是艺术选择是性能刚需门一关整个室内场景直接不渲染。一个实际经验合批和阴影往往冲突。你合好批的网格一旦需要投射阴影某些引擎会为阴影Pass重新按物体拆开Draw Call。所以性能调优要同时看主渲染Pass和阴影Pass不能只看总Draw Call一个数字。3.3 LOD与贴图流送把性能花在能看见的地方即使合批和剔除做得再好开放世界里仍然有大量看得见但看不细的东西。LODLevel of Detail多层次细节应运而生远处的物体用低精度模型近处的用高精度模型。切换距离由引擎根据物体大小、距离、屏幕占比决定。LOD切换有个经典问题叫跳变玩家走近时一个低模突然变成高模视觉上像噗地一下弹出来。3A项目会用连续LOD例如UE的Nanite直接解决了这个问题按像素粒度加载三角形或者做平滑过渡来规避。这也是为什么树、山、建筑这类大物体是LOD调优的重灾区。贴图同理。一张4096×4096的贴图如果原样加载内存和带宽都吃不消。mipmap技术为每张贴图预生成一组逐渐模糊的小尺寸版本远处采样小mip近处采样大mip。很多项目里远处纹理闪烁的Bug八成就是美术导出时没带mipmap链或者压缩格式不对。大世界还会用虚拟纹理Virtual Texture把整张地形贴图切成异步加载的小块玩家跑到哪哪一块才真正驻留在显存里。3.4 从控制台到PC性能预算表怎么列性能优化不是开发完再调而是在项目立项时就写一张预算表此后每个里程碑都拿实测数据对标这张表。以60帧目标为例一帧只有16.6毫秒预算大致这么分系统预算毫秒说明GPU主渲染8.0不透明/透明物体、场景渲染、后处理GPU阴影2.5级联阴影、阴影图生成渲染线程CPU4.0Draw Call提交、剔除计算、渲染状态切换游戏逻辑CPU5.0AI、动画、物理、脚本物理模拟1.5刚体、布娃娃、载具其他IO/UI/音频2.0流量控制、UI批量重建、音频混音这些预算加起来远超16.6所以真实项目里到处是拆东墙补西墙某个大型Boss战物理开销翻倍就得临时砍掉一部分后处理某个开放世界远景太耗GPU就得把LOD切换距离往近了压。3A开发本质上是用有限预算做无限内容的工程博弈。我在项目里只坚持一条纪律任何新系统上线前必须先报自己的毫秒预算。没有预算表的功能做得再炫也活不到正式版。4. 开源引擎的野心Godot离3A还有多远以及那坑人的乱码问题聊完商业引擎体系我想专门写一段开源引擎。这几年Godot的关注度涨得非常快某种程度上已经成了开源3A梦的象征之一。但实际用起来远没有演示视频那么美至少我上手时第一个打脸的问题就是中文乱码。4.1 Godot为什么这两年关注度暴涨Godot的好处其实很硬核开源且协议宽松MIT没有授权费引擎本体只有几十兆启动速度快节点和场景的组合式设计非常直观支持GDScript、C#、C多种写法。Godot 4引入了Vulkan后端、SDFGI全局光照、体积雾、基于物理的渲染画面表现力比Godot 3时代上了一个大台阶。但要说Godot能干翻商业引擎就太年轻了。我自己的判断是Godot非常适合中小体量的3D项目、原型验证、以及想彻底研究引擎源码的人。它的短板不是渲染API不够新而是生产工具链和中间件生态不够厚。这个后面细说先讲让我印象最深的乱码排查。4.2 复现一次Godot游戏乱码我从懵到定位的全过程事情是这样在Godot 4里做一个带中文界面的小DemoButton按钮和Label标签上的中文全部显示成方框和井号英文字符则完全正常。第一反应是编码坏了但检查脚本文件确认是UTF-8编码代码里print(你好)也能在控制台正常输出中文问题被缩小到了渲染或字体层面。如果你也遇到类似情况按这个顺序排查三分钟内通常能定位症状可能原因排查方向中文全是方框/井号英文正常字体缺少CJK中日韩字形换成包含中文的开源字体如思源黑体、Noto Sans CJK中文变成问号或乱码字符文本编码被破坏检查脚本文件编码确保是UTF-8无BOM部分控件正常、部分控件乱码主题字体覆盖不一致检查theme_override_font、LabelSettings等局部覆盖中文发虚、看起来像乱码字号过小、抗锯齿设置不合适调整字体抗锯齿、Hinting、DPI设置我当时的问题属于部分控件乱码全局Theme里设置了中文字体但某个用代码实例化的Button自带了一个默认字体覆盖覆盖层级把中文字体顶掉了。Godot的字体系统有严格的作用域优先级theme_override 控件所在Theme 全局Theme。只要UI层级里有任何一层override全局配置就管不到它。另一个高发场景是全局默认字体没生效。Godot 4的正确做法是项目设置里指定自定义字体# project.godot里这样配置全局默认字体 [gui] theme/custom_fontres://assets/fonts/SourceHanSansSC-Regular.otf但注意这个配置只对没有显式设置默认字体的控件生效。已经拖了Label、Button并把字体字段设过值的UI还是要逐一清掉override。这个规则几乎能解释九成的Godot字体乱码。字体导入设置也要留意。如果你有个中文字体文件需要缩小体积用.subset子集化工具只保留常用字形但漏掉了UI里出现的生僻字运行时就只能显示方框。而字体纹理图集会因为中文字形数量暴增导致显存占用上升这时候开启Font的Mipmaps反而容易出现小字号模糊被玩家误当成乱码。所以我的最终建议是统一用全局Theme管理字体不要在控件层级手动覆盖字体文件直接放常用CJK全量字形别省那几MB体积。4.3 Godot距离3A还有哪些短板乱码只是入门课真正让团队犹豫用Godot做3A的是下面这些结构性问题工具链密度不足商业引擎的关卡搭建、地形雕刻、植被刷、动画状态机、材质调试视图是一整套顺畅的生产管线Godot里这些基础能力都有但深度和自动化程度差一截大世界流送、Hierarchical LOD、大规模植被实例化往往需要自己写扩展。中间件集成弱物理引擎、音频中间件、面捕、动画重定向这类3A项目几乎必用的第三方中间件商业引擎都做好了官方桥接Godot社区插件的维护质量和稳定性得靠运气。平台跟手度主机平台认证、各平台SDK版本跟进的及时性开源引擎很难和商业公司的专职团队拼速度。但这不意味着开源引擎做不了高规格项目。事实上很多团队选择Godot不是因为它什么都有而是因为它什么都能改。对一个真正研究引擎原理的人来说Godot的源码就是最好的教材你可以顺着渲染管线的代码看一帧到底提交了什么可以看到SDFGI的实现细节可以自己加一个合批策略。这种透明性是闭源商业引擎永远给不了的。我自己就是这样第一次把Godot的Scene节点树和渲染列表对应起来后对商业引擎里看不见的部分也有了更具体的想象。开源引擎的价值不止于做游戏更是引擎原理这本大书的可执行版本。5. 一些关于学习引擎原理的实在建议最后说点我个人的体会不搞那些未来可期的虚词。第一学引擎原理别从API手册开始。哪怕你把Unity的接口背得滚瓜烂熟不理解Draw Call、帧预算、光照模型之间的耦合关系做出来的项目照样会卡、会糊、会难以维护。我的顺序是先搞懂渲染管线的粗骨架顶点怎么变像素、光照怎么参与、再搞懂游戏循环和逻辑更新、最后再去碰具体引擎的API。骨架稳了接口只是换皮。第二动手跑Profiler比看任何教程都管用。打开Unity Profiler或Godot的调试器跑一个Demo看一帧里CPU、GPU、Draw Call、脚本耗时的占比。你会立刻明白为什么引擎不是万能的也会对工程优化这四个字产生敬畏。第三如果你的项目里出现了Godot字体乱码、或任何类似看起来很蠢的Bug先别急着骂引擎。按作用域、编码、资源导入三个方向冷静排查绝大多数问题都是配置和资产链路的问题。能通过一次排查把引擎的字体加载链路看清远比抱怨引擎不行收获大。这篇文章到这里3A背后的技术面纱算是掀开了一角从渲染的地基、引擎的骨架、性能的预算到开源引擎的真实体验。下一篇我打算继续沿着渲染管线的脉络往材质系统深处走到时候再细聊。
返回列表