圆形模糊

圆形模糊,在Photoshop里又称镜头模糊(Lens Blur),和景深结合在一起的时候被称作散景(Bokeh),是指在摄影时失焦的区域产生的和光圈的形状一致的模糊效果,五边形八边形或是圆形都有可能。

在计算机图形学中实现景深效果基本上有两种方法:第一种也是最常用的,通过黄金率生成一系列的采样点,使得其形状接近想要模糊的形状,这种方法需要很多很多的采样点,基本上找到的都是60次以上的采样次数,由于采样点的分布不一定正好在像素点中心,也不能轻易地使用Group Shared Memory进行优化,事实上大的模糊半径很可能导致Group Shared Memory的大小不够;另一种是针对于特殊的模糊形状,比如正六边形,可以使用三次(MRT的话可以认为是两次)1D的模糊来组合而成,可以在Colin Barré-Brisebois的博客Hexagonal Bokeh Blur Revisited中看到详细的说明,值得一提的是他此前也在EA工作过(看来EA是真的很喜欢散景啊)。

EA的渲染工程师Kleber Garcia在2018年的GDC演讲Circular Separable Convolution Depth of Field中提到了通过复数的运算来实现圆形模糊的算法,其背后的数学我这里就不再赘述了,感兴趣的话可以参考Circularly symmetric convolution and lens blu这篇文章。圆形模糊的参数的生成的代码可以在Kleber Garcia的公开仓库里找到。Kleber Garcia本人也在Shadertoy上写了具体的圆形模糊的代码

Circular Dof

由于景深效果相对来说比较复杂,这里就只考虑对整个屏幕施加相同程度的圆形模糊效果。

具体的实现方法

其实大部分和之前的高斯模糊没有什么差别。在分离卷积圆形模糊的算法中,圆形的效果是通过多个Filter叠加而成的,每个Filter对应实部和虚部两个参数。以本文为例,本文使用了两个Filter,对于一个Filter的一个颜色分量,需要储存实部虚部两个数据,总体就需要2x3x2=12个通道,使用三张R16G16B16A16_SFloat就能储存所有的数据。在Kleber Garcia的演讲中他还提到了,可以使用bracket的方法,将颜色的卷积数据储存到另一张图中,这样中间的Filter的结果就会落在[0, 1]的范围内,就能使用R8G8A8B8来储存了,可以节省一半的带宽(但是颜色的卷积数据不也要一张32位的图吗,这里我没太懂,感觉优化了但又没那么优化,索性就没那么做)。

整体的操作是:1. 采样源图片,对每个Filter和每个颜色分量计算实部和虚部的值,水平累加后储存到中间贴图中;2. 采样中间贴图,对每个Filter和每个颜色分量计算实部和虚部的值,竖直累加后乘上各自的权重就得到最终的颜色了。由于我没有使用bracket的方法,Filter中会有负值存在,在仅使用两个Filter且半径较大且像素颜色过亮的时候,由于banding的存在会使最终的颜色出现负值,解决方法是在读取颜色的时候做一次Clamp或者是ToneMapping到合理范围。

CircularBlurFilterGenerator.cs

改写自Kleber Garcia的公开仓库

