泛光(Bloom)是古代电子游戏中常见的后处理特效,通过图像处理算法将画面中高亮的像素向外“扩张”造成光晕以减少画面的真实感,可能活泼地表白太阳、霓虹灯等光源的亮度。Bloom 的好坏可能极大地改善游戏的表现力。
泛光特效的原理并不简单,提取图像高亮的局部做含糊再叠加回原图。在互联网上有很多对于泛光算法原理的介绍文章或者教程,我这里就不唠叨了。
为什么写这篇文章
只管网上有十分多的材料,然而要想制作出高品质的泛光成果却没那么容易。应用最根底的办法做出的成果可能是下图这样的,显然这个后果间隔在显示器上制作闪闪发光的小太阳还很边远:
第一次写泛光特效的时候还是在高二,过后在玩 Minecraft,磕磕绊绊地抄着代码却只是在游戏里实现了一个比上图还要不堪的成果。我想在游戏外面复现文章一开始那张图的成果,却没有进一步的材料能够参考,这令我非常丧气。迫于做题家的压力也没有钻研上来,最初不了了之。
网上的教程大多数都在介绍完基础理论就戛然而止,鲜有更加深刻的探讨与实际。为了补救童年时的遗憾,萌发了写这篇文章的想法。
什么是高品质泛光
对于优良的泛光特效来说,我认为须要满足以下几个特点:
- 发光物边缘向外“扩张”的足够大
- 发光物核心足够亮(甚至超过 1.0 而被 Clamp 成红色)
- 该亮的中央(灯芯、火把)要亮,不该亮的中央(红色墙壁、皮肤)不亮
上面是一组比拟有代表性的(我认为)高质量泛光成果截图:
与之对应的,放一组(我认为)成果比拟个别的泛光。如果该亮的中央不够亮,不该亮的中央亮了,那么很容易产生场景的“含糊”感:
上面这张图则是发光处的核心和向外扩散出的轮廓都很亮,此外下图中红色土地也在发光,画面显得很脏:
下图则是发光物泛光的扩散范畴不够大,画面的表现力不够强:
高质量的泛光成果能够用一张图清晰地总结。简略来说就是两头亮的批爆,然而越往外亮度降落越快。这有点相似正态分布曲线。下图是 UE4 给出的现实泛光亮度曲线:
为何要应用 HDR 纹理
HDR 纹理容许像素的亮度超过 255,这可能很好地示意事实世界的亮度。只管最终输入到屏幕上会被 Clamp,但最重要的是在对 HDR 纹理做滤波的时候,超亮的像素能够被无效地扩散到四周区域。
滤波的实质是对 Kernal 笼罩的范畴内所有像素按某种权重做加权均匀。打个比方,我和马云的财产均匀一下,我也是富哥了。不同的 Filter 有不同的 Weight,然而只有高亮像素的值足够大,它总可能辐射到周边的像素。
上面是一组比照图,应用了大尺寸(radius=100,sigma=30)的高斯含糊进行解决。HDR 源纹理输入像素为纯白,值缩放大小由 Emissive intensity 管制:
其中 Emissive intensity = 1.0 时对应一般的 LDR 纹理。因为 Kernal 的尺寸足够大,1.0 的像素值很快被摊派洁净。如果像素足够亮,那么即便处于 Kernal 边缘也可能积攒可观的亮度。像素越亮,它能扩散的间隔就越远。这意味着单个高亮像素也能扩散出很大的范畴:
此外,HDR 纹理可能帮忙咱们疾速辨别须要进行含糊的高亮像素。这可能让美术更加灵便地依据真实世界的参数调整材质。
疾速的大范畴含糊
要想光晕扩的足够大,第一件事件就是扩充含糊的范畴。一种非常简单的思路就是加大滤波盒的尺寸,应用一个微小的 Kernal 对纹理进行含糊。然而性能上必定是吃不消,单 Pass 的纹理采样次数是 N^2 而双 Pass 是 N +N。
此外还有一个问题,在解决高分辨率纹理时你须要等比减少滤波盒的尺寸,能力造成等同大小的含糊。比方在 1000×1000 分辨率下用 250 像素的 Kernal,含糊的后果占 1 / 4 屏幕,当分辨率减少到 2000×2000 的时候,要应用 500 像素的 Kernal 能力达到同样的成果。
回到含糊的问题,含糊滤波的实质是查问 Kernal 范畴内的所有像素并加权均匀,即范畴查问问题。在计算机图形学中实现疾速范畴查问,通常会请到老朋友 Mipmap 出场。Mipmap 将图像大小顺次折半造成金字塔,Mip[i]中的单个像素代表了 Mip[i-1]中的 2 ×2 像素块均值,也代表 Mip[i-2]中的 4 ×4 像素块均值:
通过查问高 Level 的 Mipmap 能够在常数工夫内查问大范畴的源纹理。在 (w/4,h/4) 的贴图上做 3 ×3 滤波,近似于在 (w,h) 的贴图上做 12×12 的滤波。为此须要创立 size 逐级递加的纹理,并应用 downSampler 着色器将 Mip[i-1]下采样到 Mip [i],以 Unity 为例,在 OnRenderImage 中一个最简略的下采样 Mip 串实现:
在 downSample 着色器中间接输入源纹理的色彩。留神源纹理须要启用双线性滤波,这样硬件会帮忙咱们计算上一级 Mip 中 2 ×2 像素块的均值:
在足够高的 Mip 等级下,含糊的范畴的确增大了。然而含糊的后果不够好,这是因为双线性滤波实质上是个 2 ×2 的 Box Filter,方形的 Pattern 很重大:
为了取得更加圆滑的含糊咱们须要选用更高级的 Blur Kernel,高斯含糊是一个不错的抉择。一个 5 ×5,标准差为 1 的高斯含糊就足够好了。这里我抉择手动计算高斯滤波盒的权重,通常来说应用预计算的 2D 数组会放慢计算速度:
自此咱们通过多次下采样造成 Mip 链以实现大范畴的圆形含糊成果:
描述核心高亮区域
应用下采样生成大范畴的含糊仅仅是第一步,间接将最高层级 Mip 叠加到图像上尽管可能产生足够大的光晕扩散,然而发光物的核心区域不够亮堂。此外,发光物和泛光之间没有适度而是间接跳变,从高亮区域跳到低亮度区域显得十分不天然:
不论应用何种滤波器,实质上都是在做加权均匀。只有一均匀,就有人拖后腿!每次含糊都会升高源图像的亮度,并将这些亮度摊派到四周的纹理。边缘的跳变来自于高层级 Mip 和原图之间亮度差距过大:
为了实现发光物和最高层 Mip 之间的过渡,咱们须要叠加所有的 Mip 层级到原图上。因为 Mip[i]是基于 Mip[i-1]进行计算的,相邻层级之间绝对间断则不会产生跳变:
较低的 Mip 层级含糊范畴小且亮度高,次要负责发光物核心的高亮,较高的 Mip 层级含糊范畴大且亮度低,次要负责发光物边缘的泛光。叠加所有的 Mipmap 就能同时达到高质量泛光的两个要求,即够亮与够大:
是不是有感觉了?
解决方块图样
因为咱们间接从 Mipmap 链中采样到全分辨率,很难免会呈现方块状的 Pattern,因为最高级别的 Mip 分辨率小到个位数:
能够通过含糊滤波来解决方块图样。值得注意的是不能间接对小分辨率的高阶 Mip 进行滤波,因为分辨率太小,不管怎么滤波,上采样到 Full Resolution 的时候都会有方块。除非滤波产生在高分辨率纹理。
然而高分辨率纹理上一大块区域都对应低分辨率 Mip 上的同一个 Texel,如果 Kernal 不够大那么做 Filter 的时候查问的值都是同一个 Texel,这意味着在高分辨率纹理上要应用超大的滤波盒能力打消这些方块。下图很好的阐明了这一点:
问题又回到了如何应用便宜的小尺寸滤波盒实现大范畴含糊的问题。和下采样时相似,采样逐级递进的形式对低分辨率的 Mip 链进行上采样。将 Mip[i]上采样到 Mip[i-1],再和 Mip[i-1]自身叠加失去新的 Mip[i-1],这种策略在《使命号召 11 的 GDC 分享》中被提出:
进行这个操作须要额定创立一组 RenderTexture,上面是下采样 Mip 链(RT_BloomDown)和上采样 Mip 链(RT_BloomUp)之间的数据倒腾关系,以 964×460 分辨率和 N = 7 次为例:
对应的 C# 代码也比较简单,只是须要留神纹理之间尺寸、下标的关系。这里 RT_BloomUp 仅有 N - 1 个纹理,记得在 Frame Debugger 中确保尺寸关系的正确:
upSample 的着色器也比较简单,同样用的 5 ×5 的高斯含糊解决 curr_mip,对于 prev_mip 能够小滤一下也能够间接采样。通过测试最好对两者都进行滤波,可能失去更加平滑的成果。最初叠加两者作为本级 Mip 的处理结果:
当初方块图样有了显著的改善:
和闪动抗衡
如果相熟 PBR 流程的话,不难想到 Specular 的 BRDF 在 Roughness 十分小、NdotL 靠近 1.0 的时候,会输入极大的数值,尤其是当光源的强度足够高时。即高光局部十分亮,如果应用了法线贴图等高频法线信息,会导致画面闪动的很厉害:
https://www.youku.com/video/X…
对此 COD 的计划是在 Mip0 到 Mip1,即第一次下采样时,退出额定的权重来试图抹平因法线贴图碰巧 NdotL 很靠近 1.0 而引起单个超高亮像素。这个做法叫做 Karis Average:
须要一个独自的 firstDownSample 着色器来进行第一次下采样。高斯含糊版本对应的代码如下,如果应用的是自定义的 Kernal 可能须要做一些调整:
这个办法因为对亮度做了束缚,会损失肯定的 Bloom 范畴和亮度,然而失去更加稳固的高光:
https://www.youku.com/video/X…
更好的滤波盒
在高低采样都应用 5 ×5 的高斯滤波盒显得有些侈靡。采样纹理是十分低廉的操作,GPU 须要通过数百个时钟周期能力实现。间接应用 2 ×2 的 Box 尽管足够疾速,但会有很显著的 Pattern。
在 COD 的分享中应用了更为玲珑的滤波盒,下采样时依照 2 ×2 一组进行采样。采样共 5 组,并依照肯定的权重加权。这个滤波盒在高斯含糊和 2 ×2 的 Box 之间进行了平衡,既保证了效率又保障了品质:
而在上采样的 Filter 中,他们更是应用了更为简略的 3 ×3 Tent Filter,值得注意的是他们应用了一个 Radius 来管制滤波的范畴,这有点相似于深度学习中的“带洞卷积”滤波器。这也是为何游戏有些中央会有显著的格子感的起因:
像素筛选
一种常见的表现手法是让角色身上的某个部件进行高亮,比方装甲能量槽:
要做到这一点须要在下采样之前,筛选出须要计算 Bloom 的像素。只有足够高亮度的像素才有资格被计算泛光,这和事实世界的法则相符,比方白炽灯、篝火或者是太阳。这要在 HDR 环境下进行渲染。
通常状况下应用的是 1.0 作为亮度筛选的阈值,也能够不设置阈值但通过 Bloom Intensity 管制最终 Bloom 的强度,比方乘以 0.01,这样只有发光物(lum=1000)和失常场景物件(lum=1.0)亮度相差足够大就能产生泛光。
如果应用的是 PBR 工作流,那么问题变得非常简单。PBR 材质通常都带有自发光贴图(或者是任何自定义的 Mask 贴图),这是美术当时标注的模型高亮处。只须要调整其强度,在 Base Pass 中输入超高的亮度值即可:
https://www.youku.com/video/X…
此外能够为发光物件应用独自的材质,比方角色的光剑、项链等道具。
代码仓库
https://github.com/AKGWSB/Cas…
参考与援用
[1] NEXT GENERATION POST PROCESSING IN CALL OF DUTY: ADVANCED WARFARE
[2] Custom Bloom Post-Process in Unreal Engine
[3] 实时渲染学习笔记—光晕成果(bloom)
[4] 后处理 - 泛光成果
[5] Catlike Coding’s Unity tutorial
这是侑虎科技第 1258 篇文章,感激作者 AKG4e3 供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:793972859)
作者主页:https://www.zhihu.com/people/…
再次感激 AKG4e3 的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:793972859)