Back
Featured image of post Shader入门精要-更加复杂的光照

Shader入门精要-更加复杂的光照

添加阴影效果

技术美术——更加复杂的光照

前言

前面的学习里,场景中只有一个光源且为平行光,但在实际的项目中,我们需要处理数目更多、类型更加复杂的光源,最重要的是需要得到 阴影。这里,我们有必要知道 Unity 底层渲染引擎如何让我们在 shader 中访问它们的。

Unity 渲染路径

设置项目的渲染路径

Unity 主要有三种渲染路径:前向渲染路径延迟渲染路径顶点照明渲染路径,其中顶点照明渲染路径已经被淘汰,其次新的延迟渲染路径代替了原来的延迟渲染路径。

在项目中,整个项目会设置为其中一个渲染路径,默认情况下是 前向渲染路径,如果希望使用多个渲染路径,我们可以在摄像机的渲染路径设置中设置该摄像机使用的渲染路径,覆盖 Graphics 中的设置。

设置 Pass 的渲染路径

完成以上设置,我们就可以在每个 Pass 中使用标签来指定该 Pass 使用的渲染路径。这是通过设置 Pass 的 LightMode 标签实现的。例如:

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

上面代码告诉 Unity 该 Pass 使用前向渲染路径中的 ForwardBase 路径。Pass 的 LightMode 标签支持的渲染路径设置选项。

标签名描述
Always不管使用哪种渲染路径,该 Pass 总会被渲染,但 不会计算光照
ForwardBase仅用于 前向渲染,该 Pass 会计算环境光、最重要的平行光、逐顶点/ SH光源和 Lightmaps
ForwardAdd仅用于 前向渲染,该 Pass 会计算额外的逐像素光源,每个 Pass 对应一个光源
Deferred仅用于 延迟渲染,该 Pass 会渲染 G 缓冲(G-buffer)
ShadowCaster把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中

指定渲染路径的作用

借用网上一个例子,不同的渲染路径差距,如同不同的绘画方式

指定渲染路径就是告诉 Unity 底层渲染引擎以什么渲染流程去准备光照属性与光源信息。如果没有指定任何渲染路径,那么一些光照变量很可能不会被正确赋值,计算出的效果很可能就是错误的。

前向渲染路径

前向渲染路径的原理

前向渲染的光照计算由一个 Pass 块来完成,我们在这个 Pass 块中计算平行光或者其他光源。它的缺点是每个光源都要被 Pass 计算一遍才行。一个物体 n 个光源,计算 1 * n 次,m 个物体 n 个光源,计算 m * n 次。在实际情况中,后一个光源可能把前一个光源给覆盖了(由于光源强度等问题)。

缺点:很多时候如果场景中光源过多,那么前向渲染会做非常多根本不需要的工作,前向渲染路径里面不能使用过多的光源。否则性能就会急速下降。

为了改善前向渲染的问题,引擎通常会限制每个物体的逐像素光照数目。前向渲染的伪代码如下:

Pass{
    for(each primitive in this model){
        for(each fragment covered by this primitive){
             if(failed in depth test){
                  //若片元没通过深度测试,舍弃
             }else{
                  //若片元可见,进行光照的计算
                  float4 color = Shading(materialInfo,pos,normal,lightDir,viewDir);
                  //写入帧缓存
                  writeFrameBuffer(fragment,color);
             }
        }
    }
}

Unity 中的前向渲染

Unity 中,前向渲染有 3 种处理光照 (照亮物体) 的方式:逐顶点处理逐像素处理球谐函数处理 ( Spherical Harmonics,SH )

哪种方式处理光照

决定一个光源按哪种方式处理光照取决于自身的类型和渲染模式

  • 光源的 类型 是指该光源是平行光还是其他类型的光源
  • 光源的 渲染模式 是指该光源是否是 重要的 (Important)

前向渲染中,Unity 会根据场景中各个光源的设置以及光源对物体的影响程度 (距离远近、光源强度等),对光源进行一个权重排序。然后 Unity 将一定数量的光源 逐像素处理,最多 4 个光源 逐顶点处理,剩下的光源 SH方式处理