/*
Copyright 2023  zznewclear13 (zznewclear@gmail.com)
Copyright 2016  Kleber A Garcia (kecho_garcia@hotmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using UnityEngine;

public static class CircularBlurFilterGenerator
{
    public struct Filter
    {
        public Vector2 kernelWeight;
        public Vector2[] circularKernels;
    }

    public struct KernelData
    {
        public Filter[] filters;
    }

    private static Vector4 KernelFunction(float x, Vector4 C)
    {
        float real = Mathf.Cos(x * x * C.x) * Mathf.Exp(x * x * C.y);
        float imaginary = Mathf.Sin(x * x * C.x) * Mathf.Exp(x * x * C.y);
        float realWeight = C.z;
        float imaginaryWeight = C.w;
        return new Vector4(real, imaginary, realWeight, imaginaryWeight);
    }

    // 1 <= components <= 5
    // best: 0.2f <= transition <= 0.4f
    public static KernelData GenerateFilter(float radius, int components = 2, float transition = 0.2f, bool logKernel = false)
    {
        int sampleRadius = Mathf.CeilToInt(radius);
        int kernelSize = 2 * sampleRadius + 1;
        KernelData kernelData = new KernelData();
        if (transition <= -1.0f)
        {
            Debug.Log("Invalid transition bandwidth. Must be greater than -1 and preferably positive.");
            return kernelData;
        }

        Vector4[] P;
        switch (components)
        {
            case 1:
                P = new Vector4[]
                    {
                        new Vector4( 1.624835f, -0.862325f, 0.767583f, 1.862321f )
                    };
                break;
            case 2:
                P = new Vector4[]
                   {
                        new Vector4( 5.268909f, -0.886528f, 0.411259f, -0.548794f ),
                        new Vector4(1.558213f, -1.960518f, 0.513282f, 4.561110f )
                   };
                break;
            case 3:
                P = new Vector4[]
                   {
                        new Vector4( 5.043495f, -2.176490f, 1.621035f, -2.105439f ),
                        new Vector4( 9.027613f, -1.019306f, -0.280860f, -0.162882f ),
                        new Vector4( 1.597273f, -2.815110f, -0.366471f, 10.300301f )
                   };
                break;
            case 4:
                P = new Vector4[]
                   {
                        new Vector4( 1.553635f, -4.338459f, -5.767909f, 46.164397f ),
                        new Vector4( 4.693183f, -3.839993f, 9.795391f, 15.227561f ),
                        new Vector4( 8.178137f, -2.791880f, -3.048324f, 0.302959f ),
                        new Vector4( 12.328289f, -1.342190f, 0.010001f, 0.244650f )
                   };
                break;
            case 5:
                P = new Vector4[]
                   {
                        new Vector4( 1.685979f, -4.892608f, -22.356787f, 85.912460f ),
                        new Vector4( 4.998496f, -4.711870f, 35.918936f , -28.875618f ),
                        new Vector4( 8.244168f, -4.052795f, -13.212253f, -1.578428f ),
                        new Vector4( 11.900859f, -2.929212f, 0.507991f, 1.816328f ),
                        new Vector4( 16.116382f, -1.512961f, 0.138051f, -0.010000f )
                   };
                break;
            default:
                Debug.Log("Invalid component count. Must be [1-5].");
                return kernelData;
        }

        Vector4[,] kernels = new Vector4[components, kernelSize];
        float totalBandWidth = 1.0f + transition;
        for (int i = 0; i < components; i++)
        {
            Vector4 C = P[i];
            for (int j = -sampleRadius; j < sampleRadius + 1; j++)
            {
                kernels[i, j + sampleRadius] = KernelFunction(totalBandWidth * j / radius, C);
            }
        }

        // normalize kernels
        float accum = 0.0f;
        for (int i = 0; i < components; i++)
        {
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 v = kernels[i, j];
                for (int k = 0; k < kernelSize; k++)
                {
                    Vector4 w = kernels[i, k];
                    accum += v.z * (v.x * w.x - v.y * w.y) + v.w * (v.x * w.y + v.y * w.x);
                }
            }
        }

        float normConstant = 1.0f / Mathf.Sqrt(accum);
        Vector4[,] kernelsNormalized = new Vector4[components, kernelSize];
        for (int i = 0; i < components; i++)
        {
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 v = kernels[i, j];
                Vector4 vn = new Vector4(normConstant * v.x, normConstant * v.y, 0.0f, 0.0f);
                kernelsNormalized[i, j] = vn;
            }
        }

        // bracket the kernel so we maximize precision. This means figureout a Offset and a Scale
        Vector2[] scales = new Vector2[components];
        Vector2[] offsets = new Vector2[components];
        for (int i = 0; i < components; i++)
        {
            Vector2 minVector = new Vector2(kernelsNormalized[i, 0].x, kernelsNormalized[i, 0].y);
            for (int j = 1; j < kernelSize; j++)
            {
                minVector = new Vector2(Mathf.Min(minVector.x, kernelsNormalized[i, j].x), Mathf.Min(minVector.y, kernels[i, j].y));
            }
            offsets[i] = minVector;
        }

        for (int i = 0; i < components; i++)
        {
            Vector2 offset = offsets[i];
            Vector2 scale = new Vector2(0f, 0f);
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 v = kernelsNormalized[i, j];
                float realScale = v.x - offset.x;
                float immScale = v.y - offset.y;
                scale += new Vector2(realScale, immScale);
            }
            scales[i] = scale;
        }

        Vector4[,] finalKernels = new Vector4[components, kernelSize];
        for (int i = 0; i < components; i++)
        {
            Vector2 offset = offsets[i];
            Vector2 scale = scales[i];
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 v = kernelsNormalized[i, j];
                float realScale = v.x - offset.x;
                float immScale = v.y - offset.y;
                finalKernels[i, j] = new Vector4(v.x, v.y, realScale / scale.x, immScale / scale.y);
            }
        }

        Vector2[] componentWeights = new Vector2[components];
        for (int i = 0; i < components; i++)
        {
            Vector4 comp = P[i];
            componentWeights[i] = new Vector2(comp.z, comp.w);
        }

        Filter[] filters = new Filter[components];
        for (int i = 0; i < components; i++)
        {
            Vector2[] circularKernels = new Vector2[kernelSize];
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 v = finalKernels[i, j];
                circularKernels[j] = new Vector2(v.x, v.y);
            }
            Vector2 kernelWeight = componentWeights[i];
            filters[i] = new Filter
            {
                kernelWeight = kernelWeight,
                circularKernels = circularKernels
            };
        }
        kernelData.filters = filters;

        if(logKernel)
        {
            Debug.Log(Log(radius, finalKernels, componentWeights, offsets, scales));
        }
        return kernelData;
    }

    private static readonly string[] syntax = new string[] { "uint", "float", "static const", "{", "};" };
    private static string Log(float radius, Vector4[,] finalKernels, Vector2[] componentWeights, Vector2[] offsets, Vector2[] scales)
    {
        string logStr = "";
        logStr += string.Format("{0} {1} KERNEL_RADIUS = {2};\n", syntax[2], syntax[0], radius);
        int kernelSize = Mathf.CeilToInt(radius) * 2 + 1;
        logStr += string.Format("{0} {1} KERNEL_COUNT = {2};\n", syntax[2], syntax[0], kernelSize);
        int component = componentWeights.Length;
        for (int i = 0; i < component; i++)
        {
            Vector2 o = offsets[i];
            Vector2 s = scales[i];
            Vector2 comp = componentWeights[i];
            logStr += string.Format("{0} {1}4 Kernel{2}BracketsRealXY_ImZW = {1}4({3},{4},{5},{6});\n",
                                    syntax[2], syntax[1], i, o.x, s.x, o.y, s.y);
            logStr += string.Format("{0} {1}2 Kernel{2}Weights_RealX_ImY = {1}2({3},{4});\n",
                                    syntax[2], syntax[1], i, comp.x, comp.y);
            logStr += string.Format("{0} {1}4 Kernel{2}_RealX_ImY_RealZ_ImW[] = {3}\n",
                                    syntax[2], syntax[1], i, syntax[3]);
            for (int j = 0; j < kernelSize; j++)
            {
                Vector4 val = finalKernels[i, j];
                logStr += string.Format("\t{0}4(/*XY: Non Bracketed*/{1},{2},/*Bracketed WZ:*/{3},{4}){5}\n",
                                        syntax[1], val.x, val.y, val.z, val.w,
                                        (j < kernelSize - 1) ? "," : "");
            }
            logStr += string.Format("{0}\n", syntax[4]);
        }
        return logStr;
    }
}

