从Frame Debug看前向渲染和延迟渲染

摘要

提到Unity开发游戏时的性能优化,就不得不提延迟渲染技术。为了探究延迟渲染技术提高性能的原因,本文认真总结了《Unity Shader 入门精要》第9章知识和相关网络资料,首先给出了渲染流水线基本结构,其次介绍了Unity引擎自带的Frame Debug调试器,然后分别分析了在Frame Debug调试器下看到的前向渲染和延迟渲染过程,最后提出了本文关于前向渲染和延迟渲染的认识。本文没有讨论Shader的编写方法、光照模型、数学理论和具体的纹理采样方式,着重于讨论Unity引擎下,如何通过相关设置让CPU给GPU传递数据和设置渲染状态。

1 CPU和GPU

在游戏运行的每一帧,当CPU会把所需的数据从硬盘加载到系统内存中,然后,网格和纹理等数据又会被加载到显存中。之后,对于场景中的每一个物体,CPU会设置好相应的渲染状态,这些状态包括但不限于使用哪个顶点着色器/片元着色器、光源属性、材质贴图等等。接下来,CPU会发出Draw Call命令,GPU接手这些数据和状态,进行渲染,最终输出屏幕上的像素。GPU拿到CPU提交的数据后,会对这些数据进行处理,处理阶段可分为几何阶段和光栅化阶段,最后将像素存入颜色缓冲区,输出到屏幕,本文不关心GPU是怎么处理这些数据和状态的,因此不对GPU渲染流水线进行介绍。
注意:图片摘自《Unity Shader 入门精要》
注意:图片摘自《Unity Shader 入门精要》

2 Unity 中的Standard Shader

由于本文的重点在Unity引擎下CPU和GPU之间的通信设置,并不关心渲染流水线的细节,因此使用的Shader是Unity引擎提供Standard Shader。当然,我们通过阅读这个Shader编译后的代码,对于Shader中不同的Pass有一定的认识,Standard Shader一共包括7个Pass,但是FORWARD,FORWARD_DELTA和ShadowCaster各重复了一次。后面,通过在网上查找Standard Shader的源码,发现原来Standard Shader中给出了两个不同LOD的SubShader,另外给了一个FallBack的SubShader,本文绘出了Standard Shader的基本结构,如图1,表1给出了所有出现的Pass的名字及作用。
图1: StandardShader基本结构
图1: StandardShader基本结构

表1: StandardShader中的通道

Pass的名字 Light Mode Pass的作用
FORWARD ForwardBase 用于前向渲染,会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps
FORWARD_DELTA ForwardAdd 用于前向渲染,会计算额外的逐像素光源,每个Pass对应一个光源
ShadowCaster ShadowCaster 把物体的深度信息渲染到shadow map或一张深度纹理中
DEFERRED Deferred 用于延迟渲染,会渲染G-Buffer
#0(Vertex) #1(VertexLM) Vertex VertexLM 用于顶点光照渲染

3 Frame Debug 调试器

本文使用的Unity版本是2018.2.8.f1 Personal,在创建工程时会自带一个场景SampleScene,在该场景中有一个摄像机和一个平行光,新增一个Plane,调整摄像机的角度,使得摄像机可以看到场景中的Plane。在Unity中新增的Plane会自带一个Default-Material,该材质使用了Standard Shader。
现在在Unity Editor的工具栏中依次选择Window->Analysis->Frame Debugger,打开Frame Debug调试器,调试器默认是Disable的,点击调试器中的Enable选项,调试器就会对当前场景进行调试。
这里修改一下两个参数,场景中Camera的Rendering Path,默认是Use Graphics Setting,我们将之改为Forward,其实这里改不改也不是很要紧,因为Graphics Setting中默认就是Forward,这个选项其实就是指定Camera的渲染路径,Forward表明使用前向渲染;场景中Light的Render Mode模式,默认是Auto,我们将它修改为Important,后面将更一步对这两个参数做出阐述。
调试器分为三个部分,顶部工具栏,左侧过程,右侧细节,右侧细节对应于每一个过程。在调试器左侧过程中,我们可以看到图2所示的层次结构,途中标红的部分正是对于Plane的渲染部分。
图2: Frame Debug左侧过程基本结构
图2: Frame Debug左侧过程基本结构

