径向模糊

在写了上一篇文章径向分派Compute Shader之后,很自然的就需要用径向模糊来检验我们的算法。

径向模糊的效果在网上可以说是一找一大堆了,百分之九十五的教程,会使用一个很大的循环重复采样纹理,百分之四点九的教程,会使用多pass的方法减少采样的次数(当然这些数字是我瞎编的,我还留了百分之零点一,以防真的有人也用Compute Shader做了径向模糊)。这篇文章就来探讨如何使用Compute Shader中的Group Shared Memory来加速径向模糊的计算。我们的目标是在一个Compute Shader中实现超大模糊距离(甚至能超过128像素)的径向模糊。

缓存颜色信息

照例我们要再看一遍索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍的往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。演示文稿的第24-25页介绍了如何在Group Shared Memory中缓存深度信息,当然在径向模糊中我们缓存的是颜色信息了。

下图是缓存颜色信息的示意图,绿色方块对应的像素是我们整个Thread Group的起点,红色方块是我们当前计算的像素,黑色和灰色的线代表了该像素到径向模糊中心的方向。我们的Thread Group Size是12,深蓝色(包括绿色和红色)的像素同属于一个Thread Group,我们需要计算这些像素径向模糊后的颜色。深蓝色、浅蓝色、灰色和浅灰色、镂空的灰色和浅灰色,代表了我们需要缓存的颜色,因为我们需要对缓存的颜色进行双线性插值,我们除了需要缓存射线经过的最近的像素(即蓝色区域)外,还需要缓存射线经过的第二近的像素(即灰色区域)和射线经过的第三近的像素(即镂空灰色区域)。也就是说当Thread Group Size为12时,我们需要缓存6 * 12个像素,亦即每个像素六次采样。

RadialBlurDiagram.png

仅缓存第二近的像素在大部分情况下能够得到正确的双线性插值,但是注意看红色方块向右的第二次采样,仅对蓝色和实心灰色区域的颜色进行插值是不能得到正确的颜色的,因此我们需要额外缓存镂空灰色区域的颜色,亦即第三近的像素。

Group Shared Memory分配

如上所述,我们的Group Shared Memory的大小是THREAD_COUNT * 6(懒得把THREAD_COUNT统一改成THREAD_GROUP_SIZE了),其中包含了上述的深蓝色、浅蓝色、灰色、浅灰色、镂空灰色、镂空浅灰色区域对应像素的颜色信息。数组的长度是THREAD_COUNT * 6,在我们缓存时,我们会将其视为6行THREAD_COUNT列的表来储存颜色信息,而在我们读取时,我们会将其视为3行THREAD_COUNT*2列的表来读取数据。

groupshared float3 cachedColor[THREAD_COUNT * 6];
void SetCachedColor(float3 color, int2 threadPos) {cachedColor[threadPos.x+threadPos.y*THREAD_COUNT]=color;}
float3 GetCachedColor(int2 threadPos) {return cachedColor[threadPos.x+threadPos.y*THREAD_COUNT*2];}

具体的代码

RadialDispatchComputeShader.compute

核心的代码来自于上一篇文章径向分派Compute Shader。循环读取颜色值的方式有很多,可以严格按照格点来读取,这样只需要对y方向做线性插值,也可以按照统一的步长来读取,这样需要对xy方向都做线性插值,我这里使用的是统一的步长。每一组缓存的起始像素是最接近于射线的像素的下面一个像素,读取缓存使用的偏移是用的当前采样点下第一个像素的坐标和当前缓存列最下面像素的坐标相减,这里面比较绕不太好描述。。。

#pragma kernel RadialBlurMain
// #pragma warning(disable: 3556)

#define THREAD_COUNT 128

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

float2 _CenterPosSS;
float4 _TextureSize;
float _Intensity;

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;
    }
}

float3 LoadColorTexture(int2 coord)
{
    // coord.y = int(_TextureSize.y) - 1 - coord.y;
    coord = clamp(coord, int2(0, 0), int2(_TextureSize.xy - 1.0f));
    return _ColorTex.Load(uint3(coord, 0)).rgb;
}

