Unity的渲染路径(上)
在Unity里,渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的。因此,如果要和光源打交道,我们需要为每个Pass指定它使用的渲染路径,只有这样才能让Unity知道我们想要使用哪种渲染路径,从而把光源和处理后的光照信息都放在一些内置变量中,供我们访问。也就是说,只有为Shader正确地选择和设置了需要的渲染路径,该Shader的光照计算才能被正确执行。
大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。通过Edit
->Project Settings
->Graphics
->Tier Settings
->Rendering Path
中选择项目所需的渲染路径。默认情况下,该选项为前向渲染路径。如图:
但有时我们希望可以使用多个渲染路径,例如相机A渲染的物体使用前向渲染路径,而相机B渲染的物体使用延迟渲染路径。这时,我们可以通过在每个相机的渲染路径设置中设置该相机使用的渲染路径,来覆盖Graphics Settings中的设置。如图:
上面的设置中,如果选择了Use Graphics Settings,那么这个相机就会使用Graphics Settings中的设置,否则就会覆盖掉Graphics Settings中的设置。需要注意的是,如果当前的显卡并不支持所选的渲染路径,Unity会自动使用更低一级的渲染路径。例如,如果一个GPU不支持延迟渲染,那么Unity就会使用前向渲染。
完成了上面的设置后,我们就可以在每个Pass中使用标签来指定该Pass使用的渲染路径。这是通过设置Pass的LightMode标签实现的。不同类型的渲染路径可能会包含多种标签设置。
Pass{
Tags{ “LightMode” = “ForwardBase”}
上面的代码告诉Unity,该Pass使用前向渲染路径中的ForwardBase路径。而前向渲染路径还有一种路径叫做ForwardAdd。下表给出了Pass的LightMode标签支持的渲染路径设置选项。
标签名 | 说明 |
---|---|
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照 |
ForwardBase | 用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps |
ForwardAdd | 用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源。 |
Deferred | 用于延迟渲染。该Pass会渲染G缓冲(G-buffer)。 |
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中 |
MotionVectors | 用于计算每个物体的运动响亮 |
PrepassBase | 用于遗留的延迟渲染。该Pass会渲染法线和高光反射的指数部分。 |
PrepassFinal | 用于遗留的延迟渲染。该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色。 |
Vertex | 用于遗留的顶点照明渲染。当物体未烘焙光照贴图时。应用所有顶点光源。 |
VertexLMRGBM | 用于遗留的顶点照明渲染。当物体烘焙光照贴图时,并且在光照贴图采用RGBM编码的平台(PC和控制台)上。 |
VertexLM | 用于遗留的顶点照明渲染。当物体烘焙光照贴图时,并且在光照贴图采用双LDR编码(移动平台)的平台上。 |
Meta | 此Pass不用于常规渲染,仅用于光照贴图烘焙或实时全局光照。更多有关信息,请参阅光照贴图和Meta Pass标签 |
如果一个Pass没有指定任何渲染路径会有什么问题么?
通俗讲,指定渲染路径是我们和Unity的底层渲染引擎的一次重要沟通。例如,如果我们为一个Pass设置了前向渲染路径的标签,就相当于告诉Unity,我们准备使用前向渲染了。那么Unity就会把光照属性按照前向渲染的流程准备好。随后我们可以通过Unity内置的光照变量来访问这些属性值。如果我们没有指定任何的渲染路径(实际上,Unity中如果使用了前向渲染,又没有为Pass指定任何前向渲染适合的标签,就会被当成一个和顶点照明渲染路径相同的Pass),那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。
前向渲染路径
原理
每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。利用深度缓冲区来决定一个片元是否可见,如果可见,那么就刷新颜色缓冲区中的值。用伪代码来描述一下前向渲染的大概过程:
Pass { |
对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来,得到最终的颜色值。
假设场景中有N个物体,每个物体受M个光源影响,那么渲染整个场景一共需要N·M个Pass。所以,如果有大量的逐像素光照,那么需要执行的Pass数量也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。
Unity中的前向渲染
实际上,一个Pass不仅仅可以用来计算逐像素光照,也可以用来计算逐顶点等其他光照。这取决于光照计算所处流水线阶段,以及计算时使用的数学模型。当我们渲染一个物体时,Unity会计算哪些光源照亮了它,以及这些光源照亮该物体的方式。
在Unity中,前向渲染有三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)。如果我们把一个光照的模式设置为Important,意味着我们要告诉Unity,这个光源很重要,需要把它当成一个逐像素光源来处理。我们可以在光源的Light组件中设置这些属性。
在前向渲染中,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(如,距离该物体远近、光源强度等)对这些光源进行一个重要程度的排序。其中,一定数量的光源会按逐像素的方式处理,然后最多有4个光源按逐顶点的方式处理(每个顶点最多计算四次点光源),剩下的光源可以按SH方式处理。Unity使用的判断规则如下:
- 场景中最亮的平行光总是按逐像素处理。
- 渲染模式被设置为Not Important的光源,会按逐顶点或者SH处理。
- 渲染模式被设置为Important的光源,会按逐像素处理。
- 如果根据以上规则得到的逐像素光源少于Quality Setting中逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染,按照亮度递减的方式。
每个对象的渲染如下:
- Base Pass应用一个逐像素平行光和所有逐顶点/SH光源。
- 其他逐像素光源在Additional Pass中渲染,每个光源执行一次Pass。
光照计算是在Pass中进行。前面提到过,前向渲染有两种Pass:Base Pass和Additional Pass。通常来说,这两种Pass进行的标签和渲染设置以及常规光照计算如图所示。
- 在渲染设置中,我们除了设置了Pass的标签外,还使用了#pragma multi_compile_fwdbase这样的编译指令。
- 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(Base Pass也可以定义多次,例如需要双面渲染等情况)以及一个Additional Pass。一个Base Pass仅会执行一次(定义了多个Base Pass的情况除外),而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass。
上图给出的光照计算是通常情况下我们在没种Pass中进行的计算。实际上,渲染路径的设置用于告诉Unity该Pass在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如_LightColor0等),如何使用这些内置变量进行计算完全取决于开发者的选择。例如,我们完全可以利用Unity提供的内置变量在Base Pass中之进行逐顶点光照;同样,我们也完全可以在Additional Pass中按逐顶点的方式进行光照计算,不进行任何逐像素光照计算。
编译指令 | 说明 |
---|---|
multi_compile_fwdbase | 编译PassType.ForwardBase所需的所有变体。这些变体处理不同的光照贴图类型,以及启用或禁用主平行光的阴影 |
multi_compile_fwdadd | 编译PassType.ForwardAdd的变体。这会编译变体以处理定向光、聚光灯或点光类型及其带有 cookie 纹理的变体 |
multi_compile_fwdadd_fullshadows | 与multi_compile_fwdadd相同,但还包括灯光具有实时阴影的能力 |
multi_compile_fog | 扩展为多个变体,以处理不同的雾类型(off/linear/exp/exp2) |
大多数内置的编译指令会产生许多Shader变体。如果确定项目不需要它们,可以使用#pragma skip_variants跳过编译其中的一些变体。例如:
#pragma multi_compile_fwdadd |
该指令跳过所有包含POINT或POINT_COOKIE的变体。
内置的光照变量和函数
根据我们使用的渲染路径(即Pass标签中Light Mode的值),Unity会把不同的光照变量传递给Shader。
对于前向渲染(即Light Mode为ForwardBase或ForwardAdd)来说,下表给出了我们可以在Shader中访问到的光照变量:
变量名 | 类型 | 描述 |
---|---|---|
_LightColor0(在UnityLightingCommon.cginc中声明) | fixed4 | 该Pass处理的逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz是该Pass处理的逐像素光源的位置。如果该光源是平行光,那么_WorldSpaceLightPos0.w是0(因为平行光是向量,不需要矩阵的偏移部分),其他光源类型w值为1 |
unity_WorldToLight(在AutoLight.cginc中声明) | float4x4 | 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减(attenuation)纹理 |
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 | float4 | 仅用于ForwardBase Pass。前4个非重要的点光源在世界空间中的位置 |
unity_4LightAtten0 | float4 | 仅用于ForwardBase Pass。前四个非重要点光源的衰减因子 |
unity_LightColor | half4[4] | 仅用于ForwardBase Pass。前四个非重要点光源的颜色。 |
unity_WorldToShadow | float4x4[4] | 世界空间到阴影空间(World-to-shadow)的矩阵。一个用于聚光灯的矩阵,最多四个用于平行光级联。 |
仅可以在前向渲染中使用的内置光照函数:
函数名 | 描述 |
---|---|
float3 WorldSpaceLightDir(float4 v) | 给定模型空间的顶点位置,返回世界空间中从该点到光源的光照方向。未被归一化。 |
float3 ObjSpaceLightDir(float4 v) | 给定模型空间的顶点位置,返回模型空间中从该点到光源的光照方向。未被归一化。 |
float3 Shade4PointLights(…) | 计算四个点光源的光照,它的参数是已经打包进矢量的光照数据。通常就是上表中的内置变量,如unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0、unity_LightColor、unity_4LightAtten0等。前向渲染通常会使用此函数来计算逐顶点光照。 |
参考文献:
- 《Unity Shader入门经验》
- Unity官方手册