原理

实现模糊的原理比较简单:一个像素本身的颜色通过某种算法,最终计算为周围颜色的一个均值或权重值,这样看起来的图就是模糊的了。通过计算均值颜色的算法选择不同,就产生了不同效果、不同效率的模糊。具体实现算法可以看左神的这篇文章:高品质后处理:十种图像模糊算法的总结与实现,里面把所有的模糊算法的数学原理和效果都列举完了,言简意赅,字字珠玑。下面介绍我在实际工程中的实现UI背景模糊两种实现方法。

因为需要计算周围颜色的值,所以我们首先需要抓取到当前渲染的画面。目前我知道的抓取画面有两种方法:1、GrabPss。2、屏幕后处理。所以我这两把这两种方式分成了两种方法。

方法一:GrabPass

GrabPass是Unity的内置渲染管线中一个特殊的pass,可以在shader中将当前帧抓取到一张纹理贴图中。我的实现中获取了两次,分别做了垂直和水平方向的模糊处理。
效果如下:
UIBlur_GrabPass
通过调整Image位置和大小实现局部模糊:
UIBlur_GrabPass_Rect

shader代码

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
94
95
96
97
98
99
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
Shader "FT/Blur/Back"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Main Color", Color) = (1,1,1,1)
_Size ("Size", Range(0, 20)) = 1
}
Category {

// We must be transparent, so other objects are drawn before this one.
Tags {
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}


SubShader {

// Horizontal blur
GrabPass
{
Tags { "LightMode" = "Always" }
}
Pass {
Tags { "LightMode" = "Always" }

Name "BackBlurHor"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"

struct appdata_t {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float4 color : COLOR;
};

struct v2f {
float4 vertex : POSITION;
float4 uvgrab : TEXCOORD0;
float4 color : COLOR;
};

v2f vert (appdata_t v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
#if UNITY_UV_STARTS_AT_TOP
float scale = -1.0;
#else
float scale = 1.0;
#endif
o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
o.uvgrab.zw = o.vertex.zw;

o.color = v.color;
return o;
}

sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
float4 _MainTex_TexelSize;
float _Size;
uniform float4 _Color;

half4 GrabPixel(v2f i, float weight, float kernel){
kernel = sign(sign(abs(i.uvgrab.x)) + sign(abs(i.uvgrab.y))) * kernel;
return tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x + _GrabTexture_TexelSize.x * kernel * _Size, i.uvgrab.y, i.uvgrab.z, i.uvgrab.w))) * weight;
}
half4 frag( v2f i ) : COLOR {
half4 sum = half4(0,0,0,0);
sum += GrabPixel(i, 0.05, -4.0);
sum += GrabPixel(i, 0.09, -3.0);
sum += GrabPixel(i, 0.12, -2.0);
sum += GrabPixel(i, 0.15, -1.0);
sum += GrabPixel(i, 0.18, 0.0);
sum += GrabPixel(i, 0.15, +1.0);
sum += GrabPixel(i, 0.12, +2.0);
sum += GrabPixel(i, 0.09, +3.0);
sum += GrabPixel(i, 0.05, +4.0);

float4 col5 = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
fixed decayFactor = sign(sign(abs(i.uvgrab.x)) + sign(abs(i.uvgrab.y)));
sum = lerp(col5, sum, decayFactor) * i.color * _Color;

return sum;
}
ENDCG
}

// Vertical blur
GrabPass {
Tags { "LightMode" = "Always" }
}
Pass {
Tags { "LightMode" = "Always" }

Name "BackBlurVer"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"

struct appdata_t {
float4 vertex : POSITION;
float2 texcoord: TEXCOORD0;
float4 color : COLOR;
};

struct v2f {
float4 vertex : POSITION;
float4 uvgrab : TEXCOORD0;
float4 color : COLOR;
};

v2f vert (appdata_t v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
#if UNITY_UV_STARTS_AT_TOP
float scale = -1.0;
#else
float scale = 1.0;
#endif
o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
o.uvgrab.zw = o.vertex.zw;

o.color = v.color;
return o;
}

sampler2D _GrabTexture;
float4 _GrabTexture_TexelSize;
float _Size;
uniform float4 _Color;

half4 GrabPixel(v2f i, float weight, float kernel){
kernel = sign(sign(abs(i.uvgrab.x)) + sign(abs(i.uvgrab.y))) * kernel;

return tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x, i.uvgrab.y + _GrabTexture_TexelSize.y * kernel * _Size, i.uvgrab.z, i.uvgrab.w))) * weight;
}

half4 frag( v2f i ) : COLOR
{
half4 sum = half4(0,0,0,0);
sum += GrabPixel(i, 0.05, -4.0);
sum += GrabPixel(i, 0.09, -3.0);
sum += GrabPixel(i, 0.12, -2.0);
sum += GrabPixel(i, 0.15, -1.0);
sum += GrabPixel(i, 0.18, 0.0);
sum += GrabPixel(i, 0.15, +1.0);
sum += GrabPixel(i, 0.12, +2.0);
sum += GrabPixel(i, 0.09, +3.0);
sum += GrabPixel(i, 0.05, +4.0);

float4 col5 = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
fixed decayFactor = sign(sign(abs(i.uvgrab.x)) + sign(abs(i.uvgrab.y)));
sum = lerp(col5, sum, decayFactor) * i.color * _Color;

return sum;
}
ENDCG
}
}
}
}

方法二:屏幕后处理

