屏幕空间接触阴影

屏幕空间接触阴影是用来解决普通的阴影贴图精度不够的问题而提出来的一种通过深度图在屏幕空间计算阴影的方法。索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍了往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。

本篇文章相当于是Radial Dispatch系列的第三篇文章了,与上一篇文章一样,这篇文章是基于径向分派Compute Shader中相关算法的实际应用,具体的缓存方式也可以参考上一篇文章使用Group Shared Memory加速径向模糊,这里就不再赘述了。实际上我发现了这样计算接触阴影的一个缺陷,就是不太好计算软阴影了,由于缓存的限制,随机采样只能在一个很小的范围内分布,基本上用不上了。由于使用的是屏幕空间的深度图的信息,加上厚度检测之后很容易出现漏面的问题,封面中的瑕疵也有一部分是来自于我的Relaxed Cone Step Mapping本身深度值的瑕疵,屏幕上半部分的阴影就好很多。这就当作是一个Proof of Concept吧,之后有机会的话再回来优化优化。

本文使用的是Unity 2022.3.21f1,URP版本是14.0.10。

具体的代码

ContactShadowComputeShader.compute

核心的代码来自于前一篇文章径向分派Compute Shader。前一篇文章在循环中是通过统一步长进行采样的,会采样到四个像素中间因此需要双线性插值,这次我们固定水平或者竖直方向的步长为一个像素,这样我们只需要在一个方向上进行线性插值了。由于深度和颜色信息是两种不同的信息,我们仅对距离很近的深度进行线性插值,对于距离较远的两个深度值,我们使用离采样点最近像素的深度值。至于如何判断深度远近,我使用了和屏幕空间反射中相同的_ThicknessParams,默认物体的厚度为linearSampleDepth * _Thickness.y + _Thickness.x

#pragma kernel ContactShadowPoint
#pragma kernel ClearMain
// #pragma warning(disable: 3556)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

#define THREAD_COUNT 128

Texture2D<float4> _ColorTex;
Texture2D<float> _CameraDepthTexture;
RWTexture2D<float4> _RW_TargetTex;

float3 _LightPosWS;
float4 _LightPosCS;
float2 _LightPosSS;
float2 _ThicknessParams;
float4 _TextureSize;
float _LightRange;
float _Debug;

struct DispatchParams
{
    int2 offset;
    int count;
    int stride;
    int xMajor;
};
StructuredBuffer<DispatchParams> _DispatchData;

int GetDispatchType(int index, out int dispatchIndex, out DispatchParams dispatchParams)
{
    for (int i=0; i<8; ++i)
    {
        dispatchParams = _DispatchData[i];
        dispatchIndex = dispatchParams.count - 1 - index;
        if (dispatchIndex >= 0) return i;
    }
    return 0;
}

int2 GetDispatchDirection(int dispatchType, out int2 iLightPosOffset)
{
    dispatchType /= 2;
    int xDir = dispatchType / 2;
    int yDir = dispatchType % 2;
    int2 dir = int2(xDir, yDir);
    iLightPosOffset = dir - 1;
    return dir * 2 - 1;
}

