Unity 渲染原理(十三)Unity的渲染路径

之前的光照模型中,我们都只有一个光源,而且是平行光。但是实际开发过程中,我们往往需要处理数目更多,类型更为复杂的光源,重要的是,我们需要得到阴影。现在我们就要学习一下如何处理这些更为复杂的光照。

在学习这些之前,我们需要知道Unity是如何处理这些光源的。也就是说,当我们在场景里放置了各种类型的光源后,Unity底层的渲染引擎是如何让我们的Shader可以访问到它们的。

Unity的渲染路径

在Unity里,渲染路径(Render Path)决定了光照时如何应用到Unity Shader的。因此,如果要和光源打交道,我们就需要为每个Pass指定它使用的渲染路径,只有正确地为Shader选择和设置了需要的渲染路径,该Shader的光照计算才能被顺利执行。

Unity支持多种类型的渲染路径,在5.0版本之前,有三种,前向渲染路径,延迟渲染路径和顶点照明渲染路径,但是在5.0之后,定点照明渲染路径已经被抛弃,新的延迟渲染路径代替了原来的延迟渲染路径。

大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径,该设置在Peoject Settings中,默认情况下是前向渲染路径,但有时候,我们希望有多个渲染路径,例如摄像机A使用前向渲染,B使用延迟渲染,我们可以在每个摄像机的设置中修改该摄像机的渲染路径。但是如果GPU不支持延迟渲染,还是会降级成前向渲染。具体可以参考官方文档内置渲染路径的渲染管线

完成了设置之后,我们就可以在每个Pass使用LightMode Tag来指定该Pass使用的渲染路径。

这些是内置渲染管线中 LightMode 通道标签的有效值。

值 功能
Always 始终渲染;不应用任何光照。这是默认值。
ForwardBase 在前向渲染中使用;应用环境光、主方向光、顶点/SH 光源和光照贴图。
ForwardAdd 在前向渲染中使用;应用附加的每像素光源(每个光源有一个通道)。
Deferred 在延迟渲染中使用;渲染 G 缓冲区。
ShadowCaster 将对象深度渲染到阴影贴图或深度纹理中。
MotionVectors 用于计算每个对象的运动矢量。
PrepassBase 用于旧版延迟光照,渲染法线和镜面反射指数。
PrepassFinal 用于旧版延迟光照;通过组合纹理、光照和发光来渲染最终颜色。
ShadowCaster 将对象深度渲染到阴影贴图或深度纹理中。
Vertex 用于旧版顶点光照渲染(当对象不进行光照贴图时);应用所有顶点光源。
VertexLMRGBM 用于旧版顶点光照渲染(当对象不进行光照贴图时),以及光照贴图为 RGBM 编码的平台(PC 和游戏主机)。
VertexLM 用于旧版顶点光照渲染(当对象不进行光照贴图时),以及光照贴图为双 LDR 编码的平台上(移动平台)。
Meta This Pass is not used during regular rendering, only for lightmap baking or Enlighten Realtime Global Illumination. For more information, see Lightmapping and shaders.

那么指定渲染路径到底有什么用呢?如果一个Pass没有指定任何渲染路径会有什么问题?简单来说,指定渲染路径是我们和Unity底层渲染引擎的一次重要沟通。例如,如果我们为一个Pass设置了前向渲染路径的标签,相当于通知Unity将所需要的光照属性都按前向渲染的流程准备好,随后我们就可以通过Unity提供的内置变量来访问这些属性。

前向渲染路径

前向渲染路径的原理

每进行一次完整的前向渲染,我们都需要渲染该对象的渲染图元,并计算两个缓冲区的信息,一个是颜色缓冲区,一个是深度缓冲区,我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区的颜色值

1
2
3
4
5
6
7
8
9
10
11
12
13
Pass{
for(each primitive in this model) {
for(each fragment covered by this primitive) {
if (failed in depth test) {
discard;
}
else {
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
writeFrameBuffer(fragment, color);
}
}
}
}

