常见模糊后处理

模糊

最近做的很多效果最终都需要模糊,如镜面反射要模糊模拟粗糙度反射,体积光要降采样然后模糊来降低性能消耗同时柔化效果
更不要说UI上需要模糊背景,透明物体要毛玻璃等等效果,还是很有必要好好整理一下相关的方案同时打包成库方便以后调用。

这就不得不提及毛星云大佬的高品质后处理:十种图像模糊算法的总结与实现
其中的全部方案都包含在他的开源项目X-PostProcessing-Library中。
当中提及后续也将提供对Unity引擎URP/LWRP/HDRP的兼容支持。但如今也只能惋惜了。

本着学习以及方便自己日后使用的目的,暂将其中模糊相关实用的部分整理成URP支持的RendererFeature、VolumeComponent等形式。

当然本仓库也不是单纯的翻译,除了可以用作全屏后处理,也抽象成工具类库,方便其他非相机的效果调用模糊处理。
同时提供了一些特定的RenderFeature,比如3D场景中的磨砂玻璃等。

项目地址
这里只记录和URP相关的部分,具体模糊的算法就不赘述了。

模糊
VolumeComponent

Script

先把这里的各个模糊后处理Pass中通用的部分提出来,抽象成一个基类

public abstract class BaseBlurRendererPass : ScriptableRenderPass
{
    protected abstract BlurRendererFeature.ProfileId ProfileId { get; }
    
    protected abstract string ShaderName { get; }
    protected string MaskBlendShaderName = "KuanMi/MaskBlend";

    protected Shader m_Shader;

    protected ScriptableRenderer m_Renderer;
    protected Material m_Material;
    protected Material m_BlendMaterial;

    protected RenderTextureDescriptor descriptor;
    protected static readonly int BlurRadius = Shader.PropertyToID("_Offset");
    protected static readonly int BlitTexture = Shader.PropertyToID("_BlitTexture");
    protected static readonly int BlurOffset = Shader.PropertyToID("_BlurOffset");
    protected static readonly int BlitTextureSt = Shader.PropertyToID("_BlitTexture_ST");
    internal static readonly int GoldenRot = Shader.PropertyToID("_GoldenRot");
    internal static readonly int Params = Shader.PropertyToID("_Params");


    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        base.OnCameraSetup(cmd, ref renderingData);

        var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;

        descriptor = cameraTargetDescriptor;
        descriptor.depthBufferBits = 0;
        descriptor.msaaSamples = 1;
    }

    public abstract void ExecuteWithCmd(CommandBuffer cmd, ref RenderingData renderingData);

    public virtual bool Setup(ScriptableRenderer renderer)
    {
        if(!GetMaterial())
            return false;
        m_Renderer = renderer;
        return true;
    }
    
    private bool GetMaterial()
    {
        if (m_Material != null)
        {
            return true;
        }

        if (m_Shader == null)
        {
            m_Shader = Shader.Find(ShaderName);
            if (m_Shader == null)
            {
                return false;
            }
        }

        m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
        m_BlendMaterial = CoreUtils.CreateEngineMaterial(Shader.Find(MaskBlendShaderName));

        return m_Material != null;
    }

    public virtual void Dispose()
    {
        CoreUtils.Destroy(m_Material);
    }
}

为了便于在Pass中获取Volume的属性,再提一个泛型抽象类出来

public abstract class BaseBlurRendererPassWithVolume<K> : BaseBlurRendererPass where K : BaseBlur
{
    protected K blurVolume;

    protected MaskBlur maskBlur;

    protected bool isMask => maskBlur.isMask.value;


    protected RTHandle m_MaskTexture;

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        base.OnCameraSetup(cmd, ref renderingData);

        var stack = VolumeManager.instance.stack;
        blurVolume = stack.GetComponent<K>();
        maskBlur = stack.GetComponent<MaskBlur>();

        descriptor.width /= blurVolume.DownSample.value;
        descriptor.height /= blurVolume.DownSample.value;

        if (isMask)
        {
            RenderingUtils.ReAllocateIfNeeded(ref m_MaskTexture, descriptor, FilterMode.Bilinear,
                TextureWrapMode.Clamp, name: "_MaskTex");
        }
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (m_Material == null)
        {
            Debug.LogError("Material is null");
            return;
        }