int2 GetDispatchOffset(int dispatchType, int dispatchIndex, DispatchParams dispatchParams, out int groupIndex)
{
    groupIndex = 0;
    int2 dispatchOffset = int2(0, 0);
    int offsetType = dispatchType % 2;
    int colIndexOffset = max(dispatchParams.offset.x,dispatchParams.offset.y)/THREAD_COUNT;
    int2 indexOffset = dispatchParams.xMajor==1?dispatchParams.offset:dispatchParams.offset.yx;
    
    int stride = dispatchParams.stride;
    int colIndex = dispatchIndex / stride;
    int rowIndex = dispatchIndex - colIndex * stride;
    if (offsetType == 0)
    {         
    int offsetedColIndex = colIndex + colIndexOffset;
        int tempIndex = rowIndex + indexOffset.y - (offsetedColIndex + 1) * THREAD_COUNT;
        if (tempIndex >= 0)
        {
            dispatchOffset = int2(tempIndex + indexOffset.x, dispatchParams.stride - (colIndex + colIndexOffset + 1) * THREAD_COUNT + indexOffset.x + indexOffset.y);
            groupIndex = tempIndex;
        }
        else
        {
            dispatchOffset = int2((offsetedColIndex + 1) * THREAD_COUNT - 1, rowIndex + indexOffset.y);
            groupIndex = rowIndex;
        }
    }
    else
    {
        int minOffsetX = max(dispatchParams.stride + indexOffset.y, (colIndexOffset + 1) * THREAD_COUNT);
        dispatchOffset = int2(minOffsetX + colIndex * THREAD_COUNT - 1, rowIndex + indexOffset.y);
        groupIndex = rowIndex;
    }
    if (dispatchParams.xMajor == 0) dispatchOffset.xy = dispatchOffset.yx;
    return dispatchOffset;
}


void GetIndexedOffset(int index, float2 absDir, bool xMajor,
                    out int2 offset1, out int2 offset2, out int2 offset3)
{
    if (!xMajor)   
    {
        absDir = absDir.yx;
    }
    
    float val = float(index) * absDir.y / absDir.x;
    float floorVal = floor(val);
    float fracVal = frac(val);
    if (fracVal <= 0.5f)
    {
        offset1 = int2(index, floorVal - 1.0f);
        offset2 = int2(index, floorVal);
        offset3 = int2(index, floorVal + 1.0f);
    }
    else
    {
        offset1 = int2(index, floorVal);
        offset2 = int2(index, floorVal + 1.0f);
        offset3 = int2(index, floorVal + 2.0f);
    }
    
    if (!xMajor) 
    {
        offset1 = offset1.yx;
        offset2 = offset2.yx;
        offset3 = offset3.yx;
    }
}

float LoadDepthTexture(int2 coord)
{
    // coord.y = int(_TextureSize.y) - 1 - coord.y;
    coord = clamp(coord, int2(0, 0), int2(_TextureSize.xy - 1.0f));
    return _CameraDepthTexture.Load(uint3(coord, 0));
}

struct DepthData
{
    float depth;
    float linearDepth;
};

groupshared DepthData cachedDepth[THREAD_COUNT * 6];
void SetCachedDepth(DepthData depthData, int2 threadPos) {cachedDepth[threadPos.x+threadPos.y*THREAD_COUNT]=depthData;}
DepthData GetCachedDepth(int2 threadPos) {return cachedDepth[threadPos.x+threadPos.y*THREAD_COUNT*2];}
void CacheDepth(int2 groupStartSS, float2 lightPosSS, int cacheIndex)
{
    float2 toLight = lightPosSS - (groupStartSS + 0.5f);
    float2 absDir = abs(toLight);
    int2 signDir = int2(sign(toLight));
    bool xMajor = absDir.x >= absDir.y;

    float depthVal1, depthVal2, depthVal3;
    DepthData depthData1, depthData2, depthData3;
    int2 offset1, offset2, offset3;
    {
        GetIndexedOffset(cacheIndex, absDir, xMajor, offset1, offset2, offset3);
        depthVal1 = LoadDepthTexture(groupStartSS + offset1 * signDir);
        depthVal2 = LoadDepthTexture(groupStartSS + offset2 * signDir);
        depthVal3 = LoadDepthTexture(groupStartSS + offset3 * signDir);
        depthData1.depth = depthVal1;
        depthData1.linearDepth = LinearEyeDepth(depthVal1, _ZBufferParams);
        depthData2.depth = depthVal2;
        depthData2.linearDepth = LinearEyeDepth(depthVal2, _ZBufferParams);
        depthData3.depth = depthVal3;
        depthData3.linearDepth = LinearEyeDepth(depthVal3, _ZBufferParams);
        SetCachedDepth(depthData1, int2(cacheIndex, 0));
        SetCachedDepth(depthData2, int2(cacheIndex, 2));
        SetCachedDepth(depthData3, int2(cacheIndex, 4));
    }

    {
        int extIndex = cacheIndex + THREAD_COUNT;
        GetIndexedOffset(extIndex, absDir, xMajor, offset1, offset2, offset3);
        depthVal1 = LoadDepthTexture(groupStartSS + offset1 * signDir);
        depthVal2 = LoadDepthTexture(groupStartSS + offset2 * signDir);
        depthVal3 = LoadDepthTexture(groupStartSS + offset3 * signDir);
        depthData1.depth = depthVal1;
        depthData1.linearDepth = LinearEyeDepth(depthVal1, _ZBufferParams);
        depthData2.depth = depthVal2;
        depthData2.linearDepth = LinearEyeDepth(depthVal2, _ZBufferParams);
        depthData3.depth = depthVal3;
        depthData3.linearDepth = LinearEyeDepth(depthVal3, _ZBufferParams);
        SetCachedDepth(depthData1, int2(cacheIndex, 1));
        SetCachedDepth(depthData2, int2(cacheIndex, 3));
        SetCachedDepth(depthData3, int2(cacheIndex, 5));
    }
}

