《Unity Shader 入门精要》第9章 更复杂的光照

第9章 更复杂的光照

9.1 Unity 的渲染路径

在 Unity 中,渲染路径(Rendering Path)决定了光照是如何应用到 Unity Shader 中的。
Unity 支持以下几种渲染路径:

  • 前向渲染路径(Forward Rendering Path)
  • 延迟着色渲染路径(Deferred Shading rendering path)
  • 旧版延迟渲染路径(Legacy Deferred rendering path)
  • 顶点光照渲染路径(Vertex Lit Rendering Path)

大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。我们可以通过在Unity的Edit → Project Settings → Graphics→ Tier Setting → Rendering Path中为每个画质等级选择相应的渲染路径。
《Unity Shader 入门精要》第9章 更复杂的光照_第1张图片
但有时,我们希望可以使用多个渲染路径,我们可以在每个摄像机的渲染路径设置中设置该摄像机使用的渲染路径,以覆盖Project Settings中的设置:
《Unity Shader 入门精要》第9章 更复杂的光照_第2张图片
需要注意的是,如果当前的显卡并不支持所选择的渲染路径,Unity会自动使用更低一级的渲染路径。例如,如果一个GPU不支持延迟渲染,那么Unity就会使用前向渲染。
完成了上面的设置后,我们就可以在每个Pass中使用标签来指定该Pass使用的渲染路径。这是通过设置Pass的LightMode 标签实现的。不同类型的渲染路径可能会包含多种标签设置:

Pass {
	Tags {"LightMode" = "ForwardBase"}
}

ForwardBase是前向渲染路径中的一种,还有一种叫做ForwardAdd
LightMode 支持的标签如下:
《Unity Shader 入门精要》第9章 更复杂的光照_第3张图片
只有指定了渲染路径,我们才能拿到相应正确的光照变量。

9.1.1 前向渲染路径

前向渲染路径是传统的渲染方式,也是我们最常用的一种渲染路径。

前向渲染路径的原理

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

