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

10 篇文章 0 订阅

本文章用于帮助自己学习,因此只记录一些个人认为比较重要或者还不够熟悉的内容。
原作者:http://blog.csdn.net/candycat1992/article/

第九章 更复杂的光照

9.1Unity的渲染路径

在Unity里,渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的。因此,如果要和光源打交道,我们需要为每个Pass指定它使用的渲染路径,只有这样才能让Unity知道源和处理后的光照信息都放在这哪些数据里。也就是说,我们只有为Shader正确地选择和设置了需要的渲染路径,该 Shader的光照计算才能被正确执行
Unity支持多种类型的渲染路径。在Unity 5.0版本之后,主要有:前向渲染路径(Forward Rendering Path)和延迟渲染路径(Deferred Rendering Path)。
大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。在Unity2020中,可以通过在Unity 的 Edit->Project Settings->Graphics->Rendering Path中选择项目所需的渲染路径。默认情况下,该设置选择的是前向渲染路径,如图:
在这里插入图片描述
但有时,我们希望可以使用多个渲染路径,例如摄像机A渲染的物体使用前向渲染路径,而 摄像机B渲染的物体使用延迟渲染路径。这时,我们可以在每个摄像机的渲染路径设置中设置该摄像机使用的渲染路径,以覆盖Project Settings中的设置,如下图所示:
在这里插入图片描述
在上面的设置中,如果选择了 Use Graphics Settings,那么这个摄像机会使用Project Settings中 的设置;否则就会覆盖掉Project Settings中的设置。

完成了上面的设置后,我们就可以在每个Pass中使用标签来指定该Pass使用的渲染路径。 这是通过设置Pass的LightMode标签实现的。不同类型的渲染路径可能会包含多种标签设置。 例如,我们之前在代码中写的:


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

上面的代码将告诉Unity,该Pass使用前向渲染路径中的ForwardBase路径。而前向渲染路径 还有一种路径叫做FonvardAd。下表Pass的LightMode标签支持的渲染路径设置选项。
在这里插入图片描述
通俗来讲,指定渲染路径是我们和Unity的底层渲染引擎的一次重要的沟通。例如,如果我们为一个Pass设置了前向渲染路径的标签,相当于会告诉Unity: "嘿,我准备使用前向渲染了,你把那些光照属性都按前向渲染的流程给我准备好,我一会儿要用!”随后,我们可以通过Unity 提供的内置光照变量来访问这些属性。如果我们没有指定任何渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。
那么,Unity的渲染引擎是如何处理这些渲染路径的呢?下面,我们会对这些渲染路径进行更加详细的解释。

9.1.1前向渲染路径

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

1前向渲染路径的原理

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

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中的前向渲染

在Unity中,前向渲染路径有3种处理光照的方式:逐顶点处理、逐像素处 理,球谐函数(Spherical Harmonics,SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)
可以在光源组件中设置这些属性,如图:
在这里插入图片描述

Unity使用的判断规则如下:

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

对于前向渲染来说,一个Unity Shader通常会定义一个Base Pass (Base Pass也可以定义多次,例如需要双面渲染等情况)以及一个Additional Pass。一个Base Pass仅会执行一 次(定义了多个Base Pass的情况除外),而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass
实际上,渲染路径的设置用于告诉Unity该Pass在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如_LightColor0等),如何使用这些内置变量进行计算完全取决于开发者的选择。 例如,我们完全可以利用Unity提供的内置变量在Base Pass中只进行逐顶点光照;同样,我们也完全可以在Additional Pass中按逐顶点的方式进行光照计算,不进行任何逐像素光照计算。

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

根据我们使用的渲染路径(即Pass标签中LightMode的值),Unity会把不同的光照变量传递给Shader。
在Unity 5中,对于前向渲染(即 LightMode 为 ForwardBase 或 ForwardAdd)来说,下表给出了我们可以在Shader中访问到的光照变量。
在这里插入图片描述
前向渲染中可以使用的内置光照函数:
在这里插入图片描述