[numthreads(1, THREAD_COUNT, 1)]
void ContactShadowPoint(uint3 id : SV_DISPATCHTHREADID)
{
    float2 lightPosSS = _LightPosSS;
    int2 iLightPosSS = int2(floor(lightPosSS + 0.5f));

    int dispatchIndex;
    DispatchParams dispatchParams;
    int dispatchType = GetDispatchType(id.x, dispatchIndex, dispatchParams);
    int2 iLightPosOffset;
    int2 dispatchDirection = GetDispatchDirection(dispatchType, iLightPosOffset);
    int groupIndex;
    int2 dispatchOffset = GetDispatchOffset(dispatchType, dispatchIndex, dispatchParams, groupIndex);
    int2 iGroupStartSS = iLightPosSS + iLightPosOffset + dispatchDirection * dispatchOffset;

    CacheDepth(iGroupStartSS, lightPosSS, id.y);
    GroupMemoryBarrierWithGroupSync();

    float2 toLight = lightPosSS - (float2(iGroupStartSS) + 0.5f);
    float2 absDir = abs(toLight);
    int2 signDir = sign(toLight);
    bool xMajor = absDir.x >= absDir.y;
    float2 absNDir = normalize(absDir);

    float absToLightStepRatio = xMajor ?  absDir.y / absDir.x :  absDir.x / absDir.y;
    int baseOffsetY = int(float(id.y) * absToLightStepRatio + 0.5f);
    int2 iOffset = xMajor ? int2(id.y, baseOffsetY) : int2(baseOffsetY, id.y);
    int2 iPosSS = iGroupStartSS + iOffset * signDir;
    if (any(iPosSS < int2(0, 0)) || any(iPosSS >= int2(_TextureSize.xy))) return;

    float2 posSS = float2(iPosSS) + 0.5f;
    float2 toPosSS = posSS - lightPosSS;
    float2 absToPos = abs(toPosSS);
    float absToPosStepRatio = xMajor ? absToPos.y / absToPos.x :  absToPos.x / absToPos.y;
    int yIntersect = int(float(id.y) * absToPosStepRatio + 0.5f);
    int yVal = baseOffsetY;
    if (yIntersect != yVal) return;

    float lenToPosSS = xMajor ? absToPos.x : absToPos.y;
    DepthData thisDepth = GetCachedDepth(int2(id.y, 1));
    float lightLinearDepth = LinearEyeDepth(_LightPosCS.z / _LightPosCS.w, _ZBufferParams);
    float shadow = 1.0f;

    float3 positionNDC = float3(posSS*_TextureSize.zw*2.0f-1.0f, thisDepth.depth);
    positionNDC.y = -positionNDC.y;
    float4 positionWS = mul(UNITY_MATRIX_I_VP, float4(positionNDC, 1.0f));
    positionWS.xyz /= positionWS.w;
    float lenToLightWS = length(positionWS.xyz - _LightPosWS);
    if (lenToLightWS > _LightRange)
    {
        _RW_TargetTex[iPosSS] = float4(0.0f, 0.0f, 0.0f, 1.0f);
        return;
    }

    for (int i=1; i<THREAD_COUNT; ++i)
    {
        if (i>=lenToPosSS) break;

        float baseOffsetY0 = (i + id.y) * absToLightStepRatio;
        int iBaseStartY0 = int(floor(baseOffsetY0)) + (frac(baseOffsetY0)<=0.5f ? -1 : 0);
        float offsetY0 = i * absToPosStepRatio + baseOffsetY;
        int offsetStartY0 = int(floor(offsetY0));
        int sampleIndex0 = offsetStartY0 - iBaseStartY0;
        DepthData depthData0 = GetCachedDepth(int2(id.y + i, sampleIndex0));
        DepthData depthData1 = GetCachedDepth(int2(id.y + i, sampleIndex0 + 1));
        float weightY0 = frac(offsetY0);

        if (abs(depthData0.linearDepth-depthData1.linearDepth)
            >(min(depthData0.linearDepth, depthData1.linearDepth) * _ThicknessParams.y + _ThicknessParams.x))
        {
            weightY0 = step(0.5f, weightY0);
        }
        float interpolatedLinearDepth = 1.0f / lerp(1.0f/depthData0.linearDepth, 1.0f/depthData1.linearDepth, weightY0);
        float estimatedDepth = 1.0f / lerp(1.0f/thisDepth.linearDepth, 1.0f/lightLinearDepth, clamp(i/lenToPosSS, 0.0f, 1.0f));
        if (estimatedDepth>interpolatedLinearDepth &&
            ((estimatedDepth-interpolatedLinearDepth)<interpolatedLinearDepth*_ThicknessParams.y+_ThicknessParams.x))
        {
            shadow = 0.0f;
            break;
        }
    }

    float3 color = shadow;
    _RW_TargetTex[iPosSS] = float4(color, 1.0f);
}

