Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

高斯模糊

在屏幕后处理的Shader中 需要设置深度测试、剔除、深度写入 - ZTest Always 打开深度测试 - Cull Off 关闭剔除 - ZWrite Off 关闭深度写入 主要是避免它"挡住"后面的渲染物体

1. 什么是高斯模糊效果


高斯模糊效果,是一种
用于平滑图像并减少图像噪声和细节的图像处理技术
高斯模糊的主要目的是使图像的边缘和细节变得模糊和平滑
高斯模糊相当于利用 Shader 代码自动给屏幕图像进行模糊处理

2.Unity Shader中实现高斯效果的基本原理


第一步:高斯模糊是利用高斯函数计算出高斯滤波核中每个元素并进行归一化处理后
第二步:再和目标像素通过卷积计算后得到最终的效果

关键知识点:

  • 高斯函数
  • 高斯滤波核
  • 归一化处理

3.一些关键的知识点

1.)高斯滤波核


高斯滤波核也称为高斯核,它其实就是一个N x N 的卷积核
它的大小可以自定义,但是一般会是一个 奇数 x 奇数 的大小
通常会是一个3x3 、5x5、7x7、9x9 的大小
滤波核越大,模糊效果越明显
从效果和效率综合考虑,我们通常会使用 5x5 的大小
高斯滤波核中各元素的具体值,我们通过高斯函数来确定


注意:
高斯滤波核中的数值是定死的规则,可以直接写死参与计算,
不用在代码中用高斯函数计算,会浪费性能

实现高斯滤波核流程图

2.)高斯函数

高斯函数是由德国数学家和物理学家 卡尔·弗里德里希·高斯(Carl Friedrich Gauss)提出的。 我们将使用二维高斯函数计算高斯模糊效果中的卷积核

高斯函数
  • σ 是标准方差,一般取值为1即可
  • x 和 y 是相对于高斯核中心的整数距离
  • e 是自然对数的底 ≈ 2.71828
  • π 是圆周率 ≈ 3.14159
通过高斯函数计算的高斯滤波核

3.)归一化

  1. 归一化的目的: 这样是为了避免在卷积计算过程中引入额外的亮度变化或偏差
  2. 归一化的方法: 让该卷积核中 各元素值/ 所有元素总和 。 使其所有元素的和为1
归一化后的高斯滤波核

4.高斯模糊计算优化

1.)潜在问题抛出


如果我们直接基于它的基本原理进行计算,计算效率是较低的
因为对于一张长W,宽H 的图像,会进行5 * 5 * W * H 次纹理采样的颜色计算

2.)解决方案


为了降低计算次数,我们可以利用二维高斯函数的数学特性 —— 可分离性
二维高斯函数可以表示为两个一维高斯函数的乘积
从而大幅减少计算量

可分离性的推导


Gx 和 Gy 可以分别代表沿x轴和y轴的一维高斯函数
我们只需要让每个像素分别
与Gx 进行水平卷积计算,与Gy进性垂直卷积计算
再将最终的计算结果相乘即可得出和之前一样的结果

利用这两个一维高斯函数1 利用这两个一维高斯函数2


再利用计算出来的这两个一维高斯滤波核(高斯核或卷积核或滤波核)
和对应的像素点进行卷积计算,再将两个结果相乘,便可以得到最终该像素高斯模糊后的结果

得到最终该像素高斯模糊后的结果


通过对计算公式的优化,我们将原本需要N * N * W * H 的计算次数(N为高斯核大小)
优化为了 2 * N * W * H,相当于将时间复杂度从O(n²) 降低到了O(n)

5.高斯模糊效果的计算方式优化


我们刚才的知识点中提到,如果想要图片越模糊,那么需要扩大高斯滤波核的大小越大越模糊
但是同样通过刚才的基础原理和计算公式优化的讲解大家能够感受到
如果通过扩大高斯滤波核的大小来达到更模糊的目的,付出的代价就是会更加消耗性能
因此在Shader中我们不会提供控制高斯滤波核大小的参数,我们的滤波核始终会使用5x5大小的。


