后处理 - 泛光效果

https://learnopengl-cn.github.io/05 Advanced Lighting/07 Bloom/
https://threejs.org/examples/#webgl_postprocessing_unreal_bloom
https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing_unreal_bloom.html
https://docs.unrealengine.com/en-us/Engine/Rendering/PostProcessEffects/Bloom

明亮的光源和区域经常很难向观察者表达出来,因为监视器的亮度范围是有限的。一种区分明亮光源的方式是使它们在监视器上发出光芒,光源的的光芒向四周发散。这样观察者就会产生光源或亮区的确是强光区。(译注:这个问题的提出简单来说是为了解决这样的问题:例如有一张在阳光下的白纸,白纸在监视器上显示出是出白色,而前方的太阳也是纯白色的,所以基本上白纸和太阳就是一样的了,给太阳加一个光晕,这样太阳看起来似乎就比白纸更亮了)

image.png

和 HDR 的关系

Bloom 和 HDR 结合使用效果很好。常见的一个误解是 HDR 和泛光是一样的,很多人认为两种技术是可以互换的。但是它们是两种不同的技术,用于各自不同的目的上。可以使用默认的 8 位精确度的帧缓冲,也可以在不使用泛光效果的时候,使用 HDR。只不过在有了 HDR 之后再实现泛光就更简单了。

算法思路

之前介绍的 Lensflare 包括 HDR Tone Mapping 都涉及后处理中亮度区域的识别。因此通常在实现中可以放在同一个 hdr.glsl 中进行。
找到高亮区域后,模糊掉区域边缘即可。

WebGL 实现

识别高亮区域

这里以 clay.gl 为例,识别高亮区域部分和之前 Lensflare 的降采样 pass 可以共用。值得一提的是,clay.gl 还引用了另一种降采样实现以提升效果,来自 Unity 的 Bloom 实现: https://github.com/keijiro/KinoBloom

Anti Flicker

Anti Flicker - Sometimes the effect introduces strong flickers (flashing noise). This option is used to suppress them with a noise reduction filter.

为了缓解噪声,这里降采样不是简单平均四个邻居,而是根据 brightness (rgb 中的最大值)加权平均:

// downsample.glsl

float brightness(vec3 c)
{
    return max(max(c.r, c.g), c.b);
}

#ifdef ANTI_FLICKER
    // https://github.com/keijiro/KinoBloom/blob/master/Assets/Kino/Bloom/Shader/Bloom.cginc#L96
    // TODO
    vec3 s1 = decodeHDR(clampSample(texture, v_Texcoord + d.xy)).rgb;
    vec3 s2 = decodeHDR(clampSample(texture, v_Texcoord + d.zy)).rgb;
    vec3 s3 = decodeHDR(clampSample(texture, v_Texcoord + d.xw)).rgb;
    vec3 s4 = decodeHDR(clampSample(texture, v_Texcoord + d.zw)).rgb;

    // Karis's luma weighted average (using brightness instead of luma)
    float s1w = 1.0 / (brightness(s1) + 1.0);
    float s2w = 1.0 / (brightness(s2) + 1.0);
    float s3w = 1.0 / (brightness(s3) + 1.0);
    float s4w = 1.0 / (brightness(s4) + 1.0);
    float oneDivideSum = 1.0 / (s1w + s2w + s3w + s4w);

    vec4 color = vec4(
        (s1 * s1w + s2 * s2w + s3 * s3w + s4 * s4w) * oneDivideSum,
        1.0
    );
#else

在提取亮度时,同样为了减少噪音,对于四个邻居使用了一个中位数 filter:

// bright.glsl

// 3-tap median filter
vec4 median(vec4 a, vec4 b, vec4 c)
{
    return a + b + c - min(min(a, b), c) - max(max(a, b), c);
}