[numthreads(8, 8, 1)]
void ClearMain(uint3 id : SV_DISPATCHTHREADID)
{
    _RW_TargetTex[id.xy] = 0.0f;
}

ContactShadowRenderPass.cs

和上一篇文章如出一辙,看上去只是把Radial Blur替换成了Contact Shadow,把center替换成了light。这里我们只考虑了点光源的接触阴影,平行光较为简单,所有点的采样方向都是同一个方向,聚光灯和点光源实际上是一样的。

using Unity.Mathematics;

namespace UnityEngine.Rendering.Universal
{
    public class ContactShadowRenderPass : ScriptableRenderPass
    {
        public static Light lightSource;

        private static readonly string passName = "Contact Shadow Render Pass";
        private ScriptableRenderer renderer;
        private ContactShadowRendererFeature.ContactShadowSettings settings;
        private ContactShadow contactShadow;
        private ComputeShader computeShader;
        private Vector2Int textureSize;

        private static readonly string contactShadowTextureName = "_ContactShadowTexture";
        private static readonly int contactShadowTextureID = Shader.PropertyToID(contactShadowTextureName);
        private RTHandle contactShadowTextureHandle;

        private ComputeBuffer computeBuffer;

        private static readonly int THREAD_COUNT = 128;
        private static readonly int DISPATCH_DATA_COUNT = 8;
        private static readonly int DISPATCH_DATA_STRIDE = 5;
        private static readonly int DISPATCH_DATA_SIZE = DISPATCH_DATA_COUNT * DISPATCH_DATA_STRIDE;
        private int[] dispatchData = new int[DISPATCH_DATA_SIZE];

        public ContactShadowRenderPass(ContactShadowRendererFeature.ContactShadowSettings settings)
        {
            this.settings = settings;
            computeShader = settings.computeShader;
            renderPassEvent = settings.renderPassEvent;
            profilingSampler = new ProfilingSampler(passName);
        }

