我们之前编写的基于Lambert、Phong、BlinnPhong光照模型的Shader

存在一个问题🙋

那就是: ** 不能接受阴影**

左图: 自己编写的Shader

右图: 官方内建的Shader

阴影接受效果对比

所以为了解决这个问题,我们要在自己的着色器当中实现ShadowCaster

关于不同渲染路径下的Shadow

Unity支持多种类型的渲染路径,在5.0版本之前,主要有3中渲染路径

分别为正向渲染,延迟渲染以及顶点照明渲染

在5.0之后,Unity抛弃了顶点照明渲染路径,并用新的延迟渲染路径来替代了旧的延迟渲染路径

Unity中的正向渲染

  • Unity中的正向渲染有3中处理光照的方式

  • 分别为逐顶点处理,逐像素处理以及球谐函数处理

  • 而决定哪一个光源按照使用那种处理模式取决于光源的类型和渲染模式

  • 光源的类型指的是该光源是平行光或者其他类型的光

  • 渲染模式则指的是该光源是否被指定为重要光源

“Unity中的正向渲染效果”

  • 在正向渲染中一共包含两种Pass,Base Pass和Additional Pass

  • Base Pass只会被执行一次,用于计算光照纹理,环境光,自发光,阴影等效果

  • 而Additional Pass则能够被执行多次,并且默认情况下不支持阴影,但是能够通过使用宏指令来开启阴影

  • Additional Pass会根据影响物体的其他逐像素光源数目被多次调用

Unity中的顶点照明渲染

  • 顶点照明渲染是对硬件配置要求最少,运算性能最高,

  • 同时效果也是最差的一种渲染路径。它可以被视为正向渲染的一个子集,

“顶点照明渲染”

  • 因为它只能通过逐顶点的方式去计算光照,这也就意味着我们无法使用一些逐像素光照变量。

  • 顶点照明渲染在5.0以后的版本中作为一种被遗留的渲染路径存在,将来也有被移除的可能。

Unity中的延迟渲染

  • Unity中的新旧延迟渲染系统的差别非常小,只是是用来不同的技术来权衡不同的需求。

“延迟渲染效果”

– 其中包含两个Pass,第一个Pass用于渲染G缓冲,在这个Pass中会把物体的漫反射颜色,高光反射颜色,平滑度,法线,自发光和深度等信息渲染到屏幕空间的G缓冲中,对于每个物体来说这个Pass只会被执行一次。

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

这里要注意的是,如果被接受物体的Shader是Legacy的Diffuse那么在Forward中,是只能接受到Directional Light的阴影的,如果要让其支持其他类型光源的阴影,需要修改渲染路径为延迟渲染

还需要注意是渲染路径为顶点照明Legacy Vertex Lit的情况下,片元着色器是无法正常工作的

在新的Pass通道中渲染Shadow

通过Unity的官方文档,可以知道 在新的Pass通道中使用内置渲染管线中的预定义通道标记Tags,为LightMode 增加ShadowCaster

使对象深度渲染到阴影贴图或深度纹理中.

        Pass{
            Tags{"LightMode"="ShadowCaster"}
        }

“ShadowCaster效果图”

使用FallBack渲染Shadow

可以通过FallBack,当Unity物体需要投射阴影但是没有相关的Pass,就会回落到FallbBack当中

Fallback "Diffuse"

手动实现ShadowCaster

以上方式实现的Shadow存在一个问题,那就是着色器本身的对象,不能接受阴影

如下图所示,左侧的着色器之间并没有投射阴影

效果

Unity的文档当中对一块内容并没有很清晰的描述

通过翻阅AutoLight.cginc可以看到,其中对于阴影投射部分做了很多的宏定义

以旧版本4.x为例,光照以及阴影的宏定义如下

// -----------------------------
//  Light/Shadow helpers (4.x version)
// -----------------------------
// This version computes light coordinates in the vertex shader and passes them to the fragment shader.

// ---- Spot light shadows
#if defined (SHADOWS_DEPTH) && defined (SPOT)
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));
#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#endif

// ---- Point light shadows
#if defined (SHADOWS_CUBE)
#define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord.xyz = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#define READ_SHADOW_COORDS(a) unityShadowCoord4(a._ShadowCoord.xyz, 1.0)
#endif