CircularBlurComputeShader.compute

我只使用了两个Filter,且将其权重和参数合并到了一个StructuredBuffer<float2>中,每一个Filter就需要根据一定的偏移量去获取到权重和参数的数据。同时又因为只使用了两个Filter,一个颜色分量会有两组实部和虚部,刚好构成一个四通道的数据,对应到一张贴图上。这个Compute Shader很大程度地参考了Kleber Garcia的Circular Dof

#pragma kernel CircularH
#pragma kernel CircularV

Texture2D<float4> _ColorTexture;
Texture2D<float4> _RTexture;
Texture2D<float4> _GTexture;
Texture2D<float4> _BTexture;

RWTexture2D<float4> _RW_RTexture;
RWTexture2D<float4> _RW_GTexture;
RWTexture2D<float4> _RW_BTexture;
RWTexture2D<float4> _RW_CompositeTexture;

float4 _TextureSize;
int _SampleRadius;
StructuredBuffer<float2> _CircularKernels;
#define COLOR_THRESHOLD 5.0f

#define CIRCULAR_BLUR_MAX_RADIUS 128
#define THREAD_GROUP_SIZE 256
const static int CACHED_COLOR_SIZE = THREAD_GROUP_SIZE + CIRCULAR_BLUR_MAX_RADIUS * 2;
groupshared half3 cachedColor[CACHED_COLOR_SIZE];
void SetCachedColor(half3 color, int index) { cachedColor[index] = color; }
half3 GetCachedColor(int threadPos) { return cachedColor[threadPos + CIRCULAR_BLUR_MAX_RADIUS]; }
void CacheColor(int2 groupCacheStartPos, int cacheIndex)
{
    int2 texturePos = groupCacheStartPos + int2(cacheIndex, 0);
    texturePos = clamp(texturePos, 0, _TextureSize.xy - 1.0f);
    half3 color = _ColorTexture.Load(uint3(texturePos, 0)).rgb;
    color = clamp(color, 0.0f, COLOR_THRESHOLD);
    SetCachedColor(color, cacheIndex);
}