Pass 
{
	for(each primitive in model)
	{
		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 中的前向渲染

在Unity中,前向渲染路径有3种处理光照(即照亮物体)的方式:逐顶点处理、逐像素处理,球谐函数 (Spherical Harmonics,SH) 处理 。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的 (Important) 。
《Unity Shader 入门精要》第9章 更复杂的光照_第4张图片
当我们渲染一个物体时,Unity 会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行一个重要度排序。其中,一定数目的光源会按逐像素的方式处理,然后最多有4个(数量可设置,默认为4)光源按逐顶点的方式处理,剩下的光源可以按 SH 方式处理。Unity 使用的判断规则如下:

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

前向渲染有两种 Pass:Base PassAdditional Pass。通常来说,这两种 Pass 进行的标签和渲染设置以及常规光照计算如下图所示:
《Unity Shader 入门精要》第9章 更复杂的光照_第5张图片

  • 首先,可以发现在渲染设置中,我们除了设置了 Pass 的标签外,还使用了#pragma multi_ compile_fwdbase这样的编译指令。虽然#pragma multi_compile_fwdbase#pragma multi_compile_fwdadd 在官方文档中还没有给出相关说明,但实验表明,只有分别为 Bass Pass和 Additional Pass 使用这两个编译指令,我们才可以在相关的 Pass 中得到一些正确的光照变量,例如光照衰减值等。
  • 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。

内置的光照变量和函数

前向渲染可以使用的内置光照变量如下:
《Unity Shader 入门精要》第9章 更复杂的光照_第6张图片
前向渲染可以使用的内置函数如下:
在这里插入图片描述
详情可见 Unity 手册

9.1.2 顶点照明渲染路径

顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果。实际上,它仅仅是前向渲染路径的一个子集,其只能使用逐顶点的方式来计算光照。

Unity 中的顶点照明渲染

顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染。在这个Pass中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。这是Unity中最快速的渲染路径,并且具有最广泛的硬件支持(但是游戏机上并不支持这种路径)。

可访问的内置变量和函数

不在此赘述,可查看官方文档。

9.1.3 延迟渲染路径

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。
延迟渲染是一种更古老的渲染方法,但由于前向渲染的性能问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为G缓冲(Geometry Buffer)。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。

延迟渲染的原理

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

Pass 1
{
	// 不做光照计算,只做深度测试
	for(each primitive in model)
	{
		for(each fragment covered by this primitive)
		{
			if(failed in depth test)
			{
				discard;
			}else
			{
				// 写入 G-Buffer
				writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
			}
		}
	}
}
pass 2
{
	// 进行真正的光照计算
	for(each pixel in the screen)
	{
		if(pixed is valid)
		{
			readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
			// 计算光照
			float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
			// 更新帧缓存
			writeFrameBuffer(pixel, color);
		}
	}
}

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

Unity 中的延迟渲染

Unity有新旧两种延迟渲染路径,新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求,可以在官方手册查看相关差异。
对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。
但是,延迟渲染也有一些缺点:

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

默认的G缓冲区包含了以下几个渲染纹理(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文件。

9.1.4 选择哪种渲染路径

官方手册给出了四种路径的对比:
《Unity Shader 入门精要》第9章 更复杂的光照_第7张图片

9.2 Unity 的光源类型

Unity一共支持4种光源类型:平行光(direct light)、点光源(point light)、聚光灯(spot light)和面光源 (area light) 。面光源仅在烘焙时才可发挥作用,因此不在本节讨论范围内。

9.2.1 光源类型有什么影响

最常使用的光源属性有光源的位置方向 颜色 强度以及衰减 这5个属性,而这些属性和它们的几何定义息息相关。

平行光

平行光的几何定义是最简单的,它的几何属性只有方向,我们可以调整平行光的Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念。

点光源

点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。点光源可以表示由一个点发出的、向所有方向延伸的光。点光源是有位置属性的,它是由点光源的Transform组件中的Position属性定义的。对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在Light组件面板中调整。同时,点光源也是会衰减的,随着物体逐渐远离点光源,它接收到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处的最弱,值为0。其中间的衰减值可以由一个函数定义。

聚光灯

聚光灯是这3种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。聚光灯的位置同样是由Transform组件中的Position属性定义的。对于方向属性,我们需要用聚光灯的位置减去某点的位置来得到它到该点的方向。聚光灯的衰减也是随着物体逐渐远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为0。其中间的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式要更加复杂,因为我们需要判断一个点是否在锥体的范围内。

9.2.2 在前向渲染中处理不同的光源类型

我们接下来将使用 Blinn-Phong 光照模型,并为前向渲染定义 Base Pass 和 Additional Pass 来处理多个光源:

Shader "Chapter9/ForwardRendering"
{
    Properties
    {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        // Base Pass
        Pass
        {
            Name "ForwardBase"
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            // #pragma multi_ compile_fwdbase 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
                fixed atten = 1.0;
                return fixed4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }

        // Additional Pass
        pass
        {
            Name "ForwardAdd"
            Tags{ "LightMode" = "ForwardAdd" }

            // 设置混合模式
            // 我们希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加
            // 如果没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果
            Blend One One

            CGPROGRAM

            // #pragma multi_compile_fwdadd 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v) 
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // 根据光源类型不同采用不同的算法计算光的方向
                // 如果当前前向渲染Pass处理的光源类型是平行光,那么 Unity 的底层渲染引擎就会定义 USING_DIRECTIONAL_LIGHT
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0).xyz;
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 根据光源类型不同采用不同的算法计算衰减值
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else
                    #if defined (POINT)
                        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                        // 尽管我们可以使用数学表达式来计算给定点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量相对较大的操作
                        // 因此Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。
                        // 我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值
                        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #elif defined (SPOT)
                        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
                        // 尽管我们可以使用数学表达式来计算给定点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量相对较大的操作
                        // 因此Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。
                        // 我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值
                        fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #else
                        fixed atten = 1.0;
                    #endif
                #endif

				return fixed4((diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }
    }
    // 指定默认 Shader
    Fallback "Specular"   
}

我们新建场景后,去除天空盒,并添加一个绿色的平行光和4个红色的点光源,并添加一个挂载此 shader 的胶囊体,可以得到如下的效果:
《Unity Shader 入门精要》第9章 更复杂的光照_第8张图片
我们还可以使用帧调试器 (Frame Debugger) 工具来查看场景的绘制过程:
《Unity Shader 入门精要》第9章 更复杂的光照_第9张图片
《Unity Shader 入门精要》第9章 更复杂的光照_第10张图片
可见绘制了五次胶囊体,其中第一次使用的是 Base Pass,后面使用的是 Additional Pass。
Unity处理这些点光源的顺序是按照它们的重要度排序的,Unity 官方文档中并没有给出光源强度、颜色和距离物体的远近是如何具体影响光源的重要度排序的,我们仅知道排序结果和这三者都有关系。我们可以通过把光源的 Render Mode 设为 Not Important 来告诉Unity,我们不希望把该光源当成逐像素处理。如果我们把上述场景中的点光源都改为 Not Important,那么物体只会显示环境光的光照结果:
《Unity Shader 入门精要》第9章 更复杂的光照_第11张图片

9.3 Unity的光照衰减

在9.2节中,我们提到Unity使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。这样的好处在于,计算衰减不依赖于数学公式的复杂性,我们只要使用一个参数值去纹理中采样即可。但使用纹理查找来计算衰减也有一些弊端:

  • 需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度。
  • 不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其他数学公式来计算衰减。

9.3.1 用于光照衰减的纹理

Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。
为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过_LightMatrix0变换矩阵得到的。然后,我们可以使用这个坐标的模的平方对衰减纹理进行采样,得到衰减值:

// 获取光源空间的位置
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
// 对纹理采样
fixed atten = tex2D(_LightTexture0, dot(lightCoord,  lightCoord).rr).UNITY_ATTEN_CHANNEL;

我们使用了光源空间中顶点距离的平方(通过dot函数来得到)来对纹理采样,之所以没有使用距离值来采样是因为这种方法可以避免开方操作。然后,我们使用宏UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。

9.3.2 使用数学公式计算衰减

尽管纹理采样的方法可以减少计算衰减时的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。可惜的是,Unity没有在文档中给出内置衰减计算的相关说明。尽管我们仍然可以在片元着色器中利用一些数学公式来计算衰减,但由于我们无法在Shader中通过内置变量得到光源的范围、聚光灯的朝向、张开角度等信息,因此得到的效果往往在有些时候不尽如人意。

9.4 Unity的阴影

9.4.1 阴影是如何实现的

在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。而Unity就是使用的这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity 就会为该光源计算它的阴影映射纹理。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
那么,在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用Base Pass和Additional Pass来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而 Base Pass 和 Additional Pass 中往往涉及很多复杂的光照模型计算。因此,Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode 标签被设置为 ShadowCaster 的 Pass。这个 Pass 的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的 Unity Shader 中找到LightMode 为 ShadowCaster 的Pass,如果没有,它就会在 Fallback 指定的 Unity Shader 中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影。当找到了一个LightMode 为ShadowCaster 的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
在传统的阴影映射纹理的实现中,我们会在正常渲染的 Pass 中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用 xy 分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由 z 分量得到),那么说明该点位于阴影中。但Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术 (Screenspace Shadow Map) 。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台Unity都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性
当使用了屏幕空间的阴影映射技术时,Unity 首先会通过调用 LightMode 为 ShadowCaster 的Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结:

  • 如果我们想要一个物体接收来自其他物体的阴影,就必须在 Shader 中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
  • 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投影映射技术,Unity 还会使用这个 Pass 产生一张摄像机的深度纹理。

9.4.2 不透明物体的阴影

在Unity中,我们可以选择是否让一个物体投射或接收阴影。这是通过设置 Mesh Renderer 组件中的Cast ShadowsReceive Shadows属性来实现的:
《Unity Shader 入门精要》第9章 更复杂的光照_第12张图片
我们新建场景,并且新建一个正方体和两个平面,并将上一节的材质赋给正方体,并且打开正方体的 Cast Shadows 和 Receive Shadows 开光,发现正方体可以投射阴险但是无法接受阴影,效果如下:
《Unity Shader 入门精要》第9章 更复杂的光照_第13张图片

投射阴影

如果开启了 Cast Shadows 属性,那么Unity就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。正如之前所说,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。
但是我们在上节的 shader 中并未实现 LightMode 为 ShadowCast 的 Pass,为什么它还可以投射阴影呢?实际上,秘密就在于Fallback 语义:

Fallback "Specular" 

虽然 Specular 本身也没有包含这样一个 Pass,但是由于它的 Fallback 调用了 VertexLit,它会继续回调,并最终回调到内置的 VertexLit。我们可以在Unity内置的着色器里找到它:builtin-shaders-xxx->DefaultResourcesExtra->Normal- VertexLit.shader。打开它,我们就可以看到“传说中”的LightMode 为ShadowCaster 的Pass了:

在这里插入代码片

让物体接收阴影

为了让正方体接受阴影,我们需要在 Shader 中对阴影做相应处理,我们可以对 Forward-Rendering Shader 做优化如下:

Shader "Chapter9/ForwardRendering"
{
    Properties
    {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        // Base Pass
        Pass
        {
            Name "ForwardBase"
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            // #pragma multi_ compile_fwdbase 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            // 包含阴影相关引用文件
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                // 这个宏声明一个用于对阴影纹理采样的坐标,其参数需要是下一个可用的插值寄存器的索引值,在上面的例子中就是2。
                // #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
                SHADOW_COORDS(2)
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                // 这个宏用于在顶点着色器中计算上一步中声明的阴影纹理坐标
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));
                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
                fixed atten = 1.0;

                // 这个宏用于在片元着色器中计算阴影值
                // #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
                fixed shadow = SHADOW_ATTENUATION(i);
                // 把阴影值shadow和漫反射以及高光反射颜色相乘即可得到阴影效果
                return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
            }
            ENDCG
        }

        // Additional Pass
        pass
        {
            Name "ForwardAdd"
            Tags{ "LightMode" = "ForwardAdd" }

            // 设置混合模式
            // 我们希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加
            // 如果没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果
            Blend One One

            CGPROGRAM

            // #pragma multi_compile_fwdadd 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v) 
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // 根据光源类型不同采用不同的算法计算光的方向
                // 如果当前前向渲染Pass处理的光源类型是平行光,那么 Unity 的底层渲染引擎就会定义 USING_DIRECTIONAL_LIGHT
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0).xyz;
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 根据光源类型不同采用不同的算法计算衰减值
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else
                    #if defined (POINT)
                        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                        // 尽管我们可以使用数学表达式来计算给定点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量相对较大的操作
                        // 因此Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。
                        // 我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值
                        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #elif defined (SPOT)
                        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
                        // 尽管我们可以使用数学表达式来计算给定点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量相对较大的操作
                        // 因此Unity选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。
                        // 我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值
                        fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #else
                        fixed atten = 1.0;
                    #endif
                #endif

				return fixed4((diffuse + specular) * atten, 1.0);
            }

            ENDCG
        }
    }
    // 指定默认 Shader
    Fallback "Specular"    
}
  1. 首先我们在 Base Pass 中包含进一个新的内置文件AutoLight.cginc
  2. 我们在顶点着色器的输出结构体v2f中添加了一个内置宏SHADOW_COORDS
  3. 我们在顶点着色器返回之前添加另一个内置宏TRANSFER_SHADOW
  4. 接着,我们在片元着色器中计算阴影值,这同样使用了一个内置宏SHADOW_ ATTENUATION
  5. 最后我们只需要把阴影值 shadow 和漫反射以及高光反射颜色相乘即可

SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATION 是计算阴影时的“三剑客”。这些内置宏帮助我们在必要时计算光源的阴影。我们可以在AutoLight.cginc中找到它们的声明。
需要读者注意的是, 由于这些宏中会使用上下文变量来进行相关计算,例如 TRANSFER_ SHADOW 会使用 v.vertex 或 a.pos 来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2f 结构体中的顶点坐标变量名必须是 vertex ,顶点着色器的输出结构体 v2f 必须命名为v ,且 v2f 中的顶点位置变量必须命名为 pos 。
最后我们得到如下效果:
《Unity Shader 入门精要》第9章 更复杂的光照_第14张图片

9.4.3 使用帧调试器查看阴影绘制过程

9.4.4 统一管理光照衰减和阴影

光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。那么,是不是可以有一个方法可以同时计算两个信息呢?好消息是,Unity在Shader 里提供了这样的功能,这主要是通过内置的UNITY_LIGHT_ATTENUATION宏来实现的。
UNITY_LIGHT_ATTENUATION是 Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的AutoLight.cginc里找到它的相关声明。它接受3个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。它的第二个参数是结构体v2f,这个参数会传递给9.4.2节中使用的SHADOW_ATTENUATION ,用来计算阴影值。而第三个参数是世界空间的坐标
UNITY_LIGHT_ ATTENUATION源码如下:

#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
#   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
        unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
        fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
        fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif

#ifdef SPOT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
{
    return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
{
    return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
}
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
#else
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
#endif
#   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
        DECLARE_LIGHT_COORD(input, worldPos); \
        fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
        fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
#endif

#ifdef DIRECTIONAL
#   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif

#ifdef POINT_COOKIE
samplerCUBE_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
#   if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz
#   else
#       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = input._LightCoord
#   endif
#   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
        DECLARE_LIGHT_COORD(input, worldPos); \
        fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
        fixed destName = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w * shadow;
#endif

#ifdef DIRECTIONAL_COOKIE
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
#   if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy
#   else
#       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = input._LightCoord
#   endif
#   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
        DECLARE_LIGHT_COORD(input, worldPos); \
        fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
        fixed destName = tex2D(_LightTexture0, lightCoord).w * shadow;
#endif

可见,Unity 针对不同光源类型、是否启用cookie等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION。
由于使用了 UNITY_LIGHT_ATTENUATION ,我们的 Base Pass 和 Additional Pass 的代码得以统一——我们不需要在 Base Pass 里单独处理阴影,也不需要在 Additional Pass 中判断光源类型来处理光照衰减,一切都只需要通过 UNITY_LIGHT_ATTENUATION 来完成即可。
使用此宏后我们的 Shader 如下:

Shader "Chapter9/ForwardRendering"
{
    Properties
    {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        // Base Pass
        Pass
        {
            Name "ForwardBase"
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            // #pragma multi_ compile_fwdbase 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            // 包含阴影相关引用文件
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                // 这个宏声明一个用于对阴影纹理采样的坐标,其参数需要是下一个可用的插值寄存器的索引值,在上面的例子中就是2。
                // #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
                SHADOW_COORDS(2)
            };

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                // 这个宏用于在顶点着色器中计算上一步中声明的阴影纹理坐标
                // #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));
                TRANSFER_SHADOW(o);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 计算衰减和阴影
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                // 把阴影值shadow和漫反射以及高光反射颜色相乘即可得到阴影效果
                return fixed4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }

        // Additional Pass
        pass
        {
            Name "ForwardAdd"
            Tags{ "LightMode" = "ForwardAdd" }

            // 设置混合模式
            // 我们希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加
            // 如果没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果
            Blend One One

            CGPROGRAM

            // #pragma multi_compile_fwdadd 指令可以保证我们在Shader中使用的光照变量可以被正确赋值。这是不可缺少的
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            
            struct v2f 
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };
            
            v2f vert(a2v v) 
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // 根据光源类型不同采用不同的算法计算光的方向
                // 如果当前前向渲染Pass处理的光源类型是平行光,那么 Unity 的底层渲染引擎就会定义 USING_DIRECTIONAL_LIGHT
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0).xyz;
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                // 使用 Blinn-Phong 光照模型
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
				fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 计算衰减和阴影
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				return fixed4((diffuse + specular) * atten, 1.0);
            }

            ENDCG
        }
    }
    // 指定默认 Shader
    Fallback "Specular"    
}