其中的判断规则如下:

  1. 最亮的平行光逐像素处理
  2. 渲染模式设置为 Important 的光源,逐像素处理
  3. 渲染模式设置为 Not Important 的光源,逐顶点处理或者 SH 处理
  4. 若以上规则得到的逐像素光源的数量小于 Quality Setting 中的逐像素光源数量 (Pixel Light Count),会有更多的光源以逐像素方式处理。

在哪进行光照计算

前向渲染有两种 Pass:Base PassAdditional Pass。通常这两种进行标签和渲染设置以及常规光照计算如图所示:

几点说明:

  1. 使用了 #pragma multi_compile_fwdbase 编译指令,以保证 Unity 可以为相应类型的 Pass 生成所需的 Shader 变种,这些变种会处理不同条件下的渲染逻辑,例如是否使用了光照贴图,当前处理哪种光源类型,是否开启了阴影等,同时 Unity 也会在背后声明相关的内置变量并传递到 Shader 中。
  2. Base Pass 旁边的注释给出了 Base Pass 中支持的一些光照特性。例如在 Base Pass 中我们可以访问光照纹理(lightmap)
  3. Base Pass 中渲染的平行光默认是支持阴影的(如果开启了光源的阴影功能),而 Additional Pass 中渲染的光源在默认情况下是没有阴影效果的,即便我们在它的 Light 组件中设置了有阴影的 Shadow Type。但我们可以在 Additional Pass 中使用 #pragma multi_compile_fwdadd_fullshadows 代替 #pragma multi_compile_fwdadd 编译指令,为点光源和聚光灯开启阴影效果,但这需要在 Unity 内部使用更多的 Shader 变种。
  4. 环境光和自发光也是在 Base pass 中计算的。这是因为对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在 Additional Pass 中计算这两种光照,就会造成叠加多次环境光和自发光,这不是我们想要的。
  5. 在 Additional Pass 的渲染设置中,我们还开启和设置了混合模式。这是因为我们希望每个 Additional Pass 可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终有多个光照的渲染效果。如果我们没有开启和设置混合模式,那么 Additional Pass 的渲染结果会覆盖掉之前的渲染结果,看起来好像该物体只受该光源的影响。通常情况下,我们选择的混合模式是 Blend One One
  6. 对于前向渲染来说,一个 UnityShader 通常会定义一个 Base Pass(Base Pass 也可以被定义多次,例如需要双面渲染的情况)以及一个 Additional Pass。一个 Base Pass 仅会执行一次(定义了多个 Base Pass 的情况除外),而一个 Additional Pass 会根据影响该物体的其他逐像素光源数目被多次调用,即每个逐像素光源会执行一次 Additional Pass。

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

内置的光照变量和函数

根据使用的渲染路径 (即 Pass 标签中 LightMode 的值),Unity 会把不同的光照变量传递给 Shader,在 Unity5 中,对于前向渲染 (即 LightModeForwardBaseForwardAdd) 来说,下表给出了我们可以在 Shader 中访问的光照变量。

名称类型描述
_LightColor0float4该 Pass 处理的逐像素光源的颜色。
_WorldSpaceLightPos0float4_WorldSpaceLightPos0.xyz 是该 Pass 处理逐像素光源的位置。若为平行光,那么 WorldSpaceLightPos0.w 为 0,其它光源类型 w 值为 1。
_LightMatrix0float4x4从世界空间到光源空间的变换矩阵,可以用于采样 cookie 和光源衰减(attenuation)纹理。
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0float4仅用于 Base Pass,前 4 个非重要的点光源在世界空间中的位置。
unity_4LightAtten0float4仅用于 Base Pass,存储了前 4 个非重要的点光源的衰减因子。
unity_LightColorhalf4[4]仅用于 Base Pass,存储了前 4 个非重要的点光源的颜色。

可在前向渲染中可以使用的内置光照函数

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

延迟渲染路径

因解决前向渲染所带来的瓶颈问题而流行起来

原理

除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲也被称为 G 缓冲(G-buffer),其中G是英文Geometry的缩写。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。

延迟渲染主要包含量两个Pass:

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

延迟渲染的伪代码如下:

