从零开始分解UnityShader阴影,从AutoLight到PCF,从百草园杀到三味书屋

先从基础讲起:

如果你研究过Unity阴影的实现方式,那么肯定能翻到所有文章都提到那个“阴影三剑客”的感念,即:

#include "AutoLight.cginc"
...
struct v2f
            {
                ...
                SHADOW_COORDS(3)
            };
v2f vert (appdata v)
            {
                ...
                TRANSFER_SHADOW(o);
                ...
            }
fixed4 frag (v2f i) : SV_Target
            {
                fixed shadow = SHADOW_ATTENUATION(i);//无衰减
                or
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);//有衰减
            }

其中呢SHADOW_ATTENUATION(i)获取的阴影是不包含衰减的,在阴影距离之外会变成纯黑,UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)包含衰减效果,但是需要提供i.worldPos世界坐标。

但如果新人只是按照这三剑客抄上,还是不会出现阴影效果,原因是还需要声明前向渲染:

#pragma multi_compile_fwdbase

这样虽然出现阴影了,但是阴影会在不同视角出现闪烁,原因是缺少Tags

Tags{“LightMode”=”Forwardbase”}

以上全部添加完毕之后,才能获得一个正常的阴影。

虽然这个阴影仍然是AutoLight.cginc帮你实现的,但是至少能添加到自己的Shader里面了,所以,到这里算是初级篇幅结束啦~

 

那么下面再讲一些中级手法:

如何摆脱AutoLight.cginc的依赖自己实现阴影效果呢?脱离AutoLight.cginc的好处是,我们可以自己任意对阴影效果做一些修改。

如果你对着色器语言熟悉,可以自己看阴影三剑客调用的cg代码,Unity内置的所有Shader源码可以从官网上下载到。

三剑客引用的方法可以在AutoLight.cginc内找到,理解代码并不难,难点在于Unity为我们定义了不同平台和硬件下的处理方法,使用了大量的宏定义,我们需要找到我们需要的那条分支。

首先是SHADOW_COORDS(3),没什么深奥的,就是定义了一个TEXCOORD寄存器。

我们可以自己实现:

float4 _ShadowCoord : TEXCOORD3;

然后我们需要声明_ShadowMapTexture,其中保存的是灯光视口的深度贴图,由渲染管线赋值。

不同平台声明阴影贴图的方式不同,为了方便我们可以直接使用UnityCG.cginc内的

UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);

在顶点片元vert中我们为之前声明的阴影寄存器_ShadowCoord赋值,以替代TRANSFER_SHADOW(o)方法。

o._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );

这里需要着重讲一下这个_ShadowCoord的意义,此处是先把顶点转到世界空间然后乘阴影矩阵,得出的xy结果是顶点在ShadowMap上的UV坐标,而z是与灯光的距离,也就是深度值。记住这里,看下面的像素片元会用到,忘了的话再回来看这段。

如果我们使用阴影衰减,那么还需要保留顶点的世界坐标,这一步其实在上面计算ShadowCoord的时候已经计算过了,实际运用的时候可以选择先计算世界坐标。

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

然后顶点片元的准备工作就结束了,接下来我们可以直接去像素片元去读取阴影贴图了。

大家如果研究过屏幕空间阴影的实现,应该知道其原理是用摄影机渲染像素的坐标转到灯光阴影矩阵空间计算出与灯光的距离,因为灯光只能照亮一个表面,如果像素距离灯光的距离比灯光看到的第一个表面的距离更远,则像素肯定被离灯光更近的表面遮住的光线,也就是处于阴影之中。

所以我们只需要使用_ShadowCoord.xy去采样_ShadowMapTexture,然后用_ShadowCoord.z来对比这个深度值即可。

代码如下:

float dist = tex2D(_ShadowMapTexture, _ShadowCoord.xy).r;
return 1 - max(dist > _ShadowCoord.z, _LightShadowData.x);