我们就只能使用其他方式来控制模糊程度了,我们一般会使用以下三种方式 - 控制缩放纹理纹理大小
高斯模糊的目的是让纹理看起来模糊,那么通过缩放源纹理
让Shader从更小的主纹理中进行采样,尺寸小了,计算的就少了,也能达到更模糊的效果 - 控制模糊程度代码的执行次数
我们可以在OnRenderImage中多次执行Shader代码在模糊的基础之上,再添一层模糊
对源纹理进行多次迭代模糊处理 - 控制纹理采样间隔距离
我们在Shader中进行纹理UV采样,可以自己控制像素的间距位置
而不是只以一个单位来计算间隔,具体间隔几个间隔单位,可以自定义

6.高斯模糊的基础实现

1.知识补充

新的预处理指令


在实现高斯效果时,
我们将在Shader中利用两个Pass来分别计算 水平卷积 和 竖直卷积
而这两个Pass会存在相同的代码
我们将使用一个新的预处理指令

1
2
3
4
5
CGINCLUDE
···
CG代码
···
EncCG

这一段代码位于 SubShader语句块中 Pass之外

作用:
用于封装共享代码
可以在其中定义常量、函数、结构体、宏等等内容
这些封装起来的代码
可以在同一个Shader文件中的多个Pass 或 其他Shader文件中引用
使用它可以避免我们重复编写一些相同的代码
从而提高代码的重复利用性和可维护性

2.实现的基础思路

在Shader中写两个Pass - 用于计算水平卷积的Pass - 用于计算竖直卷积的Pass

  1. 两个Pass的区别:
    顶点着色器中计算的UV偏移位置不同,一个水平偏移,一个竖直偏移
  2. 两个Pass的共同点:
    1. 内置文件相同
    2. 属性使用相同
    3. 片元着色器的计算规则可以相同
      通过使用uv数组存储5个像素的UV坐标偏移
      数组中存储的像素UV偏移分别为
      index : 0,1,2,3,4
      x或者y的偏移 : 0,1,-1,2,-2
      其中:
      • 第0个元素 对应的高斯核元素 为0.4026
      • 第1,2个元素 对应的高斯核元素 为0.2442
      • 第3,4个元素 对应的高斯核元素 为0.0545

1.)Shader部分

  1. 声明变量
    主纹理 _MainTex
  2. 使用预处理指令:CGINCLUDE ... ENDCG
    实现两个Pass共享代码
    1. 内置文件的引用
    2. 属性映射,注意引用纹素 需要用到uv的偏移计算
    3. 结构体
      顶点和uv数组
    4. 两个顶点着色器函数
      一个计算水平偏移,一个计算竖直偏移
    5. 片元着色器
      共同的卷积计算方式,对位相乘后相加
  3. 屏幕后处理标配三件套
    深度测试、深度写入、关闭剔除
  4. 实现两个Pass
    主要是用编译指令指明顶点和片元着色器调用的函数即可
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
Shader "Unlit/GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
//纹素 : x = 1/宽度,y = 1/高度
half4 _MainTex_TexelSize;
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下的坐标
float4 vertex : SV_POSITION;
};
//水平方向的顶点着色器
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算5个像素的uv坐标偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(1 * _MainTex_TexelSize.x,0);
o.uv[2] = uv + half2(-1 * _MainTex_TexelSize.x,0);
o.uv[3] = uv + half2(2 * _MainTex_TexelSize.x,0);
o.uv[4] = uv + half2(-2 * _MainTex_TexelSize.x,0);
return o;
}
//竖直方向的顶点着色器
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算5个像素的uv坐标偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(0,1 * _MainTex_TexelSize.x);
o.uv[2] = uv + half2(0,-1 * _MainTex_TexelSize.x);
o.uv[3] = uv + half2(0,2 * _MainTex_TexelSize.x);
o.uv[4] = uv + half2(0,-2 * _MainTex_TexelSize.x);
return o;
}
//片元着色器
//两个Pass可以使用同一个 我们这里把里面的逻辑写的通用即可
fixed4 fragBlur(v2f i):SV_Target
{
//卷积运算
//卷积核 其中的三个数 因为只是使用这三个数,没有必要声明为5个单位的数组
float weight[3] = {0.4026,0.2442,0.0545};
//先计算当前像素点
fixed3 sum = tex2D(_MainTex,i.uv[0]).rgb * weight[0];
//去计算左右偏移一个单位和两个单位的 对位相乘累加
for (int it = 1; it < 3; ++it)
{
//要和右元素相乘
sum += tex2D(_MainTex,i.uv[it*2-1]).rgb * weight[it];
//要和左元素相乘
sum += tex2D(_MainTex,i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum,1);
}
ENDCG
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}

}
Fallback Off
}