Pass1{
    // 延迟渲染第一个 Pass 不用于计算光照,而是收集所有的深度信息,法线信息等
    // 在第二个Pass中进行实际的光照计算,因而也被称为延迟渲染。
    for(each primitive in this model){
        for(each fragment covered by this primitve){
            if(failed in depth test){
                // 如果没有通过深度测试,说明该片元是不可见的
                discard;
            }else{
                writeGBuffer(materialInfo,pos,normal,lightDir,viewDir);
                // 如果该片元可见,就把需要的信息存储到G缓冲中去
            }
        }
    }
}
Pass2{
    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();  // 更新帧缓存
        }
    }
}

延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张2D图像,我们的计算实际上就是在这些图像空间中进行的。

Unity 中的延迟渲染

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

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

unity 要求提供两个 Pass

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

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

默认的 G 缓冲区

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

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

可访问的内置变量和函数

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

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

Unity 光源类型

Unity 中提供了4种光源类型:平行光、点光源、聚光灯面光源(area light)。面光源尽在烘焙时才可发挥作用,因此我们不讨它。

光源的五大属性:位置方向颜色强度衰减

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

代码中使用了 Blinn-Phong 光照模型,并定义了 Base PassAdditional Pass 来处理多个光源,原理代码如下:

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" }
        
        // BasePass
        Pass
        {
            // 环境光和第一像素光(方向光) Pass
            Tags { "LightMode" = "ForwardBase" }
            
            CGPROGRAM
            
            // 显然需要添加此声明
            #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);
                // 计算场景中的环境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // _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
            Tags { "LightMode" = "ForwardAdd" }
            // 开启混合模式用于叠加光照
            Blend One One
            
            CGPROGRAM
            
            // 显然需要添加此声明
            #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
                    // 平行光衰减值为1
                    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"
}

上诉代码只是讲述原理,不可用于真正的项目中

渲染顺序

我们知道,如果逐像素光源的数目很多的话,该物体的 Additional Pass 就会被调用多次,影响性能,我们可以通过把光源的 Render Mode 设为 Not Important 来告诉Unity,我们不希望把该光源当成逐像素处理。

Unity 的光照衰减

Unity 中,如果是平行光的话,衰减值为 1.0。如果是其它的光源类型,那么处理更复杂一些。尽管我们可以使用数学表达式来计算给顶点相对于点光源和聚光灯的衰减,但这些计算往往涉及开根号、除法等计算量较大的操作,因此 Unity 选择了使用一张纹理作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。我们首先得到光源空间下的坐标,然后利用该坐标对衰减纹理进行采样得到衰减值。

这样的好处在于,计算衰减不依赖于数学公式的复杂性,我们只要使用一个参数值去纹理中采样即可。但使用纹理查找来计算衰减也有一些弊端:

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

但由于这种方法可以在一定程度上提升性能,而且得到的效果在大部分情况下都是良好的,因此 Unity 默认的就是使用这种纹理查找方式来计算逐像素的点光源和聚光灯的衰减的。

用于光照衰减的纹理

Unity 在内部使用一张叫做 _LightTexture0 的纹理来计算光源的衰减,需要注意的是,如果我们对光源使用了cookie(相当于一张阴影贴纸),那么衰减查找纹理就成了 _LightTextureB0 ,但这不是重点。在这张纹理上,(0,0)点表明了与光源位置重合点的衰减(就是最近点),(1,1)表明了光源涉及到的范围的最远点的衰减。

为了对 _LightTexture0 纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过 _LightMatrix0 变换矩阵得到的。在前面我们已经知道 _LightMatrix0 可以把顶点从世界空间变换到光源空间。因此,我们只需把 _LightMatrix0 和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置。

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

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

fixed atten = tex2D(_LightTexture0, dot(lightCoordlightCoord).rr).UNITY_ATTEN_CHANNEL;

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

使用数学公式计算

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

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

Unity 没有在文档中给出内置衰减计算的说明。

Unity 阴影计算

阴影实现 (shadowmap技术)

在实时渲染中,我们最常使用是一种名为 Shadow Map 的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方,而 Unity 使用的就是这样的技术。 在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity 就会为该光源计算它的 阴影映射纹理(shadowmap) 。这张阴影映射纹理本质上也是一张 深度图,它记录了从该光源位置出发、能看到的场景中距离它最近的表面位置(深度信息)。

距离阴影映射纹理最近的表面位置

