泛光(Bloom)是古代电子游戏中常见的后处理特效,通过图像处理算法将画面中高亮的像素向外“扩张”造成光晕以减少画面的真实感,可能活泼地表白太阳、霓虹灯等光源的亮度。Bloom的好坏可能极大地改善游戏的表现力。

泛光特效的原理并不简单,提取图像高亮的局部做含糊再叠加回原图。在互联网上有很多对于泛光算法原理的介绍文章或者教程,我这里就不唠叨了。

为什么写这篇文章

只管网上有十分多的材料,然而要想制作出高品质的泛光成果却没那么容易。应用最根底的办法做出的成果可能是下图这样的,显然这个后果间隔在显示器上制作闪闪发光的小太阳还很边远:

第一次写泛光特效的时候还是在高二,过后在玩Minecraft,磕磕绊绊地抄着代码却只是在游戏里实现了一个比上图还要不堪的成果。我想在游戏外面复现文章一开始那张图的成果,却没有进一步的材料能够参考,这令我非常丧气。迫于做题家的压力也没有钻研上来,最初不了了之。

网上的教程大多数都在介绍完基础理论就戛然而止,鲜有更加深刻的探讨与实际。为了补救童年时的遗憾,萌发了写这篇文章的想法。

什么是高品质泛光

对于优良的泛光特效来说,我认为须要满足以下几个特点:

  1. 发光物边缘向外 “扩张” 的足够大
  2. 发光物核心足够亮(甚至超过1.0而被Clamp成红色)
  3. 该亮的中央(灯芯、火把)要亮,不该亮的中央(红色墙壁、皮肤)不亮

上面是一组比拟有代表性的(我认为)高质量泛光成果截图:

与之对应的,放一组(我认为)成果比拟个别的泛光。如果该亮的中央不够亮,不该亮的中央亮了,那么很容易产生场景的 “含糊” 感:

上面这张图则是发光处的核心和向外扩散出的轮廓都很亮,此外下图中红色土地也在发光,画面显得很脏:

下图则是发光物泛光的扩散范畴不够大,画面的表现力不够强:

高质量的泛光成果能够用一张图清晰地总结。简略来说就是两头亮的批爆,然而越往外亮度降落越快。这有点相似正态分布曲线。下图是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。

此外还有一个问题,在解决高分辨率纹理时你须要等比减少滤波盒的尺寸,能力造成等同大小的含糊。比方在1000x1000分辨率下用250像素的Kernal,含糊的后果占1/4屏幕,当分辨率减少到2000x2000的时候,要应用500像素的Kernal能力达到同样的成果。

回到含糊的问题,含糊滤波的实质是查问Kernal范畴内的所有像素并加权均匀,即范畴查问问题。在计算机图形学中实现疾速范畴查问,通常会请到老朋友Mipmap出场。Mipmap将图像大小顺次折半造成金字塔,Mip[i]中的单个像素代表了Mip[i-1]中的2x2像素块均值,也代表Mip[i-2]中的4x4像素块均值:

通过查问高Level的Mipmap能够在常数工夫内查问大范畴的源纹理。在(w/4,h/4)的贴图上做3x3滤波,近似于在(w,h)的贴图上做12x12的滤波。为此须要创立size逐级递加的纹理,并应用downSampler着色器将Mip[i-1]下采样到Mip [i],以Unity为例,在OnRenderImage中一个最简略的下采样Mip串实现:

在downSample着色器中间接输入源纹理的色彩。留神源纹理须要启用双线性滤波,这样硬件会帮忙咱们计算上一级Mip中2x2像素块的均值:

在足够高的Mip等级下,含糊的范畴的确增大了。然而含糊的后果不够好,这是因为双线性滤波实质上是个2x2的Box Filter,方形的Pattern很重大:

为了取得更加圆滑的含糊咱们须要选用更高级的Blur Kernel,高斯含糊是一个不错的抉择。一个5x5,标准差为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)之间的数据倒腾关系,以964x460分辨率和N=7次为例:

对应的C#代码也比较简单,只是须要留神纹理之间尺寸、下标的关系。这里RT_BloomUp仅有N-1个纹理,记得在Frame Debugger中确保尺寸关系的正确:

upSample的着色器也比较简单,同样用的5x5的高斯含糊解决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...

更好的滤波盒

在高低采样都应用5x5的高斯滤波盒显得有些侈靡。采样纹理是十分低廉的操作,GPU须要通过数百个时钟周期能力实现。间接应用2x2的Box尽管足够疾速,但会有很显著的Pattern。

在COD的分享中应用了更为玲珑的滤波盒,下采样时依照2x2一组进行采样。采样共5组,并依照肯定的权重加权。这个滤波盒在高斯含糊和2x2的Box之间进行了平衡,既保证了效率又保障了品质:

而在上采样的Filter中,他们更是应用了更为简略的3x3 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)