9.1.2顶点照明渲染路径

顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。实际上,它仅仅是前向渲染路径的一个子集

1.Unity中的顶点照明渲染

顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染。在这个Pass中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的

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

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

9.1.3延迟渲染路径

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。每执行一个Pass我们都需要重新渲染一遍物体,但很多计算实际上是重复的。
延迟渲染是一种更古老的渲染方法。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为G(Geometry)缓冲(G-bufifer)。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。

1.延迟渲染的原理

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

Pass1
{  
    //第一个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);  
            }  
        }  
    }  
}

Pass2
{  
    //利用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及以上、深度渲染纹理以及双面的模板缓冲。

至于为什么不能处理半透明物体,因为之前渲染Mesh的时候,是一个一个迭代的,一个一个做深度测试的,但是,延迟渲染要先渲染到Gbuffer,Gbuffer只是把当前能看到的像素记录下来,但是透明的,同一个像素点,可能需要记录更多。

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

下表给出了处理延迟渲染路径可以使用的光照变量。这些变量都可以在UnityDeferred Library.cginc文件中找到它们的声明。
在这里插入图片描述

9.1.4选择哪种渲染路径

Unity官方文档中对几种渲染路径做出的对比:
在这里插入图片描述
总体来说,我们需要根据游戏发布的目标平台来选择渲染路径。如果当前显卡不支持所选渲染路径,那么Unity会自动使用比其低一级的渲染路径。

9.2Unity的光源类型

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

9.2.1光源类型有什么影响

Shader中最常使用的光源属性有光源的位置、方向(更具体说就是,到某点的方向)、 颜色、强度以及衰减(更具体说就是,到某点的衰减,与该点到光源的距离有关)这5个属性。 而这些属性和它们的几何定义息息相关。

1.平行光

平行光的几何定义是最简单的。平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。下图给出了 Unity中平行光在Scene视图中的表示以及Light组件的面板。
平行光没有一个唯一的位置,也就是说,它可以放在场景中的任意位置。它的几何属性只有方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念,也就是说,光照强度不会随着距离而发生改变
在这里插入图片描述

2.点光源

点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。点光源可以表示由一个点 发出的、向所有方向延伸的光。下图给出了Unity中点光源在Scene视图中的表示以及Light组件的面板。
在这里插入图片描述
点光源是会衰减的,随着物体逐渐远离点光源,它接收到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处的最弱,值为0。其中间的衰减值可以由一个函数定义。

3.聚光灯

聚光灯是这3种光源类型中最复杂的一种。它的照亮空间同样是有限的,是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。下图给出了 Unity中聚光灯在Scene视图中的表示以及Light组件的面板。
在这里插入图片描述
聚光灯的衰减也是随 着物体逐渐远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为0。 其中间的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式要更加复杂,因为我们需要判断一个点是否在锥体的范围内。

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

在了解了3种光源的几何定义后,我们来看一下如何在Unity Shader中访问它们的5个属性: 位置、方向、颜色、强度以及衰减

1.实践

代码:



Shader "Unity Shaders Book/Chapter 9/Forward Rendering" {
	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 {
			// Pass for ambient light & first pixel light (directional light)
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			
			//保证在Shader中使用光照衰减等变量可以被正确赋值
			#pragma multi_compile_fwdbase	
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.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);
				//_WorldSpaceLightPos0得到平行光方向
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				
				//Base Pass计算环境光
				//只计算一次即可
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
				//然后在Base Pass中处理场景中的最重要的平行光
				//如果场景中包含了多个平行光,
				//Unity会选择最亮的平行光传递给Base Pass进行逐像素处理,
				//其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理。
				//_LightColor0得到平行光强度
			 	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);
				
				//令平行光衰减值为1
				fixed atten = 1.0;
				
				return fixed4(ambient + (diffuse + specular) * atten, 1.0);
			}
			
			ENDCG
		}
		
		//Additional Pass
		Pass {
			// Pass for other pixel lights
			Tags { "LightMode"="ForwardAdd" }
			
			//使用Blend命令开启和设置混合模式
			//实现Additional Pass计算得到的光照结果在帧缓存中与之前的光照结果进行叠加
			Blend One One
		
			CGPROGRAM
			
			//保证在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);

				//判断当前处理的逐像素光源的类型
				#ifdef USING_DIRECTIONAL_LIGHT
					//如果是平行光,光源方向可以直接得到
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				#else
					//如果是点光源或聚光灯,
					//光源方向需要用这个位置减去世界空间下的顶点位置。
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
				#endif
				
				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;
				        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #elif defined (SPOT)
				        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
				        fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #else
				        fixed atten = 1.0;
				    #endif
				#endif

				return fixed4((diffuse + specular) * atten, 1.0);
			}
			
			ENDCG
		}
	}
	FallBack "Specular"
}