在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?

  • 一种方法是,先把摄像机放置到光源位置上,然后按正常的渲染流程,即调用 Base Pass 和 Additional Pass 来更新深度信息,得到阴影映射纹理。

    但这种方法会对性能造成一定的浪费,因为实际上我们仅仅需要深度信息而已,而 Base Pass 和 Additional Pass 往往会涉及很多复杂的光照模型计算。

  • Unity 选择使用一个额外的 Pass 来专门更新光源的阴影映射纹理,这个 Pass 就是 LightMode 标签被设置为 ShadowCaster 的 Pass。这个 Pass 的渲染目标不是帧缓存,而是 阴影映射纹理(或深度纹理)

    Unity 首先把摄像机放置到光源位置上,然后调用该 Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。

    因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的 UnityShader 中找到 LightMode 为 ShadowCaster 的 Pass,如果没有,它就会在 Fallback 指定的 Unity Shader 中继续寻找,如果仍然没有找到,该物体就无法向其它物体投射阴影(但它仍然可以接收来自其它物体的阴影)。当找到一个 LightModeShadowCaster 的 Pass 后,Unity 会使用该 Pass 来更新光源的阴影映射纹理。

    阴影映射纹理实现

    阴影映射纹理实现是如何实现的呢?

    • 在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后我们使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由z分量得到),那么说明该点位于阴影中。
    • 在 Unity5 中,Unity 使用了不同于这种传统的阴影采样技术,即 屏幕空间的阴影映射技术(Screenspace Shadow Map) 。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。

    需要注意的是,并不是所有平台的 Unity 都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持 MRT,而有些移动平台不支持这种特性。

Unity 中阴影映射技术具体实现

当使用了屏幕空间的阴影映射技术时,Unity 首先会通过调用 LightMode 为 ShadowCaster 的 Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。

如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理的深度值,就说明该表面虽然是可见的,但却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。

如果我们想要一个物体接收来自其它物体的阴影,只需要在 Shader 中对阴影图进行采样。由于阴影图是屏幕空间下的,因此我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

小结

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

Unity 中,这个过程是通过为该物体执行 LightMode 为 ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投影映射技术,Unity 还会使用这个 Pass 产生一张摄像机的深度纹理。

不透明物体阴影

为了让场景中可以产生阴影,首先需要让平行光可以收集阴影信息。这需要在光源的 Light 组件中开启阴影。

让物体投射阴影

Unity 中,我们可以选择是否让一个物体投射或接收阴影。通过设置 Mesh Renderer 组件中的 Cast Shadows 和 Receive Shadows 属性来实现的。

Cast Shadows 可以被设置为开启(On)或关闭(Off)。若开启 Cast Shadows 属性,Unity 会把该物体加入光源的阴影映射纹理的计算中,从而让其它物体在对阴影映射纹理采样时可以得到该物体的相关信息,也就是该物体可以向其他物体投射阴影。

Receive Shadows 则可以选择是否让物体接收来自其它物体的阴影。若没有开启 Receive Shadows,那么当我们调用 Unity 的内置宏和变量计算阴影时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。

让物体接受阴影

  1. 我们在Base Pass中包含进一个新的内值文件,用于获取我们后续要使用到的宏:

    #include "AutoLight.cginc"
    
  2. SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION 是计算阴影时的 “三剑客” 。我们可以在 AutoLight.cginc 中找到它们的声明。

在前向渲染中,宏 SHADOW_COORDS 实际上就是声明了一个名为 _ShadowCoord 的阴影纹理坐标变量。而 TRANSFER_SHADOW 的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了 UNITY_NO_SCREENSPACE_SHADOWS 来得到),TRANSFER_SHADOW 会调用内置的 ComputePos 函数来计算 _ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到 _ShadowCoord 中。然后 SHADOW_ATTENUATION 负责使用 _ShadowCoord 对相关纹理进行采样,得到阴影信息。

这些宏会使用上下文变量来进行相关计算,例如 TRANSFER_SHADOW 会使用 v.vertex 或 a.pos 来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2f 结构体中顶点坐标变量名必须是 vertex,顶点着色器的输出结构体 v2f 必须命名为 v,且 v2f 中的顶点位置变量必须命名为 pos。