2.)C#部分

知识补充:
由于我们需要让两个Pass对图像进行处理
相当于让捕获的图像进行水平卷积计算得到一个结果
再用这个结果进行竖直卷积计算的到的最终结果
因此我们需要利用一个中间纹理缓存区,用与记录中间的处理结果

  • RenderTexture.GetTemporary
    它的作用就是获取一个临时的RenderTexture对象,我们可以利用它来存储中间结果 注意: 需要配合RenderTextureReleaseTemporary来释放该缓存对象
    • 参数一:纹理宽
    • 参数二:纹理高
    • 参数三:深度缓冲(一般填0即可)
    • 返回值:RenderTexture对象

实现步骤: 1. 继承PostEffectBase 2. 重写OnRenderTetureImage函数 3. 在其中利用Graphics.Blit、RenderTure.GetTemporary、RenderTexture.ReleaseTemporary
利用这三个函数对纹理进行两次Pass处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GaussianBlur : PostEffectBase
{
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material)
{
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
//因为我们需要两个Pass 处理图像两次
//进行第一次水平卷积计算
Graphics.Blit(source,buffer,material,0); // Color1
//进行第二次垂直卷积计算
Graphics.Blit(buffer,destination,material,1); // 在 Color1 的基础上 乘上 Color2 得到最终的高斯模糊计算的结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}

7.高斯模糊的完整实现


想要图片变的模糊,那么需要控制高斯滤波核的大小,越大越模糊
如果通过扩大高斯滤波核的大小,来控制模糊度的话,付出的代价就是更加消耗性能
因此我们不通过修改滤波核来控制模糊程度
一般使用以下三种:

  • 控制缩放纹理的大小
  • 控制模糊代码的执行次数
  • 控制纹理采样间隔距离

1.)添加控制纹理大小的参数


在高斯模糊效果的c#代码中加入一个控制缩放的参数
主要作用就是用来控制降低采样质量的,因此取名为downSample


如何使用:
在OnRenderImage函数中,
我们使用RenderTexture.GetTemporary获取渲染纹理缓存区时,
用源纹理除以downSample
这样我们在调用Graphics.Blit进行图像复制处理时
就相当于将源纹理缩小了,同时缩小的过程中还会用材质球进行效果处理


注意:
在进行复制处理时,我们可以设置渲染纹理缓存对象的 缩放过滤模式(buffer.FilterMode)
对应枚举含义

  • FilterMode.Point : 点过滤。不进行插值。每个像素都直接从最近的纹理像素获取颜色
  • FilterMode.Bilinear : 双线性过滤。它在纹理采样时,使用相邻四个纹理像素的加权平均值进行插值,以生成更平滑的图像
  • FilterMode.Trilinear : 三线性过滤。它在双线性过略的基础上增加了在不同MIP贴图级别之间的插值
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GaussianBlur : PostEffectBase
{
[Range(1,8)]
public int downSample = 1;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;

//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式 可以让缩放效果更加平滑
buffer.filterMode = FilterMode.Bilinear;
//因为我们需要两个Pass 处理图像两次
//进行第一次水平卷积计算
Graphics.Blit(source,buffer,material,0); // Color1
//进行第二次垂直卷积计算
Graphics.Blit(buffer,destination,material,1); // 在Color1 的基础上 乘上Color2 得到最终的高斯模糊计算的结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}

2.)添加控制模糊代码执行次数参数


在高斯模糊效果的C#代码中加入一个控制模糊代码执行次数参数
主要作用就是用来多次执行材质球中的两个Pass,因此取名叫 iteration(迭代)


如何使用:
在OnRenderImage函数中
我们使用一个for循环,来对原图像进行多次高斯模糊