需要注意的是,本节只是为了讲解 处理其他类型光源的实现原理,上述代码并不会用于真正的项目中,我们会在9.5节给出包含了完整光照计算的Unity Shader。
效果如下:
在这里插入图片描述
胶囊体、平行光和点光源在场景中的相对位置 :
在这里插入图片描述

2.实验:Base Pass 和 Additional Pass 的调用

新建一个场景。新建4个点光源,调整它们的颜色为相同的红色。效果如下:
在这里插入图片描述
胶囊体、平行光和点光源在场景中的相对位置 :
在这里插入图片描述
那么,这样的结果是怎么来的呢?当我们创建一个光源时,默认情况下它的Render Mode是Auto。这意味着,Unity会在背后为我们判断哪些光源会按逐像素处理,而哪些按逐顶点或SH的方式处理。由于我们没有更改Edit -> Project Setting ->Quality ->Pixel Light Count中的数值,因此默认情况下一个物体可以接收除最亮的平行光外的4个逐像素光照。在这个例子中,场景中共包含了 5个光源,其中一个是平行光,它会在Chapter9-Forward Rendering的Base Pass中按逐像素的方式被处理;其余4个都是点光源,由于它们的Render Mode 为Auto且数目正好等于4,因此都会在Chapter9-ForwardRendering的Additional Pass中逐像素的方式被处理,每个光源会调用一次Additional Pass
在Unity 5中,我们还可以使用帧调试器(Frame Debugger)工具来査看场景的绘制过程。 使用方法是:在Window->Analysis->Frame Debugger中打开帧调试器。

9.3Unity的光照衰减