// ---- Shadows off
#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
#define SHADOW_COORDS(idx1)
#define TRANSFER_SHADOW(a)
#define SHADOW_ATTENUATION(a) 1.0
#define READ_SHADOW_COORDS(a) 0
#else
#ifndef READ_SHADOW_COORDS
#define READ_SHADOW_COORDS(a) a._ShadowCoord
#endif
#endif

#ifdef POINT
#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
#   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
#   define LIGHT_ATTENUATION(a)    (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).r * SHADOW_ATTENUATION(a))
#endif

#ifdef SPOT
#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
#   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));
#   define LIGHT_ATTENUATION(a)    ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif

#ifdef DIRECTIONAL
#   define DECLARE_LIGHT_COORDS(idx)
#   define COMPUTE_LIGHT_COORDS(a)
#   define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif

#ifdef POINT_COOKIE
#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
#   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
#   define LIGHT_ATTENUATION(a)    (tex2D(_LightTextureB0, dot(a._LightCoord,a._LightCoord).rr).r * texCUBE(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif

#ifdef DIRECTIONAL_COOKIE
#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord2 _LightCoord : TEXCOORD##idx;
#   define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
#   define LIGHT_ATTENUATION(a)    (tex2D(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif

#define UNITY_LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) UNITY_SHADOW_COORDS(idx2)
#define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)
#define UNITY_TRANSFER_LIGHTING(a, coord) COMPUTE_LIGHT_COORDS(a) UNITY_TRANSFER_SHADOW(a, coord)
#define TRANSFER_VERTEX_TO_FRAGMENT(a) COMPUTE_LIGHT_COORDS(a) TRANSFER_SHADOW(a)

#endif

UNITY_LIGHTING_COORDS 为宏定义的阴影纹理

TRANSFER_VERTEX_TO_FRAGMENT 将阴影的纹理从顶点传到片元程序

LIGHT_ATTENUATION 光衰减,其中包含了阴影衰减部分,与最终返回的颜色rgb值相乘可以得到正确的阴影投射部分的颜色

完整代码⬇️

