游戏后处理6大常用模糊算法

  • 模糊算法介绍
  • BoxBlur均值模糊
  • 多次迭代
  • 降低采样进行采样-优化
  • 9次采样BoxBlur
  • 高斯模糊
  • 双重模糊技术(Dual Blur)
  • Kawase模糊(Kawase Blur)
  • 径向模糊(Radial Blur)
  • 方向模糊(Directional Blur)


模糊算法介绍

模糊是游戏后处理特效中特别常用的一种,常用于表示速度,故障,环境视角,毛玻璃等。

深度学习去模糊原理 模糊算法有哪些_迭代

BoxBlur均值模糊

对当前像素周围4个角的像素进行采样,最后求均值。通过多次迭代叠加模糊效果可以获得更好的效果。

深度学习去模糊原理 模糊算法有哪些_算法_02

深度学习去模糊原理 模糊算法有哪些_迭代_03

  • 片段着色器代码
sampler2D _MainTex;
// Unity内置变量获取贴图的像素, x = 1/width, y = 1/height, z = width w=height
float4 _MainTex_TexelSize;
float _BoxBlurOffset;

fixed4 frag(v2f_img i) : SV_Target
{
    float4 uv = _MainTex_TexelSize.xyxy * half4(1, 1, -1, -1) * _BoxBlurOffset;
    float4 col2 = tex2D(_MainTex, i.uv + uv.xy) // 1 1
        + tex2D(_MainTex, i.uv + uv.xw) // 1 -1
        + tex2D(_MainTex, i.uv + uv.zy) // -1 1
        + tex2D(_MainTex, i.uv + uv.zw); // -1 -1
    col2 *= 0.25f;
    return fixed4(col2.rgb, 1);
}
多次迭代

可以在后处理中循环中进行多次模糊来提升模糊效果。使用滚动数组代替两个变量的拷贝,减少一半的拷贝次数。

[ExecuteInEditMode()] // 脚本在编辑模式下也运行
public class BoxBlurImageEffect : MonoBehaviour
{
    public Material material;
    public int count = 1;
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (count <= 0) //  如果不模糊,避免多余计算
        {
            Graphics.Blit(src, dest);
            return;
        }
        var tempTextureSrc = RenderTexture.GetTemporary(src.width, src.height); // 申请临时贴图(创建缓冲区) 
        var tempTextureDest = RenderTexture.GetTemporary(src.width, src.height); // 申请临时贴图(创建缓冲区) 
      	RenderTexture[] tempTexArray = { tempTextureSrc, tempTextureDest }; // 滚动数组
      	int flag = 0;
        Graphics.Blit(src, tempTextureSrc);
      
        for (int i = 0; i < count; i++) // 多次迭代进行模糊
        {
            // 使用material中的shader处理src(帧缓冲区的像素)。并将结果给dest
            Graphics.Blit(tempTexArray[flag], tempTexArray[1 - flag], material);
            flag = 1 - flag;
            // Graphics.Blit(tempTextureSrc, tempTextureDest, material);
            // Graphics.Blit(tempTextureDest, tempTextureSrc);
        }
      	Graphics.Blit(tempTexArray[flag], dest);
        //Graphics.Blit(tempTextureSrc, dest);
        RenderTexture.ReleaseTemporary(tempTextureSrc); // 不释放会造成内存泄漏
    		RenderTexture.ReleaseTemporary(tempTextureDest);
        Debug.Log("exec Render Image");
    }
}
降低采样进行采样-优化

降低每次进行模糊采样的像素

private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    if (count <= 0) //  如果不模糊,避免多余计算
    {
        Graphics.Blit(src, dest);
        return;
    }
    int width = src.width / 2;
    int height = src.height / 2;
    var tempTextureSrc = RenderTexture.GetTemporary(width, height); //  申请像素大小相同的临时贴图(创建缓冲区) 
    var tempTextureDest = RenderTexture.GetTemporary(width, height);
    RenderTexture[] tempTexArray = { tempTextureSrc, tempTextureDest }; // 滚动数组
    int flag = 0;
    Graphics.Blit(src, tempTextureSrc);
    
    for (int i = 0; i < count; i++) // 多次迭代进行模糊
    {
        // 使用material中的shader处理src(帧缓冲区的像素)。并将结果给dest
        Graphics.Blit(tempTexArray[flag], tempTexArray[1 - flag], material, 1);
        flag = 1 - flag;
    }
    Graphics.Blit(tempTexArray[flag], dest);
    RenderTexture.ReleaseTemporary(tempTextureSrc); // 不释放会造成内存泄漏
    RenderTexture.ReleaseTemporary(tempTextureDest);
}

9次采样BoxBlur

