
2025/4/1大约 5 分钟
基于Compute Shader的高斯模糊

com.qstx.rendererfeature
仓库地址
注意:本文使用的是URP14,不同版本略有差异
一、 自定义RendererFeature的实现
简单自定义RandererFeature的实现可参考URP官方教程
比较重要的是需要单独实现自己的ScriptableRendererFeature类和ScriptableRenderPass类
1. ScriptableRendererFeature类
比较重要的方法:
- Create:初始化RendererFeature所需的资源,每次序列化时调用
- Dispose:释放申请的资源
- OnCameraPreCull:在渲染管线Cull之前的准备
- AddRenderPasses:向渲染管线中加入自定义Pass,会在每次渲染之前执行一次
- SetupRenderPasses:Render Target初始化完成后的回调,保证此时可以访问Render Target
2. ScriptableRenderPass类
比较重要的属性:
- renderPassEvent:决定Pass在渲染管线中的执行时机
比较重要的方法:
- Configure:在Pass执行前调用,用于配置Render Target的属性
- ConfigureClear:配置当前Pass Render Target的Clear标志,应在Configure中调用
- ConfigureTarget:配置当前Pass Render Target的Render Target,应在Configure中调用
- Execute:Pass真正的执行逻辑
- OnCameraSetup:相机开始渲染前调用,用于配置Render Target及其Clear标志
- OnCameraCleanup:相机结束渲染后调用,用于释放当前Pass创建的资源
- OnFinishCameraStackRendering:一个相机栈中所有相机渲染完成后调用,用于释放当前Pass申请但供相机栈中其他相机使用的资源
二、 基于Compute Shader实现高斯模糊效果
1. 指定输入输出参数:
//输出要求可写
RWTexture2D<float4> Result;
Texture2D<float4> InputTexture;
//需要模糊的像素半径
int BlurRadius;2. 定义核函数
[numthreads(8, 8, 1)]
void GaussianBlurFull (uint3 id : SV_DispatchThreadID)
{    
    float4 color = float4(0, 0, 0, 0);
    float weightSum = 0.0;
    
    for (int y = -BlurRadius; y <= BlurRadius; y++)
    {
        for (int x = -BlurRadius; x <= BlurRadius; x++)
        {
            float2 offset = float2(x, y);
            //根据距离求像素对目标像素的贡献权重
            float weight = exp(-(x * x + y * y) / (2.0 * (BlurRadius+1) * BlurRadius+1));
            color += InputTexture[id.xy + offset];
            weightSum += weight;
        }
    }
    //计算卷积后的最终颜色值,这个值包含了BlurRadius范围内所有像素的加权贡献
    Result[id.xy] = color / weightSum;
}但是二维高斯函数又可以分为两个不同维度上一维的高斯函数的积,因此可以将执行复杂度从 降低到
下面分别实现了水平方向高斯模糊和竖直方向高斯模糊的核函数
[numthreads(8, 8, 1)]
void GaussianBlurHorizontal (uint3 id : SV_DispatchThreadID)
{    
    float4 color = float4(0, 0, 0, 0);
    float weightSum = 0.0;
    
    for (int x = -BlurRadius; x <= BlurRadius; x++)
    {
        float2 offset = float2(x, 0);
        float weight = exp(-(x * x) / (2.0 * (BlurRadius+1) * BlurRadius+1));
        color += InputTexture[id.xy + offset];
        weightSum += weight;
    }
    
    Result[id.xy] = color / weightSum;
}
[numthreads(8, 8, 1)]
void GaussianBlurVertical (uint3 id : SV_DispatchThreadID)
{    
    float4 color = float4(0, 0, 0, 0);
    float weightSum = 0.0;
    
    for (int y = -BlurRadius; y <= BlurRadius; y++)
    {
        float2 offset = float2(0, y);
        float weight = exp(-(y * y) / (2.0 * (BlurRadius+1) * BlurRadius+1));
        color += InputTexture[id.xy + offset];
        weightSum += weight;
    }
    
    Result[id.xy] = color / weightSum;
}三、 GaussianBlurPass的实现
1. 定义必要属性
    public ComputeShader computeShader;//用于执行高斯模糊的Compute Shader
    public int blurRadius = 5;
    public BlurMode blurMode = BlurMode.Full;//高斯模糊的方式,见下文GaussianBlurRendererFeature.BlurMode中的定义
    //用于中间渲染的临时Render Target
    private RenderTextureDescriptor tempDesc;
    private RTHandle tempTarget1;
    private RTHandle tempTarget2;
    private int horizontalKernalIdx = -1;
    private int verticalKernalIdx = -1;
    private int fullKernalIdx = -1;2. 构造时获取成员属性的设置
    public GaussianBlurPass(ComputeShader shader)
    {
        computeShader = shader;
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;//在后处理之前执行
        
        //确定不同方式的高斯模糊核函数
        horizontalKernalIdx = computeShader.FindKernel("GaussianBlurHorizontal");
        verticalKernalIdx = computeShader.FindKernel("GaussianBlurVertical");
        fullKernalIdx = computeShader.FindKernel("GaussianBlurFull");
    }3. 每次执行渲染之前配置RT
  public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        //保持与相机RT属性一致,但不需要深度且可写
        tempDesc = cameraTextureDescriptor;
        tempDesc.depthStencilFormat = GraphicsFormat.None;
        tempDesc.enableRandomWrite = true;
        
        //RT属性发生变化或RT还不存在时重新分配
        RenderingUtils.ReAllocateIfNeeded(ref tempTarget1, in tempDesc, name: "TempTarget1");
        if (blurMode == BlurMode.HorizontalAndVertical)
            RenderingUtils.ReAllocateIfNeeded(ref tempTarget2, in tempDesc, name: "TempTarget2");
    }4. 执行渲染
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        //确定执行的核函数
        int curKernelIdx = -1;
        switch (blurMode)
        {
            case BlurMode.Full:
                curKernelIdx = fullKernalIdx;
                break;
            case BlurMode.Horizontal:
            case BlurMode.HorizontalAndVertical:
                curKernelIdx = horizontalKernalIdx;
                break;
            case BlurMode.Vertical:
                curKernelIdx = verticalKernalIdx;
                break;
        }
        
        CommandBuffer cmd = CommandBufferPool.Get("Gaussian Blur");
        var src = renderingData.cameraData.renderer.cameraColorTargetHandle;
        //向Command Buffer中提交绘制
        // Dispatch the compute shader
        cmd.SetComputeTextureParam(computeShader, curKernelIdx, "InputTexture", src);
        cmd.SetComputeTextureParam(computeShader, curKernelIdx, "Result", tempTarget1);
        cmd.SetComputeFloatParam(computeShader, "BlurRadius", blurRadius);
        cmd.DispatchCompute(computeShader, curKernelIdx,
            Mathf.CeilToInt(renderingData.cameraData.cameraTargetDescriptor.width / 8.0f),
            Mathf.CeilToInt(renderingData.cameraData.cameraTargetDescriptor.height / 8.0f), 1);
        //如果是分两个阶段执行二维高斯模糊,需要继续运行第二个核函数
        if (blurMode == BlurMode.HorizontalAndVertical)
        {
            // Dispatch the compute shader
            cmd.SetComputeTextureParam(computeShader, verticalKernalIdx, "InputTexture", tempTarget1);
            cmd.SetComputeTextureParam(computeShader, verticalKernalIdx, "Result", tempTarget2);
            cmd.SetComputeFloatParam(computeShader, "BlurRadius", blurRadius);
            cmd.DispatchCompute(computeShader, verticalKernalIdx,
                Mathf.CeilToInt(renderingData.cameraData.cameraTargetDescriptor.width / 8.0f),
                Mathf.CeilToInt(renderingData.cameraData.cameraTargetDescriptor.height / 8.0f), 1);
            
            cmd.Blit(tempTarget2, src);//将结果写回相机
        }
        else
        {
            cmd.Blit(tempTarget1, src);//将结果写回相机
        }
        
        //提交Command Buffer
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }5. 释放资源
    public void Dispose()
    {
        tempTarget1?.Release();
        tempTarget2?.Release();
    }四、 GaussianBlurRendererFeature的实现