9.4.5 透明度物体的阴影

对于大多数不透明物体来说,把Fallback 设为VertexLit 就可以得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的Fallback。

透明度测试物体的阴影

透明度测试的处理比较简单,但如果我们仍然直接使用VertexLit、Diffuse、Specular等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而VertexLit 中的阴影投射纹理并没有进行这样的操作。
比如我们将之前的 shader 赋给一个半透明的正方体,可以发现不正常的阴影:
《Unity Shader 入门精要》第9章 更复杂的光照_第15张图片
为了让使用透明度测试的物体得到正确的阴影效果,我们只需要在 Unity Shader 中更改一行代码,即把 Fallback 设置为 Transparent/Cutout/VertexLit 。读者可以在内置文件中找到该 Unity Shader 的代码,它的 ShadowCaster Pass 也计算了透明度测试,因此会把裁剪后的物体深度信息写入深度图和阴影映射纹理中。但需要注意的是 ,由于 Transparent/Cutout/VertexLit 中计算透明度测试时,使用了名为 _Cutoff 的属性来进行透明度测试,因此,这要求我们的 Shader中也必须提供名为 _Cutoff 的属性。否则,同样无法得到正确的阴影结果:

Shader "Unity Shaders Book/Chapter 9/Alpha Test With Shadow" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
	}
	SubShader {
		Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			Cull Off
			
			CGPROGRAM
			
			#pragma multi_compile_fwdbase
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _Cutoff;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
				SHADOW_COORDS(3)
			};
			
			v2f vert(a2v v) {
			 	v2f o;
			 	o.pos = UnityObjectToClipPos(v.vertex);
			 	
			 	o.worldNormal = UnityObjectToWorldNormal(v.normal);
			 	
			 	o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

			 	o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
			 	
			 	// Pass shadow coordinates to pixel shader
			 	TRANSFER_SHADOW(o);
			 	
			 	return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				
				fixed4 texColor = tex2D(_MainTex, i.uv);

				clip (texColor.a - _Cutoff);
				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
							 	
			 	// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
			 	
				return fixed4(ambient + diffuse * atten, 1.0);
			}
			
			ENDCG
		}
	} 
	FallBack "Transparent/Cutout/VertexLit"
	// FallBack "VertexLit"
}