struct RGBComp
{
    half4 rComp;
    half4 gComp;
    half4 bComp;
};
groupshared RGBComp cachedComp[CACHED_COLOR_SIZE];
void SetCachedComp(RGBComp comp, int index) { cachedComp[index] = comp; }
RGBComp GetCachedComp(int threadPos) { return cachedComp[threadPos + CIRCULAR_BLUR_MAX_RADIUS]; }
void CacheComp(int2 groupCacheStartPos, int cacheIndex)
{
    int2 texturePos = groupCacheStartPos + int2(0, cacheIndex);
    texturePos = clamp(texturePos, 0, _TextureSize.xy - 1.0f);
    half4 rComp = _RTexture.Load(uint3(texturePos, 0));
    half4 gComp = _GTexture.Load(uint3(texturePos, 0));
    half4 bComp = _BTexture.Load(uint3(texturePos, 0));
    RGBComp rgbComp = (RGBComp)0;
    rgbComp.rComp = rComp;
    rgbComp.gComp = gComp;
    rgbComp.bComp = bComp;
    SetCachedComp(rgbComp, cacheIndex);
}

//(Pr+Pi)*(Qr+Qi) = (Pr*Qr+Pr*Qi+Pi*Qr-Pi*Qi)
float2 multComplex(float2 p, float2 q)
{
    return float2(p.x * q.x - p.y * q.y, p.x * q.y + p.y * q.x);
}

[numthreads(THREAD_GROUP_SIZE,1,1)]
void CircularH(uint3 groupID : SV_GroupID,
                uint3 groupThreadID : SV_GroupThreadID,
                uint groupIndex : SV_GroupIndex,
                uint3 dispatchThreadID : SV_DispatchThreadID)
{
    int2 threadGroupSize = int2(THREAD_GROUP_SIZE, 1);
    int2 groupCacheStartPos = groupID.xy * threadGroupSize - int2(CIRCULAR_BLUR_MAX_RADIUS, 0);
    int cacheIndex = groupIndex * 2;
    if (cacheIndex < CACHED_COLOR_SIZE - 1)
    {
        CacheColor(groupCacheStartPos, cacheIndex);
        CacheColor(groupCacheStartPos, cacheIndex + 1);
    }
    GroupMemoryBarrierWithGroupSync();

    int sampleRadius = _SampleRadius;
    int threadPos = groupIndex;
    
    half4 redSum = 0.0f;
    half4 greenSum = 0.0f;
    half4 blueSum = 0.0f;
    for (int i = -sampleRadius; i <=sampleRadius; ++i)
    {
        half3 color = GetCachedColor(threadPos + i);
        float2 kernel0 = _CircularKernels[1 + i + sampleRadius];
        float2 kernel1 = _CircularKernels[1 + 2 * sampleRadius + 1 + 1 + i + sampleRadius];
        redSum += color.r * float4(kernel0, kernel1);
        greenSum += color.g * float4(kernel0, kernel1);
        blueSum += color.b * float4(kernel0, kernel1);
    }

    _RW_RTexture[dispatchThreadID.xy] = redSum;
    _RW_GTexture[dispatchThreadID.xy] = greenSum;
    _RW_BTexture[dispatchThreadID.xy] = blueSum;
}