理解了屏幕空间阴影的话,上面的代码也就不难理解,其中的_LightShadowData.x是烘培系统Lightmap的Shadowmask阴影,在这里可以混在实时阴影中,如果自己想添加固有阴影之类的,也可以在这里进行添加。至于为什么是1-,因为shadowmap对比后还有shadowmask中,都是1为阴影,0为非阴影的~ 所以结果需要反转一下。

需要提一下的是,上面的结果为了我们理解屏幕空间阴影的核心原理,其实我们也可以使用UnityCG.cginc中的方法简单实现:

return UNITY_SAMPLE_SHADOW(_ShadowMapTexture,_ShadowColor.xyz);

(注意本文为了快速理解所以只提到Shader顶部的#include,有些方法并非直接存在于该文件,比如UNITY_SAMPLE_SHADOW这个方法其实定义在HLSLSupport.cginc中)

至此,阴影我们已经拿到啦,接下来怎么用全凭自己安排。

但是就像我们在基础篇提到的问题,这个阴影是没有衰减的,在游戏中我们可以看到阴影距离之外仍然是黑色,所以这个方法相当于AutoLight.cginc的SHADOW_ATTENUATION()。

那么如何实现带衰减的UNITY_LIGHT_ATTENUATION呢?首先我们看一下AutoLight.cginc内这部分衰减代码的实现:

float zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(worldPos, zDist);
half  realtimeToBakedShadowFade = UnityComputeShadowFade(fadeDist);

其中UnityComputeShadowFadeDistance仍然是AutoLight.cginc的方法,所以我们继续拆解,最终还原的代码如下:

float zDist = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
 //float fadeDist = UnityComputeShadowFadeDistance(i.worldPos, zDist);
float sphereDist = distance(i.worldPos, unity_ShadowFadeCenterAndType.xyz);
float fadeDist = lerp(zDist, sphereDist, unity_ShadowFadeCenterAndType.w);

half  shadowFade = saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
float attan = lerp(shadow,1,shadowFade);

shadow变量是我们之前对比ShadowMap产生的阴影,利用摄影机空间和阴影距离淡出了阴影,并且在淡出范围混入Lightmap阴影信息,实现平滑的阴影交接。

比较完美,虽然效果跟内置阴影别无两样,但我们能做的事情更多了。那么,中级篇到这里就该结束啦~

 

能够提升效果的高级篇:

Unity默认提供了两种阴影效果:

硬阴影

软阴影

这个软阴影其实没有什么特别的手法,只是一个双线性采样而已。在阴影贴图范围比较大的情况下,锯齿都会比较严重。

其它还能够提升的选项是级联阴影,相当于按距离范围做成阴影图集,多几次采样,减少远处Shadowmap的占比,但也仅此而已。

如果抛开Unity默认的阴影方案,能够提升阴影效果的手法其实挺多的,当然也必将付出代价~~

这里我们挑选业界比较常用的PCF阴影来讲解一下。

可能有的人会想,阴影软化不就是对像素做个模糊吗?ShadowMap是一个深度图,深度图本身做模糊只会徒降精度,毫无意义~,那么能做的方案,就只有在采样的时候对比一下周围,看自己是不是在阴影的边界了~

PCF就是基于这个方案实现的,有关于PCF的论文网上挺多,都讲解的比较详细,大家可以自行观摩,大致就是多次采样ShadowMap对比,利用像素采样的覆盖面积计算出权重占比,混合出新的阴影色值,这样在边界的阴影就回平滑淡出,淡出的范围由采样次数决定,所以阴影越软,采样次数越多,性能消耗越高。

那么代码呢?代码呢?其实Unity的UnityShadowLibrary.cginc中有PCF阴影实现代码的,分为PCF3x3、5×5、7×7三个级别,大家可以自行查阅。

利用双线性采样优化,我们可以在5×5的情况下,进行9次采样,每次覆盖4像素,实现共36个像素的软阴影过渡。效果如下:

PCF5x5阴影

在Shadowmap相相同的情况下,实现了较大的阴影质量提升。当然如果Shadowmap锯齿过大,闪烁情况仍然会比较厉害~

OK,今天的节目就到这里了,最后为大家献上实验代码:

Shader "Unlit/UnlitShadow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("ShadowColor", Color) = (0.5,0.5,0.5,1.0)
        [Toggle(_PCFSoftShadow)] _UsePcfShadow("UseSoftShadow", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Tags{"LightMode"="Forwardbase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #pragma multi_compile_fwdbase
            #pragma shader_feature _PCFSoftShadow
            #include "UnityCG.cginc"
            //#include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 worldPos:TEXCOORD1;
                UNITY_FOG_COORDS(2)
                float4 _ShadowCoord : TEXCOORD3;
                //SHADOW_COORDS(3)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _ShadowColor;
            UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
            float4 _ShadowMapTexture_TexelSize;


            float SampleShadowPCF(float3 uvd){

                return UNITY_SAMPLE_SHADOW(_ShadowMapTexture,uvd);
                //float dist = tex2D(_ShadowMapTexture, uvd.xy).r;
                //float lightShadowDataX = _LightShadowData.x;
                //float threshold = uvd.z;
                //return 1 - max(dist > threshold, lightShadowDataX);
            }

            float SampleShadowPCF(float2 uv, float d){
                return SampleShadowPCF(float3(uv,d));
            }

            #ifdef _PCFSoftShadow
            float SampleShadowPCF3x3_4Tap_Fast(float3 uvd){
                float offsetX = _ShadowMapTexture_TexelSize.x * 0.5;
                float offsetY = _ShadowMapTexture_TexelSize.y * 0.5;
                float4 result;
                result.x = SampleShadowPCF(float3(uvd.x - offsetX, uvd.y - offsetY, uvd.z));
                result.y = SampleShadowPCF(float3(uvd.x + offsetX, uvd.y - offsetY, uvd.z));
                result.z = SampleShadowPCF(float3(uvd.x - offsetX, uvd.y + offsetY, uvd.z));
                result.w = SampleShadowPCF(float3(uvd.x + offsetX, uvd.y + offsetY, uvd.z));
                return dot(result,0.25);
            }

            static void GetTent5Weights(float kernelOffset,out float4 weightsA,out float2 weightsB){
                float a = 0.5 - kernelOffset;
                float b = 0.5 + kernelOffset;
                float c = max(0,-kernelOffset);
                float d = max(0,kernelOffset);
                float w1 = a * a * 0.5;
                float w2 = (2 * a + 1) * 0.5;
                float w3 = (2 + a) * (2 + a) * 0.5 - w1 - w2 - c * c;

                float w6 = b * b * 0.5;
                float w5 = (2 * b + 1) * 0.5;
                float w4 = (2 + b) * (2 + b) * 0.5 - w5 - w6 - d * d;

                weightsA = float4(w1,w2,w3,w4);
                weightsB = float2(w5,w6);
            }

            static float2 GetGroupTapUV(float2 groupCenterCoord,float2 weightsX,float2 weightsY){
                float offsetX = weightsX.y / (weightsX.x + weightsX.y);
                float offsetY = weightsY.y / (weightsY.x + weightsY.y);
                float2 coord = groupCenterCoord - 0.5 + float2(offsetX,offsetY);
                return coord * _ShadowMapTexture_TexelSize.xy;
            }

            static void GetTent5GroupWeights(
                float4 weightsXA,float2 weightsXB,
                float4 weightsYA,float2 weightsYB,
                out float3 groupWeightsA,out float3 groupWeightsB,out float3 groupWeightsC){

                groupWeightsA.x = dot(weightsXA.xyxy,weightsYA.xxyy);
                groupWeightsA.y = dot(weightsXA.zwzw,weightsYA.xxyy);
                groupWeightsA.z = dot(weightsXB.xyxy,weightsYA.xxyy);

                groupWeightsB.x = dot(weightsXA.xyxy,weightsYA.zzww);
                groupWeightsB.y = dot(weightsXA.zwzw,weightsYA.zzww);
                groupWeightsB.z = dot(weightsXB.xyxy,weightsYA.zzww);

                groupWeightsC.x = dot(weightsXA.xyxy,weightsYB.xxyy);
                groupWeightsC.y = dot(weightsXA.zwzw,weightsYB.xxyy);
                groupWeightsC.z = dot(weightsXB.xyxy,weightsYB.xxyy);
                float w = dot(groupWeightsA,1) + dot(groupWeightsB,1) + dot(groupWeightsC,1);
                float iw = rcp(w);
                groupWeightsA *= iw;
                groupWeightsB *= iw;
                groupWeightsC *= iw;
            }

            float SampleShadowPCF5x5_9Tap(float3 uvd){
                float2 texelCoord = _ShadowMapTexture_TexelSize.zw * uvd.xy;
                float2 texelOriginal = round(texelCoord);
                float2 kernelOffset = texelCoord - texelOriginal;

                float4 weightsXA,weightsYA;
                float2 weightsXB,weightsYB;

                GetTent5Weights(kernelOffset.x,weightsXA,weightsXB);
                GetTent5Weights(kernelOffset.y,weightsYA,weightsYB);

                float2 uv0 = GetGroupTapUV(texelOriginal + float2(-2,-2),weightsXA.xy,weightsYA.xy);
                float2 uv1 = GetGroupTapUV(texelOriginal + float2(0,-2),weightsXA.zw,weightsYA.xy);
                float2 uv2 = GetGroupTapUV(texelOriginal + float2(2,-2),weightsXB.xy,weightsYA.xy);

                float2 uv3 = GetGroupTapUV(texelOriginal + float2(-2,0),weightsXA.xy,weightsYA.zw);
                float2 uv4 = GetGroupTapUV(texelOriginal + float2(0,0),weightsXA.zw,weightsYA.zw);
                float2 uv5 = GetGroupTapUV(texelOriginal + float2(2,0),weightsXB.xy,weightsYA.zw);

                float2 uv6 = GetGroupTapUV(texelOriginal + float2(-2,2),weightsXA.xy,weightsYB.xy);
                float2 uv7 = GetGroupTapUV(texelOriginal + float2(0,2),weightsXA.zw,weightsYB.xy);
                float2 uv8 = GetGroupTapUV(texelOriginal + float2(2,2),weightsXB.xy,weightsYB.xy);

                float3 groupWeightsA,groupWeightsB,groupWeightsC;
                ///5x5的TentFilter,对应9个Group
                GetTent5GroupWeights(weightsXA,weightsXB,weightsYA,weightsYB,groupWeightsA,groupWeightsB,groupWeightsC);

                float3 tapA,tapB,tapC;
                float d = uvd.z;

                tapA.x = SampleShadowPCF(uv0,d);
                tapA.y = SampleShadowPCF(uv1,d);
                tapA.z = SampleShadowPCF(uv2,d);

                tapB.x = SampleShadowPCF(uv3,d);
                tapB.y = SampleShadowPCF(uv4,d);
                tapB.z = SampleShadowPCF(uv5,d);

                tapC.x = SampleShadowPCF(uv6,d);
                tapC.y = SampleShadowPCF(uv7,d);
                tapC.z = SampleShadowPCF(uv8,d);

                return dot(tapA,groupWeightsA) + dot(tapB,groupWeightsB) + dot(tapC,groupWeightsC);
            }
            #endif

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
                //TRANSFER_SHADOW(o);
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                //fixed shadow = SHADOW_ATTENUATION(i);
                //UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                #ifdef _PCFSoftShadow
                float shadow = SampleShadowPCF5x5_9Tap(i._ShadowCoord.xyz);
                #else
                float shadow = SampleShadowPCF(i._ShadowCoord.xyz);
                #endif

                float zDist = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
                //float fadeDist = UnityComputeShadowFadeDistance(i.worldPos, zDist);
                float sphereDist = distance(i.worldPos, unity_ShadowFadeCenterAndType.xyz);
                float fadeDist = lerp(zDist, sphereDist, unity_ShadowFadeCenterAndType.w);

                half  shadowFade = saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
                float attan = lerp(shadow,1,shadowFade);
                col = lerp(col * _ShadowColor, col, attan);

                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }

        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"
            struct v2f
            {
                V2F_SHADOW_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
        }
    }
}