groupshared float3 cachedColor[THREAD_COUNT * 6];
void SetCachedColor(float3 color, int2 threadPos) {cachedColor[threadPos.x+threadPos.y*THREAD_COUNT]=color;}
float3 GetCachedColor(int2 threadPos) {return cachedColor[threadPos.x+threadPos.y*THREAD_COUNT*2];}
void CacheColor(int2 groupStartSS, float2 centerPosSS, int cacheIndex)
{
    float2 toCenter = centerPosSS - (groupStartSS + 0.5f);
    float2 absDir = abs(toCenter);
    int2 signDir = int2(sign(toCenter));
    bool xMajor = absDir.x >= absDir.y;

    float3 colorVal1, colorVal2, colorVal3;
    int2 offset1, offset2, offset3;
    {
        GetIndexedOffset(cacheIndex, absDir, xMajor, offset1, offset2, offset3);
        colorVal1 = LoadColorTexture(groupStartSS + offset1 * signDir);
        colorVal2 = LoadColorTexture(groupStartSS + offset2 * signDir);
        colorVal3 = LoadColorTexture(groupStartSS + offset3 * signDir);
        SetCachedColor(colorVal1, int2(cacheIndex, 0));
        SetCachedColor(colorVal2, int2(cacheIndex, 2));
        SetCachedColor(colorVal3, int2(cacheIndex, 4));
    }

    {
        int extIndex = cacheIndex + THREAD_COUNT;
        GetIndexedOffset(extIndex, absDir, xMajor, offset1, offset2, offset3);
        colorVal1 = LoadColorTexture(groupStartSS + offset1 * signDir);
        colorVal2 = LoadColorTexture(groupStartSS + offset2 * signDir);
        colorVal3 = LoadColorTexture(groupStartSS + offset3 * signDir);
        SetCachedColor(colorVal1, int2(cacheIndex, 1));
        SetCachedColor(colorVal2, int2(cacheIndex, 3));
        SetCachedColor(colorVal3, int2(cacheIndex, 5));
    }
}

