目录
实时渲染基础
实时渲染是一个混合了多种不同解决方案的复杂过程,从本质上可以将其看作两个阶段:预计算阶段和具体的实时渲染阶段。
要了解会影响实时渲染性能的因素,首先需要清楚以下的基础概念:
目标帧率与毫秒
与目标帧率相关的一个重要概念是ms(毫秒),它代表渲染一帧所用的时间,数值越低则帧率越高。如30FPS对应的ms即为1000/30 = 33.33毫秒,60FPS对应的ms即为1000/60 = 16.67毫秒。在引擎的各种性能工具显示中都以ms为标准。
在评估性能时,应去掉引擎限制的帧率上限。可以用以下控制台命令来解除限制。
t.MaxFPS 600
另外,通过GPU Visualizer工具(快捷键Ctrl+Shift+,)可以查看渲染中的哪些过程耗费了大量的时间。
帧时间与GPU/CPU
使用stat unit
命令时,主要关注Game和GPU这两个的渲染时长,效率最低的一方会决定游戏的性能。
从上图可以看出,CPU(Game)处理的主要是逻辑和变换方面的计算,而GPU处理的主要是渲染方面的计算,二者并行(后文有详解)。
用stat rhi
、stat engine
、stat scenerendering
等命令可以看到更细致的性能数据。
最常见的四大性能问题
-
半透明
半透明物体由于要进行多层像素的叠加绘制,会导致较大开销。当半透明物体在屏幕空间上占比越大时,消耗也越大。 -
绘制调用(Draw Call)
Draw Calls绘制调用是实时引擎的渲染方式,它是逐对象渲染而非逐面渲染,所以如果场景中网格体数量很多,即使单个网格体面数很低,渲染消耗依旧很大,相比于场景中只有一个面数极高的网格体渲染要慢很多。同时,单个对象上材质数量越多,draw calls也越多。 -
动态阴影
要注意性能开销大的是动态阴影而非动态光照,当开启动态阴影时,保持场景中对象数量不变,增加每个网格体的面数,帧率会大幅下降,这是因为网格体的多边形数量会影响阴影的计算,所以动态光照下模型面数也是影响性能的一大因素。(当使用静态光照时面数并不会影响性能) -
像素/顶点着色器
引擎要依赖像素着色器完成几乎所有的渲染工作,不光材质,包括光照、反射和雾等都由像素着色器驱动。输出分辨率越高,材质越复杂,性能影响就越大。
实时渲染深入探究
首先了解一下实时渲染的整体流程:
注意上述步骤并不是完全依次执行的,有些会同步执行,如步骤3、4、5、6。
另外要记住,实时渲染性能最好的时候是当场景里什么都没有的时候。所以,实时渲染流程的本质是管理性能的损耗。(RTR = Real Time Rendering)
有获得必有牺牲,功能Features、品质Quality、性能Performance三者不可兼得。所以制定严格的资源和内容规范(如模型面数、贴图尺寸)就成为了很重要的一件事,因为可以提前预估将会产生的性能损耗。
延迟渲染与前向渲染
延迟渲染的特点:
前向渲染的特点:
从游戏角度看:
前向渲染适合制作简单的手游或VR游戏,且能够提供更好的效果,但当功能和场景越来越复杂时,每加入一些新资源都会导致性能大幅下降。
延迟渲染则更适合制作一些复杂的大型游戏,虽然一开始性能就会低一截,但再继续添加内容时,性能基本保持稳定,不会下降很多。
渲染之前和遮挡
CPU和GPU工作流
上文提到,CPU和GPU是并行工作,下面就详细解释一下:
由图可知,CPU和GPU会顺序对每一帧的画面进行处理,但二者之间会存在一定的时间差。
- 首先CPU进行计算,计算内容包括所有逻辑和变换,因为知道一切对象所在的位置是渲染对象的前提;
- 然后CPU和部分GPU会计算遮挡过程,剔除所有不可见对象,将所有可见对象记录在一个列表中。剔除方法包括距离剔除、视锥剔除、预计算可视性、遮挡剔除。这四种剔除方式性能消耗依次增大,引擎会优先使用消耗低的剔除方式来节约性能;
(注:预计算可视性需要在世界设置中勾选Precompute Visibility,并使用PrecomputeVisibilityVolume,它是通过构建光照将场景划分为许多个立方体空间,在每一个立方体内根据玩家或摄像机的位置,记录哪些Actor可见而哪些不可见,即把Actor位置的可视性状态存储在场景中)
- GPU开始实际的渲染过程
补充:遮挡剔除属于硬件剔除,部分移动端设备无法支持,则可以通过模型LOD实现软件剔除,对应模型属性中的LOD For Occluder Mesh,此功能并不常用。
遮挡相关性能提升须知
- 始终开启距离剔除(用CullDistanceVolume),例如当制作一个大楼的室内场景时,视线距离本身受限,此时是否用距离剔除虽然看不出差别,但如果不用,引擎就会使用其他消耗更高的剔除方式如遮挡剔除,产生不可见的性能损耗。
- 当场景中的对象数量超过1-1.5万时,基本就会开始对性能造成可见的损耗(与计算机硬件性能有关),达到3-6万个对象时,则性能损耗会很明显。因为无论此时屏幕空间中有多少可见对象,只要是有那么多的对象存在于场景中,引擎就会耗费性能去计算是否要剔除这些对象,所以这个计算过程就将对性能产生影响。用
stat initviews
命令可以看到这些开销。 - 上述消耗主要在CPU上,也有部分在GPU上。
- 大型开放场景无法很好地遮挡,因为很容易看到所有对象。
- 几乎任何可见对象都能被遮挡,包括粒子(通过Bounding Box计算遮挡)。
- 大型模型很少会被遮挡,因此会增加GPU的损耗。
- 将小模型合并起来变为大模型虽然会降低CPU的损耗,因为减少了引擎计算是否遮挡的次数,但是一旦屏幕空间中出现合并后模型的哪怕一个像素,这一整个模型都会被渲染。
以上是预计算阶段,下面将开始真正的实时渲染阶段
几何结构渲染
在正式渲染几何体之前,还要处理另外一个问题,即模型的渲染顺序。
前期Z通道
因为渲染是按模型逐个进行的,而非逐像素或逐线条渲染,所以当模型产生重叠的时候,会渲染大量冗余的像素。而解决方法则是依靠一个前期的深度Z通道进行测试。用很少量的信息去预先渲染环境,预先渲染几何体,知道每一个对象占画面的具体像素位置,由此来得到一个遮罩信息,其余被挡在后面的模型就根本不会渲染这块区域了。
绘制调用Drawcalls
- 一组共享相同属性的多边形(即一个对象)称作为一个drawcall。
- 一个对象有几种材质就会有几个drawcalls。
- 蓝图中每个静态网格体组件都要算作一个drawcall。
- 开启灯光也会增加drawcalls以及三角面。
- drawcall会成为最大的性能损耗,所以数量必须合理,2000-3000较为合理。
stat rhi
命令中的DrawPrimitiveCalls就是Drawcalls。
下面是一个简单场景的drawcall绘制流程,总共分了6步,即6个drawcalls。