更复杂的光照(一)

本文最后更新于:2025年4月17日 下午

Unity 的渲染路径

在 Unity 里,**渲染路径(Rendering Path)**决定光照是如何运用到 Unity Shader 中的。我们需要为每个 Pass 指定它使用的渲染路径,只有这样才能让 Unity 正确地为我们准备光源和处理后的光照信息等数据,也就是说,我们只有为 Shader 正确地选择和设置了需要的渲染路径,该 Shader 的光照计算才能被正确执行。

Unity 支持多种类型的渲染路径。前向渲染路径(Forward Rendering Path)延迟渲染路径(Deferred Rendering Path)顶点照明渲染路径(Vertex Lit Rendering Path)。顶点照明渲染路径已经被 Unity 抛弃,新的延迟渲染路径代替了原来的延迟渲染路径(但目前仍然可以对之前使用了顶点照明渲染路径和旧的延迟渲染路径的 Unity Shader 兼容)。

大多数情况下,一个项目只用一种渲染路径,因此我们可以通过在 Unity 的 Edit -> Project Settings -> Graphics -> Tier Settings -> Rendering Path 中选择项目所需的渲染路径。默认情况下该设置选择的都是前向渲染路径。但有时我们希望使用多个渲染路径,如不同的摄像机使用不同的渲染路径,这时我们可以在摄像机的渲染路径设置中进行设置,以覆盖 Project Settings 中的设置。如果当前显卡不支持所选择的渲染路径,Unity 会自动使用更低一级的渲染路径。例如,如果一个 GPU 不支持延迟渲染,那么 Unity 就会使用前向渲染。

完成上面的设置后,我们就可以在每个 Pass 中使用 LightMode 标签来指定该 Pass 使用的渲染路径。

1
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)或一张深度纹理中
PrepassBase 用于遗留的延迟渲染。该 Pass 会渲染法线和高光反射的指数部分
PrepassFinal 用于遗留的延迟渲染。该 Pass 通过合并纹理、光照和自发光来渲染得到最后的颜色
Vertex、VertexLMRGBM 和 VertexLM 用于遗留的顶点照明渲染

指定渲染路径是我们和 Unity 底层渲染引擎的一次重要的沟通。例如,如果我们为一个 Pass 设置了前向渲染路径的标签,Unity 会把那些光照属性都按前向渲染的流程准备好,我们就可以通过 Unity 提供的内置光照变量来访问这些属性。如果我们没有指定任何渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。

前向渲染路径

1. 前向渲染路径的原理

每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。我们可以利用下面的伪代码来描述前向渲染路径的大致过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。

2. Unity 中的前向渲染

一个 Pass 不仅仅可以用来计算逐像素光照,也可以计算逐顶点等其他光照。这取决于光照计算所处流水线阶段以及计算时使用的数学模型。当我们渲染一个物体时,Unity 会计算哪些光源照亮了它以及这些光源照亮该物体的方式。

在 Unity 中,前向渲染路径有 3 种处理光照(即照亮物体)的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)。如果一个光照的模式被设置为 Important,Unity就会把它当成一个逐像素光源来处理。我们可以在光源的 Light 组件中设置这些属性,如下图所示。

Light 组件

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

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

光照计算都在 Pass 里进行。前向渲染有两种 Pass:Base Pass 和 Additional Pass。通常来说,这两种 Pass 进行的标签和渲染设置以及常规光照计算如下图所示。

前向渲染的两种Pass

上图中有几点需要说明的地方。

  • 首先,可以发现在渲染设置中,我们除了设置了 Pass 的标签外,还使用了 #pragma multi_compile_fwdbase 这样的编译指令。只有分别为 Bass Pass 和 Additional Pass 使用 #pragma multi_compile_fwdbase 和 #pragma multi_compile_fwdadd 编译指令,我们才可以在相关的 Pass 中得到一些正确的光照变量,例如光照衰减值等。
  • Bass Pass 旁边的注释给出了 Base Pass 中支持的一些光照特性。例如在 Base Pass 中,我们可以访问光照纹理(lightmap)。
  • Bass 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。一个 Bass Pass 仅会执行一次(定义了多个 Bass Pass 的情况除外),而一个 Additional Pass 会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次 Additional Pass。

上图给出的光照计算是通常情况下我们在每种 Pass 中进行的计算。实际上,渲染路径的设置用于告诉 Unity 该 Pass 在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如 _LightColor0 等),如何使用这些内置变量进行计算完全取决于开发者的选择。例如,我们完全可以利用 Unity 提供的内置变量在 Base Pass 中只进行逐顶点的光照;同样,我们也完全可以在 Additional Pass 中按逐顶点的方式进行光照计算,不进行任何逐像素光照计算。

3. 内置的光照变量和函数

对于前向渲染(即 LightModeForwardBaseForwardAdd)来说,下表给出了我们可以在 Shader 中访问到的光照变量。

名称 类型 描述
_LightColor0 float4 该 Pass 处理的逐像素光源的颜色
_WorldSpaceLightPos0 float4 _WorldSpaceLightPos0.xyz 是该 Pass 处理的逐像素光源的位置。如果该光源是平行光,那么 _WorldSpaceLightPos0.w 是 0,其他光源类型 w 值为 1
_LightMatrix0 float4x4 从世界空间到光源空间的变换矩阵。可以用于采样 cookie 和光强衰减(attenuation)纹理
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 float4 仅用于 Bass Pass。前 4 个非重要的点光源在世界空间中的位置
unity_4LightAtten0 float4 仅用于 Bass Pass。存储了前 4 个非重要的点光源的衰减因子
unity_LightColor half4[4] 仅用于 Bass Pass。存储了前 4 个非重要的点光源的颜色