对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受M个光源的影响,那么要渲染整个场景一共需要N*M个Pass,可以看出,如果有大量逐像素光照,那么需要执行的Pass数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。

Unity中的前向渲染

事实上,一个Pass不仅仅可以用来计算逐像素光照,它可以用来计算逐顶点等其他光照,这取决于光照计算所处流水线阶段以及计算时使用的数学模型。

当我们渲染一个物体时,Unity会计算哪些光源照亮了它,以及这些光源照亮该物体的方式。

在Unity中,前向渲染路径有三种处理光照方式,逐顶点处理,逐像素处理,球谐函数处理(SH)。而决定一个光源使用那种处理模式取决于它的类型和渲染模式。光源类型指的是该光源时平行光还是其他类型的光源,而光源的渲染模式是该光源是否是重要的。如果我们将一个光照的模式设置为Important,意味着我们告诉Unity,把这个光源当做一个逐像素光源来处理。

在前向渲染中,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如距离该物体的远近,光源强度等)对这些光源进行一个重要度排序。其中一定数目的光源会按照逐像素的方式来处理,然后最多有4个光源按照逐顶点的方式处理,剩下的都按照SH的方式处理。Unity使用的判断规则如下:

  • 场景中最亮的平行光总是按照逐像素处理的
  • 渲染模式被设置成 Not Important的光源会按逐顶点或者SH处理
  • 渲染模式被设置成Important的光源,会按逐像素处理
  • 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量,会有更多的光源以逐像素的方式进行渲染

那么是哪里进行光照计算呢?答案是Pass里,前向渲染有两种Pass,Base Pass和Additional Pass,这两种Pass进行的标签和渲染设置以及常规光照计算

上图中有几点需要说明:

  • 首先,可以发现在渲染设置中,我们除了设置Pass的标签外,还使用了#pragma multi_compile_fwdbase这样的编译指令。虽然#pragma multi_compile_fwdbase#pragma mulit_compile_fwdadd在官方文档中还没有给出相关说明,但是实验证明,没有这两个编译指令,我们才可以在相关的Pass中得到一些正确的光照变量,例如光照衰减值
  • Base Pass旁边的注释给出了Base Pass中支持的一些光照特性。例如在Base Pass中,我们可以访问光照纹理(lightmap)
  • Base Pass中渲染的平行光默认是支持阴影的(如果开启了光源的阴影功能),而Additional Pass 中渲染的光源在默认情况下是没有阴影效果的,即便我们在它的Light组件中设置了有阴影的Shadow Type。但是我们可以在Additional Pass中使用#pragma multi_compile_fwdadd_fullshadows代替#pragma multi_compile_fwdadd编译指令,为电光源和聚光灯开启阴影效果,但这需要Unity在内部使用更多的Shader变种
  • 环境光和自发光也是在Base Pass中计算的,这是因为,对于一个物体来说,环境光和自发光我们只希望一次即可,而如果我们在Additional Pass中计算这两种关照,就会造成得加多次环境光和自发光,这不是我们想要的。
  • 在Additional Pass的渲染设置中,我们还开启和设置了混合模式。这是因为,我们希望每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果,如果我们没有开启混合模式,那么Additional Pass的渲染结果会覆盖掉之前的渲染结果,看起来就好像该物体只收该光源的影响。通常情况下,我们选择的混合模式是Blend One One
  • 对于前向渲染来说,一个Unity Shader通常会定义一个Base Pass(也可定义多次,例如需要双面渲染等情况)以及一个Additional Pass。一个Base Pass仅会执行一次,而一个Additional Pass会根据影响该物体的其他逐像素光源数目被多次调用,即每个逐像素光源会执行一次Additional Pass

实际上,渲染路径的设置只是告诉Unity该Pass在前向渲染路径中的位置,然后底层的渲染引擎会进行相关的计算并填充一些内置变量(如_LightColor0),如何使用这些内置变量进行计算则完全是开发者的选择。

内置的光照变量和函数

在前向渲染中,我们可以在Pass中访问到以下变量,我们可以在官方文档中找到,内置着色器变量

