Unity中的阴影设置

Qulity面板

  • ​ ShadowmaskMode设置分为Shadowmask和DistanceShadowMask,如果设置为DistanceShadowMask将会采用在ShadowDistance内使用实时阴影取代光照贴图,避免阴影贴图中的阴影无法影响动态物体,当然这会有性能上的消耗,最好通过LightProbe解决这一问题
  • ​ Shadows设置分为DisabledShadows,HardShadowOnly和HardAndSoftShadows,字面意思就是无阴影,只支持硬阴影和都支持,测试下来无阴影确实不会产生,但是HardShadowOnly和HardAndSoftShadows都能产生软阴影
  • ​ Shadow Resolution设置分为Low、Medium、High 和 Very High,这会影响到ShadowMap的分辨率,分辨率越高,阴影质量越好,性能开销也越大
  • ​ Shadow Projection设置分为CloseFit和StableFit, Close Fit 可以渲染出更高分辨率的阴影,但如果摄像机移动,它们有时会略微抖动 。Stable Fit 渲染较低分辨率的阴影,但它们不会因摄像机移动而抖动,测试下来CloseFit会拉伸ShadowMap从而最大程度利用每一个纹素,而StableFit不会进行拉伸,会有很多纹素被浪费,但是阴影表现稳定
  • ​ ShadowDistance是阴影可以看到的最大距离,只有符合距离要求的物体才可能进入ShadowMap的渲染队列,实际测试下来这个距离不是相机和物体的距离,而是物体在相机forward方向上的投影距离,并且对于不在相机的视锥体的物体,如果投影距离达到要求,在光源方向到视锥体的这一块区域的物体也会产生阴影计算,但是在光源方向上视锥体以下的这部分投影距离达到要求的物体并不产生阴影计算,个人猜测这部分这样设计的原因是,天上飞过一只我们看不见的鸟,但也需要它的阴影表现,但是地下的物体就不要了
  • ​ ShadowNearPlaneOffest是光源裁剪空间近裁剪面的偏移,需要这个偏移的原因是,大三角形可能处于近裁面上造成阴影效果出现问题,对于这个问题在测试的时候没有遇到,不过这个偏移只影响了近裁面,对于ShadowMap中的阴影质量不会影响,假设我们最终光源空间的远近裁面距离是3,如果这边设置了3,距离会变成6这样ShadowMap本来是0-3数值压缩成0-1变成了0-6数值压缩成0-1,距离的精确度受到了影响,不过对于阴影图,一般精确度都是非常足够的,对于阴影的判断不大可能产生阴影,对于阴影的质量是完全不影响的
  • ​ Shadow Cascades可以设置级联阴影贴图的个数,个数越多阴影质量越好,但是性能消耗也越大

级联阴影贴图

​ 对于摄像机来说,由于透视投影有近大远小的显示效果,但对于ShadowMap,它的渲染主要依据光源方向并且是正交投影,对于近处和远处的物体的分辨率是一样的,这就造成了接近摄像机的物体的多个片元在阴影贴图中是对一个纹素进行采样,形成了锯齿

​ 对于这个问题可以通过级联阴影解决,级联阴影的办法是将视锥体按照距离进行切分,不同区域的视锥体使用不同的ShadowMap,分辨率是相同的,近端的ShadowMap覆盖的区域会更小,从而提高了ShadowMap的利用率,进而提高阴影的精确度。当然该技术由于渲染了多张ShadowMap,会增加性能消耗,对于移动平台,该功能是禁用的


光源面板

Mode设置分为Baked、Mixed、Realtime,Baked只用于烘焙光照贴图,Realtime只用于实施光照,Mixed两者都会生效

ShadowType设置分别NoShadow、HardShadow、SoftShadow,NoShadow就是不产生阴影计算,SoftShadow相对HardShadow锯齿感更低

Strength是阴影的强度,1->0是从完全黑到没阴影渐变

Resolution就是ShadowMap的分辨率,和Quality是一样的,选怎UseQualitySetting就是按那边设置的来

Bias是计算深度的偏移,这个我在GAMES202第三课的笔记有详细讲过,由于ShadowMap是分段存储深度而实际深度是连续的,不加任何偏移会造成阴影判断的误差,使得不该产生阴影的地方有很多黑纹

