【转载】URP 源码阅读笔记:MainLightShadowCasterPass

631 阅读8分钟

原文链接

URP源码阅读笔记:MainLightShadowCasterPass | 威武武武

正文

级联阴影原理:

CSM阴影技术讲解-腾讯游戏学院​gameinstitute.qq.com/community/detail/117522

一、为什么使用级联阴影

Unity 内置的方向光实时阴影技术是 Cascaded Shadow Mapping(简称 CSM)。这是因为使用基本的 Shadow mapping 在大场景下存在阴影图 精度问题

精度问题

  • 主相机整个视锥生成一张阴影图,会导致单个物体占用阴影图的比例太小,读取阴影图时出现采样精度不够(多个像素采样一个图素),产生锯齿。使用尺寸更大的阴影图可以改善这个问题,但会导致 内存使用增加
  • 相机近端和远端物体对阴影图采样精度一样,会导致近端物体采样 精度不够,远端物体采样 精度浪费

解决方法

级联阴影贴图(Cascaded Shadow Maps)是 解决 此问题的方法。这个想法是阴影投射器被渲染了不止一次,因此每个光在图集中会得到多个图块,称为级联(Cascaded )。第一个级联仅覆盖靠近相机的一小部分区域,而连续的级联会 缩小 以覆盖越来越大的具有相同像素数量的区域。然后,着色器对每个片段可用的最佳级联进行采样。

Unity 的阴影代码每个定向光最多支持 四个 级联:

image.png

二、具体使用情况

FrameDebugger:

  • FrameDebugger 中的 MainShadowMap

  • 主光源阴影 image.png

如果将 mainLightShadows 的代码注释掉:

image.png

则 FrameDebugger 结果如下:

  • 没有 MainShadowMap image.png

此时场景内也没有主光源阴影了:

  • 物体没有阴影 image.png

在 ForwardRenderer 的 Data 面板可以配置级联阴影的层级,有 024 三个选项:

image.png

选择 4 级级联阴影时 FrameDebugger 结果如下:

  • 四级级联阴影 image.png

选择二级级联阴影时,FrameDebugger 结果如下:

  • 二级级联阴影 image.png

三、代码分析

变量

        const int k_MaxCascades = 4; //最大阴影级联级数
        const int k_ShadowmapBufferBits = 16; // DepthBuffer位数
        int m_ShadowmapWidth; //ShadowMap宽
        int m_ShadowmapHeight; //ShadowMap高
        int m_ShadowCasterCascadesCount; //配置的阴影级联数
        bool m_SupportsBoxFilterForShadows; //时候支持BoxFilter(Mobile和Switch支持)

        RenderTargetHandle m_MainLightShadowmap; //只是用来存储"_MainLightShadowmapTexture"这个id
        RenderTexture m_MainLightShadowmapTexture; //渲染的目标贴图

        Matrix4x4[] m_MainLightShadowMatrices; //用来存储shadowTransform然后赋值给GPU中的"_MainLightWorldToShadow"
        ShadowSliceData[] m_CascadeSlices; //存储各级阴影的ShadowSliceData数据
        Vector4[] m_CascadeSplitDistances; //存储各级阴影的剔除球数据

ShadowSliceData 中存放的是各级阴影的 矩阵偏移分辨率 等数据:

    public struct ShadowSliceData
    {
        public Matrix4x4 viewMatrix;
        public Matrix4x4 projectionMatrix;
        public Matrix4x4 shadowTransform;
        public int offsetX;
        public int offsetY;
        public int resolution;

        public void Clear()
        {
            viewMatrix = Matrix4x4.identity;
            projectionMatrix = Matrix4x4.identity;
            shadowTransform = Matrix4x4.identity;
            offsetX = offsetY = 0;
            resolution = 1024;
        }
    }

方法

1. MainLightShadowCasterPass.Setup 方法

MainLightShadowCasterPass.Setup 方法返回为 true 的时候就会进行 MainLightShadowCasterPass 的渲染:

  • Setup 方法返回 true,则会进行 MainLightShadowCasterPass 渲染 image.png image.png