需要注意,这些变量并不是完整的,一些前向渲染可以使用的内置变量和函数官方文档中并没有给出说明。

顶点照明渲染路径

顶点照明渲染路径时对硬件配置要求最少的,运行性能最高的,但同时也是得到效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影,法线映射,高精度的高光反射等,实际上,它只是前向渲染的一个子集,所有可以在顶点照明渲染路径中实现的功能都可以在前线渲染路径中完成。就如同它的名字一样,顶点照明渲染路径只是使用了逐顶点的方式来计算光照,并没有神奇的地方。

这个渲染路径已经被放弃了,就不多介绍了。

延迟渲染路径

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影响的区域相互重叠,那么为了得到最终的光照效果,我们就需要为该区域的每个物体执行多个Pass来计算不同的光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得到最终的关照。然而,每执行一个Pass我们都需要重新渲染一遍物体,但是很多计算实际上是重复的。

延迟渲染的原理

延迟渲染主要包含两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现的,当发现有一个片元是可见的,我们就把他的相关信息存储到G缓冲区中。然后再第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线,视角方向,漫反射系数等,进行真正的光照计算.

延迟渲染过程大致可以用下面的伪代码来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Pass 1{
for(each primitive in this model) {
for(each fragment covered by this primitive) {
if (failed in depth test) {
discard;
} else {
writeGBuffer(materialInfo, pos, normal, lightDir, viewDir)
}
}
}
}

Pass 2{
for(each pixel in the screen) {
if (pixel is valid) {
// 如果该像素是有效的,读取它的G缓冲中的信息
readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
// 根据读到的信息进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
// 更新到帧缓冲
writeFrameBuffer(pixel, color);
}
}
}

可以看出,延迟渲染使用的Pass数目通常两个,这跟场景中包含的光源数目是没有关系的。换句话说,延迟渲染的效率并不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息存储在缓冲区中,而这些缓冲区可以理解为一张张2D图像,我们的计算实际上就是在这些图像空间进行的。

Unity中的延迟渲染

Unity中有两种延迟渲染路径,一种是遗留的延迟渲染路径,一种是现在的。但是无论哪一种,都需要相应的硬件支持。

新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求,例如,老版本的延迟渲染路径不支持Unity5的基于物理的Standard Shader。

以下我们进讨论新版的延迟渲染路径。

对于延迟渲染路径,它适合在场景中光源数目多,前向渲染会有性能瓶颈的情况,而且延迟渲染路径中的每个光源都可以按照逐像素的方式处理。但是,延迟渲染也有一些缺点:

  • 不支持真正的抗锯齿功能
  • 不能处理半透明物体
  • 对显卡有要求,如果要使用延迟渲染,显卡必须支持MRT(Multiple Render Target),Shader Mode3.0及以上,深度渲染纹理以及双面的模版缓冲

当使用延迟渲染时,Unity要求我们提供两个Pass

  1. 第一个Pass用于渲染G缓冲,在这个Pass中,我们会把物体的漫反射颜色,高光反射颜色,平滑度,法线,自发光和深度等信息渲染到屏幕空间的G缓冲区中。对于每个物体来说,这个Pass只会执行一次。
  2. 第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中

默认的G缓冲区包含了以下几个渲染纹理(Render Texture, RT)

  • RT0:格式是ARGB32,RBG通道用于存储漫反射颜色,A通道没有用
  • RT1: 格式是ARGB32,RGB通道用于存储高光反射颜色,A通道用于存储高光反射的指数部分
  • RT2: 格式时ARGB2101010,RGB通道用于存储法线,A通道没有用
  • RT3: 格式是ARGB32(非HDR)或ARGBHalf(HDR),用于存储自发光+lightmap+发射探针
  • 深度缓冲和模版缓冲

当在第二个Pass中计算光照时,默认情况下仅可使用Unity内置的Standard光照模型,如果我们想要使用其他光照模型,就需要替换掉原有的Internal-DefferedShading.shader文件

选择哪个渲染路径

具体选择哪个渲染路径,可以参官方文档:内置渲染管线中的渲染路径