1. 定义必要属性
开放所需的Compute Shader等属性,以便于在面板上设置
    [System.Serializable]
    public enum BlurMode
    {
        Horizontal,
        Vertical,
        HorizontalAndVertical,
        Full,
    }
    
    [System.Serializable]
    public class Settings
    {
        [Range(0,100)]public int blurRadius = 5;
        public BlurMode blurMode = BlurMode.Full;
    }
    public Settings settings = new Settings();
    public ComputeShader computeShader;
    private GaussianBlurPass blurPass;2. 创建Pass
    public override void Create()
    {
        blurPass = new GaussianBlurPass(settings.computeShader);
    }3. 提交Pass
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (settings.computeShader == null)
        {
            Debug.LogError("Compute Shader is missing!");
            return;
        }
        blurPass.computeShader = settings.computeShader;
        blurPass.blurRadius = settings.blurRadius;
        blurPass.blurMode = settings.blurMode;
        renderer.EnqueuePass(blurPass);
    }4. 释放资源
    protected override void Dispose(bool disposing)
    {
        blurPass.Dispose();
    }五、 执行效果


水平模糊

竖直模糊

先水平再竖直模糊

二维卷积模糊
重要
两个一维高斯模糊比一个二维高斯模糊更优,但效果并无差异。
将模糊半径设置为60:
- 先水平再竖直模糊,平均帧率为275FPS
- 二维卷积模糊,平均帧率为24FPS

先水平再竖直模糊

二维卷积模糊
