[Unity] 高斯模糊效果

目录

一、什么是高斯模糊?

二、Feature脚本

GaussianBlurFeature:负责参数配置和创建渲染 Pass

GaussianBlurPass:负责纹理管理、模糊绘制与资源释放

三、Shader

四、完整代码

参考资料


一、什么是高斯模糊?

定义:通过卷积操作对图像进行平滑处理,减少噪声或细节。

这个效果的使用高频且广泛。它既可以直接柔化画面,做毛玻璃、景深虚化、Bloom 泛光、发光描边,也可以作为复合效果的中间步骤,通过与深度、遮罩及颜色混合配合,得到不错的视觉效果。

对于这个效果的原理,我们需要了解:

高斯模糊的核心是高斯核,里面的每个元素是利用一个二维高斯函数做计算,再与图片中每个像素做卷积运算。

对于这个二维高斯函数:在模糊计算中,通常把当前待处理像素作为原点,即一个二维像素位置 (x,y)。此时 x,y 表示当前像素到卷积中心的整数距离,公式简化为:

G(x,y)=\frac{1}{2\pi\sigma^2} e^{-\frac{x^2+y^2}{2\sigma^2}}

此方程表示:邻域像素离当前像素越近,对当前像素影响越大;高斯核维度越大,模糊程度越大

使用NxN的高斯核对图像进行卷积滤波,则要进行NxNxWxH次纹理采样。故在此将卷积核拆分为水平、垂直的两个一维高斯核,则仅需2xNxWxH次纹理采样。

此处采用5 x 5的卷积核。左右权重对称,只需记录3个权重值。

在unity中,采用全屏后处理的方式,通过renderer feature和render pass将其叠加到屏幕上。

加前:

 

加后:

1. 增长量、降采样不变,增大迭代次数

2. 增长量、迭代次数不变,增加降采样

以下为详细步骤:

二、Feature脚本

基类脚本封装好材质检查、Pass入队、RT管理和全屏绘制。基类脚本见:[Unity] URP Renderer Feature之基类封装-CSDN博客

GaussianBlurFeature:负责参数配置和创建渲染 Pass

1. 定义模糊参数

// 模糊迭代次数
[Range(0, 4)]
public int iterations = 3;

// 每轮采样间距的增长量
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;

// 每轮采样间距的增长量
[Range(1, 8)]
public int downSample = 2;

2. 创建Pass,并传入材质、执行时机、模糊参数

 protected override ScriptableRenderPass CreatePass()
 {
     return new GaussianBlurPass(material, renderPassEvent, iterations, blurSpread, downSample);
 }

GaussianBlurPass:负责纹理管理、模糊绘制与资源释放

1. 声明RTHandle、模糊参数

2. 构造函数接收feature传入基类

3. ProfilerTag指定名字,方便在frame debugger中调试

 protected override string ProfilerTag => "GaussianBlur";