上面只对像素周围4个角进行了采样,我们可以通过对周围9个像素(包括当前像素)进行采样,然后再平均。

fixed4 box_blur_9tap_frag(v2f_img i) : SV_Target
{
    float4 uv = _MainTex_TexelSize.xyxy * half4(1, 1, -1, -1) * _BoxBlurOffset;
    float4 col2 = tex2D(_MainTex, i.uv + uv.xy) // 1 1
        + tex2D(_MainTex, i.uv + uv.xw) // 1 -1
        + tex2D(_MainTex, i.uv + uv.zy) // -1 1
        + tex2D(_MainTex, i.uv + uv.zw); // -1 -1

    // 计算中心, 和4个方向
    float2 uv2 = _MainTex_TexelSize.xy * _BoxBlurOffset;
    col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(1, 0));
    col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(0, 1));
    col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(-1, 0));
    col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(0, -1));
    col2 += tex2D(_MainTex, i.uv);
    col2 *= 0.111111; // 1/9
    return fixed4(col2.rgb, 1);
}

高斯模糊

采样次数,5x5,采样次数为25次。这样的操作实在太耗费性能了。Unity Shader入门精要中介绍了一种优化的方法。我们可以将一次模糊拆分成两次,每次采样5个像素。这样采样次数从25次降到了10次,并且两次模糊效果完全相同。这种优化技巧也可以使用到其他的卷积核中。

深度学习去模糊原理 模糊算法有哪些_迭代_04


深度学习去模糊原理 模糊算法有哪些_unity_05


实现方式和BoxBlur大同小异,不再给出代码。

双重模糊技术(Dual Blur)

双重模糊是一种模糊技巧,并不是模糊算法:

先将图像进行降采样模糊,然后升采样模糊。能够以很少的迭代次数达到非常好的模糊效果。模糊时可以使用任意的模糊算法。

缺点:在模糊半径参数较小时会出现一些奇怪的效果。

是游戏中一种性价比非常高的模糊算法。常用于光晕,Bloom效果的模糊计算。

深度学习去模糊原理 模糊算法有哪些_迭代_06

仅仅迭代2两次的效果,并且无条纹状瑕疵。

深度学习去模糊原理 模糊算法有哪些_迭代_07

[ExecuteInEditMode()] // 脚本在编辑模式下也运行
public class DualBlur : MonoBehaviour
{
    public Material material;

    [Range(0, 10)]
    public int iteration = 1;

    public float boxBlurRadius = 5.0f;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (iteration <= 0) //  如果不模糊,避免多余计算
        {
            Graphics.Blit(src, dest);
            return;
        }

        int width = src.width;
        int height = src.height;
        material.SetVector("_BoxBlurOffsetVec", new Vector4(boxBlurRadius / width, boxBlurRadius / height, 0, 0));
        
		// 提前分配好降采样使用的数组, 方便进行重用,降采样时分配,升采样可以复用完成效果
        RenderTexture[] dualIterationTempTexArray = new RenderTexture[iteration + 1]; 
        dualIterationTempTexArray[0] = RenderTexture.GetTemporary(width, height);
        var tempTextureSrc = dualIterationTempTexArray[0];
        RenderTexture tempTextureDest = null; //  申请像素大小相同的临时贴图(创建缓冲区)

        Graphics.Blit(src, tempTextureSrc);
        int flag = 0;
        // 降采样模糊 - 逐渐缩小
        for (int i = 0; i < iteration; i++) // 多次迭代进行模糊
        {
            flag++;
            width >>= 1;
            height >>= 1;
            tempTextureDest = RenderTexture.GetTemporary(width, height);
            dualIterationTempTexArray[flag] = tempTextureDest;
            
            Graphics.Blit(tempTextureSrc, tempTextureDest, material, 1);
            tempTextureSrc = dualIterationTempTexArray[flag]; // src = dest
        }

        tempTextureSrc = dualIterationTempTexArray[flag];
        // 升采样模糊 - 逐渐放大
        for (int i = 0; i < iteration; i++) // 多次迭代进行模糊
        {
            flag--;
            tempTextureDest = dualIterationTempTexArray[flag];
            Graphics.Blit(tempTextureSrc, tempTextureDest, material, 1);
            tempTextureSrc = dualIterationTempTexArray[flag]; // src = dest
        }
        
        Graphics.Blit(tempTextureSrc, dest);
        
        for (int i = 0; i <= iteration; i++) // 释放
            RenderTexture.ReleaseTemporary(dualIterationTempTexArray[i]);
    }

Kawase模糊(Kawase Blur)

多用于光晕,Bloom模糊。是一种非常好的模糊算法,多用于手游。实践数据表明,在相似的模糊表现下,Kawase Blur比经过优化的高斯模糊的性能约快1.5倍到3倍

