Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现

本篇文章将分析如何在 unity 中基于Shader实现高斯模糊屏幕后期特效。
首先放出最终的实现效果。如下几幅图,是在Unity中使用本文所实现的Shader得到的高斯模糊屏幕后期特效与原始图的效果对比图。


卡通风格的效果测试:

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第1张图片 

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第2张图片 

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第3张图片 

写实风格的效果测试:

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第4张图片 

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第5张图片 

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第6张图片 

OK,下面我们开始分析如何在Unity中实现上述的高斯模糊特效。

一、降采样与高斯模糊的原理


首先梳理一下在Unity中实现高斯模糊效果需用到的几个图像处理的知识点,说起来也很巧,正好和之前我写过一个关于OpenCV的系列博客里的这篇文章(http://blog.csdn.net/poem_qianmo/article/details/22745559)涉及的知识点类似。

1.1 关于图像的降采样

降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率。对于一幅N*M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。
不难得出,降采样系数K值越大,则需要处理的像素点越少,运行速度越快。

1.2 高斯模糊的原理

高斯模糊(Gaussian Blur),也叫高斯平滑,高斯滤波,其通常用它来减少图像噪声以及降低细节层次,常常也被用于对图像进行模糊。
通俗的讲,高斯模糊就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯模糊的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

高斯分布的数学表示如下:

 

其中,x为到像素中心的距离,σ为标准差。

Unity Shader编程】之十五 屏幕高斯模糊(Gaussian Blur)后期特效的实现_第7张图片 
高斯分布(正态分布曲线)

分条来说明一下高斯模糊的几个要点:

从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。

由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。

高斯模糊能够把某一点周围的像素色值按高斯曲线统计起来,采用数学上加权平均的计算方法得到这条曲线的色值

所谓"模糊",可以理解成每一个像素都取周边像素的平均值。

图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。

高斯模糊的原理大致如此。若各位还想进一步了解,可以参考高斯模糊的wiki,以及《Real-Time Rendering 3rd》,或各种图像处理的书籍。相关参考内容见附录中的reference。

下面主要来一起看一下高斯模糊特效在Unity中的实现。

二、高斯模糊特效在Unity中的实现

Unity中的屏幕特效,通常分为两部分来实现:

Shader代码实现部分

C#/javascript代码实现部分

上述两者结合起来,便可以在Unity中实现具有很强可控性和灵活性的屏幕后期特效。

下面即是从这两个方面对高斯模糊的特效进行实现。其实现思路类似Standard Assets/Image Effect中的Blur,但是本文的实现更简洁,有更大的可控性。


2.1 Shader代码部分

本次的高斯模糊Shader包含逐行注释后约200多行。

书写思路方面,采用了3个通道(Pass)各司其职,他们分别是:

通道0:降采样通道。
通道1:垂直方向模糊处理通道。
通道2:水平方向模糊处理通道。

而三个通道中共用的变量、函数和结构体的代码位于CGINCLUDE和ENDCG之间。

以下贴出经过详细注释的Shader源码:

[C#]  纯文本查看  复制代码
?
 
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
Shader "Learning Unity Shader/Lecture 15/RapidBlurEffect" 
     //-----------------------------------【属性 || Properties】------------------------------------------   
     Properties 
    
         //主纹理 
         _MainTex( "Base (RGB)" , 2D) = "white" {} 
    
   
     //----------------------------------【子着色器 || SubShader】---------------------------------------   
     SubShader 
    
         ZWrite Off 
         Blend Off 
   
         //---------------------------------------【通道0 || Pass 0】------------------------------------ 
         //通道0:降采样通道 ||Pass 0: Down Sample Pass 
         Pass 
        
             ZTest Off 
             Cull Off 
   
             CGPROGRAM 
   
             //指定此通道的顶点着色器为vert_DownSmpl 
             #pragma vertex vert_DownSmpl 
             //指定此通道的像素着色器为frag_DownSmpl 
             #pragma fragment frag_DownSmpl 
   
             ENDCG 
   
        
   
         //---------------------------------------【通道1 || Pass 1】------------------------------------ 
         //通道1:垂直方向模糊处理通道 ||Pass 1: Vertical Pass 
         Pass 
        
             ZTest Always 
             Cull Off 
   
             CGPROGRAM 
   
             //指定此通道的顶点着色器为vert_BlurVertical 
             #pragma vertex vert_BlurVertical 
             //指定此通道的像素着色器为frag_Blur 
             #pragma fragment frag_Blur 
   
             ENDCG 
        
   
         //---------------------------------------【通道2 || Pass 2】------------------------------------ 
         //通道2:水平方向模糊处理通道 ||Pass 2: Horizontal Pass 
         Pass 
        
             ZTest Always 
             Cull Off 
   
             CGPROGRAM 
   
             //指定此通道的顶点着色器为vert_BlurHorizontal 
             #pragma vertex vert_BlurHorizontal 
             //指定此通道的像素着色器为frag_Blur 
             #pragma fragment frag_Blur 
   
             ENDCG 
        
    
   
   
     //-------------------------CG着色语言声明部分 || Begin CG Include Part----------------------   
     CGINCLUDE 
   
     //【1】头文件包含 || include 
     #include "UnityCG.cginc" 
   
     //【2】变量声明 || Variable Declaration 
     sampler2D _MainTex; 
     //UnityCG.cginc中内置的变量,纹理中的单像素尺寸|| it is the size of a texel of the texture 
     uniform half4 _MainTex_TexelSize; 
     //C#脚本控制的变量 || Parameter 
     uniform half _DownSampleValue; 
   
     //【3】顶点输入结构体 || Vertex Input Struct 
     struct VertexInput 
    
         //顶点位置坐标 
         float4 vertex : POSITION; 
         //一级纹理坐标 
         half2 texcoord : TEXCOORD0; 
     }; 
   
     //【4】降采样输出结构体 || Vertex Input Struct 
     struct VertexOutput_DownSmpl 
    
         //像素位置坐标 
         float4 pos : SV_POSITION; 
         //一级纹理坐标(右上) 
         half2 uv20 : TEXCOORD0; 
         //二级纹理坐标(左下) 
         half2 uv21 : TEXCOORD1; 
         //三级纹理坐标(右下) 
         half2 uv22 : TEXCOORD2; 
         //四级纹理坐标(左上) 
         half2 uv23 : TEXCOORD3; 
     }; 
   
   
     //【5】准备高斯模糊权重矩阵参数7x4的矩阵 ||  Gauss Weight 
     static const half4 GaussWeight[7] = 
    
         half4(0.0205,0.0205,0.0205,0), 
         half4(0.0855,0.0855,0.0855,0), 
         half4(0.232,0.232,0.232,0), 
         half4(0.324,0.324,0.324,1), 
         half4(0.232,0.232,0.232,0), 
         half4(0.0855,0.0855,0.0855,0), 
         half4(0.0205,0.0205,0.0205,0) 
     }; 
   
   
     //【6】顶点着色函数 || Vertex Shader Function 
     VertexOutput_DownSmpl vert_DownSmpl(VertexInput v) 
    
         //【6.1】实例化一个降采样输出结构 
         VertexOutput_DownSmpl o; 
   
         //【6.2】填充输出结构 
         //将三维空间中的坐标投影到二维窗口   
         o.pos = mul(UNITY_MATRIX_MVP, v.vertex); 
         //对图像的降采样:取像素上下左右周围的点,分别存于四级纹理坐标中 
         o.uv20 = v.texcoord + _MainTex_TexelSize.xy* half2(0.5h, 0.5h);; 
         o.uv21 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, -0.5h); 
         o.uv22 = v.texcoord + _MainTex_TexelSize.xy * half2(0.5h, -0.5h); 
         o.uv23 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, 0.5h); 
   
         //【6.3】返回最终的输出结果 
         return o; 
    
   
     //【7】片段着色函数 || Fragment Shader Function 
     fixed4 frag_DownSmpl(VertexOutput_DownSmpl i) : SV_Target 
    
         //【7.1】定义一个临时的颜色值 
         fixed4 color = (0,0,0,0); 
   
         //【7.2】四个相邻像素点处的纹理值相加 
         color += tex2D(_MainTex, i.uv20); 
         color += tex2D(_MainTex, i.uv21); 
         color += tex2D(_MainTex, i.uv22); 
         color += tex2D(_MainTex, i.uv23); 
   
         //【7.3】返回最终的平均值 
         return color / 4; 
    
   
     //【8】顶点输入结构体 || Vertex Input Struct 
     struct VertexOutput_Blur 
    
         //像素坐标 
         float4 pos : SV_POSITION; 
         //一级纹理(纹理坐标) 
         half4 uv : TEXCOORD0; 
         //二级纹理(偏移量) 
         half2 offset : TEXCOORD1; 
     }; 
   
     //【9】顶点着色函数 || Vertex Shader Function 
     VertexOutput_Blur vert_BlurHorizontal(VertexInput v) 
    
         //【9.1】实例化一个输出结构 
         VertexOutput_Blur o; 
   
         //【9.2】填充输出结构 
         //将三维空间中的坐标投影到二维窗口   
         o.pos = mul(UNITY_MATRIX_MVP, v.vertex); 
         //纹理坐标 
         o.uv = half4(v.texcoord.xy, 1, 1); 
         //计算X方向的偏移量 
         o.offset = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _DownSampleValue; 
   
         //【9.3】返回最终的输出结果 
         return o; 
    
   
     //【10】顶点着色函数 || Vertex Shader Function 
     VertexOutput_Blur vert_BlurVertical(VertexInput v) 
    
         //【10.1】实例化一个输出结构 
         VertexOutput_Blur o; 
   
         //【10.2】填充输出结构 
         //将三维空间中的坐标投影到二维窗口   
         o.pos = mul(UNITY_MATRIX_MVP, v.vertex); 
         //纹理坐标 
         o.uv = half4(v.texcoord.xy, 1, 1); 
         //计算Y方向的偏移量 
         o.offset = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _DownSampleValue; 
   
         //【10.3】返回最终的输出结果 
         return o; 
    
   
     //【11】片段着色函数 || Fragment Shader Function 
     half4 frag_Blur(VertexOutput_Blur i) : SV_Target 
    
         //【11.1】获取原始的uv坐标 
         half2 uv = i.uv.xy; 
   
         //【11.2】获取偏移量 
         half2 OffsetWidth = i.offset; 
         //从中心点偏移3个间隔,从最左或最上开始加权累加 
         half2 uv_withOffset = uv - OffsetWidth * 3.0; 
   
         //【11.3】循环获取加权后的颜色值 
         half4 color = 0; 
         for ( int j = 0; j< 7; j++) 
        
             //偏移后的像素纹理值 
             half4 texCol = tex2D(_MainTex, uv_withOffset); 
             //待输出颜色值+=偏移后的像素纹理值 x 高斯权重 
             color += texCol * GaussWeight[j]; 
             //移到下一个像素处,准备下一次循环加权 
             uv_withOffset += OffsetWidth; 
        
   
         //【11.4】返回最终的颜色值 
         return color; 
    
   
     //-------------------结束CG着色语言声明部分  || End CG Programming Part------------------              
     ENDCG 
   
     FallBack Off 
}