4. OnCameraSetup:配置渲染目标(关闭深度采样、多重采样)、设置降采样尺寸、分配RTHandle、设置采样方式、名称

 RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
 descriptor.depthBufferBits = 0;
 descriptor.msaaSamples = 1;

 descriptor.width = Mathf.Max(1, descriptor.width / downSample);
 descriptor.height = Mathf.Max(1, descriptor.height / downSample);

 RenderingUtils.ReAllocateIfNeeded(ref buffer0, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer0");
 RenderingUtils.ReAllocateIfNeeded(ref buffer1, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer1");

downSample

  • 含义:降采样。使用小于原屏幕分辨率尺寸的临时RT,滤波模式双线性。
  • 优点:减少模糊处理的像素,提高性能
  • 过高降采样会导致像素化。(也许这正是你期待的)

5. Execute:高斯模糊生产线。两个临时RT在迭代中交替使用,先纵向、后横向,最后将结果写回原始RT

CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
try
{
    RTHandle source = renderingData.cameraData.renderer.cameraColorTargetHandle;

    // 对应 _MainTex_TexelSize:内置管线 blit 时自动填,URP 需要手动传(xy=1/w 1/h,zw=w h)
    material.SetVector("_MainTex_TexelSize", new Vector4(1.0f / buffer0.rt.width, 1.0f / buffer0.rt.height, buffer0.rt.width, buffer0.rt.height));
    
    // Downsample
    Blitter.BlitCameraTexture(cmd, source, buffer0);
    
    for (int i = 0; i < iterations; i++)
    {
	    // 设置本轮采样间距
        material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
        // 绘制垂直 pass:buffer0 -> buffer1
        Blit(cmd, buffer0, buffer1, material, 0);
        // 绘制水平 pass:buffer1 -> buffer0
        Blit(cmd, buffer1, buffer0, material, 1);
    }
    
    Blitter.BlitCameraTexture(cmd, buffer0, source); // 最终结果写回原始RT
    context.ExecuteCommandBuffer(cmd); // 提交给GPU执行
}
finally
{
    CommandBufferPool.Release(cmd);
}

两个 RTHandle 始终存在,只是在它们之间来回写,这叫 Ping-Pong Buffer(乒乓缓冲)。

那为什么要来回传递,而不是直接写回原图?

因为读取当前的像素,处理后传回原图位置,不安全; GPU 像素并不是按照从上到下、从左到右的固定顺序执行的,它们通常是大批并行执行。无法得知他周围的像素是否被处理过,是原像素还是新像素;

6. Dispose:释放资源、临时RT

三、Shader

1. 定义属性(与脚本中传参的属性名保持一致)

2. 定义Varyings结构体:uv[5]保存5个采样坐标(与高斯核一致)

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float2 uv[5] : TEXCOORD0;
};

3. 实现横纵坐标采样

// 竖直方向大小为5的一维纹理坐标采样
Varyings vertBlurVertical(uint vertexID : SV_VertexID)
{
    Varyings output;

    output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
    half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;

    output.uv[0] = uv;
    output.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
    output.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
    output.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
    output.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;      

    return output;
}
  • _ BlurSize表示模糊范围,即1.0f + i * blurSpread;步进距离越大,模糊时采样的点越远,模糊效果越明显

4. 片元着色器:定义高斯核权重、采样像素颜色、卷积、输出加权颜色

5. 横、纵分两个pass执行

四、完整代码

1. 脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class GaussianBlurFeature : PostEffectFeatureBase
{
    // Blur iterations - larger number means more blur.
    [Range(0, 4)]
    public int iterations = 3;

    // Blur spread for each iteration - larger value means more blur
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;

    [Range(1, 8)]
    public int downSample = 2;

    protected override ScriptableRenderPass CreatePass()
    {
        return new GaussianBlurPass(material, renderPassEvent, iterations, blurSpread, downSample);
    }
}

public class GaussianBlurPass : PostEffectPassBase
{
    private RTHandle buffer0; // 当前渲染结果所在
    private RTHandle buffer1; // 临时
    private readonly int iterations;
    private readonly float blurSpread;
    private readonly int downSample;

    // 接收feature传过来的参数
    public GaussianBlurPass(Material material, RenderPassEvent renderPassEvent, int iterations, float blurSpread, int downSample) : base(material, renderPassEvent)
    {
        this.iterations = iterations;
        this.blurSpread = blurSpread;
        this.downSample = downSample;
    }

    protected override string ProfilerTag => "GaussianBlur";

    // 对两块buffer降采样尺寸(减少处理像素个数、提高性能)、双线性过滤、无深度
    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {

        RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
        descriptor.depthBufferBits = 0;
        descriptor.msaaSamples = 1;

        descriptor.width = Mathf.Max(1, descriptor.width / downSample);
        descriptor.height = Mathf.Max(1, descriptor.height / downSample);

        RenderingUtils.ReAllocateIfNeeded(ref buffer0, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer0");
        RenderingUtils.ReAllocateIfNeeded(ref buffer1, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer1");
    }

    // 两个临时Buffer在迭代中交替使用,最终结果写回buffer0
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (material == null || buffer0 == null || buffer1 == null) return;

        CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
        try
        {
            RTHandle source = renderingData.cameraData.renderer.cameraColorTargetHandle;

            // 对应 _MainTex_TexelSize:内置管线 blit 时自动填,URP 需要手动传(xy=1/w 1/h,zw=w h)
            material.SetVector("_MainTex_TexelSize", new Vector4(1.0f / buffer0.rt.width, 1.0f / buffer0.rt.height, buffer0.rt.width, buffer0.rt.height));
            
            // Downsample
            Blitter.BlitCameraTexture(cmd, source, buffer0);
            
            for (int i = 0; i < iterations; i++)
            {
                material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
                // 绘制垂直 pass:buffer0 -> buffer1
                Blit(cmd, buffer0, buffer1, material, 0);
                // 绘制水平 pass:buffer1 -> buffer0
                Blit(cmd, buffer1, buffer0, material, 1);
            }
            
            Blitter.BlitCameraTexture(cmd, buffer0, source); // 最终结果写回原始RT
            context.ExecuteCommandBuffer(cmd); // 提交给GPU执行
        }
        finally
        {
            CommandBufferPool.Release(cmd);
        }
    }

    public override void Dispose()
    {
        base.Dispose();
        buffer0?.Release(); // 非空时释放buffer0的RTHandle
        buffer0 = null;
        buffer1?.Release();
        buffer1 = null;
    }
}

2. shader

Shader "Custom/S_GaussianBlur"
{
    Properties {
		_BlurSize ("Blur Size", Float) = 1.0
	}
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
      
            HLSLINCLUDE
 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
         
            CBUFFER_START(UnityPerMaterial)
                float _BlurSize;
            CBUFFER_END

            TEXTURE2D_X(_BlitTexture);
            SAMPLER(sampler_LinearClamp); 
            float4 _BlitScaleBias;  // Blitter 用于处理源/目标 RT 的 UV 翻转,顶点必须应用,否则画面可能上下颠倒     
            sampler2D _MainTex;  
		    half4 _MainTex_TexelSize;
			
            // 当前采样和4个邻域纹理坐标
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv[5] : TEXCOORD0;
            };

            // 竖直方向大小为5的一维纹理坐标采样
            Varyings vertBlurVertical(uint vertexID : SV_VertexID)
            {
                Varyings output;

                output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
                half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;

                output.uv[0] = uv;
                output.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
                output.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
                output.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
                output.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;      
            
                return output;
            }

            // 水平方向大小为5的一维纹理坐标采样
            Varyings vertBlurHorizontal(uint vertexID : SV_VertexID)
            {
                Varyings output;

                output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
                half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;

                output.uv[0] = uv;
                output.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
                output.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
                output.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
                output.uv[4] = uv - float2( _MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;      
            
                return output;
            }

			 
			// 片元着色器
            half4 fragBlur (Varyings input) : SV_Target
            {
                float weight[3] = {0.4026, 0.2442, 0.0545}; // 各个领域像素对应权重

                half3 sum = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[0]).rgb * weight[0];

                for (int it = 1; it < 3; ++it)
                {
                    sum += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[it * 2 - 1]).rgb * weight[it];
                    sum += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[it * 2]).rgb * weight[it];
                }

                return half4(sum, 1.0);
            }
            ENDHLSL
        
            // 单Pass无法指定两个顶点着色器,拆成两个Pass
            // Pass 0:垂直
            Pass
            {
                Name "GAUSSIAN_BLUR_VERTICAL"
                ZTest Always Cull Off ZWrite Off

                HLSLPROGRAM
                #pragma vertex vertBlurVertical
                #pragma fragment fragBlur
                ENDHLSL
            }

             // Pass 1:水平
             Pass
            {
                Name "GAUSSIAN_BLUR_HORIZONTAL"
                ZTest Always Cull Off ZWrite Off

                HLSLPROGRAM
                #pragma vertex vertBlurHorizontal
                #pragma fragment fragBlur
                ENDHLSL
            }
    }
    FallBack "Hidden/Shader Graph/FallbackError"
}

参考资料

  1. 冯乐乐.《Unity Shader入门精要》.人民邮电出版社,2016.第12章,12.4 高斯模糊
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值