为了进一步看到Plane被渲染的细节,展开RenderForward.RenderLoopJob,看到有一个Draw Mesh Plane过程,选中这个过程,在右侧细节栏中可以得到这个过程的细节,如图3所示。原来,Plane身上使用了Standard Shader,尽管这个Shader上有2个SubShader,7个Pass,但是只有1个SubShader中的1个Pass被GPU使用了,那为什么会这样呢?为什么不是7个Pass都被使用呢?
回想我们在第一个部分所总结的内容,就能知道原因。GPU在进行渲染前,CPU会加载数据,会设置渲染状态,会调用Draw Call,所以,在这个场景中,由于Camera设置了前向渲染模式,CPU就会告诉GPU,对于Plane对象,你使用它的Shader中的第1个SubShader(#0)中的名字叫FORWARD的Pass进行渲染。但是,图2所展示的左侧过程,我们依然有以下问题:
1 绿色和蓝色过程是用来做什么的?
2 为什么CPU会指定这样的渲染状态,为什么不适用其他的Pass?
接下来,我们将分别解释这两个问题。
在这里插入图片描述
图3: RenderForward.RenderLoopJob细节

3.1 UpdateDepthTexture过程

可能和想象中不同,Unity在渲染场景时,是先渲染物体的阴影,再渲染物体本身,而渲染阴影又分为两步,首先渲染得到Depth Texture,这也就是UpdateDepthTexture过程所做的事情,为后续的ShadowMap奠定基础。展开左侧UpdateDepthTexture过程,点击Draw Mesh Plane,再右侧细节中看到使用的是Standard Shader中的名字为ShadowCaster的通道,如图4所示。
在这里插入图片描述
图4:UpdateDepthTexture细节

3.2 Shadows.RenderShadowMap过程

展开Shadows.RenderShadowMap,有两个Draw Mesh Plane过程,这两个过程都是得到ShadowMap,因此,他们使用的仍然是Standard Shader中的ShadowCaster通道,如图5所示。
在这里插入图片描述
图5:Shadows.RenderShadowMap细节
另外,这里的过程数是可以调节的,打开Edit->Project Settings->Quality,找到Shadows下的Shadow Cascades,就可以改变Draw mesh Plane的过程数,图5展示了不同过程数下的左侧过程。
在这里插入图片描述
图6:左侧是2 Cascades,右侧是4 Cascades

3.3 RenderForwardOpaque.CollectShadows过程

在这个过程中,Unity收集之前的ShadowMap,然后合并,将ShadowMap输出到屏幕空间中,具体使用的Shader没有弄清楚,在以后的学习中将继续关注。

在这里插入图片描述
图7:RenderForwardOpaque.CollectShadows过程

3.4 Camera.RenderSkyBox过程

从名字上来看,不难理解,这个过程是在渲染天空盒,展开Camera.RenderSkyBox,我们得到了其使用的Shader,这里不对此过多叙述。
在这里插入图片描述
图8:Camera.RenderSkyBox过程
此外,我们可以通过设置Camera上的Clear Flags来取消这个过程,例如,我们将Camera上的Clear Flags改为Solid Color,那么这个过程就会消失。

3.5 Camera.ImageEffects过程

最后,这个过程属于摄像机的图像特效,本文也不做讨论。

3.6 渲染状态指定

在前文中我们提出了一个问题,为什么CPU会指定GPU使用Standard Shader中的FORWARD和ShadowCaster通道对Plane进行渲染?我们再回头来观察一下场景中Camera中的Rendering Path,没错,它是Forward,这就是CPU指定渲染状态的一个设置,这个设置会指定Camera中的物体使用符合前向渲染路径的通道渲染,正如表1,前向渲染路径中有三个Light Mode通道,分别是ForwardBase、ForwardAdd和ShadowCaster,对应到Standard Shader中的名字正好是FORWARD、FORWARD_Delta和ShadowCaster。
对,确实是这样,通过指定Camera中的RenderPath标签,我们指定了使用前向渲染路径,也就指定了在渲染Plane时,需要使用其中的FORWARD、FORWARD_Delta和ShadowCaster通道。本场景中我们没有使用到FORWARD_Delta通道,这是因为场景中只有一个逐像素光源。至此,我们就明白在Unity Editor中设定Camera的Rendering Path,从而让CPU指定GPU的相应渲染状态了。

4 Unity中前向渲染路径

在《Unity Shader 入门精要》第9章介绍了前向渲染路径的工作原理,并有说明,对于每一个逐像素光源,都需要进行一次这样的完整渲染过程,也就是说,如果一个物体被多个逐像素光源所影响,那么这个物体将会执行多个Pass,下面我们从Frame Debug来具体看看这个过程。
在这里插入图片描述
注意:图片摘自《Unity Shader 入门精要》
首先假设场景中影响Plane的逐像素光源只有一个,那么我们可以得到关于Plane渲染的通道,是Standard Shader中的FORWARD通道。
在这里插入图片描述
图9:单逐像素光源的渲染通道
其次,我们在场景中再添加一个平行光源,并将RenderMode设置为Important,再来查看Frame Debug,发现Plane被两个Pass进行渲染,一个是FORWARD,另一个是FORWARD_DELTA。
在这里插入图片描述
图10:双逐像素光源的渲染通道

最后,如果逐像素光源增加到3个时,我们看到后两个逐像素光源对Plane的影响都会经由FORWARD_DELTA通道渲染,而这正如《Unity Shader 入门精要》一书中所说。
在这里插入图片描述
图11:三逐像素光源的渲染通道

5 Unity中延迟渲染路径

之前部分我们详细讨论了前向渲染路径的结构和多逐像素光源下渲染状态的指定问题,这个部分我们将讨论延迟渲染路径。同样的,首先将Camera中的Rendering Path更改为Defered,然后在Frame Debug中给出了延迟渲染路径下的左侧过程图,红色部分是Plane的阴影和光照渲染部分,绿色部分是为光照渲染做铺垫,蓝色部分和前向渲染一样,是天空盒的渲染和摄像机特效。
在这里插入图片描述
图12: Frame Debug左侧过程基本结构
正如《Unity Shader 入门精要》一书中所说,在延迟渲染路径中,对于Plane的渲染,通常采用两个Pass,第一个Pass,不进行任何光照计算,而是将片元的相关信息存储到G缓冲中,然后再第二个Pass中,利用G缓冲的各个片元信息,进行真正的光照计算。
在这里插入图片描述
注意:图片摘自《Unity Shader 入门精要》
那么,我们借助Frame Debug来观察一下Unity是在哪里使用这两个通道的,展开RenderDeferred.GBuffer,看到了位于Standard Shader里第一个SubShader中名字为DEFERRED的通道,回顾表1,这个通道正是用来渲染GBuffer的;展开RenderDeferred.Lighting,我们看到了位于Hidden/Internal-DeferredShading里第一个SubShader中的第一个通道#0,猜测这应该就是利用GBuffer来进行光照计算的第二个通道了,本文没有对Hidden/Internal-DeferredShading进行研究。
在这里插入图片描述
图13: GBuffer渲染细节
在这里插入图片描述
图14: 光照计算细节
接下来,还有一个问题值得思考,现在场景里只采用了一个逐像素光源(平行光),如果将其他的逐像素光源(其他两个平行光)加上去,会有什么变化吗?在场景里,实际增加两个平行光源后,RenderDeferred.Light下面确实发生了变化,新增了两个Draw GL,并且这两个过程都是使用Hidden/Internal-DeferredShading里第一个SubShader中的第一个通道#0进行渲染的,这就意味着光照的计算还是使用了多个通道的。因此,延迟渲染并不是完全只使用两个通道进行计算,而是说把每次光照渲染过程中相同的部分单独列出来,计算完存入GBuffer,而后续的光照计算,对于不同的光源则是不同的,所以分开计算,这样,就会利用空间来换取时间,从而提升性能。
在这里插入图片描述
图15: 多逐像素光源渲染过程细节

6 总结

在阅读《Unity Shader 入门精要》一书的前9章内容时,对于第9章中所提及前向渲染和延迟渲染有着非常大的疑惑,因此认真地梳理了CPU调用Draw Call的过程,并且使用Frame Debug调试器来从细节中观察了前向渲染和延迟渲染的各个子过程,最终对前向渲染和延迟渲染的区别形成了自己的认识。前向渲染中一个逐像素光源就会从前到后计算一次光照,包括漫反射、高光、法线等等,n个逐像素光源就要计算n次;而延迟渲染中会对场景中的物体会在一个通道里将漫反射系数、高光系数、法线等写入GBuffer中,再针对各个光源直接计算光照,减轻了很大的工作量。
此外,本文主要熟悉了Frame Debug调试器的使用,不得不说这是一个非常强大的渲染调试器,对于初学者来说更是一个非常有价值的学习帮手。
最后,第9章中还有让我迷惑的内容,比如说逐像素光源和逐顶点光源的设置以及他们渲染过程的细节,限于文章的篇幅,这里并没有对此做出讨论,但是借助于Frame Debug,仍然可以找到这些问题的答案。

猜你喜欢

转载自blog.csdn.net/Abecedarian_CLF/article/details/88757146