注意:
要保证每次使用完 RenderTexture.GetTemporary 分配的缓存区
都要使用 RenderTexture.ReleaseTemporary 将其释放

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GaussianBlur : PostEffectBase
{
[Range(1,8)]
public int downSample = 1;
[Range(1,100)]
public int iterations = 1;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式 可以让缩放效果更加平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放 写入到缓存纹理中
Graphics.Blit(source, buffer);
//多次去执行 高斯模糊逻辑
for (int i = 1; i <= iterations; i++)
{
//又声明一个新的缓存区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

//因为我们需要两个Pass 处理图像两次
//进行第一次水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); // Color1
//这时 关键内容都在buffer1中 相当于buffer已经没用了 可以直接释放
RenderTexture.ReleaseTemporary(buffer);

buffer = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1); // 在Color1 的基础上 乘上Color2 得到最终的高斯模糊计算的结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//相当于 buffer和buffer1都是指向的 这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的纹理效果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}

2.)添加控制纹理采样间隔距离


在高斯模糊的Shader代码中加入一个控制纹理采样间隔距离的属性
主要作用: 控制间隔多少个单位偏移的uv坐标,因此取名 _BlurSpread(模糊半径)


如何使用:
在顶点着色器进行uv坐标偏移时,乘以该属性,可以通过它控制偏移的多少


注意:
理论上来说像素是以一个单位一个单位偏移的
但是为了更精确的控制模糊程度,我们可以让其为小数
小数变化可以更精细的调整模糊程序

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
Shader "Unlit/GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSpread("BlurSpread",Float) = 1
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"

sampler2D _MainTex;
//纹素 : x = 1/宽度,y = 1/高度
half4 _MainTex_TexelSize;
//纹理偏移间隔单位
float _BlurSpread;
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下的坐标
float4 vertex : SV_POSITION;
};
//水平方向的顶点着色器
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算5个像素的uv坐标偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(1 * _MainTex_TexelSize.x,0) * _BlurSpread;
o.uv[2] = uv + half2(-1 * _MainTex_TexelSize.x,0)* _BlurSpread;
o.uv[3] = uv + half2(2 * _MainTex_TexelSize.x,0)* _BlurSpread;
o.uv[4] = uv + half2(-2 * _MainTex_TexelSize.x,0)* _BlurSpread;
return o;
}
//竖直方向的顶点着色器
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//计算5个像素的uv坐标偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(0,1 * _MainTex_TexelSize.x)* _BlurSpread;
o.uv[2] = uv + half2(0,-1 * _MainTex_TexelSize.x)* _BlurSpread;
o.uv[3] = uv + half2(0,2 * _MainTex_TexelSize.x)* _BlurSpread;
o.uv[4] = uv + half2(0,-2 * _MainTex_TexelSize.x)* _BlurSpread;
return o;
}
//片元着色器
//两个Pass可以使用同一个 我们这里把里面的逻辑写的通用即可
fixed4 fragBlur(v2f i):SV_Target
{
//卷积运算
//卷积核 其中的三个数 因为只是使用这三个数,没有必要声明为5个单位的数组
float weight[3] = {0.4026,0.2442,0.0545};
//先计算当前像素点
fixed3 sum = tex2D(_MainTex,i.uv[0]).rgb * weight[0];
//去计算左右偏移一个单位和两个单位的 对位相乘累加
for (int it = 1; it < 3; ++it)
{
//要和右元素相乘
sum += tex2D(_MainTex,i.uv[it*2-1]).rgb * weight[it];
//要和左元素相乘
sum += tex2D(_MainTex,i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum,1);
}
ENDCG
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
}
Fallback Off
}
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
59
60
61
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GaussianBlur : PostEffectBase
{
[Range(1,8)]
public int downSample = 1;
[Range(1,100)]
public int iterations = 1;

[Range(0,3)]
public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式 可以让缩放效果更加平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放 写入到缓存纹理中
Graphics.Blit(source, buffer);
//在使用材质球之前设置 模糊半径
//material.SetFloat("_BlurSpread", blurSpread);

//多次去执行 高斯模糊逻辑
for (int i = 1; i <= iterations; i++)
{
//优化写法 更加常用
//一般我们可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//又声明一个新的缓存区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

//因为我们需要两个Pass 处理图像两次
//进行第一次水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); // Color1
//这时 关键内容都在buffer1中 相当于buffer已经没用了 可以直接释放
RenderTexture.ReleaseTemporary(buffer);

buffer = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1); // 在Color1 的基础上 乘上Color2 得到最终的高斯模糊计算的结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//相当于 buffer和buffer1都是指向的 这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的纹理效果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}

评论