Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom

Posted 九九345

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom相关的知识,希望对你有一定的参考价值。

原本打算写高斯模糊和双重模糊两个实现Bloom方法的对比,但两个加在一起篇幅过长,于是拆成两篇文章来进行。

学习前建议应先搞清楚的几个概念

  • HDR
  • LDR
  • ToneMapping
  • 几种模糊算法

1 高斯模糊实现Bloom

最近一直在学习Unity Shader实现各种后处理效果,Bloom效果就是其中之一,它也是游戏中最常见的效果之一,也是必不可少的效果之一吧!百人计划就有专门介绍实现Bloom的过程,跟《Unity Shader 入门精要》12.5章介绍的Bloom效果实现方法是一样的:

  • 根据一个阈值提取屏幕图像中较亮的区域,储存在一张RT中
  • 利用高斯模糊进行模糊处理
  • 将模糊后的结果与最初的屏幕图像混合

经过以上三个大步骤就得到了最终的Bloom效果,步骤的Shader一共包含了4个Pass(中间2个Pass是高斯模糊分成的水平和竖直方向)。先不谈这个方法跟其他方法比较有什么优劣,我们先过一遍实现过程。

1.1 C#脚本

跟之前写过的边缘检测/高斯模糊后处理一样,实现Bloom也需要C#脚本和Unity Shader一起完成。

完整脚本在这:

//jiujiu345
//2022.11.14
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;

[ExecuteInEditMode]
public class Bloom : MonoBehaviour

    public Shader bloomShader;
    public Material bloomMaterial;
    [Header("模糊迭代次数")]
    [Range(0, 4)]
    public int interations = 2;

    [Header("模糊范围")]
    [Range(0.3f, 3.0f)]
    public float blurSpread = 0.3f;

    [Header("降采样次数")]
    [Range(1, 8)]
    public int downSample = 1;

    //控制提取较亮区域时使用的阈值大小
    //开启HDR后,亮度值会超过1,所以范围在1~4
    [Header("阈值")]
    [Range(0.0f, 4.0f)]
    public float luminanceThreshold = 0.6f;
    public void OnRenderImage (RenderTexture source, RenderTexture destination)
    
        if(bloomMaterial != null)
        
            bloomMaterial.SetFloat("_LuminanceThreshold", luminanceThreshold);

            int rtW = source.width / downSample;
            int rtH = source.height / downSample;
            //定义rt
            RenderTexture rt0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            rt0.filterMode = FilterMode.Bilinear;
            //第一个Pass提取图片较亮的部分
            Graphics.Blit(source, rt0, bloomMaterial, 0);

            for(int i = 0; i < interations; i++)
            
                bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);

                RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                //第二个Pass
                Graphics.Blit(rt0, rt1, bloomMaterial, 1);
                RenderTexture.ReleaseTemporary(rt0);
                rt0 = rt1;
                rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                //第三个Pass
                Graphics.Blit(rt0, rt1, bloomMaterial, 2);
                RenderTexture.ReleaseTemporary(rt0);
                rt0 = rt1;
            
            //rt0储存模糊后的图片
            bloomMaterial.SetTexture("_Bloom", rt0);
            //第四个Pass-混合
            Graphics.Blit(source, destination, bloomMaterial, 3);
            RenderTexture.ReleaseTemporary(rt0);
        
        else
        
            Debug.Log("Please input your Material");
            Graphics.Blit(source, destination);
        
    

可控参数

脚本中提供了四个用户可控参数:

  • 模糊迭代次数——模糊效果不断叠加
  • 模糊范围——每次高斯模糊的模糊范围
  • 降采样次数
  • 阈值——亮度超过该阈值的区域才能被提取

传递给Material的参数

通过.SetFloat()进行了基本参数传递,分别传递了:

  • luminanceThreshold——阈值
  • blurspread——模糊范围,_BlurSize=1+i*blurSpread,迭代次数越多范围越大

通过.SetTexture()传递了储存模糊结果的渲染纹理_Bloom

四个Pass

四个Pass都是通过调用Graphics.Blit()实现的,高斯模糊的第二和第三个Pass由于要实现模糊迭代,引入了一个for循环进行模糊迭代,两个渲染纹理rt0rt1交替储存结果:

for(int i = 0; i < interations; i++)

    bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);

    RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

    //第二个Pass
    Graphics.Blit(rt0, rt1, bloomMaterial, 1);
    RenderTexture.ReleaseTemporary(rt0);
    rt0 = rt1;
    rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);

    //第三个Pass
    Graphics.Blit(rt0, rt1, bloomMaterial, 2);
    RenderTexture.ReleaseTemporary(rt0);
    rt0 = rt1;

