默认渲染管线和URP渲染管线的问题
教程中作者采用的Unity版本为Unity 2020.3.6f1,我采用的版本为2022.3.27f1c1,渲染管线的更改上因为版本的不同有一些差别,一些默认渲染管线的设置更改了位置。
切换渲染管线,通过以下操作:
打开 Edit > Project Settings。
在 Quality 面板中,有 Render Pipeline Asset 选项,这就是用于切换渲染管线的设置位置。
将 Render Pipeline Asset 的字段选择None,以切换回Built-in渲染管线,如果未成功切换,把Graphic > Scriptable Render Pipeline Settings处的字段也选择None。
如果需要重新启用URP渲染管线,只需将 Render Pipeline Asset 设置为URP资源文件即可。
在Unity 2022.3版本中,如果没有找到SRP Batcher以及Dynamic Batching这个选项,可以参照如下设置修改,打开Edit > Preferences > Core Render Pipeline > Additional Properties > Visibility,改为All Visible(此处参考Unity 在URP中显示动态批处理 Dynamic Batching 选项)。
分析Unity性能
1. 帧速率 FPS(frame per second)
Unity在Play Mode的时候会持续不断的渲染帧,但是每一帧都是静止的一张图片,如果希望通过人眼观察到的是一个流畅的画面,那么每秒至少要渲染30帧,即FPS(每秒渲染的帧数)(帧速率)要大于等于30,我们观察到的画面才会比较流畅。
帧速率也有另一种表达方式,帧率(frame rate)单位为赫兹(Hz),表示的也是每秒可以产生多少帧。
能否达到目标帧率取决于处理单个帧所需要的时间。要到达60FPS,我们需要在六十分之一秒,即16.67ms,的时间内渲染和更新每一帧。
仅通过观察运行时候的流畅程度(帧率)来进行性能分析是非常不精确的,有很多因素会造成流畅程度时好时坏,可能是应用程序引起的,也有可能是本机其他应用程序运行所造成的,或是Unity的编译器性能受到其他模块的占用,因此我们需要其他工具来帮助我们了解更多信息。
2. 游戏窗口数据(Stats)
游戏窗口有一个统计信息面板,它显示对于最后渲染帧的测量结果。如图:
BRP统计信息窗口
该面板并未给出太多信息,但它是较为简单的工具,可以用来了解正在发生的事情。
在本次运行中,我采用的是圆环函数和分辨率为100的图形,打开了游戏窗口的VSync功能,使用默认渲染管线(Build-In Render Pipeline),之后简称为BRP。
小插曲------什么是VSync功能?
在Unity中,VSync(Vertical Synchronization)是一项用于同步游戏的帧率与显示器刷新率的功能。它的主要目的是防止屏幕撕裂(Screen Tearing),一种由于显示器刷新与游戏帧率不同步而导致的画面分裂现象。
具体来说,当VSync开启时,Unity会在渲染新帧时等待显示器的垂直刷新信号,确保每一帧都完整显示,而不是在刷新中途更新显示内容。这样可以让画面更流畅并避免撕裂,但也会带来一些额外的延迟,尤其在帧率和刷新率不匹配时可能出现卡顿。
根据统计数据显示,在某一帧中,CPU主线程(CPU main)耗时为19.6ms,渲染线程(render thread)耗时15.3ms,这表明整个帧的渲染耗时为34.9ms,但是在FPS处注意到它的值是51.1(19.6ms),与CPU主线程耗时是匹配的,此处FPS显示的值看起来是采用了主线程和渲染线程中耗时最多的,并且假设它与目标帧率匹配。这种过度简化只考虑了CPU,忽略了GPU和显示,真实帧率很可能要更低。
除了持续时间和FPS指示以外,统计面板提供了其他信息。在批次处(Batches)显示共有40003个批次(原作者是30003个批次,从Shadow casters:20000来看,我应该是多了一次Shadow Caster Pass),同时显示节省的批次(Saved by batching)是0,这些是发送到GPU的绘制命令。我的分辨率是100,一共存在100*100个立方体,因此看起来每个立方体渲染了4次,按照默认的BRP渲染步骤分析,每个立方体可能经历了以下步骤:
一次深度通道(Depth Pass):这主要是为了生成深度缓冲,用于优化后续的渲染阶段。
两次阴影投射器(Shadow Caster Pass):生成一次阴影贴图,另一次原因暂不明(可能是Cascaded Shadow Maps的分区为2)。
一次基础通道渲染(Base Pass):渲染立方体的颜色和材质,这是最终可见的部分。
一次后处理或者反射(Post-Processing Pass、Reflection/Light Probe Pass):用于处理后处理效果、反射探针或者光照探针。
另外的三个批次是处理额外工作,像独立于我们Graph的天空盒,阴影处理。
此外,还有7次 set-pass 调用(set-pass call),可以认为是 GPU 被重新配置,以不同的方式进行渲染,比如使用不同的材质。
如果我们采用URP管线,我更改为Ultra_PipelineAsset,发现它的渲染速度更快,可以观察到渲染批次(batches)只有30003,比BRP减少了10000个,这是因为URP不使用单独的深度通道来处理定向阴影,它确实有更多的set-pass调用,但是这不影响其速度。
URP统计窗口
可以看出Saved by batching显示为0,但URP其实默认使用了SRP的批处理程序,SRP批处理程序不会消除单个绘制命令,但可以使它们更加高效,为了说明这一点, 打开Ultra_Pipeline Asset的Inspector窗口,取消其SRP Batcher,可以观察到下图在禁用掉SRP批处理器后(SRP Batcher),会导致URP的性能变差。
SRP和动态SRP位置
没有 SRP 批处理器的 URP 统计数据
3. 动态批处理
除了SRP Bathcer,URP还有另一个用于动态批处理的开关。这是一种古老的技术,可以动态地将小网格组合成一个较大的网格,然后进行渲染。从图上可以看出,为URP启用动态批处理减少到了20019,消除了9984次绘制。
具有动态批处理的URP统计数据
在作者看来,SRP批处理器和动态批处理具有相对不错的性能,因为我们图形点的立方体网格是动态批处理的理想作用对象。
SRP批处理不适用于BRP,但是也可以为它启用动态批处理,在Edit > Project Settings > Player > Other Settings 中可以找到。可以发现动态批处理对于BRP来说效率更高,消除到51个批次,但对实际效率没有太大帮助。
具有动态批处理的BRP统计数据
4. GPU Instancing(GPU实例化)
提高渲染性能的另一种方法是启用GPU实例化,使用这种方法可以用一个draw command让GPU 绘制具有相同材质的一个网格的多个实例,并提供一个变换矩阵数组和其他可选的实例数据。在这种情况下,我们需要针对每个材质启用它,打开Enable GPU Instancing。
URP更倾向于使用SRP批处理程序而不是GPU实例化,我们要观察GPU实例化的效果,需要先关闭SRP batcher,我们可以看出批处理数量减少到只有99个,要比动态批处理好的多。对于BRP来说,GPU实例化产生的批次比动态批处理更多,但帧速率略高。
具有 GPU 实例的 URP 统计数据
具有 GPU 实例的BRP统计数据
我们可以从这些数据中得出结论:对于URP,GPU实例化是最好的,其次是动态批处理,然后是SRP批处理器,但差异很小,因此在表中看起来实际上是等效的。唯一明确的是我们应该使用GPU实例化,或者SRP批处理器。
5. Frame Debugger
位置: Windows > Analysis > Frame Debugger。
为了更好地理解使用动态批处理和使用GPU实例化有什么不同,我们可以使用Frame Debugger,点击Enable按钮启动Debugger以后,它会在左侧显示发送给GPU的所有绘制命令的列表,这些命令是游戏窗口最后一帧的,并且按照分析样本分组,右侧则显示特定选定绘制命令的详细信息。 此外,游戏窗口还会显示直至所选命令之后的渐进绘制状态。
注:打开Frame Debugger以后直到它关闭,Unity会一直重复渲染相同的帧来展示绘制帧的中间状态,记得在不需要的时候就关闭Frame Debugger
打开播放模式(Play Mode),然后启动Frame Debugger,此时Frame Debugger会暂停播放模式来检查当前的绘制命令层次结构。
首先对BRP执行此操作,不使用动态批处理或者GPU实例化。
BRP的Frame Debugger
如图所示一共是40007次Draw Call,比之前Statistics面板要多,因为有一些命令不计入批次,例如清除目标缓冲区。我们点的绘制被单独列为Draw Mesh Point(Clone),在DepthPass.Job、Shadows.RenderDirJob和RenderForward.RenderLoopJob下。
我发现我此处的Shadows.RenderDirJob次数要比作者多出10000次,在经过一番寻找以后,发现是我的场景启用了四级联阴影,因此导致了大量额外的阴影渲染调用(每个立方体需要多次参与级联生成)。
Four Shadow Cascades
什么是级联阴影映射(Cascaded Shadow Mapping):级联阴影将摄像机的视锥划分为多个范围(通常为近、中、远、极远4个区域),每个级联会生成一张独立的阴影贴图。每个级联会为阴影投射体(Shadow Caster)生成单独的渲染调用,从理论上来说,如果有10000个立方体,启用了4级联阴影,Unity需要为每个级联分别渲染这些立方体的阴影,理论上会有10000 * 4 = 40000次阴影渲染调用,但是实际观察图中有一个Cascade splits,有每个层级的百分比,Unity根据每个物体的距离动态确定其需要参与的级联层级,可以看出某些物体只在部分级联范围内可见,所以最终调用是20000次,不是40000次。
在关闭Cascaded Shadow Mapping后,我的统计面板以及对应的Frame Debugger如下:
关闭级联阴影映射的统计面板
Frame Debugger面板
如果我们再次启用动态批处理,命令结构不会发生变化,每组的10000次绘制减少到12次的Draw Dynamic调用。这对于CPU-GPU通信开销是一个显著的改进。
具有动态批处理的BRP
如果使用GPU实例化,每个组会减少到20个Draw Mesh(Instanced)Point(Clone)调用。这又是一个很大的改进,但是方法上不同。
使用了GPU实例化的BRP
对于URP来说,我们可以看到情况差不多,但是命令层次不同。同样的是,我的MainLightShadow > Shadows.Draw要比原作者多出10000次,与BRP相同,是因为Shadows Cascade层级为4,改成1后,可以和原作者保持一致。
修改级联阴影为1
在URP中,点被绘制了两次,第一次是在Shadows.Draw下,第二次是在RenderLoop.Draw下。一个显著的区别是,动态批处理似乎对阴影贴图不起作用,这也解释了为什么动态批处理对 URP 效果较差。 在开启动态批处理后,相比较BRP,在RenderLoop.Draw中,URP使用了 16 个批次,BRP只有 12 个批次,这表明 URP 材质比标准 BRP 材质依赖更多的网格顶点数据,因此单个批次可容纳的点数更少。 与动态批处理不同的是,GPU 实例化对阴影有效,因此在这种情况下更有优势。
URP的Frame Debugger
开启动态批处理的URP
开启GPU实例化的URP
启用SRP批处理器是,绘制10000个立方体会被列为16个SRP批处理命令,但这些是单独的绘制调用,只是效率比较高。
开启SRP batcher的URP