深入理解 Bloom

0. 写在之前的话

JI Stage2 with Bloom

JI Stage2 without Bloom

这两天,我深入的理解,并编写实现了 Bloom 效果。本来打算系统的写一下的,但是尝试了一下,发现太长了,所以打算以链接的方式给出学习路径。后面几节的内容都是学习路径。

我自己的感受:

因为这是第一次接触这种可以说是工业级别的渲染代码(Unity 官方后处理中的Bloom代码),我受到很大的震撼。在花了整整两天时间仔细深入学习了其中各个步骤渲染,为什么这么做,为什么不那么做的原因后,我觉得我收获了非常非常多的东西。

这篇文章中所谈到的 Bloom 效果的源代码:Bloom-Effect-Unity

1. 最好的学习模板

我最初是在 keijiro/KinoBloom 这个 Github 项目上看到 Bloom 的实现方式的。之后,在下载了 Post Processing Stack 这个 Unity 官方的后处理系统后,我认真的阅读、学习了其中关于实现 Bloom 部分的代码。(其实这两份代码都是 keijiro 一个人写的)

在看过这两份代码后,我发现 Post Processing Stack 的代码质量更高。在这里推荐给大家包含 Bloom 实现的几个文件。

  • Resources/Shaders/Bloom.shader。包含了实现 Bloom 效果的具体各个的 Pass 的 Shader 代码。
  • Resources/Shaders/Bloom.cginc。包含了各个 Pass 的 vertex 和 fragment 的 Shader 代码。
  • Resources/Shaders/Common.cginc。包含了一些渲染所需的基础工具函数。
  • Runtime/Components/BloomComponent.cs。包含了调用 Shader 进行后处理的代码。

2. 初步了解 Bloom

  1. Wiki 中关于 Bloom 的定义:wiki_Bloom_shader_effect)
  2. Wiki 中关于 HDR 的定义:wiki_High_dynamic_range
  3. 朴素的关于实现 Bloom 的原理:how-to-do-good-bloom-for-hdr-rendering

3. 亮度筛选(Light Prefilter)

这部分主要是关于 Bloom Shader(在上文中提到的Resources/Shaders/Bloom.shader)中第一个 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
half4 frag_prefilter(v2f_img i) : SV_Target
{
float2 uv = i.uv;
#if ANTI_FLICKER
float3 d = _MainTex_TexelSize.xyx * float3(1, 1, 0);
half4 s0 = SafeHDR(tex2D(_MainTex, uv));
half3 s1 = SafeHDR(tex2D(_MainTex, uv - d.xz)).rgb;
half3 s2 = SafeHDR(tex2D(_MainTex, uv + d.xz)).rgb;
half3 s3 = SafeHDR(tex2D(_MainTex, uv - d.yz)).rgb;
half3 s4 = SafeHDR(tex2D(_MainTex, uv + d.yz)).rgb;
half3 c = Median3(Median3(s0.rgb, s1, s2), s3, s4); // Roughly get the midient of s1, s2, s3, s4
#else
half4 s0 = SafeHDR(tex2D(_MainTex, uv));
half3 c = s0.rgb;
#endif
#if UNITY_COLORSPACE_GAMMA
c = GammaToLinearSpace(c);
#endif
half bright = Brightness(c);
half knee = _Curve.x * _Curve.y;
half soft = bright - (_Curve.x - knee);
soft = clamp(soft, 0, 2 * knee);
soft = soft * soft * 1 / (4 * knee + 0.00001);
c *= max(soft, bright - _Curve.x) / max(bright, 1e-5);
return half4(c, 0);
}

这个 Pass 是用来对图片的亮度进行筛选:选出高亮度的像素,去掉低亮度的像素。

Gamma 空间和 Linear 空间:

边缘柔和

  • 如何在对亮度筛选时,让亮度边缘变得柔和:catlike_bloom 中的 Soft Threshold 一节

在对亮度筛选时,如何避免闪烁的斑点:

4. 模糊(Down Sample)

代码部分

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
half4 frag_downsample(v2f_img i) : SV_Target
{
float4 d = _MainTex_TexelSize.xyxy * float4(-1.0, -1.0, 1.0, 1.0);
half3 s;
// Box filter
half3 s1 = tex2D(_MainTex, i.uv + d.xy).rgb;
half3 s2 = tex2D(_MainTex, i.uv + d.zy).rgb;
half3 s3 = tex2D(_MainTex, i.uv + d.xw).rgb;
half3 s4 = tex2D(_MainTex, i.uv + d.zw).rgb;
#if ANTI_FLICKER
// ref : http://graphicrants.blogspot.com.br/2013/12/tone-mapping.html
// Karis's anti-flicker tonemapping
half s1w = 1.0 / (Brightness(s1) + 1.0);
half s2w = 1.0 / (Brightness(s2) + 1.0);
half s3w = 1.0 / (Brightness(s3) + 1.0);
half s4w = 1.0 / (Brightness(s4) + 1.0);
s = (s1 * s1w + s2 * s2w + s3 * s3w + s4 * s4w) / (s1w + s2w + s3w + s4w);
#else
s = (s1 + s2 + s3 + s4) * 0.25;
#endif
return half4(s, 1.0);
}

这里的要点和前一节的差不多。所以就不细讲了。

这个 Pass 的主要作用是:对图像采样并模糊。所以这里用到了 BoxFilter 对图像进行采样模糊。

5. 扩大采样(Up Sample)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
half4 frag_upsample(MutiVertex i) : SV_Target
{
half3 base = tex2D(_BaseTex, i.uvBase);
float4 d = _MainTex_TexelSize.xyxy * float4(1.0, 1.0, -1.0, 0) * _SamplerScale;
// 9-tap bilinear unsampler(tent filter)
half3 s;
s = tex2D(_MainTex, i.uvMain - d.xy);
s += tex2D(_MainTex, i.uvMain - d.wy) * 2.0;
s += tex2D(_MainTex, i.uvMain - d.zy);
s += tex2D(_MainTex, i.uvMain + d.zw) * 2.0;
s += tex2D(_MainTex, i.uvMain) * 4.0;
s += tex2D(_MainTex, i.uvMain + d.xw) * 2.0;
s += tex2D(_MainTex, i.uvMain + d.zy);
s += tex2D(_MainTex, i.uvMain + d.wy) * 2.0;
s += tex2D(_MainTex, i.uvMain + d.xy);
return half4(base + s * 1.0 / 16.0, 1);
}

这里采用的卷积核是 Tent 卷积核:

1 2 1
2 4 2
1 1 1

这里的主要作用就是对小的图像进行采样,从而形成一张大的图像。

总流程概览

初始:

初始

亮度筛选:

亮度筛选

缩小并模糊(2次)

第一次 DownSample

第二次 DownSample

放大并叠加

第一次 UpSample

第二次 UpSample

最后和原图叠加

最终