#ifdef ANTI_FLICKER
    // Use median filter to reduce noise
    // https://github.com/keijiro/KinoBloom/blob/master/Assets/Kino/Bloom/Shader/Bloom.cginc#L96
    vec3 d = 1.0 / textureSize.xyx * vec3(1.0, 1.0, 0.0);

    vec4 s1 = decodeHDR(texture2D(texture, v_Texcoord - d.xz));
    vec4 s2 = decodeHDR(texture2D(texture, v_Texcoord + d.xz));
    vec4 s3 = decodeHDR(texture2D(texture, v_Texcoord - d.zy));
    vec4 s4 = decodeHDR(texture2D(texture, v_Texcoord + d.zy));
    texel = median(median(texel, s1, s2), s3, s4);

#endif

模糊

仍旧使用高斯模糊,由于上一 pass 使用了 16x 降采样,这里需要恢复至 16x 和 8x 两遍,各包含水平垂直方向:

{
  "name" : "bright_upsample_16_blur_h",
  "shader" : "#source(clay.compositor.gaussian_blur)",
  "inputs" : {
    "texture" : "bright_downsample_32"
  },
  "parameters" : {
    "blurSize" : 1,
    "blurDir": 0.0,
    "textureSize": "expr( [width * dpr / 32, height / 32] )"
  },
  "defines": {
    "RGBM": null
  }
},

混合两次不同分辨率高斯模糊的结果,同样的 4x,2x 也进行一遍,最终得到混合多次类似 Mipmap 的模糊结果:

{
  "name" : "bloom_composite",
  "shader" : "#source(clay.compositor.blend)",
  "inputs" : {
    "texture1" : "bright_upsample_full_blur_v",
    "texture2" : "bright_upsample_2_blend"
  },
  "outputs" : {
    "color" : {
      "parameters" : {
        "width" : "expr(width * dpr)",
        "height" : "expr(height * dpr)"
      }
    }
  },
  "parameters" : {
    "weight1" : 0.3,
    "weight2" : 0.7
  },
  "defines": {
    "RGBM": null
  }
},

混合

和之前 lensflare 使用的是同一个混合 pass:

{
  "name" : "composite",
  "shader" : "#source(clay.compositor.hdr.composite)",
  "inputs" : {
    "texture" : "source",
    "bloom" : "bloom_composite",
    "lensflare" : "lensflare_blur_v"
  }
},

hdr.glsl 中简单的混合实现:

uniform sampler2D texture; // 原始场景
uniform sampler2D bloom;

vec4 bloomTexel = decodeHDR(texture2D(bloom, v_Texcoord));
texel.rgb += bloomTexel.rgb * bloomIntensity;
texel.a += bloomTexel.a * bloomIntensity;

最终效果

注意房顶边缘的泛光效果:
image.png

结合之前介绍的 Lensflare 效果:
image.png

其他实现

Three.js 中的例子借鉴了 Unreal 中的思路
https://docs.unrealengine.com/en-us/Engine/Rendering/PostProcessEffects/Bloom

Bloom can be implemented with a single Gaussian blur. For better quality, we combine multiple Gaussian blurs with different radius. For better performance, we do the very wide blurs in much lower resolution. In UE3, we had 3 Gaussian blurs in the resolution 1/4, 1/8, and 1/16. We now have multiple blurs name Blur1 to 5 in the resolution 1/2 (Blur1) to 1/32 (Blur5).

可见实现思路还是叠加多组不同半径的高斯模糊。

For best performance, the high resolution blurs (small number) should be small and wide blurs should mostly make use of the low resolution blurs (large number).

Bloom Convolution

The Bloom Convolution effect enables you to add custom bloom kernel shapes with a texture that represent physically realistic bloom effects whereby the scattering and diffraction of light within the camera or eye that give rise to bloom is modeled by a mathematical convolution of a source image with a kernel image.

Bloom Dirt Mask

类似之前 lensflare 中使用的镜头落灰效果

The Bloom Dirt Mask effect uses a texture to brighten up the bloom in some defined screen areas. This can be used to create a war camera look, more impressive HDR effect, or camera imperfections.