Shader "Unity Shaders Book/Chapter 9/Shadow"
{
    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" }
        
        Pass
        {
            // 环境光和第一像素光(方向光)Pass
            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;
                
                // 这个宏用于将阴影坐标传递到像素着色器
                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;

                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;
                // 使用宏坐标来采样阴影贴图
                fixed shadow = SHADOW_ATTENUATION(i);
                
                return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
            }
            
            ENDCG
            
        }
        
        Pass
        {
            // 其他像素光源Pass
            Tags { "LightMode" = "ForwardAdd" }
            
            Blend One One
            
            CGPROGRAM
            
            // 显然需要添加此声明
            #pragma multi_compile_fwdadd
            // 使用下面的line为点光源和聚光灯添加阴影
            //			#pragma multi_compile_fwdadd_fullshadows
            
            #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 position: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.position = 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
                    float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                    fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                #endif
                
                return fixed4((diffuse + specular) * atten, 1.0);
            }
            
            ENDCG
            
        }
    }
    FallBack "Specular"
}

统一管理光照和衰减

在前面,我们已经讲过如何在 UnityShader 的前向渲染路径中计算光照衰减——在 Base Pass 中,平行光的衰减因子总是等于1,而在 Additional Pass 中,我们需要判断该 Pass 处理的光源类型,再使用内置变量和宏计算衰减因子。那么是不是有一个方法可以同时计算两个信息呢?好消息是,Unity 在 Shader 里提供了这样的功能,这主要是通过内置的 UNITY_LIGHT_ATTENUATION 宏来实现的。

UNITY_LIGHT_ATTENUATION 是 Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的 AutoLight.cginc 里找到它们的相关声明。它接收3个参数:

  • atten:光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数atten,这是因为 UNITY_LIGHT_ATTENUATION 会帮我们声明这个变量。
  • 结构体 v2f,这个参数会传递给 SHADOW_ATTENUATION ,用来计算阴影值。
  • 世界空间的坐标,正如我们在前面讲的那样,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样得到的光照衰减。
Shader "Unlit/ShadowRecive"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _Specualr("Specualr", Color) = (1,1,1,1)
        _SpecularPower("SpecularPower", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

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

            float4  _Color;
            float4 _Specualr;
            float _SpecularPower;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                SHADOW_COORDS(2)
            };

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

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half NdotV = max(0.0, dot(worldNormal, halfDir));

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;
                half3 specular = _LightColor0.rgb * _Specualr.rgb * pow(NdotV, _SpecularPower);

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }

        Pass
        {
            Tags {"LightMode"="ForwardAdd"}

            Blend One One

            CGPROGRAM

            #pragma multi_compile_fwdadd
            #pragma vertex vert
            #pragma fragment frag

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

            float4  _Color;
            float4 _Specualr;
            float _SpecularPower;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                SHADOW_COORDS(2)
            };

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

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half NdotV = max(0.0, dot(worldNormal, halfDir));

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                half3 diffuse = _LightColor0.rgb * _Color.rgb * NdotL;
                half3 specular = _LightColor0.rgb * _Specualr.rgb * pow(NdotV, _SpecularPower);

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4((diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

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

如果我们希望可以在 Additional Pass 中添加阴影效果,就需要使用 #pragma multi_compile_fwdadd_fullshadows 编译指令来代替 Additional Pass 中的 #pragma multi_compile_fwdadd 指令。这样一来,Unity 也会为这些额外的逐像素光源计算阴影,并传递给 Shader。

透明物体的阴影

透明度测试的阴影

透明度测试的处理比较简单,但如果我们仍然直接使用 VertexLitDiffuseSpecular 等做为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而 Transparent/Cutout/VertexLit 中的阴影投射纹理并没有进行这样的操作。

我们采用和透明度测试时同样的 Shader 代码进行改造:

  1. 添加需要的头文件:

    #include "Lighting.cginc"
    #include "AutoLight.cginc"
    
  2. 在 v2f 中使用内置宏 SHADOW_COORDS 声明阴影纹理坐标:

    注意到,由于我们已经占用了3个插值寄存器(使用 TEXCOORD0TEXCOORD1TEXCOORD2 修饰的变量),因此 SHADOW_COORDS 传入的参数是3,这意味着阴影纹理坐标将占用第四个插值寄存器 TEXCOORD3

    struct v2f{
        float4 pos:SV_POSITION;
        float3 worldNormal:TEXCOORD0;
        float3 worldPos:TEXCOORD1;
        float2 uv:TEXCOORD2;
        SHADOW_COORDS(3)
    };
    
  3. 在顶点着色器中使用内值宏 TRANSFER_SHADOW 计算阴影纹理坐标后传递给片元着色器

  4. 在片元着色器中,使用内置宏 UNITY_LIGHT_ATTENUATION 计算阴影和光照衰减

  5. 这次,我们更改它的 Fallback,使用 Transparent/Cutout/VertexLit 作为它的回调 Shader

Shader "Unlit/TransparentVetexLit"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Color("Color", Color) = (1,1,1,1)
        _Specular("Specular Color", Color) = (1,1,1,1)
        _SpecualrPower("Specualr Power", Range(8.0, 256)) = 20
        _Cutoff("CutOff", Range(0.0, 1)) = 0.2
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

            Cull Off

            CGPROGRAM

            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float4 _Specular;
            float _SpecualrPower;
            float _Cutoff;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                SHADOW_COORDS(3)
            };

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

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half3 NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half3 NdotV = max(0.0, dot(worldNormal, halfDir));

                half4 albedo = tex2D(_MainTex, i.uv);
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb;
                half3 diffuse = _LightColor0.rgb * albedo.rgb * _Color.rgb * NdotL;
                half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecualrPower);

                clip(albedo.a - _Cutoff);
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }

        Pass
        {
            Tags {"LightMode"="ForwardAdd"}

            Cull Off
            Blend One One

            CGPROGRAM

            #pragma multi_compile_fwdadd
            #pragma vertex vert
            #pragma fragment frag

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float4 _Specular;
            float _SpecualrPower;
            float _Cutoff;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                SHADOW_COORDS(3)
            };

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

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half3 NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half3 NdotV = max(0.0, dot(worldNormal, halfDir));

                half4 albedo = tex2D(_MainTex, i.uv);
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb;
                half3 diffuse = _LightColor0.rgb * albedo.rgb * _Color.rgb * NdotL;
                half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecualrPower);

                clip(albedo.a - _Cutoff);
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                return half4((diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }
    }
    // Fallback "VertexLit"
    Fallback "Transparent/Cutout/VertexLit"
}

