Home

Awesome

Bloom-Effect-Unity

Myself Bloom Effect Implementation

<br /> <br /> <br />

0. 写在之前的话

JI Stage2 with Bloom

JI Stage2 without Bloom

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

我自己的感受:

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

<!-- more -->

1. 最好的学习模板

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

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

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 的实现方法。

代码部分:

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 空间:

边缘柔和

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

4. 模糊(Down Sample)

代码部分

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)

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 卷积核:

121
242
121

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

总流程概览

初始:

初始

亮度筛选:

亮度筛选

缩小并模糊(2次)

第一次 DownSample

第二次 DownSample

放大并叠加

第一次 UpSample

第二次 UpSample

最后和原图叠加

最终