Kawase Blur的思路是对距离当前像素越来越远的地方对四个角进行采样,且在两个大小相等的纹理之间进行乒乓式的blit。

深度学习去模糊原理 模糊算法有哪些_游戏_08


深度学习去模糊原理 模糊算法有哪些_unity_09

具体思路是在runtime层(CPU),基于当前迭代次数,对每次模糊的半径进行设置,而Shader层实现一个4 tap的Kawase Filter即可:

片元shander

half4 KawaseBlur(TEXTURE2D_ARGS(tex, samplerTex), float2 uv, float2 texelSize, half pixelOffset)
{
    half4 o = 0;
    o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(pixelOffset +0.5, pixelOffset +0.5) * texelSize); 
    o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(-pixelOffset -0.5, pixelOffset +0.5) * texelSize); 
    o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(-pixelOffset -0.5, -pixelOffset -0.5) * texelSize); 
    o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(pixelOffset +0.5, -pixelOffset -0.5) * texelSize); 
    return o * 0.25;
}

同样,对模糊半径(Blur Radius)参数的调节,可以控制Dual Kawase Blur模糊的程度:

径向模糊(Radial Blur)

径向模糊(Radial Blur)可以给画面带来很好的速度感,是各类游戏中后处理的常客,也常用于Sun Shaft等后处理特效中作为光线投射的模拟。

深度学习去模糊原理 模糊算法有哪些_迭代_10

径向模糊的原理比较直接,首先选取一个径向轴心(Radial Center),然后将每一个采样点的uv基于此径向轴心进行偏移(offset),并进行一定次数的迭代采样,最终将采样得到的RGB值累加,并除以迭代次数。

深度学习去模糊原理 模糊算法有哪些_unity_11

后处理代码

//径向模糊
[ExecuteInEditMode()]
public class RadialBlur : MonoBehaviour
{
    public Material material;
    public Vector2 radialCenter; // 径向模糊中心 [0, 1], uv坐标系中心
    [Range(0, 30)]
    public int iteration = 1; // 超过30会出现画面变暗(留个坑,不知道为啥)
    [Range(0, 30)]
    public float blurRadius = 1.0f;
    
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (iteration <= 0)
        {
            Graphics.Blit(src, dest, material);
            return;
        }
        material.SetInt("_Iteration", iteration);
        var blurRadiusVec = new Vector4(blurRadius/src.width, blurRadius/src.height);
        material.SetVector("_BlurRadius", blurRadiusVec);
        material.SetVector("_RadialCenter", new Vector4(radialCenter.x, radialCenter.y, 0, 0));
        Graphics.Blit(src, dest, material);
    }
}
片元shader

float4 _RadialCenter;
float4 _BlurRadius;
int _Iteration;

fixed4 frag (v2f_img i) : SV_Target
{
    const float2 offsetVec =  (_RadialCenter.xy - i.uv) * _BlurRadius;
    half4 color = 0;
    
    // [unroll(30)] --- 留个坑 好像和GPU循环优化有关
    for (int j = 0; j < _Iteration; j++)
    {
        color += tex2D(_MainTex, i.uv + offsetVec);
        i.uv.xy += offsetVec;
    }
    return color / _Iteration;
}

方向模糊(Directional Blur)

方向模糊(Directional Blur)可以看做是径向模糊(Radial Blur)的一个变体。其主要思路是传入一个角度,然后在runtime层计算出对应的矢量方向:
然后,在Shader层,将每一个采样点的uv基于此方向进行正负两次偏移(offset),接着进行一定次数的迭代采样,最终将采样得到的RGB值累加,并除以迭代次数,得到最终的输出。

后处理代码

[ExecuteInEditMode()]
public class DirectionBlur : MonoBehaviour
{
    public Material material;
    public float directionAngle; // 模糊方向
    [Range(0, 3)]
    public float blurRadius; 
    [Range(0, 20)]
    public int iteration; // 迭代次数
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (iteration <= 0)
        {
            Graphics.Blit(src, dest);
            return;
        }
        var sinVal = Mathf.Sin(directionAngle) * blurRadius * 0.05f / iteration;
        var cosVal = Mathf.Cos(directionAngle) * blurRadius * 0.05f / iteration;
        material.SetVector("_Direction", new Vector2(sinVal, cosVal));
        material.SetInt("_Iteration", iteration);
        Graphics.Blit(src, dest, material);
    }
}
片元shader

int _Iteration;
float4 _Direction;

fixed4 frag (v2f_img i) : SV_Target
{
    half4 color = 0;
    for (int k = -_Iteration; k < _Iteration; k++)
    {
        color += tex2D(_MainTex, i.uv - _Direction.xy * k);
    }
    
    return color / (_Iteration * 2);
}