透明度混合的阴影

事实上,所有内置的透明度混合的 Unity Shader,如 Transparent/VertexLit 等,都没有包含阴影投射的 Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接受来自其它物体的阴影 。

由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些透明半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且会影响性能。因此,在 Unity 中,所有内置的半透明 Shader 是不会产生任何阴影效果的。

强制阴影效果

我们可以使用一些 dirty trick 来强制为半透明物体生成阴影,这可以通过把它们的 Fallback 设置为 VertexLit、Diffuse 这些不透明物体使用的 UnityShader,这样 Unity 就会在它的 Fallback 找到一个阴影投射的Pass,然后我们可以通过物体的 Mesh Render 组件上的 Cast ShadowsReceive 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);
                
                // 传递阴影坐标到像素着色器
                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)不仅可以计算衰减,还可以计算阴影信息
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                
                return fixed4(ambient + diffuse * atten, texColor.a * _AlphaScale);
            }
            
            ENDCG
            
        }
    }
    FallBack "Transparent/VertexLit"
    // 或用force应用阴影
    //	FallBack "VertexLit"
}

可使用的标准光照着色器

在这里,这个 Shader 包含了对法线纹理、多光源、光照衰减和阴影的相关处理。

Shader "Unlit/DiffuseBump"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Color("Color", Color) = (1,1,1,1)
        _BumpTex("BumpTex", 2D) = "white" {}
        _Specular("Specular", Color) = (1,1,1,1)
        _SpecualrPower("Specualr Power", Range(8.0, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

            CGPROGRAM

            // Base Pass 平行光逐像素渲染
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

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

            
            sampler2D _MainTex;
            float4 _MainTex_ST;           
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            float4 _Color;
            float4 _Specular;
            float _SpecualrPower;

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
                SHADOW_COORDS(4)         // 声明一个光源的阴影映射纹理
            };


            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 应用 _MainTex 的缩放和平移
                o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw; // 应用 _BumpTex 的缩放和平移

                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;   // 顶点位置转到世界空间
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);    // 顶点法线转到世界空间
                half3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 顶点切线转到世界空间
                half3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 计算得到世界空间下的顶点副切线

                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                TRANSFER_SHADOW(o);  // 计算声明过的阴影映射纹理
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldPos = half3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);    // 世界空间下的坐标
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); // 世界空间下的光源方向
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));   // 世界空间下的视角方向
                half3 halfDir = normalize(worldLightDir + worldViewDir);            // 半角方向

                half3 bump = UnpackNormal(tex2D(_BumpTex, i.uv.zw));                // 采样法线贴图
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));// 法线坐标从切线空间转到世界空间

                half NdotL = max(0.0, dot(bump, worldLightDir));  // lambert
                half NdotV = max(0.0, dot(bump, halfDir));        // blinn-phong

                half4 albedo = tex2D(_MainTex, i.uv.xy) * _Color; // 采样常规纹理,计算漫反射系数
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb; // 计算环境光
                half3 diffuse = _LightColor0.rgb * albedo.rgb * NdotL;     // 计算漫反射光
                half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecualrPower); // 计算高光

                UNITY_LIGHT_ATTENUATION(atten, i, worldPos);      // 计算光照衰减和阴影
                return half4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }

         Pass
        {
            Tags {"LightMode"="ForwardAdd"}

            CGPROGRAM

            // addtional Pass 逐像素渲染
            #pragma multi_compile_fwdadd
            #pragma vertex vert
            #pragma fragment frag

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

            
            sampler2D _MainTex;
            float4 _MainTex_ST;           
            sampler2D _BumpTex;
            float4 _BumpTex_ST;
            float4 _Color;
            float4 _Specular;
            float _SpecualrPower;

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 TtoW0 : TEXCOORD1;
                float4 TtoW1 : TEXCOORD2;
                float4 TtoW2 : TEXCOORD3;
                SHADOW_COORDS(4)         // 声明一个光源的阴影映射纹理
            };


            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 应用 _MainTex 的缩放和平移
                o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpTex_ST.zw; // 应用 _BumpTex 的缩放和平移

                half3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;   // 顶点位置转到世界空间
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);    // 顶点法线转到世界空间
                half3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); // 顶点切线转到世界空间
                half3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; // 计算得到世界空间下的顶点副切线

                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

                TRANSFER_SHADOW(o);  // 计算声明过的阴影映射纹理
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldPos = half3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);    // 世界空间下的坐标
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos)); // 世界空间下的光源方向
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));   // 世界空间下的视角方向
                half3 halfDir = normalize(worldLightDir + worldViewDir);            // 半角方向

                half3 bump = UnpackNormal(tex2D(_BumpTex, i.uv.zw));                // 采样法线贴图
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));// 法线坐标从切线空间转到世界空间

                half NdotL = max(0.0, dot(bump, worldLightDir));  // lambert
                half NdotV = max(0.0, dot(bump, halfDir));        // blinn-phong

                half4 albedo = tex2D(_MainTex, i.uv.xy) * _Color; // 采样常规纹理,计算漫反射系数
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb; // 计算环境光
                half3 diffuse = _LightColor0.rgb * albedo.rgb * NdotL;     // 计算漫反射光
                half3 specular = _LightColor0.rgb * _Specular.rgb * pow(NdotV, _SpecualrPower); // 计算高光

                UNITY_LIGHT_ATTENUATION(atten, i, worldPos);      // 计算光照衰减和阴影
                return half4((diffuse + specular) * atten, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

总结

该章主要讲述了前向渲染中,不同类型光源的处理方式,以及 Unity 如何计算光照阴影 (shadowmap 技术),最后又探讨了一下透明物体的阴影计算。

  • shader 中有两个 Pass,Base Pass 逐像素计算最重要的平行光,以及所有逐顶点光源和 SH 光源;Additional Pass 根据权重逐像素计算其它类型的光源,每个光源执行一次 Pass。

  • 计算光照衰减,常用的是纹理查找,也可直接用数学方法进行计算。

    #ifdef USING_DIRECTIONAL_LIGHT
        // 获取平行光的方向
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    #else
        // 通过点光源(或聚光灯)的位置减去世界空间下顶点位置,获得光的方向
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
    #endif
    // 判断当前光源是否是平行光
    #ifdef USING_DIRECTIONAL_LIGHT
        // 平行光衰减值为1
        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