下表列出了前向渲染中可以使用的内置光照函数。

函数名 描述
float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了 UnityWorldSpaceLightDir 函数。没有被归一化
float3 UnityWorldSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化
float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化
float3 Shade4PointLights(…) 仅可用于前向渲染中。计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,通常就是上表中的内置变量,如 unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,unity_LightColor 和 unity_4LightAtten0 等。前向渲染通常会使用这个函数来计算逐顶点光照

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

顶点照明渲染路径

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

1. Unity 中的顶点照明渲染

顶点照明渲染路径通常在一个 Pass 中就可以完成对物体的渲染。在这个 Pass 中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。这是 Unity 中最快速的渲染路径,并且具有最广泛的硬件支持(但是游戏机上并不支持这种路径)。由于顶点照明渲染路径仅仅是前向渲染路径的一个子集,因此顶点照明渲染路径成为了一个遗留的渲染路径,在未来的版本中,顶点照明渲染路径的相关设定可能会被移除。

2. 可访问的内置变量和函数

在 Unity 中,我们可以在一个顶点照明的 Pass 中最多访问到 8 个逐顶点光源。如果我们只需要渲染其中两个光源对物体的照明,可以仅使用下表中内置光照数据的前两个。如果影响该物体的光源数目小于 8,那么数组中剩下的光源颜色会设置成黑色。

名称 类型 描述
unity_LightColor half4[8] 光源颜色
unity_LightPosition float4[8] xyz 分量是视角空间中的光源位置。如果光源是平行光,那么 z 分量值为 0,其他光源类型 z 分量值为 1
unity_LightAtten half4[8] 光源衰减因子。如果光源是聚光灯,x 分量是 cos(spotAngle/2),y 分量是 1/cos(spotAngle/4);如果是其他类型的光源,x 分量是 -1,y 分量是 1。z 分量是衰减的平方,w 分量是光源范围开根号的结果
unity_SpotDirection float4[8] 如果光源是聚光灯的话,值为视角空间的聚光灯的位置;如果是其他类型的光源,值为(0, 0, 1, 0)

可以看出,一些变量我们同样可以在前向渲染路径中使用,例如 unity_LightColor。但这些变量数组的维度和数值在不同渲染路径中的值是不同的。下表给出了顶点照明渲染路径中可以使用的内置函数。

函数名 描述
float3 ShadeVertexLights(float4 vertex, float3 normal) 输入模型空间中的顶点位置和法线,计算四个逐顶点光源的光照以及环境光。内部实现实际上调用了 ShadeVertexLightsFull 函数
float3 ShadeVertexLightsFull(float4 vertex,float3 normal,int lightCount,bool spotLight) 输入模型空间中的顶点位置和法线,计算 lightCount 个光源的光照以及环境光。如果 spotLight 值为 true,那么这些光源会被当成聚光灯来处理,虽然结果更精确,但计算更加耗时;否则,按点光源处理

延迟渲染路径

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

延迟渲染是一种更古老的渲染方法,但是由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为 G 缓冲(G-buffer),其中 G 是英文 Geometry 的缩写。G 缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。

1. 延迟渲染的原理

延迟渲染主要包含了两个 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
25
26
27
28
Pass 1 {
//第一个 Pass 不进行真正的光照计算,仅仅把光照计算需要的信息存储到 G 缓冲中
for (each primitive in this model) {
for (each fragment covered by this primitive) {
if (failed in depth test) {
//如果没有通过深度测试,说明该片元是不可见的
discard;
} else {
//如果该片元可见,就把需要的信息存储到 G 缓冲中
writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
}
}
}
}

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

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

2. Unity 中的延迟渲染

对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染也有一些缺点

  • 不支持真正的抗锯齿(anti-aliasing)功能。
  • 不能处理半透明物体。
  • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持 MRT(Multiple Render Targets)、Shader Mode 3.0 及以上、深度渲染纹理以及双面的模板缓冲。

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

(1) 第一个 Pass 用于渲染 G 缓冲。在这个 Pass 中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的 G 缓冲区中。对于每个物体来说,这个 Pass 仅会执行一次。

(2) 第二个 Pass 用于计算真正的光照模型。这个 Pass 会使用上一个 Pass 中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

默认的 G 缓冲区(注意,不同 Unity 版本的渲染纹理存储内容会有所不同)包含了以下几个渲染纹理(Render Texture,RT)。

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

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

3. 可访问的内置变量和函数

下表给出了处理延迟渲染路径可以使用的光照变量。这些变量都可以在 UnityDeferredLibrary.cginc 文件中找到它们的声明。

名称 类型 描述
_LightColor float4 光源颜色
_LightMatrix0 float4x4 从世界空间到光源空间的变换矩阵。可以用于采样 cookie 和光强衰减纹理

更复杂的光照(一)
http://example.com/posts/更复杂的光照(一)/
作者
祭零小白
发布于
2022年2月17日
许可协议