效果如下:

《Unity Shader 入门精要》第9章 更复杂的光照_第16张图片

但是,这样的结果仍然有一些问题,例如出现了一些不应该透过光的部分。出现这种情况的原因是,默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑物体的正面。但对于本例的正方体来说,由于一些面完全背对光源,因此这些面的深度信息没有加入到阴影映射纹理的计算中。为了得到正确的结果,我们可以将正方体的 Mesh Renderer 组件中的 Cast Shadows 属性设置为 Two Sided ,强制Unity在计算阴影映射纹理时计算所有面的深度信息
《Unity Shader 入门精要》第9章 更复杂的光照_第17张图片

透明度混合物体的阴影

与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的 Unity Shader,如 Transparent/VertexLit 等,都没有包含阴影投射的Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接收来自其他物体的阴影。
Unity 会这样处理半透明物体是有它的原因的。由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且也会影响性能。因此,在Unity中,所有内置的半透明 Shader 是不会产生任何阴影效果的。当然,我们可以使用一些 dirty trick 来强制为半透明物体生成阴影,这可以通过把它们的 Fallback 设置为 VertexLit、Diffuse 这些不透明物体使用的 Unity Shader,这样 Unity 就会在它的 Fallback 找到一个阴影投射的 Pass。然后,我们可以通过物体的Mesh Renderer组件上的Cast Shadows 和 Receive Shadows 选项来控制是否需要向其他物体投射或接收阴影。

Shader "Unity Shaders Book/Chapter 9/Alpha Blend With Shadow" {
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
	}
	SubShader {
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			
			CGPROGRAM
			
			#pragma multi_compile_fwdbase
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _AlphaScale;
			
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
				SHADOW_COORDS(3)
			};
			
			v2f vert(a2v v) {
			 	v2f o;
			 	o.pos = UnityObjectToClipPos(v.vertex);
			 	
			 	o.worldNormal = UnityObjectToWorldNormal(v.normal);
			 	
			 	o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

			 	o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
			 	
			 	// Pass shadow coordinates to pixel shader
			 	TRANSFER_SHADOW(o);
			 	
			 	return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				
				fixed4 texColor = tex2D(_MainTex, i.uv);
				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

			 	// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
			 	
				return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale);
			}
			
			ENDCG
		}
	} 
	FallBack "Transparent/VertexLit"
	// Or  force to apply shadow
	// FallBack "VertexLit"
}

FallBack "Transparent/VertexLit"效果如下:
《Unity Shader 入门精要》第9章 更复杂的光照_第18张图片
FallBack "VertexLit" 为其开启阴影,效果如下:
《Unity Shader 入门精要》第9章 更复杂的光照_第19张图片

你可能感兴趣的:(unity,游戏引擎)