写在前面

bloom效果直接用unity自带的Post Processing就好。我哈皮没上网好好查就开始造轮子,好孩子不要学。

只适用于默认渲染管线,Urp后处理可以看这篇文章

参考的文章:https://zhuanlan.zhihu.com/p/91390940

这篇文章的原理部分讲的很好,不过代码部分有一些错误,我进行了改正。

最终效果

正常

Bloom

制作思路

具体的原理可以看最上面发的参考文章,这里只做简述:

将由一个挂在相机上的脚本实现后处理效果。调用一个shader处理图像。

大致步骤:

  1. 提取图片中亮的地方的信息;
  2. 模糊提取出的图像;
  3. 将模糊后的图像与原图像叠加输出;

注:这里要开启HDR,因为只有HDR可以储存高亮度信息;

制作步骤

准备

首先在场景中放入几个自发光物体;

新建一个脚本和Image Effect Shader,我这里就命名为Bloom.cs和Bloom.shader。打开shader,看一下里面的片元着色器咋写的:

1
2
3
4
5
6
7
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// just invert the colors
col.rgb = 1 - col.rgb;
return col;
}

很明显就是一个反色,咱们可以先用这个来测试一下。

先把shader上面的路径改一下:

1
Shader "PostProcess/Bloom"

打开脚本,把他自带的Start和Update函数删了。然后新建两个变量,一个是咱们的shader,另一个是使用shader的材质。我这里把shader的路径也储存为了const字符串。然后在Awake函数中找到咱们的bloom shader并赋给材质。

1
2
3
4
5
6
7
8
9
10
11
12
13
[ExecuteInEditMode] // 可在编辑模式
[ImageEffectAllowedInSceneView]
public class Bloom1 : MonoBehaviour
{
[SerializeField] Shader bloomShader; // shader
[SerializeField] Material mat; // 材质
const string shaderPath = "PostProcess/Bloom";
private void Awake()
{
bloomShader = Shader.Find(shaderPath); // 找到shader
mat = new Material(bloomShader); // 创建一个材质并赋咱的shader
}
}

这时候把脚本挂在主相机上就应该看到两个变量都出来东西了:

变量创建成功

然后在脚本中添加OnRenderImage函数,自动会补齐参数。其中参数source是传入的图片,destination是传出参数。添加如下代码:

1
2
3
4
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination);
}

鼠标放函数上看看这函数是干啥的:

函数介绍

意思是用shader把source传给destination,那咋赋shader呢,就看函数的重载,发现第三个参数应该填材质,第四个参数是Pass,也就是用材质的shader的哪个通道,咱们目前的shader只有一个默认的通道,所以pass是0,将代码改为如下:

1
Graphics.Blit(source, destination, mat, 0);

然后就能看到反色效果嘞!

image

到这里都没有问题的话就可以开始做了。

提取亮部

直接在shader的这个pass上进行更改。提取亮部肯定需要有个阈值拉,所以先在shader的属性中加一个_Threshold。然后新建一个提取亮度的函数,在片段着色器调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float _Threshold;
// 提取亮度函数
half3 PreFilter(half3 c)
{
// 选RGB中最亮的作为亮度
half brightness = c.r * 0.3 + c.g * 0.59 + c.b * 0.11;
half contribution = max(0, brightness - _Threshold);
contribution /= max(brightness, 0.00001);
return c * contribution;
}

...

fixed4 frag (v2f i) : SV_TARGET
{
return fixed4(PreFilter(tex2D(_MainTex, i.uv)), 1);
}

然后在脚本中也暴露一个Threshold变量,并设置shader属性:

1
2
3
4
5
6
7
8
9
[SerializeField, Range(0, 3)] public float threshold = 0.8f;

...

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
mat.SetFloat("_Threshold", threshold);
Graphics.Blit(source, destination, mat, 0);
}

现在在就可以在场景中看到提取亮部的效果啦,还可以通过调节变量threshold来调节阈值:

阈值为2时的场景亮部

这一步就算做完啦。

模糊图像

模糊图像的基本原理就是把图像变小再变大,自然就模糊了。这里先把提取亮度的代码注释掉,先测试一下通过向下采样模糊的效果。更改脚本中的代码:

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
[SerializeField, Range(0, 7)] public int blurTime = 5;

...