如果配置中不支持主光源阴影或者没有主光源,或者可见的光中,设置的阴影类型是 None,都不会进行主光源阴影的渲染:

除此之外,可见光最终可能不会影响任何投射阴影的对象,这可能是因为它们没有配置,或者是因为光线仅影响了超出最大阴影距离的对象。我们可以通过在剔除结果上调用 GetShadowCasterBounds 以获得可见光索引来进行检查。它具有边界的第二个输出参数(我们不需要),并返回边界是否有效。如果不是,则没有阴影可渲染,因此应将其忽略:

image.png

最后按级联 Index 进行了遍历,调用了ShadowUtils.ExtractDirectionalLightMatrix 方法:

这里面主要调用了 cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives 方法:

  • ExtractDirectionalLightMatrix 方法

Api 文档:

CullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives

阴影贴图的 原理 是,我们从灯光的角度渲染场景,只存储深度信息。用结果标记出,光线在击中某物之前会传播多远。但是,定向光被假定为无限远,没有真实位置。因此,我们要做的是找出与灯光方向匹配的 视图投影矩阵,并为我们提供一个剪辑空间立方体,该立方体与包含可见光阴影的摄像机可见区域重叠。这个不用自己去实现,我们可以使用 culling results 的 ComputeDirectionalShadowMatricesAndCullingPrimitives 方法为我们完成此工作,并为其传递 9 个参数。

第一个 参数是可见光指数。接下来的 三个参数 是两个整数和一个Vector3,它们控制阴影级联。稍后我们将处理级联,因此现在使用零,一和零向量。然后是纹理尺寸,我们需要使用平铺尺寸。第六个 参数是靠近平面的阴影,我们现在将其忽略并将其设置为零。

这些是输入参数,其余三个 是输出参数。首先是视图矩阵,然后是投影矩阵,最后一个参数是 ShadowSplitData 结构。

cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives 返回的 bool 值就是表示这项转换能否成功完成。

因为每个级联都需要其自己的变换矩阵,所以需要根据每个级联 Index 进行遍历计算。

2. MainLightShadowCasterPass.Configure

创建了一个临时的 Texture,并将其设置为 Target。

3. MainLightShadowCasterPass.Execute

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            RenderMainLightCascadeShadowmap(ref context, ref renderingData.cullResults, ref renderingData.lightData, ref renderingData.shadowData);
        }

RenderMainLightCascadeShadowmap 用来完成MainLightCascadeShadowmap Texture 的绘制工作:

void RenderMainLightCascadeShadowmap(ref ScriptableRenderContext context, ref CullingResults cullResults, ref LightData lightData, ref ShadowData shadowData)
        {
            int shadowLightIndex = lightData.mainLightIndex;
            if (shadowLightIndex == -1)
                return;

            VisibleLight shadowLight = lightData.visibleLights[shadowLightIndex];

            CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
            using (new ProfilingScope(cmd, m_ProfilingSampler))
            {
                var settings = new ShadowDrawingSettings(cullResults, shadowLightIndex);

                for (int cascadeIndex = 0; cascadeIndex < m_ShadowCasterCascadesCount; ++cascadeIndex)
                {
                    var splitData = settings.splitData;
                    splitData.cullingSphere = m_CascadeSplitDistances[cascadeIndex];
                    settings.splitData = splitData;
                    Vector4 shadowBias = ShadowUtils.GetShadowBias(ref shadowLight, shadowLightIndex, ref shadowData, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].resolution);
                    ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias);
                    ShadowUtils.RenderShadowSlice(cmd, ref context, ref m_CascadeSlices[cascadeIndex],
                        ref settings, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].viewMatrix);
                }

                bool softShadows = shadowLight.light.shadows == LightShadows.Soft && shadowData.supportsSoftShadows;
                CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.MainLightShadows, true);
                CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.MainLightShadowCascades, shadowData.mainLightShadowCascadesCount > 1);
                CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.SoftShadows, softShadows);

                SetupMainLightShadowReceiverConstants(cmd, shadowLight, shadowData.supportsSoftShadows);
            }

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