[numthreads(1,THREAD_GROUP_SIZE,1)]
void CircularV(uint3 groupID : SV_GroupID,
                uint3 groupThreadID : SV_GroupThreadID,
                uint groupIndex : SV_GroupIndex,
                uint3 dispatchThreadID : SV_DispatchThreadID)
{
    int2 threadGroupSize = int2(1, THREAD_GROUP_SIZE);
    int2 groupCacheStartPos = groupID.xy * threadGroupSize - int2(0, CIRCULAR_BLUR_MAX_RADIUS);
    int cacheIndex = groupIndex * 2;
    if (cacheIndex < CACHED_COLOR_SIZE - 1)
    {
        CacheComp(groupCacheStartPos, cacheIndex);
        CacheComp(groupCacheStartPos, cacheIndex + 1);
    }
    GroupMemoryBarrierWithGroupSync();


    int sampleRadius = _SampleRadius;
    int threadPos = groupIndex;

    half4 redSum = 0.0f;
    half4 greenSum = 0.0f;
    half4 blueSum = 0.0f;
    for (int i = -sampleRadius; i <= sampleRadius; ++i)
    {
        RGBComp comp = GetCachedComp(threadPos + i);
        float2 kernel0 = _CircularKernels[1 + i + sampleRadius];
        float2 kernel1 = _CircularKernels[1 + 2 * sampleRadius + 1 + 1 + i + sampleRadius];

        redSum.xy += multComplex(comp.rComp.xy, kernel0);
        redSum.zw += multComplex(comp.rComp.zw, kernel1);
        greenSum.xy += multComplex(comp.gComp.xy, kernel0);
        greenSum.zw += multComplex(comp.gComp.zw, kernel1);
        blueSum.xy += multComplex(comp.bComp.xy, kernel0);
        blueSum.zw += multComplex(comp.bComp.zw, kernel1);
    }
    float2 weight0 = _CircularKernels[0];
    float2 weight1 = _CircularKernels[1 + 2 * sampleRadius + 1];

    half r = dot(redSum.xy, weight0) + dot(redSum.zw, weight1);
    half g = dot(greenSum.xy, weight0) + dot(greenSum.zw, weight1);
    half b = dot(blueSum.xy, weight0) + dot(blueSum.zw, weight1);

    _RW_CompositeTexture[dispatchThreadID.xy] = half4(r, g, b, 1.0f);
}

CircularBlur.cs

这里需要保证radius的最大值不大于Compute Shader里的CIRCULAR_BLUR_MAX_RADIUS

using System;

namespace UnityEngine.Rendering.Universal
{
    [Serializable, VolumeComponentMenuForRenderPipeline("Post-processing/Circular Blur", typeof(UniversalRenderPipeline))]
    public class CircularBlur : VolumeComponent, IPostProcessComponent
    {
        public BoolParameter isEnabled = new BoolParameter(false);
        public ClampedFloatParameter radius = new ClampedFloatParameter(0.0f, 0.0f, 128.0f);

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

        public bool IsTileCompatible()
        {
            return false;
        }
    }
}

CircularBlurRenderPass.cs

和高斯模糊的Render Pass大同小异。

using System.Collections.Generic;

namespace UnityEngine.Rendering.Universal
{
    public class CircularBlurRenderPass : ScriptableRenderPass
    {
        static readonly string passName = "Circular Blur Render Pass";

        private CircularBlurRendererFeature.CircularBlurSettings settings;
        private CircularBlur circularBlur;
        private ComputeShader computeShader;

        static readonly string cameraColorTextureName = "_CameraColorAttachmentA";
        static readonly int cameraColorTextureID = Shader.PropertyToID(cameraColorTextureName);
        RenderTargetIdentifier cameraColorIden;

        static readonly string circularBlurTextureRName = "_CircularBlurTextureR";
        static readonly int circularBlurTextureRID = Shader.PropertyToID(circularBlurTextureRName);
        RenderTargetIdentifier circularBlurTextureRIden;

