拆开“光的魔法盒“:Bloom 工作原理的算法实现全解析
引子从知道到做到的鸿沟上一篇我们为 Bloom 写了一篇动人的传记——我们知道了它模拟强光溢出晕染的现象也理解了它提取—模糊—叠加的三步思路还领略了它天使与恶魔的两副面孔。读完之后你或许信心满满觉得自己已经懂了 Bloom。可是如果此刻我把你放到一位真正的图形程序员的位置上递给你一张刚刚渲染好的画面对你说“好现在请你把 Bloom 效果一行一行代码地真正实现出来。”你可能会瞬间卡住。“提取最亮的部分——具体怎么提取拿什么标准边缘会不会很生硬”“模糊——用什么方法模糊我要一个又大又柔和的光晕可模糊范围一大计算量岂不是要爆炸”“叠加——直接加回去吗会不会过曝强度怎么控制”你会发现知道一个思路和真正实现它之间隔着一条巨大的鸿沟。那三步提取—模糊—叠加说起来轻巧可一旦要真刀真枪地在代码里、在每秒60帧的性能压力下实现出来每一步都藏着精妙的算法设计、无数的性能陷阱和巧妙的工程智慧。今天我们就要跨越这条鸿沟。我们要像一个好奇的孩子拆开一个魔法盒子那样把 Bloom 的算法实现一个零件、一个齿轮地拆开来看——看看那三步优雅的思路在真实的工程里究竟是如何被一步步、精打细算地实现出来的。这一次我们会更硬核一点但我保证依然会用最生动的比喻让你看懂每一个齿轮的转动。准备好了吗让我们打开这个光的魔法盒。一、第一步的实现亮度提取——如何挑出该发光的部分Bloom 的第一步是提取画面里最亮的部分。听起来简单但真正实现起来第一个问题就来了用什么标准来衡量一个像素够不够亮1.1 计算亮度值不是所有颜色通道一视同仁一个像素有红R、绿G、蓝B三个颜色分量。我们要判断它亮不亮就得先把这三个分量浓缩成一个单一的亮度Luminance数值。一个朴素的想法是直接把 R、G、B 加起来平均一下。但这不符合人眼的感受——人眼对绿色最敏感、对蓝色最不敏感。同样的数值绿色看起来比蓝色亮得多。所以真正的实现里会用一个加权公式来计算亮度最经典的是亮度 0.2126 × R 0.7152 × G 0.0722 × B看这三个权重——绿色占了 0.7152 的大头红色 0.2126蓝色只有可怜的 0.0722。这正是模拟人眼绿敏感、蓝迟钝的特性算出的亮度才符合我们的真实感受。比喻:这就像给三种颜色投票选亮度,但这不是一人一票的公平选举,而是加权投票——绿色是话语权最大的’大股东’,蓝色则是几乎没什么发言权的’小散户’。因为在人眼这个董事会里,绿色的意见最举足轻重。1.2 阈值判断设一道门槛算出了每个像素的亮度接下来就是判断它是否超过阈值Threshold这道门槛如果 亮度 阈值 保留这个像素它参与泛光 否则 把它变成黑色0它不参与泛光经过这一步我们就得到了一张新图只有原画面里够亮的部分被保留了下来其余全是黑色。这张图我们叫它亮度图或Bloom源图——它就是即将绽放光晕的那些光的种子。1.3 一个陷阱硬阈值的生硬跳变但这里藏着一个恼人的陷阱。如果我们用上面那种非黑即留的硬阈值Hard Threshold,会出现一个问题想象画面里有一片亮度渐变的区域亮度从 0.9 平滑地过渡到 1.1而阈值设在 1.0。那么亮度 0.99 的地方 → 低于阈值 →突然变黑;亮度 1.01 的地方 → 高于阈值 →突然保留。在阈值 1.0 这条线上画面会出现一个生硬的、突兀的跳变边界——本该平滑过渡的地方硬生生被切出一道断崖。最终泛光效果里就会出现难看的、不自然的硬边。解法软阈值Soft Threshold。在阈值附近设置一个平滑的过渡带——不是要么全留、要么全丢,而是让接近阈值的像素按比例地、渐进地参与泛光比如亮度刚过阈值的只保留一点点远超阈值的才完整保留。这样那道生硬的断崖,就被抹平成了柔和的缓坡泛光的过渡也就自然了。认知点:硬阈值 vs 软阈值,是一个绝佳的工程细节——理论上非此即彼的简单判断,在追求视觉品质时,往往需要被替换成平滑过渡的精细处理。生硬的一刀切总是简单的,但柔和的渐变,才更接近自然、更耐看。这个化生硬为柔和的思路,我们在后面还会反复遇到。二、第二步的实现模糊——性能与效果的核心战场提取出了光的种子,接下来要让它们晕染扩散——也就是模糊。这是整个 Bloom 算法里最核心、最考验智慧、也最容易出性能问题的一步。这里的每一个决策都是在效果和性能之间的精妙博弈。2.1 最经典的模糊高斯模糊Gaussian Blur要模拟光柔和地向四周晕染,最经典、最自然的方法是高斯模糊Gaussian Blur。它的核心思想是一个像素模糊后的颜色等于它周围一圈像素颜色的加权平均而权重服从高斯分布——离得越近的邻居权重越大离得越远的权重越小。打个比方一个像素要模糊,就相当于它向周围的邻居们打听颜色、取个平均。但它更相信近邻的话权重大对远处邻居的话则听听就好权重小。这样平均出来的结果就是一片自然、柔和的模糊——中心浓、边缘淡恰如光晕的样子。2.2 第一个性能陷阱模糊范围一大计算量就爆炸但高斯模糊有个致命的问题。我们想要的 Bloom 光晕往往是又大又柔和的——这意味着模糊的范围半径要很大。可是模糊的计算量是随半径急剧膨胀的。如果一个像素模糊时要参考周围 50×50 范围的邻居那就是每个像素要采样2500 次一张 1080P 的画面有两百多万个像素乘起来……那是几十亿次的计算显卡当场罢工给你看。大范围的模糊直接硬算性能上是绝对无法承受的。这就是横在 Bloom 面前的第一座性能大山。2.3 第一个优化可分离高斯模糊——把二维拆成两个一维工程师们的第一个绝招是利用高斯模糊一个美妙的数学性质——它是可分离的Separable。意思是一次二维的高斯模糊同时在横竖两个方向上模糊可以等价地拆分成两次一维的模糊——先只在水平方向模糊一遍再只在垂直方向模糊一遍。两者的结果完全一样这个拆分威力巨大。假设模糊半径要参考一行/一列 50 个像素直接二维模糊:每个像素采样 50×50 2500 次;拆成两次一维:水平采样 50 次 垂直采样 50 次 100 次!2500 次 vs 100 次——性能提升了 25 倍而且半径越大这个优势越夸张。绝妙的比喻:这就像给一块正方形的地板拖地。笨办法(二维):你拿着抹布,一个格子一个格子地、把整个 50×50 的方形区域,挨个擦一遍——累死。聪明办法(可分离):你先横着拖一遍(50下),再竖着拖一遍(50下),交叉之下,整块地板同样擦干净了——却只花了 100 下!先横后竖这一拆,就把令人绝望的工作量,砍掉了绝大半。这份化二维为两次一维的数学智慧,是 Bloom 性能优化的第一块基石。2.4 第二个优化降采样——在缩小的画布上模糊即便有了可分离优化要在一张全分辨率1080P、4K的大图上做大范围模糊依然很贵。工程师的第二个绝招堪称神来之笔——降采样Downsampling。核心思想是我先把图片缩小,在缩小的、像素少得多的小图上做模糊,再把结果放大回去。为什么这么做可行因为——Bloom 本来就是模糊的、朦胧的东西,它根本不需要精确的细节!既然最终要糊掉那我在一张缩小的、本来就糊的小图上操作最后放大回去人眼根本看不出区别反而正合适而缩小的收益是惊人的把图片的宽和高各缩小一半 → 像素数量变成原来的1/4→ 计算量直接降到1/4;再缩小一半 → 像素变成 1/16 → 计算量降到1/16……而且在小图上模糊,还有一个额外的甜头:在缩小4倍的图上,模糊半径为5的效果,相当于在原图上模糊了20——你想要的大范围晕染,在小图上,用很小的模糊半径就轻松实现了!一举两得。比喻:这就像画一幅朦胧的远山写意画。你何必用最高精度的画布、拿最细的笔去精雕细琢呢?那纯属浪费。你完全可以在一张小小的、粗糙的草稿纸上,几笔就晕染出那份朦胧的意境,然后把它放大挂到墙上——反正要的就是朦胧,在小纸上画,又快又好,何乐不为?三、进阶实现现代 Bloom 的金字塔结构把上面的思路(降采样 模糊)推向极致就诞生了现代游戏里主流的、极其优雅的 Bloom 实现方案——多级降采样金字塔Mip Pyramid。这套方案被虚幻引擎、《使命召唤》等顶级作品广泛采用我们重点讲清它。3.1 “下山再上山”金字塔的两段旅程这套方案的形状像一座金字塔包含逐级缩小和逐级放大两段旅程第一段——逐级降采样Downsample“下山”:从提取好的亮度图开始把它一级一级地不断缩小:原图 → 缩小到 1/2 → 缩小到 1/4 → 1/8 → 1/16 → ……一直缩到很小很小比如几十像素。每缩小一级就在这一级上做一点轻微的模糊。于是我们得到了一系列从大到小、从清晰到模糊的图片——这就是金字塔从底座到塔尖的各个层级。第二段——逐级升采样并叠加Upsample Combine“上山”:然后反过来从最小的塔尖开始一级一级地放大,并且每放大一级,就和金字塔同尺寸的那一层相加融合:最小层 → 放大 → 加上大一级的层 → 再放大 → 再加上更大一级的层 → ……一直放大叠加回原始尺寸。3.2 为什么要搞这么复杂的金字塔——大小光晕的完美融合你可能会问搞这么一座下山又上山的金字塔图什么图的是——不同尺度光晕的完美融合!想想真实的强光光晕它既有紧贴光源的、小而亮的核心光晕,又有向远处弥漫的、大而淡的弥散辉光。一个只有单一模糊半径的 Bloom要么只能做出小光晕不够弥散要么只能做出大光晕丢了核心细节无法两者兼得。而金字塔结构天生就解决了这个问题金字塔的大尺寸层缩小得少→ 贡献小而集中的核心光晕;金字塔的小尺寸层缩小得多→ 放大回来后贡献大而弥散的柔和辉光;把所有层级层层叠加起来 → 就得到了一个同时包含大、中、小各种尺度、层次极其丰富、过渡极其自然的光晕这正是现代游戏里那些高质量 Bloom 看起来如此高级电影感十足的秘密——它不是单一尺度的模糊而是一整座从小到大、层层叠加的光晕金字塔的完美合奏。绝妙的比喻:这座金字塔,就像一支演奏光之交响乐的乐队。大尺寸层是小提琴,奏出紧凑明亮的高音(核心光晕);中尺寸层是大提琴,奏出温润的中音(中层辉光);小尺寸层是低音贝斯,奏出弥漫悠远的低音(弥散大辉光)。单独任何一个乐器,都只是单薄的一个声部;唯有把它们层层叠加、和谐合奏,才汇成了一曲层次丰富、饱满动人的光的交响乐。这,就是金字塔式 Bloom 的精髓——用多个尺度的声部,合奏出真实光晕那份丰富而自然的美。3.3 一个细节升采样时的柔和放大在上山放大的过程中还藏着一个提升品质的细节。如果只是简单粗暴地把小图放大最近邻放大会出现难看的块状马赛克。所以现代实现如《使命召唤》著名的技术分享里会采用一种特殊的、带模糊的柔和升采样滤波器比如一种九点采样的帐篷滤波在放大的同时顺便柔化让层与层之间融合得天衣无缝、如丝般顺滑,彻底杜绝块状瑕疵。认知点:又一次,我们看到了化生硬为柔和的思路(前面软阈值也是)。从提取、到模糊、到升采样——Bloom 算法的每一个环节,工程师都在孜孜不倦地抹平生硬、追求顺滑。因为 Bloom 的灵魂,就是柔和;任何一丝生硬,都是对它的背叛。对柔和的极致追求,贯穿了整个算法的始终。四、第三步的实现叠加合成——最后的调味光晕金字塔炼好了最后一步是把它叠加回原始画面,得到最终成片。这一步看似简单却也有讲究。4.1 混合而非简单相加最朴素的做法是把光晕直接加到原图上。但更精细的做法是用一个**强度系数Intensity“和混合比例”**来控制最终画面 原画面 光晕 × 强度系数或者用线性插值Lerp在原图和光晕之间按比例混合。这个强度系数,就是暴露给美术和玩家调节的那个Bloom 强度参数——它决定了光晕融入画面的浓淡,是锦上添花还是泛滥成灾的分水岭。4.2 与色调映射的先后顺序一个关键的工程抉择这里有一个专业、却极其重要的细节Bloom 的叠加应该发生在色调映射Tone Mapping之前还是之后回忆上一篇讲过的Bloom 工作在 HDR高动态范围空间里才能精准识别真正的强光。所以正确的顺序通常是先在 HDR 空间下完成 Bloom 的提取、模糊、叠加然后再把叠加了光晕的 HDR 画面交给色调映射,压缩到屏幕能显示的范围。如果顺序搞反了先色调映射、再加 Bloom强光的亮度信息已经被压缩、被削平Bloom 就失去了那份宽广而精准的亮度依据,效果会大打折扣、失真变味。认知深化:做对的事,还要按对的顺序做。Bloom 与色调映射的先后,是一个典型例子——同样的几个步骤,顺序对了,珠联璧合;顺序错了,满盘皆输。流程的次序,本身就是一种关键的智慧。这在做任何复杂的事情时,都同样成立。五、完整流程回顾一次 Bloom 的诞生让我们把整个算法实现从头到尾串成一条清晰的流水线看一次完整的 Bloom 是如何诞生的【第一步·提取】遍历渲染好的 HDR 画面用加权公式0.2126R 0.7152G 0.0722B算出每个像素的亮度;用软阈值筛选出够亮的部分得到亮度图其余置黑避免生硬跳变。【第二步·模糊金字塔下山】3. 把亮度图逐级降采样1/2、1/4、1/8……每一级做轻微的可分离高斯模糊;4. 得到一座从大到小、从清晰到模糊的图像金字塔。【第三步·升采样融合金字塔上山】5. 从最小层开始用柔和的升采样滤波逐级放大;6. 每放大一级就与金字塔同尺寸的那一层相加融合;7. 层层叠加得到一个包含大、中、小各种尺度、过渡自然的丰富光晕。【第四步·合成】8. 用强度系数把最终光晕叠加回原始 HDR 画面;9. 把叠加好的 HDR 画面交给色调映射处理输出到屏幕。至此一朵层次丰富、柔和自然、恰到好处的光花就通过这条精密的流水线绽放在了我们的画面里。当然实现方案不止一种——除了这套主流的金字塔方案还有更省性能的双重滤波Dual Filtering / Kawase等变体思路大同小异都是降采样—模糊—升采样叠加这套核心智慧的不同演绎。尾声优雅的思路背后是无数精算的齿轮我们拆开了 Bloom 这个光的魔法盒,看清了它内部每一个齿轮的转动提取藏着加权亮度与软阈值的细腻;模糊藏着可分离、降采样、金字塔层层递进的性能智慧;融合藏着多尺度叠加与柔和升采样对自然的极致追求;合成藏着强度控制与流程次序的工程严谨。回过头来品味从上一篇那优雅简洁的提取—模糊—叠加三步思路到这一篇里如此精密复杂的算法实现——这中间的巨大反差本身就道出了一个深刻的真理。我们太容易停留在思路的层面,以为知道了大概,就等于掌握了。“不就是提取、模糊、叠加嘛”——多简单可当你真正俯身下去、要把它一行行实现出来时才会发现魔鬼,全在细节里。那个提取,要顾及人眼的色彩敏感、要抹平硬阈值的跳变那个模糊,要拆解二维、要降采样、要搭建金字塔才能在性能的刀锋上跳出优美的舞蹈那个叠加,还要斟酌强度、讲究次序……每一个看似轻描淡写的思路步骤背后,都站着无数个需要被认真对待、精心打磨的工程细节。思路,给你指明了做什么;而实现,才逼着你去回答那千百个到底怎么做。前者是浪漫的蓝图,后者是脚下的每一块砖。真正把事情做成的人,是那些既仰望蓝图、又肯俯身砌砖的人。这,正是眼高手低与知行合一的分野。太多人,满足于我知道原理“我懂那个思路”,却从未真正俯下身去趟过实现这条布满细节陷阱的、真实的河流。而所有真正的创造、真正的精通都诞生在那条河的对岸——在你为一个软阈值反复调试、为一次降采样精打细算、为一套金字塔苦苦推敲的、那些不浪漫却无比扎实的时刻里。所以当你下次在游戏里看到那轮夕阳绽放出层次丰富、柔和动人的光晕时——愿你不仅能想起它提取—模糊—叠加的优雅思路更能想起这背后那一座默默运转的光晕金字塔,那一个个被工程师精心打磨过的、看不见的齿轮。并愿你记得任何一份看起来轻盈优雅的美好,其背后,往往都是无数个沉重而精密的细节,在默默地支撑与运转。欣赏那份轻盈,更要敬重那份沉重——因为真正的高手,永远是把简单的思路,用不简单的功夫,老老实实地,一步步做出来的人。