        public void Setup(ScriptableRenderer renderer, ContactShadow contactShadow)
        {
            this.renderer = renderer;
            this.contactShadow = contactShadow;
        }

        private void EnsureComputeBuffer(int count, int stride)
        {
            if (computeBuffer == null || computeBuffer.count != count || computeBuffer.stride != stride)
            {
                if (computeBuffer != null)
                {
                    computeBuffer.Release();
                }
                computeBuffer = new ComputeBuffer(count, stride, ComputeBufferType.Structured);
            }
        }

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            EnsureComputeBuffer(DISPATCH_DATA_COUNT, DISPATCH_DATA_STRIDE * 4);

            RenderTextureDescriptor desc = renderingData.cameraData.cameraTargetDescriptor;
            textureSize = new Vector2Int(desc.width, desc.height);

            desc.enableRandomWrite = true;
            desc.graphicsFormat = Experimental.Rendering.GraphicsFormat.R16G16B16A16_SFloat;
            desc.depthBufferBits = 0;
            desc.msaaSamples = 1;
            desc.useMipMap = false;
            RenderingUtils.ReAllocateIfNeeded(ref contactShadowTextureHandle, desc, FilterMode.Point, TextureWrapMode.Clamp, false, 1, 0, contactShadowTextureName); ;
        }

        private Vector4 GetTextureSizeParameter(Vector2Int textureSize)
        {
            return new Vector4(textureSize.x, textureSize.y, 1.0f / textureSize.x, 1.0f / textureSize.y);
        }

        private struct DispatchParams
        {
            public int2 offset;
            public int count;
            public int stride;
            public int xMajor;
            public DispatchParams(int2 offset, int count, int stride, int xMajor)
            {
                this.offset = offset; this.count = count; this.stride = stride; this.xMajor = xMajor;
            }
        }

        private void GetDispatchParams(int2 coord, int2 offset, out DispatchParams dp1, out DispatchParams dp2)
        {
            int colIndexOffset = math.max(offset.x, offset.y) / THREAD_COUNT;
            int yIndexOffset;
            int minVal, maxVal, xMajor;
            if (coord.x >= coord.y)
            {
                minVal = coord.y;
                maxVal = coord.x;
                yIndexOffset = offset.y;
                xMajor = 1;
            }
            else
            {
                minVal = coord.x;
                maxVal = coord.y;
                yIndexOffset = offset.x;
                xMajor = 0;
            }

            int stride1 = math.max(0, (minVal + colIndexOffset + 1) * THREAD_COUNT - 1 - offset.x - offset.y);
            int count1 = stride1 * math.max(0, minVal - colIndexOffset);
            int stride2 = math.max(0, (minVal + 1) * THREAD_COUNT - yIndexOffset);
            int count2 = stride2 * math.max(0, maxVal - math.max(minVal, colIndexOffset));
            dp1 = new DispatchParams(offset, count1, stride1, xMajor);
            dp2 = new DispatchParams(offset, count2, stride2, xMajor);
        }

        private void GetDispatchList(int2 iCenterPosSS, int2 textureSize, out DispatchParams[] dispatchList)
        {
            int2 offsetLB = math.max(0, iCenterPosSS - textureSize);
            int2 offsetRT = math.max(0, new int2(0, 0) - iCenterPosSS);
            int2 coordLB = (iCenterPosSS + THREAD_COUNT - 1) / THREAD_COUNT;
            int2 coordRT = (textureSize - iCenterPosSS + THREAD_COUNT - 1) / THREAD_COUNT;

            int2 coordRB = new int2(coordRT.x, coordLB.y);
            int2 coordLT = new int2(coordLB.x, coordRT.y);
            int2 offsetRB = new int2(offsetRT.x, offsetLB.y);
            int2 offsetLT = new int2(offsetLB.x, offsetRT.y);

            GetDispatchParams(coordLB, offsetLB, out DispatchParams dpLB1, out DispatchParams dpLB2);
            GetDispatchParams(coordLT, offsetLT, out DispatchParams dpLT1, out DispatchParams dpLT2);
            GetDispatchParams(coordRB, offsetRB, out DispatchParams dpRB1, out DispatchParams dpRB2);
            GetDispatchParams(coordRT, offsetRT, out DispatchParams dpRT1, out DispatchParams dpRT2);
            dispatchList = new DispatchParams[] { dpLB1, dpLB2, dpLT1, dpLT2, dpRB1, dpRB2, dpRT1, dpRT2 };
        }