1.2 Shader代码

即四个Pass的具体顶点和片元着色器,完整代码在这:

//jiujiu345
//2022.11.14
Shader "Unity Shaders Book/Chapter 12/Bloom_GaussianBlur"

    Properties
    
        _MainTex ("Base(RGB)", 2D) = "white"  //src
        _Bloom("Bloom(RGB)", 2D) = "black"  //高斯模糊后的较亮区域
        //无须定义在Shader面板,采取C#脚本控制
        //_LuminanceThreshold("Luminance Threshold", Float) = 0.5  //提取较亮区域的阈值
        //_BlurSize("Blur Size", Float) = 1.0  //模糊区域范围
    
    SubShader
    
        CGINCLUDE

        #include "UnityCG.cginc"

        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _Bloom;
        float _LuminanceThreshold;
        float _BlurSize;

        //Pass0-提取较亮区域
        struct v2f_Extract 
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0; 
        ;

        v2f_Extract vertExtractBright(appdata_img v) 
            v2f_Extract o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        
        //计算像素的亮度
        fixed Luminance(fixed4 color) 
            return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
        

        //用luminanceThreshold控制亮度强度
        fixed4 fragExtractBright(v2f_Extract i) : SV_Target 
            fixed4 c = tex2D(_MainTex, i.uv);
            fixed val = saturate(Luminance(c) - _LuminanceThreshold);
            return val * c;
        

        //Pass2&3-高斯模糊
        struct v2f_Gaussian 
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;  
        ;

        //水平
        v2f_Gaussian vertBlurVertical(appdata_img v) 
            v2f_Gaussian o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

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

            return o;
        

        //竖直
        v2f_Gaussian vertBlurHorizontal(appdata_img v) 
            v2f_Gaussian o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

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

            return o;
        

        fixed4 GaussianBlur(v2f_Gaussian i) : SV_Target 
            float weight[3] = 0.4026, 0.2442, 0.0545;
            fixed3 color = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
            
            for(int j=1;j<3;j++) 
                color += tex2D(_MainTex, i.uv[2*j-1]).rgb * weight[j];
                color += tex2D(_MainTex, i.uv[2*j]).rgb * weight[j];
            
            return fixed4(color, 1.0);
        

        //Pass3-混合亮度和原图
        struct v2f_Bloom 
            float4 pos : SV_POSITION;
            half4 uv : TEXCOORD0;
        ;

        v2f_Bloom vertBloom(appdata_img v) 
            v2f_Bloom o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv.xy = v.texcoord;
            o.uv.zw = v.texcoord;

            //用以判断是否在Direct3D平台
            #if UNITY_UV_STARTS_AT_TOP
            if(_MainTex_TexelSize.y < 0.0) 
                o.uv.w = 1.0 - o.uv.w;
            
            #endif

            return o;
        

        fixed4 fragBloom(v2f_Bloom i) : SV_Target 
            return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
        

        ENDCG
        
        ZTest Always
        Cull Off
        ZWrite Off

        Pass 
            CGPROGRAM
            #pragma vertex vertExtractBright
            #pragma fragment fragExtractBright

            ENDCG
        
        
        Pass 
            CGPROGRAM
            #pragma vertex vertBlurVertical
            #pragma fragment GaussianBlur

            ENDCG
        

        Pass 
            CGPROGRAM
            #pragma vertex vertBlurHorizontal
            #pragma fragment GaussianBlur

            ENDCG
        

        Pass 
            CGPROGRAM
            #pragma vertex vertBloom
            #pragma fragment fragBloom

            ENDCG
        
    
    FallBack  Off

关于Properties语义块声明的属性

发现在Properties语义只声明了两个用到的Texture属性:

  • 一个是source纹理会被传递给Shader中的_MainTex
  • 一个是C#脚本中定义的储存模糊后图像的渲染纹理_Bloom

其实这里_Bloom也是可以省略的,但是加上的话就可以在材质面板上实时看到模糊后的图像了。接下来可以直接在CGINCLUDE--ENDCG定义的代码段中定义Shader需要的变量,很大一部分直接来自C#脚本传入的变量。

还有一点废话:如果一个变量在Properties中被声明,又在C#脚本中被定义,可能会出现脚本界面改动变量无效的情况,为了避免这个情况我们只其一就行(我一般选择脚本界面改动)。

如何提取亮度区域?

根据RGB三通道的值转亮度值,具体为什么这么做可以参考这篇文章的亮度部分:

//计算像素的亮度
        fixed Luminance(fixed4 color) 
            return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
        

 接着加入C#脚本传递过来的阈值参数,用一个saturate(Luminance(c) - _LuminanceThreshold)提取阈值以上的亮度。

关于高斯模糊那块儿,上一篇博客已经涉及到了,这里直接搬过来代码就行!

1.3 Bloom效果展示

参数设置如下:

后处理前后对比如下:

1.4 FrameDebug看看过程

又要打开我们的老朋友Frame Debugger了:

可以看到整个后处理经历了6个步骤,如下动图:

经历的Pass顺序是:Pass0 -> Pass1 -> Pass2 -> Pass1 -> Pass2 -> Pass3,经历两边1&2是因为迭代次数选择了2.

2 进一步实现自发光Bloom

Unity其实是自带Bloom效果的,我们直接上对比就能感受到区别了。

2.1 准备场景

场景中拖入随便一个模型,关闭场景中的光源(为了更好的观察效果),然后给模型拖入Unity的Standard材质,打开Emission,设置如下:

未进行任何后处理的初始效果如下:

接下来进行对比操作。

2.2 方法1:高斯模糊

基于当前的代码实现自发光Bloom,效果其实是比较差的。。.下图是我尝试调整到了最能体现自发光的效果:

2.3 方法2:Unity自带的Bloom

顺便提一嘴:Unity自带的Bloom应该是采用降采样+升采样——双重模糊的模糊算法(我不是特别确定),关于到底如何降/升采样我们后面就会讲到。

我们再用Unity自带的Post Processing里的Bloom效果 (添加方式可参考(50) EPIC GLOW IN UNITY 2020.2 - YouTube),看看出来的效果是什么样的:

由于参数设置等一些原因没办法从性能上进行一个对比,但单从两种方法“能调整到的最好的实现效果”上,后者完胜。

3 谈谈高斯模糊实现Bloom的优缺点

推荐结合这一篇文章来看第3小节。

3.1 优点

不知道算不算“优点”:这个方法对于实现简单的大片的泛光加强本来亮点就很明显的场景泛光效果还是蛮不错的,除了上述天空的例子,再举两个例子:

(原图来自ArtStation - Jinji's Grotto, Connor Sheehan

 (原图来自ArtStation - Vermillion Forest, Anton Fadeev

3.2 缺点1:性能上

因为模糊算法用的是高斯模糊,高斯模糊本质还是卷积核,如果我们想要大范围的Bloom效果,就只能靠增大滤波范围or增加滤波次数来实现,基于上述代码的话采取操作分别是:

  • 增加滤波次数——Interation↑
  • 增大滤波范围——BlurSpread↑

首先增大滤波次数相当于多来几次Pass1&2,一下子性能消耗就上去了,其次如果真的实践你会发现,无论是增加滤波次数还是增大滤波范围,达到的扩大效果也不是很能让人满意。

3.3 缺点2:实现效果上

除了上述的自发光实现效果,我在调整的过程中还感受到:高斯模糊实现的自发光Bloom总是有一种明显的边界感。这是为什么?

我猜应该是因为高斯模糊(感觉只要是基于卷积核的模糊都是一样的)总是依据卷积核每一格的权重进行加权平均计算出中间项的值,所以即使降采样了,每一level之间还是会存在明显的亮度突变,于是源图和模糊后的图的亮度无论调整哪个参数都不会过渡均匀。


下一篇将会介绍双重模糊实现Bloom的方法。(断更了很久了,最近简直是从石头缝里挤时间来额外学习TA的内容,太难了太难了)

Unity shader学习之屏幕后期处理效果之高斯模糊

高斯模糊,见 百度百科

也使用卷积来实现,每个卷积元素的公式为:

其中б是标准方差,一般取值为1。

x和y分别对应当前位置到卷积中心的整数距离。

由于需要对高斯核中的权重进行归一化,即使所有权重相加为1,因此e前面的系数实际不会对结果产生任何影响。

转载请注明出处:http://www.cnblogs.com/jietian331/p/7238032.html

综上,公式简化为:

G(x,y) = e-(x*x+y*y)/2

 

因此,高斯核计算代码如下:

  1 using System;
  2 
  3 namespace TestShell
  4 {
  5     class Program
  6     {
  7         static void Main(string[] args)
  8         {
  9             Console.WriteLine("输入需要得到的高斯卷积核的维数(如3,5,7...):");
 10 
 11             string input = Console.ReadLine();
 12             int size;
 13 
 14             if (!int.TryParse(input, out size))
 15             {
 16                 Console.WriteLine("不是数字...");
 17                 return;
 18             }
 19 
 20             // 计算
 21             double[] r2 = null;
 22             double[,] r = null;
 23             try
 24             {
 25                 r = CalcGaussianBlur(size, out r2);
 26             }
 27             catch (Exception ex)
 28             {
 29                 Console.WriteLine("错误: " + ex.Message);
 30             }
 31 
 32             if (r != null && r2 != null)
 33             {
 34                 // 卷积如下:
 35                 Console.WriteLine();
 36                 Console.WriteLine("{0}x{0}的高斯卷积核如下:", size);
 37                 for (int i = 0; i < r.GetLongLength(0); i++)
 38                 {
 39                     for (int j = 0; j < r.GetLongLength(1); j++)
 40                     {
 41                         Console.Write("{0:f4}\\t", r[i, j]);
 42                     }
 43                     Console.WriteLine();
 44                 }
 45                 Console.WriteLine();
 46 
 47                 Console.WriteLine("可拆成2个一维的数组:");
 48                 for (int i = 0; i < r2.Length; i++)
 49                 {
 50                     Console.Write("{0:f4}\\t", r2[i]);
 51                 }
 52                 Console.WriteLine();
 53                 Console.WriteLine();
 54 
 55                 Console.WriteLine("验证,使用这2个一维的数组也可以得到同样的结果:");
 56                 for (int i = 0; i < size; i++)
 57                 {
 58                     for (int j = 0; j < size; j++)
 59                     {
 60                         Console.Write("{0:f4}\\t", r2[i] * r2[j]);
 61 
 62                     }
 63                     Console.WriteLine();
 64                 }
 65             }
 66 
 67             Console.WriteLine();
 68             Console.WriteLine("按任意键结束...");
 69             Console.ReadKey();
 70         }
 71 
 72         static double[,] CalcGaussianBlur(int size, out double[] r2)
 73         {
 74             if (size < 3)
 75                 throw new ArgumentException("size < 3");
 76             if (size % 2 != 1)
 77                 throw new ArgumentException("size % 2 != 1");
 78 
 79             double[,] r = new double[size, size];
 80             r2 = new double[size];
 81             int center = (int)Math.Floor(size / 2f);
 82             double sum = 0;
 83 
 84             for (int i = 0; i < size; i++)
 85             {
 86                 for (int j = 0; j < size; j++)
 87                 {
 88                     int x = Math.Abs(i - center);
 89                     int y = Math.Abs(j - center);
 90                     double d = CalcItem(x, y);
 91                     r[i, j] = d;
 92                     sum += d;
 93                 }
 94             }
 95 
 96             for (int i = 0; i < size; i++)
 97             {
 98                 for (int j = 0; j < size; j++)
 99                 {
100                     r[i, j] /= sum;
101                     if (i == j)
102                         r2[i] = Math.Sqrt(r[i, i]);
103                 }
104             }
105 
106             return r;
107         }
108 
109         static double CalcItem(int x, int y)
110         {
111             return Math.Pow(Math.E, -(x * x + y * y) / 2d);
112         }
113     }
114 }
View Code

 

工具在: http://files.cnblogs.com/files/jietian331/CalcGaussianBlur.zip

 

一个5 x 5的高斯核如下:

 

使用2个一维数组可简化计算量,提高性能,通过观察可知,只需要计3个数:

 

使用unity shader屏幕后期处理来实现高斯模糊,代码如下。

子类:

 1 using UnityEngine;
 2 
 3 public class GaussianBlurRenderer : PostEffectRenderer
 4 {
 5     [Range(1, 8)]
 6     [SerializeField]
 7     public int m_downSample = 2;      // 降采样率
 8     [Range(0, 4)]
 9     [SerializeField]
10     public int m_iterations = 3;        // 迭代次数
11     [Range(0.2f, 3f)]
12     [SerializeField]
13     public float m_blurSpread = 0.6f;        // 模糊扩散量
14 
15     protected override void OnRenderImage(RenderTexture src, RenderTexture dest)
16     {
17         int w = (int)(src.width / m_downSample);
18         int h = (int)(src.height / m_downSample);
19         RenderTexture buffer0 = RenderTexture.GetTemporary(w, h);
20         RenderTexture buffer1 = RenderTexture.GetTemporary(w, h);
21         buffer0.filterMode = FilterMode.Bilinear;
22         buffer1.filterMode = FilterMode.Bilinear;
23         Graphics.Blit(src, buffer0);
24 
25         for (int i = 0; i < m_iterations; i++)
26         {
27             Mat.SetFloat("_BlurSpread", 1 + i * m_blurSpread);
28 
29             Graphics.Blit(buffer0, buffer1, Mat, 0);
30             Graphics.Blit(buffer1, buffer0, Mat, 1);
31         }
32 
33         Graphics.Blit(buffer0, dest);
34         RenderTexture.ReleaseTemporary(buffer0);
35         RenderTexture.ReleaseTemporary(buffer1);
36     }
37 
38     protected override string ShaderName
39     {
40         get { return "Custom/Gaussian Blur"; }
41     }
42 }
GaussianBlurRenderer

 

shader:

 1 // Upgrade NOTE: replaced \'mul(UNITY_MATRIX_MVP,*)\' with \'UnityObjectToClipPos(*)\'
 2 
 3 Shader "Custom/Gaussian Blur"
 4 {
 5     Properties
 6     {
 7         _MainTex("Main Texture", 2D) = "white" {}
 8         _BlurSpread("Blur Spread", float) = 1
 9     }
10 
11     SubShader
12     {
13         CGINCLUDE
14         
15         sampler2D _MainTex;
16         float4 _MainTex_TexelSize;
17         uniform float _BlurSpread;
18 
19         struct appdata
20         {
21             float4 vertex : POSITION;
22             float2 uv : TEXCOORD0;
23         };
24 
25         struct v2f
26         {
27             float4 pos : SV_POSITION;
28             float2 uv[5] : TEXCOORD0;
29         };
30 
31         v2f vertHorizontal(appdata v)
32         {
33             v2f o;
34             o.pos = UnityObjectToClipPos(v.vertex);
35             float tsx = _MainTex_TexelSize.x * _BlurSpread;
36             o.uv[0] = v.uv + float2(tsx * -2, 0);
37             o.uv[1] = v.uv + float2(tsx * -1, 0);
38             o.uv[2] = v.uv;
39             o.uv[3] = v.uv + float2(tsx * 1, 0);
40             o.uv[4] = v.uv + float2(tsx * 2, 0);
41             return o;
42         }
43 
44         v2f vertVertical(appdata v)
45         {
46             v2f o;
47             o.pos = UnityObjectToClipPos(v.vertex);
48             float tsy = _MainTex_TexelSize.y * _BlurSpread;
49             o.uv[0] = v.uv + float2(0, tsy * -2);
50             o.uv[1] = v.uv + float2(0, tsy * -1);
51             o.uv[2] = v.uv;
52             o.uv[3] = v.uv + float2(0, tsy * 1);
53             o.uv[4] = v.uv + float2(0, tsy * 2);
54             return o;
55         }
56 
57         fixed4 frag(v2f i) : SV_TARGET
58         {
59             float g[3] = {0.0545, 0.2442, 0.4026};
60             fixed4 col = tex2D(_MainTex, i.uv[2]) * g[2];
61             for(int k = 0; k < 2; k++)
62             {
63                 col += tex2D(_MainTex, i.uv[k]) * g[k];
64                 col += tex2D(_MainTex, i.uv[4 - k]) * g[k];
65             }
66             return col;
67         }
68 
69         ENDCG
70 
71         Pass
72         {
73             Name "HORIZONTAL"
74             ZTest Always
75             ZWrite Off
76             Cull Off
77 
78             CGPROGRAM
79             #pragma vertex vertHorizontal
80             #pragma fragment frag
81             ENDCG
82         }
83 
84         Pass
85         {
86             Name "VERTICAL"
87             ZTest Always
88             ZWrite Off
89             Cull Off
90 
91             CGPROGRAM
92             #pragma vertex vertVertical
93             #pragma fragment frag
94             ENDCG
95         }
96     }
97 
98     Fallback Off
99 }
Custom/Gaussian Blur

 

调整参数:

DownSample,即降采样率,越大性能越好,图像越模糊,但过大可能会使图像像素化。

Iteraitions, 即迭代次数,越大图像模糊效果越好,但性能也会下降。

BlurSpread,即模糊扩散量,越大图像越模糊,但过大会造成虚影。

 

 

效果如下:

 

 

以上是关于Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom的主要内容,如果未能解决你的问题,请参考以下文章

Unity Shader​ 屏幕后处理5.0:讨论双重模糊的Bloom

Unity Shader 屏幕后效果——Bloom外发光

Unity shader学习之屏幕后期处理效果之高斯模糊

unity shader 热扭曲 (屏幕后处理)

Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果

unity shader 基础之十一 动画屏幕后处理深度纹理