[numthreads(1, THREAD_COUNT, 1)]
void RadialBlurMain(uint3 id : SV_DISPATCHTHREADID)
{
    float2 centerPosSS = _CenterPosSS;
    int2 iCenterPosSS = int2(floor(centerPosSS + 0.5f));

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

    CacheColor(iGroupStartSS, centerPosSS, id.y);
    GroupMemoryBarrierWithGroupSync();

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

    float absToCenterStepRatio = xMajor ?  absDir.y / absDir.x :  absDir.x / absDir.y;
    int baseOffsetY = int(float(id.y) * absToCenterStepRatio + 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 - centerPosSS;
    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;

    float2 marchDir = normalize(absToPos);
    float lenToPosSS = length(toPosSS);
    float3 color = float3(0.0f, 0.0f, 0.0f);
    float weight = 0.0f;
    for (int i=0; i<THREAD_COUNT; ++i)
    {
        float intensity = float(i) * min(1.0f, lenToPosSS * _TextureSize.w * _Intensity);
        float2 sampleOffset = marchDir * intensity;
        if (length(sampleOffset) > lenToPosSS) continue;
        float xOffset = xMajor ? sampleOffset.x : sampleOffset.y;
        float floorXOffset = floor(xOffset);
        float fracXOffset = frac(xOffset);

        float baseOffsetY0 = (floorXOffset + id.y) * absToCenterStepRatio;
        int iBaseStartY0 = int(floor(baseOffsetY0)) + (frac(baseOffsetY0)<=0.5f ? -1 : 0);
        float offsetY0 = floorXOffset * absToPosStepRatio + baseOffsetY;
        int offsetStartY0 = int(floor(offsetY0));
        int sampleIndex0 = offsetStartY0 - iBaseStartY0;
        float3 col00 = GetCachedColor(int2(id.y + floorXOffset, sampleIndex0));
        float3 col01 = GetCachedColor(int2(id.y + floorXOffset, sampleIndex0 + 1));
        float weightY0 = frac(offsetY0);

        float baseOffsetY1 = (floorXOffset + 1.0f + id.y) * absToCenterStepRatio;
        int iBaseStartY1 = int(floor(baseOffsetY1)) + (frac(baseOffsetY1)<=0.5f ? -1 : 0);
        float offsetY1 = (floorXOffset + 1.0f) * absToPosStepRatio + baseOffsetY;
        int offsetStartY1 = int(floor(offsetY1));
        int sampleIndex1 = offsetStartY1 - iBaseStartY1;
        float3 col10 = GetCachedColor(int2(id.y + floorXOffset + 1.0f, sampleIndex1));
        float3 col11 = GetCachedColor(int2(id.y + floorXOffset + 1.0f, sampleIndex1 + 1));
        float weightY1 = frac(offsetY1);

        color += lerp(lerp(col00, col01, weightY0), lerp(col10, col11, weightY1), fracXOffset);
        weight += 1.0f;
    }

    float3 thisColor = GetCachedColor(int2(id.y, 0));
    color /= weight;

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

RadialBlurRenderPass.cs

和上一篇文章如出一辙,看上去只是把Radial Dispatch替换成了Radial Blur。

using Unity.Mathematics;

namespace UnityEngine.Rendering.Universal
{
    public class RadialBlurRenderPass : ScriptableRenderPass
    {
        public static Transform centerTrans;

        private static readonly string passName = "Radial Blur Render Pass";
        private ScriptableRenderer renderer;
        private RadialBlurRendererFeature.RadialBlurSettings settings;
        private RadialBlur radialBlur;
        private ComputeShader computeShader;
        private Vector2Int textureSize;

        private static readonly string radialBlurTextureName = "_RadialBlurTexture";
        private static readonly int radialBlurTextureID = Shader.PropertyToID(radialBlurTextureName);
        private RTHandle radialBlurTextureHandle;

        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 RadialBlurRenderPass(RadialBlurRendererFeature.RadialBlurSettings settings)
        {
            this.settings = settings;
            computeShader = settings.computeShader;
            renderPassEvent = settings.renderPassEvent;
            profilingSampler = new ProfilingSampler(passName);
        }

        public void Setup(ScriptableRenderer renderer, RadialBlur radialBlur)
        {
            this.renderer = renderer;
            this.radialBlur = radialBlur;
        }

        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 radialBlurTextureHandle, desc, FilterMode.Point, TextureWrapMode.Clamp, false, 1, 0, radialBlurTextureName); ;
        }

        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 || centerTrans == null) return;

            using (new ProfilingScope(cmd, profilingSampler))
            {
                float4 centerPosWS = new float4(centerTrans.position, 1.0f);
                float4x4 viewMat = renderingData.cameraData.GetViewMatrix();
                float4x4 projMat = renderingData.cameraData.GetGPUProjectionMatrix();
                float4x4 vpMat = math.mul(projMat, viewMat);
                float4 centerPosCS = math.mul(vpMat, centerPosWS);
                centerPosCS.xyz /= math.abs(centerPosCS.w);
                centerPosCS.y = -centerPosCS.y;

                float2 centerPosSS = (centerPosCS.xy * 0.5f + 0.5f) * new float2(textureSize.x, textureSize.y);
                int2 iCenterPosSS = new int2(math.floor(centerPosSS + 0.5f));
                int2 ts = new int2(textureSize.x, textureSize.y);
                GetDispatchList(iCenterPosSS, 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", radialBlurTextureHandle);
                // 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("RadialBlurMain");
                cmd.SetComputeTextureParam(computeShader, kernelID, "_ColorTex", backBuffer);
                cmd.SetComputeTextureParam(computeShader, kernelID, "_RW_TargetTex", radialBlurTextureHandle);
                cmd.SetComputeVectorParam(computeShader, "_CenterPosSS", new float4(centerPosSS, 0.0f, 0.0f));
                cmd.SetComputeVectorParam(computeShader, "_TextureSize", GetTextureSizeParameter(textureSize));
                cmd.SetComputeFloatParam(computeShader, "_Intensity", radialBlur.intensity.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(radialBlurTextureHandle, backBuffer);
            }
        }

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

RadialBlurRendererFeature.cs

using System;

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

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

        public RadialBlurSettings settings = new RadialBlurSettings();
        private RadialBlurRenderPass radialBlurRenderPass;

        public override void Create()
        {
            radialBlurRenderPass = new RadialBlurRenderPass(settings);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            RadialBlur rb = VolumeManager.instance.stack.GetComponent<RadialBlur>();
            if (rb.IsActive())
            {
                radialBlurRenderPass.Setup(renderer, rb);
                renderer.EnqueuePass(radialBlurRenderPass);
            }
        }

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

RadialBlur.cs

using System;

namespace UnityEngine.Rendering.Universal
{
    [Serializable, VolumeComponentMenuForRenderPipeline("Post-processing/Radial Blur", typeof(UniversalRenderPipeline))]
    public sealed class RadialBlur : VolumeComponent, IPostProcessComponent
    {
        public BoolParameter isEnabled = new BoolParameter(false);
        public ClampedFloatParameter intensity = new ClampedFloatParameter(1.0f, 0.0f, 1.0f);

        public bool IsActive()
        {
            return isEnabled.value && intensity.value > 0.0f;
        }

        public bool IsTileCompatible() => false;
    }
}

RadialBlurCenter.cs

using UnityEngine;

[ExecuteAlways]
public class RadialBlurCenter : MonoBehaviour
{
    public static RadialBlurCenter Instance { get; private set; }

    private void OnEnable()
    {
        if (Instance == null)
        {
            Instance = this;
            UnityEngine.Rendering.Universal.RadialBlurRenderPass.centerTrans = this.transform;
        }
        else
        {
            Debug.LogError("Only one instance of RadialBlurCenter is allowed to exist at the same time.");
            enabled = false;
        }
    }

    private void OnDisable()
    {
        if (Instance == this)
        {
            Instance = null;
            UnityEngine.Rendering.Universal.RadialBlurRenderPass.centerTrans = null;
        }
    }

    private void OnDestroy()
    {
        if (Instance == this)
        {
            Instance = null;
            UnityEngine.Rendering.Universal.RadialBlurRenderPass.centerTrans = null;
        }
    }
}

后记

非常快地做完了径向模糊的效果,完成了径向分派Compute Shader之后简直就如顺水推舟一般。别的似乎也没啥好说的了,因为使用了新版本的Unity,懒得再复制之前祖传的模糊场景文件了,现场搭了一个。我还挺喜欢Unity URP的这张写着UP的图片的,可惜新版本不再提供了。2024年4月13日更新了,缓存前三近像素以进行双线性插值的逻辑,我自己看下来感觉似乎没什么明显的错误。