        var stack = VolumeManager.instance.stack;
        blurVolume = stack.GetComponent<K>();

        maskBlur = stack.GetComponent<MaskBlur>();

        var cmd = CommandBufferPool.Get();

        using (new ProfilingScope(cmd, ProfilingSampler.Get(ProfileId)))
        {
            // Debug.Log("ad" + Time.frameCount);
            ExecuteWithCmd(cmd, ref renderingData);


            if (isMask)
            {
                m_BlendMaterial.SetFloat("_Spread", maskBlur.areaSmooth.value);
                m_BlendMaterial.SetColor("_MaskColor",maskBlur.maskColor.value);
                if (maskBlur.maskType.value == MaskBlur.MaskType.Circle)
                {
                    m_BlendMaterial.EnableKeyword("_CIRCLE");
                    m_BlendMaterial.SetVector("_Center", maskBlur.center.value);
                    m_BlendMaterial.SetFloat("_Area", maskBlur.radius.value);
                }
                else
                {
                    m_BlendMaterial.DisableKeyword("_CIRCLE");
                    m_BlendMaterial.SetFloat("_Area", maskBlur.areaSize.value);
                    m_BlendMaterial.SetFloat("_Offset", maskBlur.offset.value);
                }

                Blit(cmd, m_MaskTexture, m_Renderer.cameraColorTargetHandle, m_BlendMaterial);
            }
        }

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

    public override bool Setup(ScriptableRenderer renderer)
    {
        if (!base.Setup(renderer))
            return false;

        blurVolume = VolumeManager.instance.stack.GetComponent<K>();
        return blurVolume.IsActive();
    }

    public override void Dispose()
    {
        base.Dispose();
        m_MaskTexture?.Release();
    }
}

这里为了方便复用,把模糊的遮罩单独提出来,单独用一个Pass来混合,但这其实是浪费性能的做法。

然后留给具体的模糊后处理的事情就只剩申请RT与绘制调用了。
以DualBlurRenderPass为例子

public class DualBlurRenderPass : BaseBlurRendererPassWithVolume<DualBlur>
{
    protected override BlurRendererFeature.ProfileId ProfileId => BlurRendererFeature.ProfileId.DualBlur;
    
    protected override string ShaderName => "KuanMi/DualBlur";

    RTHandle[] m_Down;
    RTHandle[] m_Up;

    const int k_MaxPyramidSize = 16;

    public DualBlurRenderPass()
    {
        m_Down = new RTHandle[k_MaxPyramidSize];
        m_Up = new RTHandle[k_MaxPyramidSize];
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        base.OnCameraSetup(cmd, ref renderingData);
        for (int i = 0; i < blurVolume.Iteration.value; i++)
        {
            RenderingUtils.ReAllocateIfNeeded(ref m_Down[i], descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp,
                name: "_Down" + i);
            RenderingUtils.ReAllocateIfNeeded(ref m_Up[i], descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp,
                name: "_Up" + i);

            descriptor.width /= 2;
            descriptor.height /= 2;
        }
    }

    public override void ExecuteWithCmd(CommandBuffer cmd, ref RenderingData renderingData)
    {
        var blurRadius = blurVolume.BlurRadius.value;
        var iteration = blurVolume.Iteration.value;

        m_Material.SetVector(BlitTextureSt, new Vector4(1, 1, 0, 0));
        m_Material.SetFloat(BlurRadius, blurRadius);

        Blitter.BlitCameraTexture(cmd, m_Renderer.cameraColorTargetHandle, m_Down[0],
            RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, m_Material, 1);

        var lastDown = m_Down[0];

        for (int i = 1; i < iteration; i++)
        {
            Blitter.BlitCameraTexture(cmd, lastDown, m_Down[i], RenderBufferLoadAction.DontCare,
                RenderBufferStoreAction.Store, m_Material, 1);
            lastDown = m_Down[i];
        }

        var lastUp = lastDown;
        for (int i = iteration - 2; i >= 0; i--)
        {
            Blitter.BlitCameraTexture(cmd, lastUp, m_Up[i], RenderBufferLoadAction.DontCare,
                RenderBufferStoreAction.Store, m_Material, 0);
            lastUp = m_Up[i];
        }

        // Blit(cmd, lastUp, m_Renderer.cameraColorTargetHandle);
        Blitter.BlitCameraTexture(cmd, lastUp, isMask ? m_MaskTexture : m_Renderer.cameraColorTargetHandle, RenderBufferLoadAction.DontCare,
            RenderBufferStoreAction.Store, m_Material, 0);
    }