RenderMainLightCascadeShadowmap 方法中,首先调用 ShadowUtils.GetShadowBias 方法获取 shadow 的 bias 值。之所以需要 bias,是为了减轻阴影痤疮的现象。具体的可以参考下面的文章:

LearnOpenGL-CN —— 高级光照 » 阴影 » 阴影映射

然后调用 ShadowUtils.SetupShadowCasterConstantBuffer 进行 shader 参数的配置。最后调用了 关键 方法 ShadowUtils.RenderShadowSlice

ShadowUtils.RenderShadowSlice 是真正进行 MainLightCascadeShadowmap 绘制的方法。

流程如下:

  • 计算该级联阴影级别在 Atlas 对应的 Viewport
  • 通过 CommandBuffer 设置好 ViewPort 和 View&Project 矩阵
  • 绘制 ShadowMap 到 Atlas 上

4.球形剔除

Unity 通过为其创建一个选择球来 确定每个级联覆盖的区域。由于阴影投影是正交的且呈正方形,因此它们最终会紧密契合其剔除球,但还会覆盖周围的一些空间。这就是为什么可以在剔除区域之外看到一些阴影的原因。同样,光的方向与球无关,因此所有定向光最终都使用相同的剔除球。

  • 使用透明的球体来让剔除球可视化 image.png

还需要这些球体来确定从哪个级联进行采样,因此我们需要将它们发送到GPU。为级联计数和级联的剔除球体数组添加一个标识符,并为球体数据添加一个数组。它们由四分量矢量定义,包含其 XYZ 位置及其在 W 分量中的半径。

MainLightShadowCasterPass 中使用 m_CascadeSplitDistances 来存储剔除球信息。

  • 存储剔除球信息 image.png

跟踪代码可以发现,级联的剔除球是ComputeDirectionalShadowMatricesAndCullingPrimitives 输出的拆分数据的一部分。

ComputeDirectionalShadowMatricesAndCullingPrimitives 计算剔除球数据

m_CascadeSplitDistances 的数据计算好后,会在 RenderMainLightCascadeShadowmap 时将数据传递给 ShadowDrawingSettings,用于 context.DrawShadows 时使用。

  • 将剔除球信息传递给 ShadowDrawingSettings,用于绘制 shadow

RenderMainLightCascadeShadowmap 方法的最后会调用 SetupMainLightShadowReceiverConstants 方法,用于将计算所得的各种数据传递给 GPU:

void SetupMainLightShadowReceiverConstants(CommandBuffer cmd, VisibleLight shadowLight, bool supportsSoftShadows)
        {
            Light light = shadowLight.light;
            bool softShadows = shadowLight.light.shadows == LightShadows.Soft && supportsSoftShadows;

            int cascadeCount = m_ShadowCasterCascadesCount;
            for (int i = 0; i < cascadeCount; ++i)
                m_MainLightShadowMatrices[i] = m_CascadeSlices[i].shadowTransform;

            // We setup and additional a no-op WorldToShadow matrix in the last index
            // because the ComputeCascadeIndex function in Shadows.hlsl can return an index
            // out of bounds. (position not inside any cascade) and we want to avoid branching
            Matrix4x4 noOpShadowMatrix = Matrix4x4.zero;
            noOpShadowMatrix.m22 = (SystemInfo.usesReversedZBuffer) ? 1.0f : 0.0f;
            for (int i = cascadeCount; i <= k_MaxCascades; ++i)
                m_MainLightShadowMatrices[i] = noOpShadowMatrix;

            float invShadowAtlasWidth = 1.0f / m_ShadowmapWidth;
            float invShadowAtlasHeight = 1.0f / m_ShadowmapHeight;
            float invHalfShadowAtlasWidth = 0.5f * invShadowAtlasWidth;
            float invHalfShadowAtlasHeight = 0.5f * invShadowAtlasHeight;
            float softShadowsProp = softShadows ? 1.0f : 0.0f;
            cmd.SetGlobalTexture(m_MainLightShadowmap.id, m_MainLightShadowmapTexture);
            cmd.SetGlobalMatrixArray(MainLightShadowConstantBuffer._WorldToShadow, m_MainLightShadowMatrices);
            cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowParams, new Vector4(light.shadowStrength, softShadowsProp, 0.0f, 0.0f));

            if (m_ShadowCasterCascadesCount > 1)
            {
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._CascadeShadowSplitSpheres0,
                    m_CascadeSplitDistances[0]);
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._CascadeShadowSplitSpheres1,
                    m_CascadeSplitDistances[1]);
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._CascadeShadowSplitSpheres2,
                    m_CascadeSplitDistances[2]);
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._CascadeShadowSplitSpheres3,
                    m_CascadeSplitDistances[3]);
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._CascadeShadowSplitSphereRadii, new Vector4(
                    m_CascadeSplitDistances[0].w * m_CascadeSplitDistances[0].w,
                    m_CascadeSplitDistances[1].w * m_CascadeSplitDistances[1].w,
                    m_CascadeSplitDistances[2].w * m_CascadeSplitDistances[2].w,
                    m_CascadeSplitDistances[3].w * m_CascadeSplitDistances[3].w));
            }

            // Inside shader soft shadows are controlled through global keyword.
            // If any additional light has soft shadows it will force soft shadows on main light too.
            // As it is not trivial finding out which additional light has soft shadows, we will pass main light properties if soft shadows are supported.
            // This workaround will be removed once we will support soft shadows per light.
            if (supportsSoftShadows)
            {
                if (m_SupportsBoxFilterForShadows)
                {
                    cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowOffset0,
                        new Vector4(-invHalfShadowAtlasWidth, -invHalfShadowAtlasHeight, 0.0f, 0.0f));
                    cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowOffset1,
                        new Vector4(invHalfShadowAtlasWidth, -invHalfShadowAtlasHeight, 0.0f, 0.0f));
                    cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowOffset2,
                        new Vector4(-invHalfShadowAtlasWidth, invHalfShadowAtlasHeight, 0.0f, 0.0f));
                    cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowOffset3,
                        new Vector4(invHalfShadowAtlasWidth, invHalfShadowAtlasHeight, 0.0f, 0.0f));
                }

                // Currently only used when !SHADER_API_MOBILE but risky to not set them as it's generic
                // enough so custom shaders might use it.
                cmd.SetGlobalVector(MainLightShadowConstantBuffer._ShadowmapSize, new Vector4(invShadowAtlasWidth,
                    invShadowAtlasHeight,
                    m_ShadowmapWidth, m_ShadowmapHeight));
            }
        }

总结一下渲染流程:

  • Setup() 方法判断是否需要绘制阴影,以及调用 ShadowUtils.ExtractDirectionalLightMatrix 进行各级阴影数据的计算。
  • Configure() 方法配置渲染 TargetClear 的参数。
  • Execute() 方法调用 RenderMainLightCascadeShadowmap() 方法,用于阴影贴图的绘制。其中 最核心 的绘制方法是 ShadowUtils.RenderShadowSlice。绘制结束后会调用 SetupMainLightShadowReceiverConstants 方法将一些数据传递给 GPU,用于阴影贴图的采样。