        private int SetDispatchData(DispatchParams[] dispatchList)
        {
            if (dispatchList.Length != 8) return 0;
            int totalCount = 0;
            for (int i = 0; i < 8; ++i)
            {
                var param = dispatchList[i];
                totalCount += param.count;
                dispatchData[5 * i + 0] = param.offset.x;
                dispatchData[5 * i + 1] = param.offset.y;
                dispatchData[5 * i + 2] = totalCount;
                dispatchData[5 * i + 3] = param.stride;
                dispatchData[5 * i + 4] = param.xMajor;
            }
            computeBuffer.SetData(dispatchData);
            return totalCount;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = renderingData.commandBuffer;
            UniversalRenderer universalRenderer = renderer as UniversalRenderer;
            if (universalRenderer == null || computeShader == null || lightSource == null || lightSource.type != LightType.Point) return;

            using (new ProfilingScope(cmd, profilingSampler))
            {
                float4 lightPosWS = new float4(lightSource.transform.position, 1.0f);
                float4x4 viewMat = renderingData.cameraData.GetViewMatrix();
                float4x4 projMat = renderingData.cameraData.GetGPUProjectionMatrix();
                float4x4 vpMat = math.mul(projMat, viewMat);
                float4 lightPosCS = math.mul(vpMat, lightPosWS);
                float3 lightPosNDC = lightPosCS.xyz / lightPosCS.w;
                lightPosNDC.y = -lightPosNDC.y;

                float2 lightPosSS = (lightPosNDC.xy * 0.5f + 0.5f) * new float2(textureSize.x, textureSize.y);
                int2 iLightPosSS = new int2(math.floor(lightPosSS + 0.5f));
                int2 ts = new int2(textureSize.x, textureSize.y);
                GetDispatchList(iLightPosSS, ts, out DispatchParams[] dispatchList);
                int totalDispatchCount = SetDispatchData(dispatchList);

                var backBuffer = universalRenderer.m_ColorBufferSystem.GetBackBuffer(cmd);
                // int clearID = computeShader.FindKernel("ClearMain");
                // cmd.SetComputeTextureParam(computeShader, clearID, "_RW_TargetTex", contactShadowTextureHandle);
                // computeShader.GetKernelThreadGroupSizes(clearID, out uint x1, out uint y1, out uint z1);
                // cmd.DispatchCompute(computeShader, clearID,
                //                     Mathf.CeilToInt((float)textureSize.x / x1),
                //                     Mathf.CeilToInt((float)textureSize.y / y1),
                //                     1);

                int kernelID = computeShader.FindKernel("ContactShadowPoint");
                cmd.SetComputeTextureParam(computeShader, kernelID, "_ColorTex", backBuffer);
                cmd.SetComputeTextureParam(computeShader, kernelID, "_RW_TargetTex", contactShadowTextureHandle);
                cmd.SetComputeVectorParam(computeShader, "_LightPosWS", lightPosWS);
                cmd.SetComputeVectorParam(computeShader, "_LightPosCS", lightPosCS);
                cmd.SetComputeVectorParam(computeShader, "_LightPosSS", new float4(lightPosSS, 0.0f, 0.0f));
                cmd.SetComputeVectorParam(computeShader, "_ThicknessParams", contactShadow.thicknessParams.value);
                cmd.SetComputeVectorParam(computeShader, "_TextureSize", GetTextureSizeParameter(textureSize));
                cmd.SetComputeFloatParam(computeShader, "_LightRange", lightSource.range);
                cmd.SetComputeFloatParam(computeShader, "_Debug", contactShadow.debug.value);
                cmd.SetComputeBufferParam(computeShader, kernelID, "_DispatchData", computeBuffer);

                computeShader.GetKernelThreadGroupSizes(kernelID, out uint x, out uint y, out uint z);
                cmd.DispatchCompute(computeShader, kernelID,
                                     Mathf.CeilToInt((float)totalDispatchCount / x),
                                     1,
                                     1);
                cmd.Blit(contactShadowTextureHandle, backBuffer);
            }         
        }

