关于SHADOWS_SCREEN

Posted by 恶毒的狗 on December 19, 2019

关于SHADOWS_SCREEN

在读Unity内置shader源码的过程中,我们经常看到这样的代码:

1
2
3
4
5
#if SHADOWS_SCREEN       
    // xxx
#else
    // xxx
#endif

那么,上面代码中的 SHADOWS_SCREEN 代表的是什么呢?

通过仔细阅读代码发现,SHADOWS_SCREEN 代表的是 屏幕空间阴影

URP 管线 主灯的实时阴影 为例,我们看一下相关代码:

主灯影衰减的入口

1
2
3
4
5
6
Light GetMainLight(float4 shadowCoord)
{
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord);
    return light;
}

主灯实时阴影的主要函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
half MainLightRealtimeShadow(float4 shadowCoord)
{
#if !defined(_MAIN_LIGHT_SHADOWS) || defined(_RECEIVE_SHADOWS_OFF)
    return 1.0h;
#endif

#if SHADOWS_SCREEN
    return SampleScreenSpaceShadowmap(shadowCoord);
#else
    ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
    half4 shadowParams = GetMainLightShadowParams();
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false);
#endif
}

这里我们看到,如果定义了 SHADOWS_SCREEN,那么会走 屏幕空间阴影 的采样流程,否则,走 ShadowMap 的采样流程。

SHADOWS_SCREEN的定义

那么 SHADOWS_SCREEN 在什么情况下会被定义呢?我们继续看代码:

1
2
3
4
5
6
7
#ifndef SHADOWS_SCREEN
    #if defined(_MAIN_LIGHT_SHADOWS) && defined(_MAIN_LIGHT_SHADOWS_CASCADE) && !defined(SHADER_API_GLES)
        #define SHADOWS_SCREEN 1
    #else
        #define SHADOWS_SCREEN 0
    #endif
#endif

这里是否采用 屏幕空间阴影,主要取决于是否开启了 阴影级联

对于大部分使用 SHADER_API_GLESAndroid 设备来说,也会关闭 屏幕空间阴影


开启屏幕空间阴影的渲染流程

Unity针对 GLES 设备关闭 屏幕空间阴影,说明它的渲染开销较高。

屏幕空间阴影 多用于 延迟渲染,不过 URP 还是一个 前向渲染 的流程,那么我们就来看一下开启 屏幕空间阴影URP 的渲染流程。

以Unity自带的 SampleScene 为例,并且只考虑 一盏主灯 的情况,如下图:

img

这里开了 2级阴影级联ShadowMap 的分辨率是 1024Shadow Distance64

第一步,绘制ShadowMap

因为开了 2级级联,实际创建的 ShadowMap 的分辨率是 1024 x 512,左边的 512 x 512 绘制 0级阴影,右边的 512 x 512 绘制 1级阴影,如下:

img

要写入 ShadowMap,shader必须有 ShadowCaster 这个Pass。

第二步,绘制CameraDepthTexture

要生成 屏幕空间阴影 贴图,我们需要通过 屏幕坐标当前像素的深度 还原出屏幕上一个点的 世界坐标

而要获取 当前像素的深度,我们就必须依靠深度贴图 CameraDepthTexture,如下:

img

要写入 CameraDepthTexture,shader必须有 DepthOnly 这个Pass。

对于 移动设备 来说,如果场景比较复杂,drawcall 会大幅增加,这一步会比较昂贵。

第三步,生成屏幕空间阴影贴图

有了 ShadowMapCameraDepthTexture,就可以生成名为 _ScreenSpaceShadowmapTexture屏幕空间阴影贴图 了:

img

生成的代码见 Shaders/Utils/ScreenSpaceShadows.shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
half4 Fragment(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float deviceDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, input.uv.xy).r;

    #if UNITY_REVERSED_Z
            deviceDepth = 1 - deviceDepth;
    #endif
    deviceDepth = 2 * deviceDepth - 1; //NOTE: Currently must massage depth before computing CS position.

    float3 vpos = ComputeViewSpacePosition(input.uv.zw, deviceDepth, unity_CameraInvProjection);
    float3 wpos = mul(unity_CameraToWorld, float4(vpos, 1)).xyz;

    //Fetch shadow coordinates for cascade.
    float4 coords = TransformWorldToShadowCoord(wpos);

    // Screenspace shadowmap is only used for directional lights which use orthogonal projection.
    ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
    half4 shadowParams = GetMainLightShadowParams();
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), coords, shadowSamplingData, shadowParams, false);
}

第四步,渲染场景

有了 屏幕空间阴影贴图,我们就可以根据 屏幕坐标 采样贴图,得到光的 影衰减,最终用于光照计算。

相关代码如下:

顶点着色器:

1
output.ShadowCoords = GetShadowCoord(vertexInput); 
1
2
3
4
5
6
7
8
float4 GetShadowCoord(VertexPositionInputs vertexInput)
{
#if SHADOWS_SCREEN
    return ComputeScreenPos(vertexInput.positionCS);
#else
    return TransformWorldToShadowCoord(vertexInput.positionWS);              
#endif
}
1
2
3
4
5
6
7
float4 ComputeScreenPos(float4 positionCS) 
{               
    float4 o = positionCS * 0.5f;
    o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w;
    o.zw = positionCS.zw;
    return o;
}

像素着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
half MainLightRealtimeShadow(float4 shadowCoord)
{
#if !defined(_MAIN_LIGHT_SHADOWS) || defined(_RECEIVE_SHADOWS_OFF)
    return 1.0h;
#endif

#if SHADOWS_SCREEN
    return SampleScreenSpaceShadowmap(shadowCoord);
#else
    ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
    half4 shadowParams = GetMainLightShadowParams();
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false);
#endif
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
half SampleScreenSpaceShadowmap(float4 shadowCoord)
{
    shadowCoord.xy /= shadowCoord.w;

    // The stereo transform has to happen after the manual perspective divide 
    shadowCoord.xy = UnityStereoTransformScreenSpaceTex(shadowCoord.xy);

#if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
    half attenuation = SAMPLE_TEXTURE2D_ARRAY(_ScreenSpaceShadowmapTexture, sampler_ScreenSpaceShadowmapTexture, shadowCoord.xy, unity_StereoEyeIndex).x;
#else
    half attenuation = SAMPLE_TEXTURE2D(_ScreenSpaceShadowmapTexture, sampler_ScreenSpaceShadowmapTexture, shadowCoord.xy).x;
#endif

    return attenuation;
}

题外话

很多游戏的实时阴影都不是Unity内置的,如今在 URP 开放源码的情况下,我正在考虑把我们游戏的实时阴影切回Unity的内置版本。

去年为了抄 塞尔达,我们在移动设备搞过 全场景的实时阴影

  • 2级级联
  • 70米实时影
  • PCF2X2采样

当时基于一个叫 Sunshine 的Unity插件做了大量优化,在我的 小米MIX2 上还是非常流畅的,怀念一下:

img