        static readonly string circularBlurTextureGName = "_CircularBlurTextureG";
        static readonly int circularBlurTextureGID = Shader.PropertyToID(circularBlurTextureGName);
        RenderTargetIdentifier circularBlurTextureGIden;

        static readonly string circularBlurTextureBName = "_CircularBlurTextureB";
        static readonly int circularBlurTextureBID = Shader.PropertyToID(circularBlurTextureBName);
        RenderTargetIdentifier circularBlurTextureBIden;

        static readonly string compositeTextureName = "_CompositeTexture";
        static readonly int compositeTextureID = Shader.PropertyToID(compositeTextureName);
        RenderTargetIdentifier compositeTextureIden;

        CircularBlurFilterGenerator.KernelData kernelData;
        private ComputeBuffer computeBuffer;
        private Vector2Int textureSize;
        private float lastRadius = 0.0f;
        
        static readonly string HorizontalKernelName = "CircularH";
        static readonly string VerticalKernelName = "CircularV";
        static readonly int _ColorTexture = Shader.PropertyToID("_ColorTexture");
        static readonly int _RTexture = Shader.PropertyToID("_RTexture");
        static readonly int _GTexture = Shader.PropertyToID("_GTexture");
        static readonly int _BTexture = Shader.PropertyToID("_BTexture");
        static readonly int _RW_RTexture = Shader.PropertyToID("_RW_RTexture");
        static readonly int _RW_GTexture = Shader.PropertyToID("_RW_GTexture");
        static readonly int _RW_BTexture = Shader.PropertyToID("_RW_BTexture");
        static readonly int _RW_CompositeTexture = Shader.PropertyToID("_RW_CompositeTexture");
        static readonly int _TextureSize = Shader.PropertyToID("_TextureSize");
        static readonly int _SampleRadius = Shader.PropertyToID("_SampleRadius");
        static readonly int _CircularKernels = Shader.PropertyToID("_CircularKernels");

        public CircularBlurRenderPass(CircularBlurRendererFeature.CircularBlurSettings settings)
        {
            profilingSampler = new ProfilingSampler(passName);

            this.settings = settings;
            renderPassEvent = settings.renderPassEvent;
            computeShader = settings.computeShader;

            cameraColorIden = new RenderTargetIdentifier(cameraColorTextureID);
            circularBlurTextureRIden = new RenderTargetIdentifier(circularBlurTextureRID);
            circularBlurTextureGIden = new RenderTargetIdentifier(circularBlurTextureGID);
            circularBlurTextureBIden = new RenderTargetIdentifier(circularBlurTextureBID);
            compositeTextureIden = new RenderTargetIdentifier(compositeTextureID);
        }

        public void Setup(CircularBlur circularBlur)
        {
            this.circularBlur = circularBlur;
        }

        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)
        {
            if(lastRadius != circularBlur.radius.value)
            {
                kernelData = CircularBlurFilterGenerator.GenerateFilter(circularBlur.radius.value, 2, 0.2f, false);
                int count = kernelData.filters.Length * (1 + kernelData.filters[0].circularKernels.Length);
                EnsureComputeBuffer(count, 2 * sizeof(float));
                List<Vector2> data = new List<Vector2>();
                foreach (var filter in kernelData.filters)
                {
                    data.Add(filter.kernelWeight);
                    data.AddRange(filter.circularKernels);
                }
                computeBuffer.SetData(data);

                lastRadius = circularBlur.radius.value;
            }            
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            textureSize = new Vector2Int(cameraTextureDescriptor.width, cameraTextureDescriptor.height);
            RenderTextureDescriptor desc = cameraTextureDescriptor;
            desc.enableRandomWrite = true;
            desc.msaaSamples = 1;
            desc.depthBufferBits = 0;

            desc.graphicsFormat = Experimental.Rendering.GraphicsFormat.R16G16B16A16_SFloat;
            cmd.GetTemporaryRT(circularBlurTextureRID, desc);
            cmd.GetTemporaryRT(circularBlurTextureGID, desc);
            cmd.GetTemporaryRT(circularBlurTextureBID, desc);
            cmd.GetTemporaryRT(compositeTextureID, desc);
        }

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