int width = source.width;
int height = source.height;
// 新建tmp
RenderTexture tmpSource, tmpDest;
tmpSource = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(source, tmpSource);
tmpDest = tmpSource;
// 进行模糊采样
int i = 1;
// 向下采样
for (; i < blurTime; i++)
{
width /= 2;
height /= 2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
// 向上采样
for (i -= 1; i > 0; i--)
{
width *= 2;
height *= 2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
// 传出
Graphics.Blit(tmpSource, destination);

现在已经可以看到模糊的效果了:

模糊效果

但这个模糊效果未免太辣鸡了点,这是因为咱们没用任何模糊算法,就直接硬采样。模糊算法有高斯算法啊之类的,但没必要,用Box Blur就够了,所以在shader中添加一个新的pass,做Box Blur(别的东西直接复制Pass0就成):

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
sampler2D _MainTex;
float4 _MainTex_TexelSize; // 储存了图片像素大小

half3 BoxFliter(float2 uv, float t)
{
half2 upL, upR, downL, downR;
// 计算偏移量
upL = _MainTex_TexelSize.xy * half2(t, 0);
upR = _MainTex_TexelSize.xy * half2(0, t);
downL = _MainTex_TexelSize.xy * half2(-t, 0);
downR = _MainTex_TexelSize.xy * half2(0, -t);

half3 col = 0;
// 平均采样
col += tex2D(_MainTex, uv + upL).rgb * 0.25;
col += tex2D(_MainTex, uv + upR).rgb * 0.25;
col += tex2D(_MainTex, uv + downL).rgb * 0.25;
col += tex2D(_MainTex, uv + downR).rgb * 0.25;

return col;
}

...

fixed4 frag (v2f i) : SV_Target
{
return fixed4(BoxFliter(i.uv, 1).rgb, 1);
}

在脚本中将盒体模糊的Blit函数设置为使用通道1就好了(注意有两个位置):

1
Graphics.Blit(tmpSource, tmpDest, mat, 1);

现在的效果就绝美了,甚至完全能匹敌高斯模糊:

加了盒体模糊后的效果

现在在脚本中,新建tmp的Blit函数中,让其使用一开始创建的提取亮部的Pass0:

1
Graphics.Blit(source, tmpSource, mat, 0);

现在就可以看到模糊亮部的效果啦:

模糊亮部效果

现在又发现了一个问题,当物体发光的时候,光晕确实是这样的,但是物体本身会趋向于变白,但我们这里还是保留了物体的原色,所以需要改一下。

具体改的方法就是,在每一次向下采样的时候,都将图存在一个数组里,再向上采样的时候用一个与咱们的Pass1完全相同,但是混合模式为Blend one one的通道做叠加。首先先更改脚本:

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
RenderTexture[] textures = new RenderTexture[blurTime];
// 进行模糊采样
int i = 1;
// 向下采样
for (; i < blurTime; i++)
{
width /= 2;
height /= 2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
// 将贴图储存在数组里
textures[i] = tmpDest;
Graphics.Blit(tmpSource, tmpDest, mat, 1);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
// 向上采样
for (i -= 1; i > 0; i--)
{
// 直接取数组里的图片赋值给tmpDest
RenderTexture.ReleaseTemporary(tmpDest);
tmpDest = textures[i];
textures[i] = null;
Graphics.Blit(tmpSource, tmpDest, mat, 1);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}

然后直接复制Pass1粘在下面,在最前面加个blend one one:

1
2
3
4
5
6
7
8
Pass // 2 合体后期 + 亮度累加
{
blend one one
CGPROGRAM

...
ENDCG
}

然后把脚本中向上采样的时候的Blit函数改为使用Pass2:

1
Graphics.Blit(tmpSource, tmpDest, mat, 2);

现在就可以看到效果很好的Bloom效果嘞:

Bloom效果

下面就是把这个Bloom叠加到原图上就好嘞!

叠加输出

在shader中再新建一个2D变量,用于储存一开始的原图。然后再新建一个Pass(放心这是最后一个Pass了),用于叠加图片:

1
2
3
4
5
6
7
8
9
10
11
sampler2D _SourceTex;
sampler2D _MainTex;

...

fixed4 frag (v2f i) : SV_TARGET
{
fixed3 source = tex2D(_SourceTex, i.uv).rgb;
fixed3 blur = tex2D(_MainTex, i.uv).rgb;
return fixed4(source + blur, 1);
}

在脚本中传入_SourceTex,并且在最后传出的时候使用Pass3:

1
2
3
4
5
6
mat.SetTexture("_SourceTex", source);

...

// 传出
Graphics.Blit(tmpSource, destination, mat, 3);

然后就大功告成嘞!

最终效果

完整代码

脚本:

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

[ExecuteInEditMode] // 可在编辑模式
[ImageEffectAllowedInSceneView]
public class Bloom1 : MonoBehaviour
{
[SerializeField] Shader bloomShader;
[SerializeField] Material mat;
[SerializeField, Range(0, 3)] public float threshold = 0.8f;
[SerializeField, Range(0, 7)] public int blurTime = 5;

const string shaderPath = "PostProcess/Bloom";

private void Awake()
{
bloomShader = Shader.Find(shaderPath);
mat = new Material(bloomShader);
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
mat.SetFloat("_Threshold", threshold);
mat.SetTexture("_SourceTex", source);
// 模糊图像
int width = source.width;
int height = source.height;
// 新建tmp
RenderTexture tmpSource, tmpDest;
tmpSource = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(source, tmpSource, mat, 0);
tmpDest = tmpSource;
RenderTexture[] textures = new RenderTexture[blurTime];
// 进行模糊采样
int i = 1;
// 向下采样
for (; i < blurTime; i++)
{
width /= 2;
height /= 2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
// 将贴图储存在数组里
textures[i] = tmpDest;
Graphics.Blit(tmpSource, tmpDest, mat, 1);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
// 向上采样
for (i -= 1; i > 0; i--)
{
// 直接取数组里的图片赋值给tmpDest
RenderTexture.ReleaseTemporary(tmpDest);
tmpDest = textures[i];
textures[i] = null;
Graphics.Blit(tmpSource, tmpDest, mat, 2);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
// 传出
Graphics.Blit(tmpSource, destination, mat, 3);
}
}

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
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
Shader "PostProcess/Bloom"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Threshold ("Thresold", Range(0, 1)) = 0.8
_SourceTex ("Source Texture", 2D) = "white" {}
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always

Pass // 0 提取亮度
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

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

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

float _Threshold;
sampler2D _MainTex;

half3 PreFilter(half3 c)
{
half brightness = max(c.r, max(c.g, c.b));
half contribution = max(0, brightness - _Threshold);
contribution /= max(brightness, 0.00001);
return c * contribution;
}

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

fixed4 frag (v2f i) : SV_Target
{
return fixed4(PreFilter(tex2D(_MainTex, i.uv)), 1);
}
ENDCG
}
Pass // 1 盒体模糊
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#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;

half3 BoxFliter(float2 uv, float t)
{
half2 upL, upR, downL, downR;
// 计算偏移量
upL = _MainTex_TexelSize.xy * half2(t, 0);
upR = _MainTex_TexelSize.xy * half2(0, t);
downL = _MainTex_TexelSize.xy * half2(-t, 0);
downR = _MainTex_TexelSize.xy * half2(0, -t);

half3 col = 0;
// 平均盒体采样
col += tex2D(_MainTex, uv + upL).rgb * 0.25;
col += tex2D(_MainTex, uv + upR).rgb * 0.25;
col += tex2D(_MainTex, uv + downL).rgb * 0.25;
col += tex2D(_MainTex, uv + downR).rgb * 0.25;

return col;
}


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

fixed4 frag (v2f i) : SV_Target
{
return fixed4(BoxFliter(i.uv, 1).rgb, 1);
}
ENDCG
}
Pass // 2 盒体模糊 + 亮度累加
{
blend one one
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#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;

half3 BoxFliter(float2 uv, float t)
{
half2 upL, upR, downL, downR;
// 计算偏移量
upL = _MainTex_TexelSize.xy * half2(t, 0);
upR = _MainTex_TexelSize.xy * half2(0, t);
downL = _MainTex_TexelSize.xy * half2(-t, 0);
downR = _MainTex_TexelSize.xy * half2(0, -t);

half3 col = 0;
// 平均盒体采样
col += tex2D(_MainTex, uv + upL).rgb * 0.25;
col += tex2D(_MainTex, uv + upR).rgb * 0.25;
col += tex2D(_MainTex, uv + downL).rgb * 0.25;
col += tex2D(_MainTex, uv + downR).rgb * 0.25;

return col;
}


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

fixed4 frag (v2f i) : SV_Target
{
return fixed4(BoxFliter(i.uv, 1).rgb, 1);
}
ENDCG
}

Pass // 3 合并
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

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

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

sampler2D _SourceTex;
sampler2D _MainTex;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_TARGET
{
fixed3 source = tex2D(_SourceTex, i.uv).rgb;
fixed3 blur = tex2D(_MainTex, i.uv).rgb;
return fixed4(source + blur, 1);
}

ENDCG
}
}
}