9.3.1用于光照衰减的纹理

Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了 cookie,那么衰减查找纹理是_LightTextureBO,但这里不讨论这种情况。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。例如,(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了在光源空间中所关心的距离最远的点的衰减
为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过_LightMatrix0变换矩阵和世界空间中的顶点坐标相乘得到的:

float3 lightCoord = mul(_LightMatrixO, float4(i.worldPosition, 1)).xyz;

然后,我们可以使用这个坐标的模的平方对衰减纹理进行釆样,得到衰减值:

//使用宏UN1TY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量
//,以得到最终的衰减值。
fixed atten = tex2D(_LightTextureO, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

9.3.2使用数学公式计算衰减

尽管纹理釆样的方法可以减少计算衰减时的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。例如,下面的代码可以计算光源的线性衰减:

float distance = length(_WorldSpaceLightPosO.xyz - i.worldPosition.xyz);
atten =1.0/ distance; // 线性衰减

由于我们无法在Shader中通过内置变量得到光源的范 围、聚光灯的朝向、张开角度等信息,因此得到的效果往往在有些时候不尽如人意,尤其在物体离开光源的照明范围时会发生突变(这是因为,如果物体不在该光源的照明范围内,Unity就不 会为物体执行一个Additional Pass)。

9.4Unity的阴影

9.4.1阴影是如何实现的

在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。而Unity就是使用的这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)
Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Unity Shader中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个LightMode为 ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。

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

9.4.2不透明物体的阴影

1.让物体投射阴影

在场景中创建一个正方体、两个平面,将shader为Chapter9-ForwardRendering的材质赋给正方体,并把正方体和两个平面的Cast Shadows和Receive Shadows都设为开启状态,效果如图:
在这里插入图片描述
从图中可以发现,尽管我们没有对正方体使用的Chapter9-ForwardRendering进行任何更改, 但正方体仍然可以向下面的平面投射阴影。
这是因为在 Chapter9-ForwardRendering 中,我们为它的Fallback指定了一个用于回调Unity Shader,即内置的Specular。虽然Specular本身也没有包含这 样一个Pass,但是由于它的Fallback调用了VertexLit,它会继续回调,并最终回调到内置的VertexLit:

Pass
 {
	 Name "ShadowCaster"
	Tags { "LightMode" = "ShadowCaster" }
	
	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag 
	#pragma multi_compile_shadowcaster 
	#include"UnityCG.cginc*'
	struct v2f
	{
		V2F_SHAD0W_CASTER;
	};
	v2f vert( appdata_base v )
	{
		v2f o;
		TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) 
		return o;
	)
	float4 frag( v2f i ) : SV_Target
	(
		SHADOW_CASTER_FRAGMENT(i)
	}
	ENDCG
}

上面的代码的用处实际上就是为了把深度信息写入渲染目标中。在Unity中,这个Pass的渲染目标可以是光源的阴影映射纹理,或是摄像机的深度纹理。
上图中右侧的平面并没有向最下面的平面投射阴影,尽管它的Cast Shadows已经被开启了。在默认情况下,我们在计算光源的阴影映射纹理时会剔除掉物体的背面。但对于内置的平面来说,它只有一个面,因此在本例中当计算阴影映射纹理时不会添加到阴影映射纹理中。我们可以将Cast Shadows设置为Two Sided来允许对物体的所有面都计算阴影信息。效果如图:
在这里插入图片描述

1.让物体接收阴影

为了让正方体可以接收阴影,新建一个shader,将Chapter9-ForwardRendering的代码复制进去,并对Base Pass中的代码进行修改,修改如下:

//Base Pass
		Pass {
			// Pass for ambient light & first pixel light (directional light)
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			
			
			#pragma multi_compile_fwdbase	
			
			#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;
				//添加内置宏,
				//用于声明一个用于对阴影纹理采样的坐标。
				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;

				//计算v2f中声明的阴影纹理坐标
				TRANSFER_SHADOW(o);

				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				//_WorldSpaceLightPos0得到平行光方向
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

				//使用阴影坐标对阴影贴图进行采样
				fixed shadow = SHADOW_ATTENUATION(i);
				
				//最后乘上阴影值shadow
			 	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)) * shadow;

			 	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
			 	fixed3 halfDir = normalize(worldLightDir + viewDir);
				//最后乘上阴影值shadow
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss) * shadow;
				
				fixed atten = 1.0;

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

效果如下:
在这里插入图片描述

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

光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的一一都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。而Unity在Shader里提供了这样的功能,这主要是通过内置的UNITY_LIGHT_ATTENUATION宏来实现的。
尽管Chapter9-Shadow中的代码可以让我们得到正确的阴影,但在实践中我们通常会使用Unity的内置宏和函数来计算衰减和阴影,从而隐藏一些实现细节,只需在Chapter9-Shadow的基础上修改片元着色器(Base Pass和Additional Pass)代码即可,关键代码如下:

fixed4 frag(v2f i) : SV_Target {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				
			 	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

			 	fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
			 	fixed3 halfDir = normalize(worldLightDir + viewDir);
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				//用于计算光照衰减和阴影的宏,
				//第一个参数用于存储将光照衰减和阴影值相乘后的结果。
				//第二个参数是结构体v2f,这个参数会传递给SHADOW_ATTENUATION,用来计算阴影值。
				//第三个参数是世界空间的坐标,用于计算光源空间下的坐标,再对光照衰减纹理釆样来得到光照衰减。
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				return fixed4(ambient + (diffuse + specular) * atten, 1.0);
			}

由于使用了 UNITY_LIGHT_ATTENUATION,我们的 Base Pass 和 Additional Pass 的代码得以统一——我们不需要在Base Pass里单独处理阴影,也不需要在Additional Pass中判断光源类型来处理光照衰减,一切都只需要通过UNITY_LIGHT_ATTENUATION来完成即可。

9.4.4透明度物体的阴影

对于大多数不透明物体来说,把Fallback设为VertexLit就可以得到正确的阴影。但对于透 明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混 合,我们需要小心设置这些物体的Fallback。
由于透明度测试需要在片元着色器中舍弃某些片元,而 VertexLit中的阴影投射纹理并没有进行这样的操作,因此无法得到正确的阴影。
我们使用之前学习的透明度测试+阴影的方法来渲染一个正方体,它使用的Unity Shader和8.3节透明度测试中几乎完全相同的代码,只是添加了关于阴影的计算,代码如下:

Shader "Unity Shaders Book/Chapter 8/AlphatestWithShadow"
{
    Properties
    {
        _Color("Main Tint",Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        //为了在材质面板中控制透明度测试时使用的阈值,声明一个属性_Cutoff
        _Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
    }
    SubShader
    {
        //通常,使用了透明度测试的Shader都应该在SubShader中设置这三个标签。
        Tags { 
        //在Unity中透明度测试使用的渲染队列是名为AlphaTest的队列
        "Queue"="AlphaTest" 
        //把IgnoreProjector设置为True,意味着这个Shader不会受到投影器(Projectors)的影响。
        "IgnoreProjector"="Ture"
        //RenderType标签可以让Unity把这个Shader归入到提前定义的组(TransparentCutout组)中,
        //以指明该Shader是一个使用了透明度测试的Shader。
        "RenderType"="TransparentCutout"
        }


        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #include "Lighting.cginc"
            //包含进需要的头文件
            #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;
                //添加内置宏,
				//用于声明一个用于对阴影纹理采样的坐标。
                //由于已经占用了3个插值寄存器,
                //因此传入的参数是3,阴影纹理坐标将占用第四个寄存器。
				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);
                //等于o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                //计算v2f中声明的阴影纹理坐标后传递给片元着色器
				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);
                // 等于
            // if ((texColor.a - _Cutoff) < 0.0) {
            // discard;
            // }
                //如果texColor的a通道数值小于阈值参数_Cutoff,
                //该片元就产生完全透明的效果。

                //使用内置宏计算阴影和光照衰减
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

                fixed3 albedo = texColor.rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rbg * albedo * max(0, dot(worldNormal, worldLightDir));

                //阴影和光照衰减与漫反射相乘
                return fixed4(ambient + diffuse * atten, 1.0);
            }
            ENDCG
        }
    }
            //更改Fallback,它的ShadowCasterPass计算了透明度测试
            //该Fallback使用名为_Cutoff属性
            //,因此我们Shader中也必须提供名为_Cutoff的属性。
            Fallback "Transparent/Cutout/VertexLit"
}

效果如下:
在这里插入图片描述
在Unity中,所有内置的半透明Shader是不会产生任何阴影效果的。当然,我们可以使用一些dirty trick来强制为半透明物体生成阴影,这可以通过把它们的Fallback设置为 VertexLit、Diffuse这些不透明物体使用的Unity Shader,这样Unity就会在它的Fallback找到一个阴影投射的Pass。然后,我们可以通过物体的Mesh Renderer组件上的Cast Shadows和Receive Shadows选项来控制是否需要向其他物体投射或接收阴影。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值