NormalBias和Bias一样是深度的偏移,但不一样的是,NormalBias主要作用于法线和光源方向接近垂直的片元,这些片元需要一个和角度相关的深度偏移进行深度修正,此外,NormalBias和Bias会在ShadowMap时就产生影响

Near Plane是设置光源的阴影最小计算距离


物体面板

Cast Shadows设置物体是否投射阴影

Receive Shadows设置物体是否接受阴影,即显示阴影


Unity中的阴影解决方案

传统方案

​ 对于移动平台的阴影,Unity通常采用传统方案,传统方案是先通过视锥体和光源位置方向信息计算出记录光源空间中物体的深度值,生成ShadowMap,在渲染时将片元转换到光源空间,比对光源空间的距离和ShadowMap的深度值进行阴影判断

屏幕空间阴影

​ 对于PC平台的阴影,Unity通常采用屏幕空间阴影,其主要流程如下

  1. 首先得到从当前摄像机处观察到的深度纹理。在延迟渲染里这张深度图本来就有,如果是前向渲染的话就需要把场景整个渲染一遍,把深度渲染到深度图中。

  2. 然后再从光源出发得到从该光源处观察到的深度纹理,也被称为这个光源的ShadowMap

  3. 然后在屏幕空间做一次阴影收集计算(Shadows Collector),这次计算会得到一张屏幕空间阴影纹理,也就是说这张图里面需要有阴影的部分已经显示在图上了。这个过程概括来说就是把每一个像素根据它在摄像机深度纹理中的深度值得到世界空间坐标,再把它的坐标从世界空间转换到光源空间中,和光源的ShadowMap里面的深度值对比,如果大于ShadowMap中的深度距离,那么就说明光源无法照到,在阴影内。

  4. 最后,在正常渲染物体为它计算阴影的时候,只需要按照当前处理的fragment在屏幕空间中的位置对步骤3得到的屏幕空间阴影图采样就可以了。

    从流程来看,屏幕空间阴影相比传统方案需要多渲染一张相机深度和一张屏幕空间阴影纹理,性能消耗会更大,但是对于OverDraw很高的情况,屏幕空间阴影的阴影判断计算会比传统空间快很多,因此PC平台上才会使用屏幕空间阴影,移动端很少会有屏幕空间阴影效率高于传统方案的情况。


ShadowMap的计算

平行光

平行光的光源空间包围盒是个立方体,由四个面加远近裁面围成,通过正交投影生成ShadowMap,那这6个面是如何确定的呢,以下是我在传统阴影方式,CloseFit模式下经过测试获得的一些结论,无奈找不到源码,并不一定100%准确

​ 我们可以看到Unity渲染序列中Shadow.RendorJobDir是影响ShadowMap中深度值的物体,RenderForward.RenderLoopJob是最终渲染在屏幕空间的物体

​ 首先,我们可以通过摄像机的视锥体和ShadowDistance,可以确认Shadows.RendorJobDir中应该包含哪些物体(具体规则参考Quality面板设置的说明)

​ 然后,我们可以通过摄像机的视锥体确认RenderForward.RenderLoopJob中包含的物体

​ 这两者的交集和光源方向以及摄像机视锥体在经过ShadowDistance裁剪的边界会决定包围盒的的大小和位置,先会用四个于光源方向平行的面包围住物体,然后用2个和光源方向垂直的面(近远裁面)包围,形成最终包围盒

​ 但是这还没有结束,值得注意的是,在近裁面上方不在包围盒内的物体也会影响ShadowMap,ShadowMap会记录它的深度信息,但它不会影响包围盒

​ 此外,Quaity面板设置的ShadowNearPlaneOffest会在计算深度值的时候,会改变近裁面,而Bias是会提高物体的深度,这些都会影响所有要计算深度值的物体(包括上面说的不在包围盒的物体),但它们也不会影响包围盒

​ 还有一点是,Unity深度存储时按照从近到远1->0计算的,这一点在深度图那篇有说过,而最终深度的计算就是从偏移后的近裁剪面到远裁剪面从1->0的变化

​ 最终由于四个面围成的形状大概率不是正方形,在CloseFit模式下会拉伸成正方形来最大化利用所有像素点

​ 依据以上结论,如下图,假设ABCDEF都开启阴影相关功能,则Shadow.RendorJobDir中会有BCDE,RenderForward.RenderLoopJob会有DEF,而两者的交集DE和光源方向将会决定包围盒的6个面,C由于在DE形成的包围盒的四个面的上方,它不会改变包围盒的近远裁面,但是会在ShadowMap上有深度信息,如果它的深度值是按照偏移后的近裁面算的,如果高于偏移后的近裁剪面,其深度值就是1

