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


    模糊算法介绍

    模糊是游戏后处理特效中特别常用的一种,常用于表示速度,故障,环境视角,毛玻璃等。
    在这里插入图片描述

    BoxBlur均值模糊

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

    • 片段着色器代码
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    多次迭代

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

    [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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    降低采样进行采样-优化

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    高斯模糊

    采样次数,5x5,采样次数为25次。这样的操作实在太耗费性能了。Unity Shader入门精要中介绍了一种优化的方法。我们可以将一次模糊拆分成两次,每次采样5个像素。这样采样次数从25次降到了10次,并且两次模糊效果完全相同。这种优化技巧也可以使用到其他的卷积核中。
    在这里插入图片描述
    在这里插入图片描述
    实现方式和BoxBlur大同小异,不再给出代码。

    双重模糊技术(Dual Blur)

    双重模糊是一种模糊技巧,并不是模糊算法:
    先将图像进行降采样模糊,然后升采样模糊。能够以很少的迭代次数达到非常好的模糊效果。模糊时可以使用任意的模糊算法。
    缺点:在模糊半径参数较小时会出现一些奇怪的效果。
    是游戏中一种性价比非常高的模糊算法。常用于光晕,Bloom效果的模糊计算。
    在这里插入图片描述

    仅仅迭代2两次的效果,并且无条纹状瑕疵。
    在这里插入图片描述

    [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]);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    Kawase模糊(Kawase Blur)

    多用于光晕,Bloom模糊。是一种非常好的模糊算法,多用于手游。实践数据表明,在相似的模糊表现下,Kawase Blur比经过优化的高斯模糊的性能约快1.5倍到3倍
    Kawase Blur的思路是对距离当前像素越来越远的地方对四个角进行采样,且在两个大小相等的纹理之间进行乒乓式的blit。
    在这里插入图片描述
    在这里插入图片描述

    具体思路是在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    径向模糊(Radial Blur)

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

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

    后处理代码
    
    //径向模糊
    [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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    片元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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    方向模糊(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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    片元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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    CoreData事务记录查询(Query)中NSPersistentHistoryTransaction.fetchRequest总返回nil的解决
    java通过opencv解析二维码(微信开源解码工具)
    startUML设计
    打印机选择问题咨询 -11111
    Android 自定义view中根据状态修改drawable图片
    docker基础
    百度安全多篇议题入选Blackhat Asia以硬技术发现“芯”问题
    01-Sentinel与spring-cloud的整合
    利用热点事件来创作软文的3大技巧?自媒体人必看
    程序猿学习抖音短视频制作
  • 原文地址:https://blog.csdn.net/qq_41709801/article/details/127767515