前言
游戏中物体的加载和物体的渲染很多时候是独立又相关的两件事,两者既有很多完全不同的关注点,又都需要对最终呈现的效果负责。出现性能问题时,也需要单独分析加载和渲染两个方面。
加载更多是一个面向CPU和内存的课题,因为内存大小限制和一定程度缓解渲染的压力,所以需要流式加载(Streaming)。流式加载也在不断优化和演变,例如演变出分块加载、按需加载等策略(例如《黑暗之魂》系列游戏中祖传的按摄像机范围加载)。
而渲染一定程度上是一个CPU和GPU结合的课题(随着硬件发展越来越是一个GPU的课题)。传统渲染过程中先会计算一下每个物体的多边形的矩阵变换,后续又会计算每个像素的着色,最后可能还会对整体画面进行各种后处理;这个过程的各个步骤都可以进行一些分布式的计算,充分利用硬件多线程的优势。(最新的渲染流程会把物体打碎成三角面进行各种处理,但没有广泛应用,本人也没有实际研究过这种管线,就还是介绍偏传统的)
细节层级(LOD——Level of Details)是一个既涉及加载也涉及渲染的课题,这个方案更关注物体在远近不同时的显示状态,但是理解上不复杂,篇幅原因本文里不展开了。
在游戏引擎中,判断物体是否需要显示的方法就被称为剔除(Culling)。剔除算法既可以影响按需加载,也可以影响渲染(本文主要以影响渲染的方面进行展开)。
(图片展示了制作草地剔除的一个经典学习案例。左边是可见性预览,不是实际加载的物体数量)
1 视锥体剔除(Frustum Culling)
最初能想到的肯定是按照摄像机可视范围进行剔除。游戏引擎中摄像机主要包含锥体和长方体两种视口,分别被称为透视摄像机(Perspective)和正交摄像机(Orthographic)。(某种意义上视锥体剔除这个翻译不那么准确,知道意思就好)
(图中是一个透视摄像机的六面范围)
比较物体时肯定不能和每个三角面都进行计算,为此引擎引入了包围盒(Bounding Box)
和轴向包围盒(AABB—— Axially Aligned Bounding Box)这2种用于物体比较计算的中间结构,牺牲精度以大幅提高性能。前者可以在模型导入时就预计算生成,后者——如果是静态物体(Static)可以在模型编辑入场景时按照其当时的旋转与缩放进行计算。轴向包围盒主要用于减少乘除计算步骤,以提升性能。
(图中展示了几种不同的包围盒)
动态物体(Dynamic)可以有不同的策略,最省心的就是不去剔除动态物体,最精确的就是每帧实时更新包围盒。介于两者之间的,如果物体不会频繁的变化其旋转与缩放值,则可以在发生变化时再更新包围盒。(事实上由于包围盒往往要放到一个层级加速架构里计算,所以早期这种更新也不是那么容易,计算量比想象的会大很多)
(图中演示了Unity中视锥体透视的效果。左边是可见性预览,不是实际加载的物体数量)
有了包围盒就可以从进行射线检测(Ray Casting)和平面比较了。简单来说,射线检测可以判断摄像机发出的一根射线与某个包围盒的相交情况;平面比较可以判断一个点与一个平面的位置关系,判断6个端点则可以判断出一个物体的包围盒与目标平面的相交关系(还有别的方法,这个是相对好理解的)。
通过计算摄像机视锥体的六个平面(包含近平面和远平面)与包围盒的位置关系,就可以判断物体是否在摄像机范围内,以实现视锥体剔除了。
这里面有各种细节例如怎么计算、怎么设计加速数据结构(主要是把大量物体分层组织起来,避免很多不必要的查询),有兴趣可以去看B站Games101课程,这里就不展开了。
现在的主流引擎会自带视锥体剔除功能,基本不需要做什么配置在运行时就会自动剔除了。
2 遮挡剔除(Occlusion Culling)
(图中演示了Unity 中的遮挡剔除效果)
在视锥体范围内的物体,对于类似开放世界追尾视角的摄像机还往往需要进行遮挡剔除,即排除那些被完全挡住的物体不进行渲染。
遮挡剔除的核心肯定就是可见性检测算法了,单个物体肯定还是基于射线检测与平面比较的;对于多个大量物体,有很多优化方案,传统的例如物体划分、空间划分等等——整体思路都是预计算出一个加速结构,把部分固定计算结果存储下来,用来减少运行时的计算步骤;运行时直接查询这个加速结构,从树状数据中尽量上层的部分开始剔除。
比较老牌的遮挡剔除中间件(例如Umbra)需要定义遮挡物与被遮挡物,以及前面提到的——物体是静态物体还是动态物体。如果都是静态物体,则其相对遮挡关系可以预计算并存储下来,这一过程往往被称为烘培(Bake)了遮挡数据;运行时在确认了摄像机位置后,算法就可以依据遮挡数据返回剔除结果了。另外需要说明,剔除的都是被遮挡物,所以场景编辑时需要一定人工来保证划分正确。
(图中展示了遮挡烘焙数据的一种组织方式,本身也是一种空间划分方式)
越新的遮挡剔除算法对动态物体的支持就越好,毕竟整体算力也提升了,很多东西可以放到GPU实时计算(诞生了叫做ComputeShader的东西,现在都有AI了多少都不难理解GPU纯运算这回事了)。
另外,被检测物体包围盒也不一定都是和其它物体的包围盒比较了,有些可以和物体在深度缓冲(下一个话题会提到)中的深度值(也叫Z值)进行换算比较,以提高精度。想象一个大山中间有一个小洞,如果是包围盒模式会额外剔除小洞能看到的一些东西——当然也可以通过拼接包围盒的方式来规避这个问题,这里只是举个例子。
遮挡剔除对于性能的贡献是因时而异的。如果画面前有一座山,山后面的物体都是被遮挡的小物体,那么这类剔除肯定益处很大;但如果画面上主要是平地和很多稀疏的植物和草地,则在性能范围内几乎不必要进行遮挡剔除(可能算了也白算,性能搞不定时往往用地表景观范围之类别的方案来替代了)。当然这里的例子也不是绝对的,由于距离的影响,靠近摄像机的很小的物体也可能成为巨大的阻挡物(一叶障目)。
在很长的时间里,把物体遮挡数据配置对、烘焙好,都是场景美术和技术美术的一项繁重的工作。大家熟知的《巫师3》中就是运用的预计算好的遮挡数据,用Umbra3中间件进行剔除(参考标题图片)。
在物体级别进行了剔除后,其结果往往还是偏保守的(宁可放过不能错杀)。要进一步提升性能,在具体绘制每一个像素时,需要在绘制前排除不必绘制的物体片元(Fragment ),这就引入了后面一个话题——深度的缓冲与比较。
3 像素绘制与深度缓冲
(对照图中下图即为深度缓冲可视化的结果,把距离归一化后0-1的值换算为灰度预览出来)
1)像素着色的预判断
把游戏一帧画面想象成一个画布,可以想象的渲染顺序无非是两种——从远到近或从近到远(从远到近也被成为画家算法)。这里的远近可以只依赖物体的中心点或锚点来判断,那么依据其距离摄像机的位置就可以进行排序了。
在没有任何优化的年代,很多游戏绘制物体就是这么从远到近一个一个来绘制的。绘制时需要计算并着色物体在屏幕对应的每一个像素的颜色。
可以想象,如果物体是不透明(Opaque)的,当物体的一个像素被距离摄像机更近的物体的另一个像素覆盖时,前一个像素的绘制开销就是纯浪费——物体叠在一起的部分越多绘制就越浪费。(透明裁剪和半透明离今天的话题更远,就不展开了。简单理解就是透明裁剪和半透物体在这一步骤没办法用深度缓冲来优化)
在图形学和硬件共同发展的过程中,帧缓冲(FrameBuffer)这一方案被提出了,其中就包含深度缓冲(Depth Buffer或Z-Buffer)。其基本思想是缓存下一帧中每一个像素对应的深度,如果下一个需要绘制的像素深度比较大(以归一化之后近处0远处1来算),则可以不进行绘制。
2)深度缓冲与物体遮挡剔除
前面也提到,物体包围盒在计算剔除时可以和深度缓冲来比对。包围盒可大可小,往往不需要和包围盒范围覆盖的每个像素的深度值都进行比对,此时就需要生成不同精度范围的深度缓冲。例如,一个包围盒覆盖原始尺寸约400像素范围的深度缓冲区,其实只用和其中最大深度比较即可;查询时,先用屏幕分辨率和物体像素范围换算一个合适的缓冲图层级,再遍历其中覆盖的所有格子的深度值。
(多层深度缓冲的效果,比较时以被检测物体的尺寸选择一个合适的分辨率进行比对,性能最好。这种多层结构也被称为mip,这里不展开了)
(图中说明了多层深度缓冲其实存储的是最大深度值,数字只是示意便于理解)
有了这个多层结构,只需要选取合适的分辨率,比对一定范围的深度最大值即可决定物体是否要剔除。这里有个小问题,就是第一帧渲染没有深度缓冲用来剔除,是否需要特殊处理一下以提高性能,这个有一些办法解决,对这个课题做原理上的把握不需要关注这个点。
4 景深效果(Depth of Field)
由于深度缓冲可以计算像素的远近,传统渲染管线里还可以用来实现景深效果。
(图中的远景部分通过分层制作了景深效果)
景深其实是摄影设备拍摄才会有的现象,表现为一种基于距离的清晰和模糊范围。由于摄影是光线经过透镜折射后记录到传感器的结果,光圈、焦距、及焦平面到拍摄物的距离是都影响景深的重要因素。
(景深原理一图流。简单来说就是景深范围外的物体无法聚焦到光传感器所在平面上的一点里)
由于人眼比较接近一类可变透镜结构,又是双眼同时定位的模式,所以正常的人眼看物体感官上是不会有景深效果的。游戏中制作景深效果可以提高类似影片的质感,也能让画面的主体更突出。
结合前面提到的深度缓冲,如果给定某一个距离作为聚焦范围,一个想法就是距离这个聚焦点越远的深度值就越模糊,越近就越清晰;沿着这个思路就已经能实现一个基于屏幕空间后处理的方式渲染的景深效果了(指整个画幅都渲染完之后,对整体进行一些额外渲染,比如模糊等)。
(图中展示了基于Unity后处理的景深效果)
比较前沿的图形学方向一直考虑以更接近物理学原理的方式来设计渲染流程。对于透镜折射光线产生景深这个模式来说,在离线渲染中也完全可以用光线追踪的方式来进行,一切计算都体现为光能量的传播(只是加了一层透镜)。当然这一过程展开过于复杂,也超出了我个人能说明白的范围,有兴趣的仍然是可以去看B站Games101系列课。
结语
在有限的运行资源内让游戏场景及其物体渲染得尽量多而精细,是一个不断发展又一直很困难的事。即使主机和PC性能逐步解放了,这些技术下沉到移动端和低性能设备,仍然有着持久的生命力。
一些参考资料:
*这里的链接都会尽量找英文原址(Wiki、GDC、论文等),主要是想让大家感受到很多资料的年代感。拿着关键字在知乎之类的可以搜出很多翻译版和二手知识版。
《巫师3》在GDC上分享关于Umbra3中间件的使用
虚幻引擎中的剔除
深度缓冲(也称为Z-Buffering)
动态剔除算法(Hi-Z)