一、Bloom算法介绍

Bloom,也称辉光效果。模拟摄像机的一种图像效果,让物体具有真实的明亮效果。
1.1 实现思路

- 提取原图较量区域
- 模糊该图像
- 与原图混合
1.2 前置知识
1.2.1 HDR与LDR
https://xzyw7.github.io/post/1_JYIZ9Hm/
如果在LDR中 使用Bloom,那么阈值提取的亮度区域不会超过1,并且光源的亮度很可能和环境中的亮度接近,导致不希望出现bloom的地方也被模糊了,因此HDR更适合使用Bloom。
1.2.2 高斯模糊
- 高斯模糊(Gaussian Blur)
- 一种图像模糊
- 减少图像噪声、降低细节层次
- 通过高斯函数定义一个卷积核,对图像进行卷积。
- 二维高斯核
- 计算量大,N*N*W*H次纹理采样
- 可分离性:可拆成两个一维高斯核,计算次数为(N+N)*W*H

二、Bloom算法应用
- 配合自发光贴图
- 配合特效
- GodRay效果(基于径向模糊的后处理)
- 配合Tone Mapping
三、Bloom算法实现
- 思路
- C#:调用OnRenderImage函数
- Shader:使用4个Pass完成Bloom效果
- 但是要做后处理的效果,我们首先使用入门精要中的PostEffectsBase作为基类。这个基类就暂且不研究了。
MonoBehaviour.OnRenderImage这个生命周期函数,用于获取RT与输出处理后的RT
这里还用到了一个重要的api,Graphics.Blit。用途是用指定的某个材质(的某个pass)来处理一个Texture,并储存到结果的RT。(pass为-1时,将依次调用所有pass)
https://docs.unity3d.com/ScriptReference/Graphics.Blit.html
其中src纹理会被传递给Shader中的_MainTex纹理属性。所以我们在脚本中看不到对_MainTex的处理,因为这是默认的
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
| using System.Collections; using System.Collections.Generic; using UnityEngine;
public class Bloom : PostEffectsBase { public Shader bloomShader; private Material bloomMaterial = null; public Material material{ get{ bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial); return bloomMaterial; } }
[Range(0,4)] public int iterations = 3; [Range(0.2f, 3.0f)] public float blurSpread = 0.6f; [Range(1,8)] public int downSample = 2; [Range(0.0f, 4.0f)] public float luminanceThreshold = 0.6f; void OnRenderImage(RenderTexture src, RenderTexture dest) { if (material != null) { material.SetFloat("_LuminanceThreshold", luminanceThreshold); int rtW = src.width / downSample; int rtH = src.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0); buffer0.filterMode = FilterMode.Bilinear; Graphics.Blit(src,buffer0, material, 0); for(int i=0;i<iterations;i++) { material.SetFloat("_BlurSize", 1.0f+i*blurSpread); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0); Graphics.Blit(buffer0,buffer1,material,1);
RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW,rtH,0); Graphics.Blit(buffer0,buffer1,material,2); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } material.SetTexture("_Bloom", buffer0); Graphics.Blit(src,dest,material,3); RenderTexture.ReleaseTemporary(buffer0); } else { Graphics.Blit(src,dest); } } }
|
同样地,在shader里我们也要写好这4个pass。并且,这个shader是不需要创建材质然后赋给物体对象的。
材质是在脚本中创建的,并且通过Graphics.Blit这一API来调用pass对RT进行处理。
屏幕后处理实际上是在场景中绘制一个与屏幕同款同高的quad,
我们片段着色器输出pos并对x,y分别除以分辨率,就是这个结果。Z分量始终为1,x,y以左上角为坐标原点。所以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
| Shader "Custom/Bloom" { Properties { _MainTex ("Base(RGB)", 2D) = "white" {} _Bloom("Bloom(RGB)",2D)="black"{} _LuminanceThreshold("Luminance Threshold",Float)=0.5 _BlurSize("Blur Size", Float) = 0.5 } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _Bloom; float _LuminanceThreshold; float _BlurSize;
struct v2fExtractBright { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; }; v2fExtractBright vertExtractBright(appdata_img v) { v2fExtractBright o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed luminance(fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; }
fixed4 fragExtractBright ( v2fExtractBright i) : SV_Target { fixed4 c =tex2D(_MainTex, i.uv); fixed val = clamp(luminance(c)-_LuminanceThreshold, 0.0 ,1.0); return c*val; }
struct v2fBlur { float4 pos: SV_POSITION; half2 uv[5] : TEXCOORD0; };
v2fBlur vertexBlurVertical(appdata_img v) { v2fBlur o; o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y *1.0) * _BlurSize; o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y *1.0) * _BlurSize; o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y *2.0) * _BlurSize; o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y *2.0) * _BlurSize; return o; } v2fBlur vertexBlurHorizontal(appdata_img v) { v2fBlur o; o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2( _MainTex_TexelSize.x *1.0, 0.0) * _BlurSize; o.uv[2] = uv - float2( _MainTex_TexelSize.x *1.0, 0.0) * _BlurSize; o.uv[3] = uv + float2( _MainTex_TexelSize.x *2.0, 0.0) * _BlurSize; o.uv[4] = uv - float2( _MainTex_TexelSize.x *2.0, 0.0) * _BlurSize; return o; } fixed4 fragBlur(v2fBlur i) :SV_Target { float weight[3] = {0.4026, 0.2442, 0.0545}; fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3;it++) { sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it]; sum += tex2D(_MainTex, i.uv[it*2]).rgb* weight[it];
} return fixed4(sum, 1.0); } struct v2fBloom { float4 pos : SV_POSITION; half4 uv : TEXCOORD0; }; v2fBloom vertBloom(appdata_img v) { v2fBloom o; o.pos = UnityObjectToClipPos(v.vertex)/3; o.uv.xy = v.texcoord; o.uv.zw = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y<0.0) { o.uv.w = 1.0-o.uv.w; } #endif return o; }
fixed4 fragBloom(v2fBloom i): SV_Target { return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw); }
ENDCG ZTest Always Cull off ZWrite off Pass { CGPROGRAM #pragma vertex vertExtractBright #pragma fragment fragExtractBright ENDCG
} Pass { CGPROGRAM #pragma vertex vertexBlurVertical #pragma fragment fragBlur ENDCG
} Pass { CGPROGRAM #pragma vertex vertexBlurHorizontal #pragma fragment fragBlur ENDCG
} Pass { CGPROGRAM #pragma vertex vertBloom #pragma fragment fragBloom ENDCG
} } FallBack "Diffuse" }
|
模糊后
但是依然存在一个问题。
就是这个downSample和高斯模糊什么关系?很容易想到,用同样的核进行滤波,下采样后滤波再上采样回去,会更模糊。这就相当于用更大的核来进行滤波。
【笔记】【百人计划】图形2.7 LDR与HDR | XZYW (xzyw7.github.io)
在LDR与HDR的笔记中曾经提到过Unity的Bloom方式。Unity就是多次下采样来完成Bloom效果的,不妨把它叫做coreSize的方法
我们也可以做这样的处理,只需要在每次迭代中增加下采样的次数就可以了。
还要做一个小小的修改就是blurSize的处理
material.SetFloat(“_BlurSize”, 1.0f);教程中为了达到这样的效果,采用了每次迭代增加BlurSize的方法,如下图( https://zhuanlan.zhihu.com/p/471923875)。
我们这里每次迭代都增加下采样,自然不用做这个处理。

得到的更优的结果就是,在更大的迭代数下能够减少虚影
blurSpread过大会导致严重的虚影
到这里还有一个问题
就是其实我们会发现我们的辉光,一点都不辉,一点都不光,每次下采样后,模糊是模糊了,但是整个“光晕”也变暗了。
按照上面的说法,我们应该把每次下采样的结果叠加起来。于是我们再新建一个RT缓冲区,用来储存下采样模糊的叠加结果。
1 2 3 4 5
| buffer1 = RenderTexture.GetTemporary(rtW,rtH,0); material.SetTexture("_Bloom", buffer0); Graphics.Blit(bufferResult1,buffer1,material,3); RenderTexture.ReleaseTemporary(bufferResult1); bufferResult1 = buffer1;
|
是理想中的辉光没错了。但是这种做法要注意控制迭代次数,因为亮度是叠加的,迭代次数太多会过曝。
但是这样做的时候,用blurSize的结果是非常好的,但是用coreSize就出问题了。迭代次数越多,像素化越严重,以下是3次迭代,再增加就没法看了。正如入门精要中提及,尽管downSample值越大,性能越好,但过大的DownSample可能会造成图像像素化。
的确,blurSize的方法只在最开始的时候做downSample,而coreSize的方法每一次迭代都进行下采样,是非常节省性能的,但在这种叠加的时候,就出现问题了,因为下采样的区域差别是指数级的,叠加后会形成明显的区分。
而blurSize的方法始终是在同一分辨率上做间隔采样,每次迭代只是增加了间隔数量,不会出现这种问题。
所以,为了好看的效果,还是老老实实用blurSize吧。
(不知道有没有更好的做法,能够用coreSize的方法的同时,得到均匀的Bloom)
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
| RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0); buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src,buffer0, material, 0);
RenderTexture bufferResult1 = RenderTexture.GetTemporary(rtW,rtH,0); for(int i=0;i<iterations;i++) {
material.SetFloat("_BlurSize", 1 + i * blurSpread); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0); Graphics.Blit(buffer0,buffer1,material,1);
RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW,rtH,0); Graphics.Blit(buffer0,buffer1,material,2);
RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; material.SetTexture("_Bloom", buffer0); RenderTexture bufferResult2 = RenderTexture.GetTemporary(rtW,rtH,0); Graphics.Blit(bufferResult1,bufferResult2,material,3); RenderTexture.ReleaseTemporary(bufferResult1); bufferResult1 = bufferResult2; }
|
https://blog.csdn.net/leonwei/article/details/54972653
https://docs.unity3d.com/ScriptReference/RenderTexture.html
- 实际上,我们的bloom的范围还应该受到亮度的影响
https://zhuanlan.zhihu.com/p/91390940发现这篇文章也做了相同的Bloom累加操作
看SSAO的开头的时候发现有同学提到catlike中也有Bloom的教程,去找了下放在这,之后可以看看这个是怎么处理这些细节的。
https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/
作业
- 实现Bloom效果
- 思考如何实现Bloom的mask功能,让区域不产生bloom效果
首先是理解这个问题,什么叫做Bloom的Mask。我的理解就是,正常的Bloom是对整张图片做后处理,那么整个画面都会受到一个全局的影响,而施加mask就是让场景中的一部分物体,虽然亮度超过阈值,但是不产生Bloom。
也就是说,在亮度提取的部分,给它一个Mask,不会提取到这部分亮度。
- FB的Alpha通道
- 我们可以在物体渲染的最后在alpha通道加上我们的mask参数,然后在亮度提取的时候乘上alpha通道(mask)

- 可以看到问题就是,半透明发光物体也会被影响,但控制效果是达到了。
- 模板测试
- 很自然而然的,这张mask我们可以用模板缓冲来做。
- 就不实操了,纸上谈兵一下,渲染每个物体的时候把相关的mask写入模板缓冲,
- 在亮度提取pass,用Equal或者其他什么操作,通过测试再提取亮度
- 传入一张Mask
- 可以直接public指定一个Texture2D,然后作为参数
- 我这里随便拖了个黑白的贴图,效果还是很好的。但是实用性好像不是很大

参考资料
[1] https://www.bilibili.com/video/BV1a3411z7LC
【技术美术百人计划】图形 4.1 Bloom算法 游戏中的辉光效果实现
[2] https://zhuanlan.zhihu.com/p/471923875
[3] https://kalogirou.net/2006/05/20/how-to-do-good-bloom-for-hdr-rendering/
[4] https://docs.unity3d.com/ScriptReference/Graphics.Blit.html
[5] https://blog.csdn.net/leonwei/article/details/54972653
[6] https://docs.unity3d.com/ScriptReference/RenderTexture.html
[7] https://zhuanlan.zhihu.com/p/91390940