2.2 C#代码部分

C#脚本文件的代码可以从我们之前的几篇分析屏幕特效实现的文章中重用(如这篇实现屏幕油画特效的文章:http://blog.csdn.net/poem_qianmo/article/details/49719247),只用稍微改一点细节即可。 

贴出详细注释的配合Shader实现此特效的C#脚本:

[C#]  纯文本查看  复制代码
?
 
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
using UnityEngine; 
using System.Collections; 
   
//设置在编辑模式下也执行该脚本 
[ExecuteInEditMode] 
//添加选项到菜单中 
[AddComponentMenu( "Learning Unity Shader/Lecture 15/RapidBlurEffect" )] 
public class RapidBlurEffect : MonoBehaviour 
     //-------------------变量声明部分------------------- 
     #region Variables 
       
     //指定Shader名称 
     private string ShaderName = "Learning Unity Shader/Lecture 15/RapidBlurEffect"
   
     //着色器和材质实例 
     public Shader CurShader; 
     private Material CurMaterial; 
   
     //几个用于调节参数的中间变量 
     public static int ChangeValue; 
     public static float ChangeValue2; 
     public static int ChangeValue3; 
   
     //降采样次数 
     [Range(0, 6), Tooltip( "[降采样次数]向下采样的次数。此值越大,则采样间隔越大,需要处理的像素点越少,运行速度越快。" )] 
     public int DownSampleNum = 2; 
     //模糊扩散度 
     [Range(0.0f, 20.0f), Tooltip( "[模糊扩散度]进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。" )] 
     public float BlurSpreadSize = 3.0f; 
     //迭代次数 
     [Range(0, 8), Tooltip( "[迭代次数]此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大。" )] 
     public int BlurIterations = 3; 
   
     #endregion 
   
     //-------------------------材质的get&set---------------------------- 
     #region MaterialGetAndSet 
     Material material 
    
         get 
        
             if (CurMaterial == null
            
                 CurMaterial = new Material(CurShader); 
                 CurMaterial.hideFlags = HideFlags.HideAndDontSave; 
            
             return CurMaterial; 
        
    
     #endregion 
   
     #region Functions 
     //-----------------------------------------【Start()函数】---------------------------------------------   
     // 说明:此函数仅在Update函数第一次被调用前被调用 
     //-------------------------------------------------------------------------------------------------------- 
     void Start() 
    
         //依次赋值 
         ChangeValue = DownSampleNum; 
         ChangeValue2 = BlurSpreadSize; 
         ChangeValue3 = BlurIterations; 
   
         //找到当前的Shader文件 
         CurShader = Shader.Find(ShaderName); 
   
         //判断当前设备是否支持屏幕特效 
         if (!SystemInfo.supportsImageEffects) 
        
             enabled = false
             return
        
    
   
     //-------------------------------------【OnRenderImage()函数】------------------------------------   
     // 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果 
     //-------------------------------------------------------------------------------------------------------- 
     void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture) 
    
         //着色器实例不为空,就进行参数设置 
         if (CurShader != null
        
             //【0】参数准备 
             //根据向下采样的次数确定宽度系数。用于控制降采样后相邻像素的间隔 
             float widthMod = 1.0f / (1.0f * (1 << DownSampleNum)); 
             //Shader的降采样参数赋值 
             material.SetFloat( "_DownSampleValue" , BlurSpreadSize * widthMod); 
             //设置渲染模式:双线性 
             sourceTexture.filterMode = FilterMode.Bilinear; 
             //通过右移,准备长、宽参数值 
             int renderWidth = sourceTexture.width >> DownSampleNum; 
             int renderHeight = sourceTexture.height >> DownSampleNum; 
   
             // 【1】处理Shader的通道0,用于降采样 ||Pass 0,for down sample 
             //准备一个缓存renderBuffer,用于准备存放最终数据 
             RenderTexture renderBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format); 
             //设置渲染模式:双线性 
             renderBuffer.filterMode = FilterMode.Bilinear; 
             //拷贝sourceTexture中的渲染数据到renderBuffer,并仅绘制指定的pass0的纹理数据 
             Graphics.Blit(sourceTexture, renderBuffer, material, 0); 
   
             //【2】根据BlurIterations(迭代次数),来进行指定次数的迭代操作 
             for ( int i = 0; i < BlurIterations; i++) 
            
                 //【2.1】Shader参数赋值 
                 //迭代偏移量参数 
                 float iterationOffs = (i * 1.0f); 
                 //Shader的降采样参数赋值 
                 material.SetFloat( "_DownSampleValue" , BlurSpreadSize * widthMod + iterationOffs); 
   
                 // 【2.2】处理Shader的通道1,垂直方向模糊处理 || Pass1,for vertical blur 
                 // 定义一个临时渲染的缓存tempBuffer 
                 RenderTexture tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format); 
                 // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass1的纹理数据 
                 Graphics.Blit(renderBuffer, tempBuffer, material, 1); 
                 //  清空renderBuffer 
                 RenderTexture.ReleaseTemporary(renderBuffer); 
                 // 将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0和pass1的数据已经准备好 
                  renderBuffer = tempBuffer; 
   
                 // 【2.3】处理Shader的通道2,竖直方向模糊处理 || Pass2,for horizontal blur 
                 // 获取临时渲染纹理 
                 tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format); 
                 // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass2的纹理数据 
                 Graphics.Blit(renderBuffer, tempBuffer, CurMaterial, 2); 
   
                 //【2.4】得到pass0、pass1和pass2的数据都已经准备好的renderBuffer 
                 // 再次清空renderBuffer 
                 RenderTexture.ReleaseTemporary(renderBuffer); 
                 // 再次将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0、pass1和pass2的数据都已经准备好 
                 renderBuffer = tempBuffer; 
            
   
             //拷贝最终的renderBuffer到目标纹理,并绘制所有通道的纹理到屏幕 
             Graphics.Blit(renderBuffer, destTexture); 
             //清空renderBuffer 
             RenderTexture.ReleaseTemporary(renderBuffer); 
   
        
   
         //着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的 
         else 
        
             //直接拷贝源纹理到目标渲染纹理 
             Graphics.Blit(sourceTexture, destTexture); 
        
    
   
   
     //-----------------------------------------【OnValidate()函数】--------------------------------------   
     // 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用 
     //-------------------------------------------------------------------------------------------------------- 
     void

你可能感兴趣的:(Shader)