    public override void Dispose()
    {
        base.Dispose();
        foreach (var rtHandle in m_Down)
        {
            rtHandle?.Release();
        }

        foreach (var rtHandle in m_Up)
        {
            rtHandle?.Release();
        }
    }
}

申请需要的两组上下采样的RT,先降采样,然后升采样,最后依据是否需要和遮罩混合决定是直接绘制到相机上还是遮罩上。

VolumeComponentEditor

这是控制在Volume组件里显示属性的类。
写法很套路化

[CustomEditor(typeof(MaskBlur))]
public class MaskBlurEditor : VolumeComponentEditor
{
    SerializedDataParameter m_isMask;
    SerializedDataParameter m_areaSmooth;
    SerializedDataParameter m_maskColor;
    ......
    public override void OnEnable()
    {
        var o = new PropertyFetcher<MaskBlur>(serializedObject);
        m_isMask = Unpack(o.Find(x => x.isMask));
        m_maskType = Unpack(o.Find(x => x.maskType));
        m_maskColor = Unpack(o.Find(x => x.maskColor))
        ......
    }
    public override void OnInspectorGUI()
    {
        PropertyField(m_isMask);
        PropertyField(m_maskType);
        
        if(m_maskType.value.intValue == (int)MaskBlur.MaskType.Rectangle)
        {
            PropertyField(m_offset);
            
        }
        else if(m_maskType.value.intValue == (int)MaskBlur.MaskType.Circle)
        {
            PropertyField(m_center);
        }
    }
}

Shader

因为原仓库也是用hlsl,所以基本没什么变换,
顶点着色器仿照URP其他后处理,采用VERTEXID_SEMANTIC来计算裁切坐标。

DefaultVaryings defaultVert(DefaultAttributes IN)
{
    DefaultVaryings output;
    output.positionCS = GetFullScreenTriangleVertexPosition(IN.vertexID);
    output.uv = output.positionCS.xy * 0.5 + 0.5;

    #if UNITY_UV_STARTS_AT_TOP
    output.uv.y = 1 - output.uv.y;
    #endif
    return output;
}

工具库

然后就是要考虑怎么让其他效果能方便的调用。
大致思路有两个,一个就通过RTID来在RendererFeature之间传递。
或者把全部Pass都提成工具类,在其他需要模糊效果的Pass中实例化然后调用。
最后还是觉得后者更靠谱一点。

所以把具体的模糊算法再抽象一层

public abstract class BaseTool
{
    public abstract string ShaderName { get; }
    public Material Material { get; protected set; }
    
    internal static readonly int GoldenRot = Shader.PropertyToID("_GoldenRot");
    internal static readonly int Params = Shader.PropertyToID("_Params");
    
    protected ScriptableRenderPass _renderPass;
    
    public BaseTool(ScriptableRenderPass renderPass)
    {
        _renderPass = renderPass;
        
        Material = CoreUtils.CreateEngineMaterial(Shader.Find(ShaderName));
    }
    public abstract void OnCameraSetup(RenderTextureDescriptor descriptor);
    public abstract void Execute(CommandBuffer cmd, RTHandle source, RTHandle target);
    
    public virtual void Dispose()
    {
        CoreUtils.Destroy(Material);
    }
}

把具体的参数的配置剥离开,相机视口的属性也剥离开。
这样就方便了在其他Pass中直接引用模糊的功能。
比如在之前写的体积光中

不直接绘制到视窗中,而是先绘制到一张RT上,把这张RT传给模糊工具,让其模糊后绘制到另一张RT上,然后再与视窗加法混合。
当然如果考虑性能,应该是能合并很多操作到一个Shader中的。但为了开发测试方便,这里先分开,真的有需要再汇总全部后处理。


常见模糊后处理
https://www.kuanmi.top/2023/08/04/Blur/
作者
KuanMi
发布于
2023年8月4日
许可协议