Shader "ShaderLearning/ShadowCaster/VertexShadowBlinnPhongShader"
{
    Properties
    {
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _SpecularColor ("SpecularColor", Color) = (0.5,0.5,0.5,1)
        _Shineness("Shineness",Range(1,32))=8
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex: POSITION1;
                LIGHTING_COORDS(1,2)
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            float _Shineness;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.normal=v.normal;
                o.vertex=v.vertex;
                TRANSFER_VERTEX_TO_FRAGMENT(o)

                return o;
            }

            fixed4 frag (v2f i) : COLOR
            {
                //环境光颜色 
                fixed4 col=UNITY_LIGHTMODEL_AMBIENT;
                //模型坐标系下的法线转位世界坐标系下
                float3 N=UnityObjectToWorldNormal(i.normal);
                //获取世界坐标系下的光照方向
                float3 L=normalize(WorldSpaceLightDir(i.vertex));
                //计算漫反射
                float diffuse=saturate(dot(L,N)); 
                //计算摄像机观察视角向量
                float3 V=normalize(WorldSpaceViewDir(i.vertex));
                //计算镜面高光,使用半角向量
                float specular=pow(saturate(dot(N,normalize(L+V))),_Shineness);
                //叠加上漫反射与镜面高光的颜色乘上光照颜色
                col+= _LightColor0*((_MainColor*diffuse)+(_SpecularColor*specular));
                //计算世界坐标系下的顶点位置 
                float3 wpos=mul(unity_ObjectToWorld,i.vertex).xyz;
                //计算4个点光源颜色
                col.rgb+=Shade4PointLights(unity_4LightPosX0,
                unity_4LightPosY0,
                unity_4LightPosZ0,
                unity_LightColor[0].rgb,unity_LightColor[1].rgb,
                unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                unity_4LightAtten0,wpos,N);

                float atten = LIGHT_ATTENUATION(i);

                col.rgb*=atten;

                return col;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

阴影投射效果

通过使用AutoLight.cginc当中的宏,实现了阴影的投射

但是对于点光源的阴影,并没有得到正确的结果

实现Forward Add 通道中点光源阴影的支持

在标准的Forward前向渲染当中,unity中分为两种不同的光照模式通道

ForwardBase

主要应用于环境光,平行光,它使用标准照明函数计算一个,并通过将它们近似为球谐函数来计算额外的定向光

ForwardAdd

主要应用附加的像素灯,以及聚光灯,每个灯一个通道

注意ForwardBase和ForwardAdd之前采用Blend混合片段输出的颜色

混合方程为:

finalValue = sourceFactor * sourceValue operation destinationFactor * destinationValue

在这个等式中:

  • finalValue是 GPU 写入目标缓冲区的值。
  • sourceFactor在 Blend 命令中定义。
  • sourceValue是片段着色器输出的值。
  • operation是混合操作。
  • destinationFactor在 Blend 命令中定义。
  • destinationValue是目标缓冲区中已有的值。

所以在ForwardAdd Pass当中片段输出的一句放在了目标缓冲区中,在ForwardAdd Pass中使用Blend命令混合两者的结果

用法 示例语法 功能
Blend <state> Blend Off 禁用默认渲染目标的混合。这是默认值。
Blend <render target> <state> Blend 1 Off 如上所述,但对于给定的渲染目标。(1)
Blend <source factor> <destination factor> Blend One Zero 为默认渲染目标启用混合。设置 RGBA 值的混合因子。
Blend <render target> <source factor> <destination factor> Blend 1 One Zero 如上所述,但对于给定的渲染目标。(1)
Blend <source factor RGB> <destination factor RGB>, <source factor alpha> <destination factor alpha> Blend One Zero, Zero One 启用混合默认渲染目标。为 RGB 和 alpha 值设置单独的混合因子。(2)
Blend <render target> <source factor RGB> <destination factor RGB>, <source factor alpha> <destination factor alpha> Blend 1 One Zero, Zero One 如上所述,但对于给定的渲染目标。(1) (2)

有效参数值

范围 价值 功能
渲染目标 整数,范围为 0 到 7 渲染目标索引。
状态 Off 禁用混合。
因素 One 此输入的值为一。使用它来使用源颜色或目标颜色的值。
Zero 此输入的值为零。使用它来删除源或目标值。
SrcColor GPU 将此输入的值乘以源颜色值。
SrcAlpha GPU 将此输入的值乘以源 alpha 值。
SrcAlphaSaturate source alphaGPU 将此输入的值乘以和的最小值(1 - destination alpha)
DstColor GPU 将此输入的值乘以帧缓冲区源颜色值。
DstAlpha GPU 将此输入的值乘以帧缓冲区源 alpha 值。
OneMinusSrcColor GPU 将此输入的值乘以(1 - 源颜色)。
OneMinusSrcAlpha GPU 将此输入的值乘以(1 - 源 alpha)。
OneMinusDstColor GPU 将此输入的值乘以(1 - 目标颜色)。
OneMinusDstAlpha GPU 将此输入的值乘以(1 - 目标 alpha)。

着色器变体和关键字

在Unity中可以编写着色器代码片段来共享通用代码,但在启用或禁用给定关键字时具有不同功能。

Unity 编译这些着色器代码片段时,它将为已启用和已禁用关键字的不同组合创建单独的着色器程序。

这些各个着色器程序被称为着色器变体。

简而言之就是可以启用或者关闭给定的关键字,来使用各种着色器变体当中的功能,从而实现着色器的效果

预编译指令multi_compile 的工作方式

指令示例:

# pragma multi_compile FANCY_STUFF_OFF FANCY_STUFF_ON

此指令示例生成两个着色器变体:一个定义了 FANCY_STUFF_OFF,另一个定义了 FANCY_STUFF_ON。在运行时,Unity 根据材质或全局着色器关键字来激活其中一个变体。如果这两个关键字均未启用,则 Unity 使用第一个关键字(在此示例中为 FANCY_STUFF_OFF)。

内置 multi_compile 快捷方式

有几个“快捷方式”符号用于编译多个着色器变体。这些变体主要处理 Unity 中的不同光源、阴影和光照贴图类型。请参阅有关渲染管线的文档以了解详细信息。

  • multi_compile_fwdbase 编译 PassType.ForwardBase 所需的所有变体。这些变体处理不同的光照贴图类型以及启用或禁用的方向光主要阴影。
  • multi_compile_fwdadd 编译 PassType.ForwardAdd 的变体。这将编译变体来处理方向光、聚光灯或点光源类型,以及它们带有剪影纹理的变体。
  • multi_compile_fwdadd_fullshadows - 与 multi_compile_fwdadd 相同,但还能够让光源具有实时阴影。
  • multi_compile_fog 扩展为多个变体以处理不同的雾效类型 (off/linear/exp/exp2)。

所以想要实现能够接受点光源的阴影投射,就需要在ForwardAdd通道中 添加预编译指令multi_compile_fwdadd_fullshadows

完整的光照以及阴影投影支持的代码如下:

Shader "ShaderLearning/ShadowCaster/VertexShadowBlinnPhongShader"
{
    Properties
    {
        _MainColor ("MainColor", Color) = (1,1,1,1)
        _SpecularColor ("SpecularColor", Color) = (0.5,0.5,0.5,1)
        _Shineness("Shineness",Range(1,32))=8
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex: POSITION1;
                LIGHTING_COORDS(1,2)
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            float _Shineness;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.normal=v.normal;
                o.vertex=v.vertex;
                TRANSFER_VERTEX_TO_FRAGMENT(o)

                return o;
            }

            fixed4 frag (v2f i) : COLOR
            {
                //环境光颜色 
                fixed4 col=UNITY_LIGHTMODEL_AMBIENT;
                //模型坐标系下的法线转位世界坐标系下
                float3 N=UnityObjectToWorldNormal(i.normal);
                //获取世界坐标系下的光照方向
                float3 L=normalize(WorldSpaceLightDir(i.vertex));
                //计算漫反射
                float diffuse=saturate(dot(L,N)); 
                //计算摄像机观察视角向量
                float3 V=normalize(WorldSpaceViewDir(i.vertex));
                //计算镜面高光,使用半角向量
                float specular=pow(saturate(dot(N,normalize(L+V))),_Shineness);
                //叠加上漫反射与镜面高光的颜色乘上光照颜色
                col+= _LightColor0*((_MainColor*diffuse)+(_SpecularColor*specular));
                //计算世界坐标系下的顶点位置 
                float3 wpos=mul(unity_ObjectToWorld,i.vertex).xyz;
                //计算4个点光源颜色
                col.rgb+=Shade4PointLights(unity_4LightPosX0,
                unity_4LightPosY0,
                unity_4LightPosZ0,
                unity_LightColor[0].rgb,unity_LightColor[1].rgb,
                unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                unity_4LightAtten0,wpos,N);

                float atten = LIGHT_ATTENUATION(i);

                col.rgb*=atten;

                return col;
            }
            ENDCG
        }
        Pass
        {
            Blend One One//使用Blend混合 帧缓冲里的结果 默认方法为Add
            Tags { "LightMode"="ForwardAdd" }// ForwarAdd 附加支持逐像素光以及聚光灯
            CGPROGRAM
            //使用
            #pragma multi_compile_fwdadd_fullshadows
            #pragma vertex vert
            #pragma fragment frag

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : TEXCOORD0;
                float4 vertex: POSITION1;

                LIGHTING_COORDS(1,2)
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            float _Shineness;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.normal=v.normal;
                o.vertex=v.vertex;
                TRANSFER_VERTEX_TO_FRAGMENT(o)

                return o;
            }

            fixed4 frag (v2f i) : COLOR
            {
                //环境光颜色 

                //模型坐标系下的法线转位世界坐标系下
                float3 N=UnityObjectToWorldNormal(i.normal);
                //获取世界坐标系下的光照方向
                float3 L=normalize(WorldSpaceLightDir(i.vertex));
                //计算漫反射
                float diffuse=saturate(dot(L,N)); 
                //计算摄像机观察视角向量
                float3 V=normalize(WorldSpaceViewDir(i.vertex));
                //计算镜面高光,使用半角向量
                float specular=pow(saturate(dot(N,normalize(L+V))),_Shineness);
                //叠加上漫反射与镜面高光的颜色乘上光照颜色
                fixed4 col = _LightColor0*((_MainColor*diffuse)+(_SpecularColor*specular));
                // //计算世界坐标系下的顶点位置 
                float3 wpos=mul(unity_ObjectToWorld,i.vertex).xyz;
                //计算4个点光源颜色
                col.rgb+=Shade4PointLights(unity_4LightPosX0,
                unity_4LightPosY0,
                unity_4LightPosZ0,
                unity_LightColor[0].rgb,unity_LightColor[1].rgb,
                unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                unity_4LightAtten0,wpos,N);

                float atten = LIGHT_ATTENUATION(i);

                col.rgb*=atten;

                return col;
            }
            ENDCG
        }

    }
    Fallback "Diffuse"
}

最终效果图