        private void DoCircularBlur(CommandBuffer cmd, RenderTargetIdentifier colorid,
            RenderTargetIdentifier rid, RenderTargetIdentifier gid, RenderTargetIdentifier bid,
            RenderTargetIdentifier compositeid, ComputeShader computeShader)
        {
            if (!computeShader) return;

            {
                int kernelID = computeShader.FindKernel(HorizontalKernelName);
                computeShader.GetKernelThreadGroupSizes(kernelID, out uint x, out uint y, out uint z);
                cmd.SetComputeTextureParam(computeShader, kernelID, _ColorTexture, colorid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _RW_RTexture, rid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _RW_GTexture, gid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _RW_BTexture, bid);
                cmd.SetComputeBufferParam(computeShader, kernelID, _CircularKernels, computeBuffer);
                cmd.SetComputeVectorParam(computeShader, _TextureSize, GetTextureSizeParams(textureSize));
                cmd.SetComputeIntParam(computeShader, _SampleRadius, Mathf.CeilToInt(circularBlur.radius.value));
                cmd.DispatchCompute(computeShader, kernelID,
                    Mathf.CeilToInt((float)textureSize.x / x),
                    Mathf.CeilToInt((float)textureSize.y / y),
                    1);
            }

            {
                int kernelID = computeShader.FindKernel(VerticalKernelName);
                computeShader.GetKernelThreadGroupSizes(kernelID, out uint x, out uint y, out uint z);
                cmd.SetComputeTextureParam(computeShader, kernelID, _RTexture, rid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _GTexture, gid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _BTexture, bid);
                cmd.SetComputeTextureParam(computeShader, kernelID, _RW_CompositeTexture, compositeid);
                cmd.SetComputeBufferParam(computeShader, kernelID, _CircularKernels, computeBuffer);
                cmd.SetComputeVectorParam(computeShader, _TextureSize, GetTextureSizeParams(textureSize));
                cmd.SetComputeIntParam(computeShader, _SampleRadius, Mathf.CeilToInt(circularBlur.radius.value));
                cmd.DispatchCompute(computeShader, kernelID,
                    Mathf.CeilToInt((float)textureSize.x / x),
                    Mathf.CeilToInt((float)textureSize.y / y),
                    1);
            }
            cmd.Blit(compositeid, cameraColorIden);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, profilingSampler))
            {
                DoCircularBlur(cmd, cameraColorIden,
                    circularBlurTextureRIden, circularBlurTextureGIden, circularBlurTextureBIden,
                    compositeTextureIden, computeShader);
            }
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(circularBlurTextureRID);
            cmd.ReleaseTemporaryRT(circularBlurTextureGID);
            cmd.ReleaseTemporaryRT(circularBlurTextureBID);
            cmd.ReleaseTemporaryRT(compositeTextureID);
        }

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

CircularBlurRendererFeature.cs

和高斯模糊的Renderer Feature如出一辙。

namespace UnityEngine.Rendering.Universal
{
    public class CircularBlurRendererFeature : ScriptableRendererFeature
    {
        [System.Serializable]
        public class CircularBlurSettings
        {
            public RenderPassEvent renderPassEvent;
            public ComputeShader computeShader;
        }

        public CircularBlurSettings settings = new CircularBlurSettings();
        private CircularBlurRenderPass circularBlurRenderPass;

        public override void Create()
        {
            circularBlurRenderPass = new CircularBlurRenderPass(settings);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            CircularBlur circularBlur = VolumeManager.instance.stack.GetComponent<CircularBlur>();
            if (circularBlur != null && circularBlur.IsActive())
            {
                circularBlurRenderPass.Setup(circularBlur);
                renderer.EnqueuePass(circularBlurRenderPass);
            }
        }

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

后记

太棒了太棒了,迅速地写完了EA的Circular Blur!本来想配一张夕阳下的椰树剪影和海面波光粼粼反光的场景,对其进行圆形模糊的,发现水的Shader还要自己写,Unity的默认材质球的Smoothness还要使用Albedo或者Metallic图的Alpha通道,导致MegaScans的素材还不能直接用,就暂时放弃了。等写完景深之后再整这些吧,顺便一提为了让封面图能有那个小小的散景的圆形,我偷偷地另外打了一盏灯。下一步,景深!!!