四、MainLightShadow 的采样

  1. 先看一下 Shadow.hlsl 中定义的变量,他们存储了 MainLightShadowPass 中传递给 GPU 的数据:

  2. MainLightShadow 的采样从 LitForwardPass 的 LitPassFragment 阶段开始,其中会调用 InitializeInputData 方法:image.png 这个方法位于 Shadow.hlsl 中,里面主要是调用 ComputeCascadeIndex 方法来计算级联 index

  3. ComputeCascadeIndex 的代码 InitializeInputData 方法计算除了 InputData.shadowCoord,然后调用 UniversalFragmentPBR 方法进行光照计算:

  4. UniversalFragmentPBR 方法 UniversalFragmentPBR 中使用 InputData.shadowCoord 的是 GetMainLight 方法:

  5. GetMainLight 方法 image.pngGetMainLight 方法会调用 MainLightRealtimeShadow 方法,来采样 _MainLightShadowmapTexture

  6. MainLightRealtimeShadow 方法

  7. SampleShadowmap 方法:

real SampleShadowmap(TEXTURE2D_SHADOW_PARAM(ShadowMap, sampler_ShadowMap), float4 shadowCoord, ShadowSamplingData samplingData, half4 shadowParams, bool isPerspectiveProjection = true)
{
    // Compiler will optimize this branch away as long as isPerspectiveProjection is known at compile time
    if (isPerspectiveProjection)
        shadowCoord.xyz /= shadowCoord.w;

    real attenuation;
    real shadowStrength = shadowParams.x;

    // TODO: We could branch on if this light has soft shadows (shadowParams.y) to save perf on some platforms.
#ifdef _SHADOWS_SOFT
    attenuation = SampleShadowmapFiltered(TEXTURE2D_SHADOW_ARGS(ShadowMap, sampler_ShadowMap), shadowCoord, samplingData);
#else
    // 1-tap hardware comparison
    attenuation = SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz);
#endif

    attenuation = LerpWhiteTo(attenuation, shadowStrength);

    // Shadow coords that fall out of the light frustum volume must always return attenuation 1.0
    // TODO: We could use branch here to save some perf on some platforms.
    return BEYOND_SHADOW_FAR(shadowCoord) ? 1.0 : attenuation;
}
  1. SampleShadowmapFiltered 方法:
real SampleShadowmapFiltered(TEXTURE2D_SHADOW_PARAM(ShadowMap, sampler_ShadowMap), float4 shadowCoord, ShadowSamplingData samplingData)
{
    real attenuation;

#if defined(SHADER_API_MOBILE) || defined(SHADER_API_SWITCH)
    // 4-tap hardware comparison
    real4 attenuation4;
    attenuation4.x = SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz + samplingData.shadowOffset0.xyz);
    attenuation4.y = SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz + samplingData.shadowOffset1.xyz);
    attenuation4.z = SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz + samplingData.shadowOffset2.xyz);
    attenuation4.w = SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, shadowCoord.xyz + samplingData.shadowOffset3.xyz);
    attenuation = dot(attenuation4, 0.25);
#else
    float fetchesWeights[9];
    float2 fetchesUV[9];
    SampleShadow_ComputeSamples_Tent_5x5(samplingData.shadowmapSize, shadowCoord.xy, fetchesWeights, fetchesUV);

    attenuation = fetchesWeights[0] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[0].xy, shadowCoord.z));
    attenuation += fetchesWeights[1] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[1].xy, shadowCoord.z));
    attenuation += fetchesWeights[2] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[2].xy, shadowCoord.z));
    attenuation += fetchesWeights[3] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[3].xy, shadowCoord.z));
    attenuation += fetchesWeights[4] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[4].xy, shadowCoord.z));
    attenuation += fetchesWeights[5] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[5].xy, shadowCoord.z));
    attenuation += fetchesWeights[6] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[6].xy, shadowCoord.z));
    attenuation += fetchesWeights[7] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[7].xy, shadowCoord.z));
    attenuation += fetchesWeights[8] * SAMPLE_TEXTURE2D_SHADOW(ShadowMap, sampler_ShadowMap, float3(fetchesUV[8].xy, shadowCoord.z));
#endif

    return attenuation;
}
  1. SAMPLE_TEXTURE2D_SHADOW 宏定义:
#define SAMPLE_TEXTURE2D_SHADOW(textureName, samplerName, coord3)  textureName.SampleCmpLevelZero(samplerName, (coord3).xy, (coord3).z)