点光源

后续再补充

聚光灯

后续再补充


阴影采样

在Unity中我们通过ShadowCaster这个Pass来标记需要进行投影的物体,对于需要接受阴影的物体,我们可以通过Unity提供的宏定义来获取到片元像素的阴影值

        Tags { "LightMode" = "ForwardBase" }
        ...
        #pragma multi_compile_fwdbase
        #include "AutoLight.cginc"
        ..,
        struct appdata
        {
            fixed4 vertex : POSITION;
            ...
        };
        struct v2f
        {
            fixed4 pos : SV_POSITION;
            SHADOW_COORDS(1)
            ...
        };
        v2f vert (appdata v)
        {
            v2f o;
            ...
            TRANSFER_SHADOW(o);
            return o;
        }
        FRAG_OUT_TYPE frag(v2f i) : SV_Target
        {
            /*compute shading col*/
            fixed atten = SHADOW_ATTENUATION(i);
            col = col * atten;
        }
        ENDCG

需要注意的是这边的vertex、pos都是必须要有且名称不能变

如果要实现多光源阴影,需要在Add中进行宏调用,方式基本相同,就是要加一句#pragma multi_compile_fwdadd_fullshadows

那么问题来了Unity到底在这个些宏里面做了啥?软阴影是如何实现的?带着疑问需要对Unity的内建Shader进行探究


平行光

1
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;

可以看到SHADOW_COORDS就是简单申明了一个存储阴影坐标的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
#if defined(SHADOWS_NATIVE)
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
return shadow;
#else
unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}

#else // UNITY_NO_SCREENSPACE_SHADOWS
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}

#endif

可以看到TRANSFER_SHADOW会根据阴影实现的方式有不同处理,如果是传统方式则将顶点坐标转到光源空间,如果是屏幕空间则会计算屏幕空间的坐标

1
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

​ 最后是SHADOW_ATTENUATION,发现就是调用了unitySampleShadow,而Unity又按阴影实现方式有区分,如果是传统方式并且Unity_Sample_Shadow可用则直接取出并和_LightShadowData.r(其实就是1-strength)进行插值后获得shadow值,如果不可以则比较采样值和实际深度判断是否为阴影,如果使用屏幕空间阴影则直接采样屏幕空间阴影贴图获得shadow值

点光源

后续再补充

聚光灯

后续再补充


软阴影实现

​ 可以看到,平行光是直接对阴影贴图进行单词采样获得阴影值,那么平行光的阴影软化是如何进行的呢?测试后发现平行光在选择不同的阴影类型是,ShadowMap是会发生变化的,所有是对ShadowMap进行了处理,具体的处理方式猜想是对原始的ShadowMap进行了类似双线性插值的处理,实现了简单的模糊效果,并且几乎没有性能消耗

​ 对于点光源和聚光灯,他们的采样调用的是UnitySampleShadowmap ,其中就有软阴影和硬阴影的不同的采样处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
inline fixed UnitySampleShadowmap (float4 shadowCoord)
{
#if defined (SHADOWS_SOFT)

half shadow = 1;

// No hardware comparison sampler (ie some mobile + xbox360) : simple 4 tap PCF
#if !defined (SHADOWS_NATIVE)
float3 coord = shadowCoord.xyz / shadowCoord.w;
float4 shadowVals;
shadowVals.x = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[0].xy);
shadowVals.y = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[1].xy);
shadowVals.z = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[2].xy);
shadowVals.w = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[3].xy);
half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f;
shadow = dot(shadows, 0.25f);
#else
// Mobile with comparison sampler : 4-tap linear comparison filter
#if defined(SHADER_API_MOBILE)
float3 coord = shadowCoord.xyz / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadow = dot(shadows, 0.25f);
// Everything else
#else
float3 coord = shadowCoord.xyz / shadowCoord.w;
float3 receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coord, 1.0f);
shadow = UnitySampleShadowmap_PCF3x3(float4(coord, 1), receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#endif
#else
// 1-tap shadows
#if defined (SHADOWS_NATIVE)
half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord);
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#else
half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)) < (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0;
#endif

#endif

return shadow;

}