Unity通过给相机添加脚本并重写OnRenderImage方法,可以拿到当前渲染画面的纹理,通过对改变此纹理可以实现各种后处理效果。这里我使用了双重模糊算法,通过2倍升/降分辨率从而优化每帧传递给GPU的带宽和计算,并且因为先降低分辨率再放大本身就会产生模糊,所以双重模糊使用比较少的迭代次数就能获得其他方法比较高迭代次数的效果。
效果如下:
UIBlur_PostProcessing

C#代码

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
using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class SimpleDualBlur : MonoBehaviour
{
[Range(0, 8)] public int iterations = 4;

public Shader blurShader;
private Material blurMaterial;

private Material BlurMaterial
{
get
{
if (blurMaterial == null)
{
blurMaterial = new Material(blurShader);
blurMaterial.hideFlags = HideFlags.DontSave;
}

return blurMaterial;
}
}

void OnDisable()
{
if (blurMaterial)
DestroyImmediate(blurMaterial);
}

void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (BlurMaterial == null || iterations <= 0)
{
Graphics.Blit(source, destination);
return;
}

int width = source.width;
int height = source.height;

// 第1个临时纹理
RenderTexture rt1 = RenderTexture.GetTemporary(width, height, 0, source.format);
RenderTexture rt2 = RenderTexture.GetTemporary(width, height, 0, source.format);
// 第1次下采样
Graphics.Blit(source, rt1);

// 双重模糊循环
// 下采样
for (int i = 0; i < iterations; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = Mathf.Max(width / 2, 1);
height = Mathf.Max(height / 2, 1);
rt2 = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(rt1, rt2, BlurMaterial, 0);

RenderTexture.ReleaseTemporary(rt1);
width = Mathf.Max(width / 2, 1);
height = Mathf.Max(height / 2, 1);
rt1 = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(rt2, rt1, BlurMaterial, 0);
}

// 上样
for (int i = 0; i < iterations; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width * 2;
height = height * 2;
rt2 = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(rt1, rt2, BlurMaterial, 0);

RenderTexture.ReleaseTemporary(rt1);
width = width * 2;
height = height * 2;
rt1 = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(rt2, rt1, BlurMaterial, 0);
}

// 最终输出
Graphics.Blit(rt1, destination);
RenderTexture.ReleaseTemporary(rt1);
RenderTexture.ReleaseTemporary(rt2);
}
}

shader代码

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
Shader "Hidden/SimpleDualBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

SubShader
{
Cull Off
ZWrite Off
ZTest Always

// Pass 0: 下采样Pass (5点采样)
Pass
{
Name "DOWNSAMPLE"

CGPROGRAM
#pragma vertex vert
#pragma fragment frag_downsample
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag_downsample(v2f i) : SV_Target
{
float2 texelSize = _MainTex_TexelSize.xy;
float2 offset = texelSize * 2.0; // 2倍下采样

// 5点采样:中心 + 4个角
fixed4 col = tex2D(_MainTex, i.uv) * 0.6; // 中心
col += tex2D(_MainTex, i.uv + float2(-offset.x, -offset.y)) * 0.1; // 左上
col += tex2D(_MainTex, i.uv + float2(offset.x, -offset.y)) * 0.1; // 右上
col += tex2D(_MainTex, i.uv + float2(-offset.x, offset.y)) * 0.1; // 左下
col += tex2D(_MainTex, i.uv + float2(offset.x, offset.y)) * 0.1; // 右下

return col;
}
ENDCG
}

// Pass 1: 上采样Pass (9点采样)
Pass
{
Name "UPSAMPLE"

CGPROGRAM
#pragma vertex vert
#pragma fragment frag_upsample
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_TexelSize;

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag_upsample(v2f i) : SV_Target
{
float2 texelSize = _MainTex_TexelSize.xy;

// 9点采样:中心 + 周围8个点
fixed4 col = tex2D(_MainTex, i.uv) * 0.2; // 中心 (1/9)

col += tex2D(_MainTex, i.uv + float2(-texelSize.x, -texelSize.y)) * 0.1; // 左上
col += tex2D(_MainTex, i.uv + float2(0.0, -texelSize.y)) * 0.1; // 上
col += tex2D(_MainTex, i.uv + float2(texelSize.x, -texelSize.y)) * 0.1; // 右上

col += tex2D(_MainTex, i.uv + float2(-texelSize.x, 0.0)) * 0.1; // 左
col += tex2D(_MainTex, i.uv + float2(texelSize.x, 0.0)) * 0.1; // 右

col += tex2D(_MainTex, i.uv + float2(-texelSize.x, texelSize.y)) * 0.1; // 左下
col += tex2D(_MainTex, i.uv + float2(0.0, texelSize.y)) * 0.1; // 下
col += tex2D(_MainTex, i.uv + float2(texelSize.x, texelSize.y)) * 0.1; // 右下

return col;
}
ENDCG
}
}
FallBack Off
}

方法对比与性能分析

通用性:GrabPass只适用于Unity的内置渲染管线,剩下的URP、HDRP、SRP统统都不支持;屏幕后处理则都通用,只需要实现对应的接口即可。
需求实现:GrabPass因为生成了材质挂载到Image中,所以可以非常简单的通过调整Image的大小和位置,实现UI局部模糊;屏幕后处理则只能支持全屏。
性能消耗:GrabPass在Unity的官方文档中明确指出:“会显著增加CPU和GPU帧时间。除了快速原型制作之外,您通常应该避免使用此命令,并尝试通过其他方式实现您的效果”。而这里使用材质挂载的方式,一个材质就两个pass,所以在多个材质的情况下性能会有严重的问题;对于屏幕后处理只会对渲染管线最后的画面做一次处理,通过双重模糊算法降低与GPU交互数据的带宽,性能较好。

参考资料

[1].【Unity渲染——屏幕后处理】Cg代码实现多种模糊算法