        public void Dispose()
        {
            contactShadowTextureHandle?.Release();
            if (computeBuffer != null)
            {
                computeBuffer.Release();
                computeBuffer = null;
            }
        }
    }
}

ContactShadowRendererFeature.cs

using System;

namespace UnityEngine.Rendering.Universal
{
    public class ContactShadowRendererFeature : ScriptableRendererFeature
    {

        [Serializable]
        public class ContactShadowSettings
        {
            public ComputeShader computeShader;
            public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        }

        public ContactShadowSettings settings = new ContactShadowSettings();
        private ContactShadowRenderPass contactShadowRenderPass;

        public override void Create()
        {
            contactShadowRenderPass = new ContactShadowRenderPass(settings);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            ContactShadow cs = VolumeManager.instance.stack.GetComponent<ContactShadow>();
            if (cs.IsActive())
            {

                contactShadowRenderPass.Setup(renderer, cs);
                renderer.EnqueuePass(contactShadowRenderPass);
            }
        }

        protected override void Dispose(bool disposing)
        {
            contactShadowRenderPass?.Dispose();
            base.Dispose(disposing);
        }
    }
}

ContactShadow.cs

using System;

namespace UnityEngine.Rendering.Universal
{
    [Serializable, VolumeComponentMenuForRenderPipeline("Post-processing/Contact Shadow", typeof(UniversalRenderPipeline))]
    public sealed class ContactShadow : VolumeComponent, IPostProcessComponent
    {
        public BoolParameter isEnabled = new BoolParameter(false);
        public Vector2Parameter thicknessParams = new Vector2Parameter(new Vector2(0.1f, 0.02f));
        public FloatParameter debug = new FloatParameter(0.0f);

        public bool IsActive()
        {
            return isEnabled.value;
        }

        public bool IsTileCompatible() => false;
    }
}

ScreenSpaceContactShadowLightSource.cs

using UnityEngine;

[ExecuteAlways]
[RequireComponent(typeof(Light))]
public class ScreenSpaceContactShadowLightSource : MonoBehaviour
{
    public static ScreenSpaceContactShadowLightSource Instance { get; private set; }

    private void OnEnable()
    {
        if (Instance == null)
        {
            Instance = this;
            UnityEngine.Rendering.Universal.ContactShadowRenderPass.lightSource = GetComponent<Light>();
        }
        else
        {
            Debug.LogError("Only one instance of ScreenSpaceContactShadowLightSource is allowed to exist at the same time.");
            enabled = false;
        }
    }

    private void OnDisable()
    {
        if (Instance == this)
        {
            Instance = null;
        }
    }

    private void OnDestroy()
    {
        if (Instance == this )
        {
            Instance = null;
        }
    }
}

后记

摸了两周,但也没摸,但是确实有点没有动力了。Radial Dispatch系列估计到这里就告一段落了,这个屏幕空间接触阴影没有我之前想象中的效果那么好,之后会在草场、云、海洋、Radiance Cascade GI中间选一个来做吧,不过估计要很久了。本来封面是想放一个点光源在视差映射贴图上的接触阴影和平行光在远处山上森林的接触阴影的,不过有点懒得整了。2024虽然糟透了,但也还有一丝丝的好消息吧。