可以看到,如果是硬阴影,和平行光一样进行单词采样,但如果是软阴影,在移动平台,会进行四次采样并取平均,如果是PC平台则会调用UnitySampleShadowmap_PCF3x3进行采样

UnitySampleShadowmap_PCF3x3实际就是UnitySampleShadowmap_PCF3x3Tent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
half UnitySampleShadowmap_PCF3x3Tent(float4 coord, float3 receiverPlaneDepthBias)
{
half shadow = 1;

#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED

#ifndef SHADOWS_NATIVE
// when we don't have hardware PCF sampling, fallback to a simple 3x3 sampling with averaged results.
return UnitySampleShadowmap_PCF3x3NoHardwareSupport(coord, receiverPlaneDepthBias);
#endif

// tent base is 3x3 base thus covering from 9 to 12 texels, thus we need 4 bilinear PCF fetches
float2 tentCenterInTexelSpace = coord.xy * _ShadowMapTexture_TexelSize.zw;
float2 centerOfFetchesInTexelSpace = floor(tentCenterInTexelSpace + 0.5);
float2 offsetFromTentCenterToCenterOfFetches = tentCenterInTexelSpace - centerOfFetchesInTexelSpace;

// find the weight of each texel based
float4 texelsWeightsU, texelsWeightsV;
_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.x, texelsWeightsU);
_UnityInternalGetWeightPerTexel_3TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.y, texelsWeightsV);

// each fetch will cover a group of 2x2 texels, the weight of each group is the sum of the weights of the texels
float2 fetchesWeightsU = texelsWeightsU.xz + texelsWeightsU.yw;
float2 fetchesWeightsV = texelsWeightsV.xz + texelsWeightsV.yw;

// move the PCF bilinear fetches to respect texels weights
float2 fetchesOffsetsU = texelsWeightsU.yw / fetchesWeightsU.xy + float2(-1.5,0.5);
float2 fetchesOffsetsV = texelsWeightsV.yw / fetchesWeightsV.xy + float2(-1.5,0.5);
fetchesOffsetsU *= _ShadowMapTexture_TexelSize.xx;
fetchesOffsetsV *= _ShadowMapTexture_TexelSize.yy;

// fetch !
float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;
shadow = fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));

#endif

return shadow;

}

​ 而UnitySampleShadowmap_PCF3x3Tent在没有硬件支持的情况下就是进行了9个点的采样,如果有硬件支持则会采样一种加速算法,以4个点的采样达到近似9个点的采样效果从而提升效率

​ 还可以看到Unity定义了UnitySampleShadowmap_PCF5x5Tent、UnitySampleShadowmap_PCF7x7Tent,就就是25和49个采样点的算法,在内建材质中应用于Internal-ScreenSpaceShadows.shader,看到名字就知道是屏幕空间阴影的用来渲染屏幕空间阴影纹理的

1
2
3
4
5
6
7
8
9
10
11
#if defined(SHADER_API_MOBILE)

half shadow = UnitySampleShadowmap_PCF5x5(coord, receiverPlaneDepthBias);

#else

half shadow = UnitySampleShadowmap_PCF7x7(coord, receiverPlaneDepthBias);

#endif

shadow = lerp(_LightShadowData.r, 1.0f, shadow);

也就是说,屏幕空间阴影材质在构建的时候就依据机型的不同进行了PCF采样,软化了阴影

总的来说,软阴影的采样会依据设备类型按照不同的方式进行采样,尽量利用硬件特性,以尽量低的性能消耗来达到尽可能好的阴影效果


提高阴影质量方案

了解了上述的知识点后就能对阴影质量进行优化了

牺牲性能

  1. Quality设置中Shadow Resolution,提高ShadowMap分辨率
  2. Quality设置Shadow Cascades,提高近处的阴影效果(仅PC端)
  3. 使用PCF,增加采样点来提高阴影的软化效果,降低锯齿感

空手套白狼

​ 在项目中,我们对性能有较高需求,尤其是移动端,那我们如果在几乎没有性能消耗的情况下提高阴影质量呢?

设置合理ShadowDistance:ShadowDistance越大,越有可能把不需要参与阴影计算的物体包围进来,从而导致ShadowMap的像素利用率降低,因此需要根据场景个功能的具体情况对该值进行调整

减少阴影计算:项目中那些不参与阴影计算的物体都把阴影投射和接受选项关闭,这样包围盒就不容易把不需要参与阴影计算的物体包含进来,提高了ShadowMap像素利用率