← ALL ARTICLES

Bloom

全库目录221
本文目录10

小亮点只模糊一次,容易得到单一大小的光圈。Bloom先从HDR颜色构建多尺度图,再从粗层向细层合成,让光晕同时保留近处亮核和远处扩散。每级过滤与能量权重共同决定形状和闪烁。

高亮信号

Bloom 模拟强光在镜头、传感器和视觉系统中向周围扩散。它应该由 HDR 高亮驱动。

阈值与曝光

根据亮度或颜色阈值提取高亮。Soft Knee 在阈值附近平滑过渡,避免亮度稍微变化时 Bloom 突然出现。

也可以让全部线性HDR颜色参与扩散,保留弱光贡献。带阈值的方案需要明确阈值是在场景尺度还是曝光尺度定义:若输入已乘预曝光 pp,场景阈值 TT 和过渡半宽 kk 也应分别换成 pTpT 与 pkpk,否则曝光变化会改变入选的光源。

下面的HLSL函数输入有限、非负的线性RGB,用最大RGB分量定义亮度 bb,保持提取后的RGB比例。阈值 T≥0T\ge0,过渡半宽 0≤k≤T0\le k\le T,均与输入处于同一线性尺度。在 [T−k,T+k][T-k,T+k] 内构造二次过渡,k=0k=0 时退化为硬阈值。

hlsl
float3 ExtractBloom(float3 color, float threshold, float knee)
{
    float brightness=max(color.r,max(color.g,color.b));
    if (brightness<=0.0) return float3(0,0,0);
    float soft=0.0;
    if (knee>0.0) {
        float q=clamp(brightness-threshold+knee,0.0,2.0*knee);
        soft=q*q/(4.0*knee);
    }
    float contribution=max(max(brightness-threshold,soft),0.0);
    return color*(contribution/brightness);
}

在上边界 b=T+kb=T+k,二次项的值为 kk、导数为1,正好接上线性项 b−Tb-T;下边界处值与导数都为0。半宽不超过阈值,使曲线从非负亮度开始过渡,并保持提取量不超过输入亮度。零输入直接返回黑色;正输入使用自身亮度作为除数,颜色、阈值和半宽同时缩放时,输出随同一倍率缩放。固定的绝对亮度下限会破坏极低预曝光尺度下的这个关系。它是美术提取模型,并非镜头散射的光学定律。

多尺度扩散

提取图已有亮核,直接把它合回原图只会让原位置更亮。光晕需要把这份信号送到邻近像素。大半径卷积能做到这一点,但半径增大时,逐像素采样的成本也随之增长。金字塔先缩小图像,让粗层的一个纹素代表原图的一片区域;再把粗层结果逐级展开,就能用小核产生大范围的扩散。

降采样与采样位置

先固定一个可复现模型:输入宽高为偶数,下一层各减半,纹素中心位于整数索引加0.5的位置。目标像素(x,y)覆盖源图(2x,2y)到(2x+1,2y+1)的四个纹素,把它们平均就得到最小的2×2盒式降采样。

python
def down2(src):
    h, w = len(src), len(src[0])
    if h % 2 or w % 2:
        raise ValueError("本模型要求偶数尺寸")
    return [[sum(src[2*y+j][2*x+i] for j in range(2) for i in range(2))/4
             for x in range(w//2)] for y in range(h//2)]

这里每个值代表线性HDR颜色的一个分量,RGB逐分量执行同样运算。四个权重相加为1,所以全图为4时,下一层仍为4。孤立的16则变成一个4;它对应的像素面积扩大四倍,因此面积加权总量仍是16。把这些值误当作应当直接相加的总能量,会在每次降采样时错误补偿亮度。

盒平均便宜,但它只提供有限的低通能力。亚像素亮点移动时,输入光栅覆盖率和滤波相位都可能变化;粗层会把变化扩大成明显闪烁。扩大首层低通核可以削弱这种相位敏感性,代价是近处亮核更软。若用非线性的亮度权重抑制极亮点,还会改变本页线性模型的能量关系,须单独检查。

奇数尺寸的右侧与下侧无法按完整2×2块配对。工程实现可使用向上取整尺寸、边界钳位与明确的采样中心,但这会让边缘纹素被重复计入。参考模型主动拒绝奇数尺寸,使数值例子的覆盖范围保持精确;目标工程应单独用右下角亮点检查所选边界策略。

上采样核与扩散半径

粗层已有远距离信息,还需要把它连续地重建到细层。令目标中心的归一化坐标为uv,粗层尺寸为(W,H)。对应粗层索引坐标为(uv.x*W-0.5, uv.y*H-0.5)。双线性重建先取这个坐标周围四个粗层纹素,再按小数部分加权;边界在本模型中钳位到最近纹素。

只做双线性重建可以平滑块边界,但扩散形状仍容易显出网格。这里在重建前增加一个可分离的帐篷核,沿每个轴使用[1,2,1]/4,二维权重是两轴的乘积:

text
1 2 1
2 4 2   × 1/16
1 2 1

中心贡献1/4,四个轴向邻居各贡献1/8,四个对角各贡献1/16。九个权重相加为1,因而保持常量。这个核是方形可分离近似;重复扩大半径时仍可能出现轴向形状,不能把它当成真实镜头的点扩散函数。

下面的HLSL是机制片段。Source是当前粗层的线性HDR纹理,LinearClamp使用线性过滤和边界钳位,sourceTexelSize为粗层纹素尺寸倒数。偏移使用粗层单位;若误用输出细层的倒数,粗层越小,实际核就越窄,远处光晕会收缩。每个样本的双线性插值完成重建,外层九点权重完成帐篷过滤。

hlsl
float3 TentUpsample(float2 uv, float2 sourceTexelSize)
{
    float3 result = 0;
    const float weights[3] = { 0.25, 0.5, 0.25 };
    for (int y=-1; y<=1; ++y)
        for (int x=-1; x<=1; ++x)
            result += weights[x+1]*weights[y+1]*
                Source.SampleLevel(LinearClamp,
                    uv+float2(x,y)*sourceTexelSize,0).rgb;
    return result;
}

高斯核同样可以拆成横向和纵向两遍,核宽由标准差决定;Kawase类方法通过多次偏移采样扩大支撑范围。选择它们会改变频率响应、轴向形状和采样数。本文的完整数值链固定使用上面的帐篷核,读者可以先追踪一种确定的实现,再更换核比较亮点响应。

尺度混合与光晕形状

设完整提取图为D0D_0,连续降采样得到D1D_1到DmD_m。最低层从Um=DmU_m=D_m开始,逐级计算:

Ui=(1−s)Di+s Ti(Ui+1),0≤s≤1U_i=(1-s)D_i+s\,T_i(U_{i+1}),\qquad 0\le s\le1

TiT_i表示用帐篷核重建到第ii层尺寸,ss是本模型的尺度混合系数。它只决定粗细层的比例,最终强度另由gg控制。展开三级链可得:

U0=(1−s)D0+s(1−s)T0(D1)+s2T0(T1(D2))U_0=(1-s)D_0+s(1-s)T_0(D_1)+s^2T_0(T_1(D_2))

当s=0.5s=0.5时,三个尺度的系数为0.5、0.25、0.25;当s=0.8s=0.8时,系数为0.2、0.16、0.64。粗层贡献增加,能量分布向更远处移动,亮核占比下降。权重之和始终为1,所以增加层数不会把常量场直接加亮;直接逐层相加则会随层数增加常量输出。

以远离边缘的16亮点为例,D0中亮点为16,D1中对应块为4,D2为1。帐篷核把D2的1分到中心1/4、轴向1/8和对角1/16的位置,再通过双线性插值铺到D1。在最外侧,D0、D1都为零,仍能收到最粗层的贡献;靠近亮核则会叠加细层的局部信息。因此只显示最低层再放大,能看见大光晕,却会损失中间尺度的形状。

每个粗层纹素覆盖的原图宽度按2、4、8倍增长。更深的层数扩展最大支撑范围,ss改变这些范围的相对权重。这两个参数不能互相替代。有限图像的钳位边界会重复边缘样本,常量保持并不保证边缘孤立亮点的面积加权总量守恒;能量测试应同时放置中心与边缘亮点。

合成与资源执行

最终采用C′=C+gU0C'=C+gU_0时,原始颜色CC完整保留,光晕是额外增加的美术贡献。若目标是把一部分光从亮核分散出去,可对无阈值输入使用(1−g)C+gU0(1-g)C+gU_0,其中0≤g≤10\le g\le1。这要求滤波及边界满足所需归一化,阈值提取与边缘钳位还会改变总量;“保持常量”本身不能证明每个有限图像都严格守恒。

合成在线性HDR空间、色调映射之前进行。若先把亮点压到显示范围,再提取高亮,原来强度16与强度4的差别就会被压缩,光晕无法表达这份亮度差。曝光关系见 HDR与曝光,显示映射见 Tone Mapping。

实际纹理链按下面顺序提交。每个D在生成之后供下一级读取;每个U写到另一张纹理,避免同一子资源一边被采样一边被覆盖。最低层可以直接作为只读的Um,其余Ui等最后消费者完成后才回收。

text
Extract(C) → D0
Down2(D0) → D1 → Down2(D1) → D2
U2 = D2
Mix(D1, Tent(U2), s) → U1
Mix(D0, Tent(U1), s) → U0
Compose(C, U0, g) → output

动态分辨率改变时,重新计算尺寸、层数和每次采样的源纹素倒数。若仅调整输出视口而继续使用旧纹理尺寸,边缘可能采到未写区域;若读写次序声明遗漏,则关闭某级后也可能留下上帧数据。把每级D和U单独显示,比只观察最终光晕更容易定位这些问题。

验证方法

固定曝光,先用常量图核对每一级的归一化,再用中心亮点核对扩散支撑范围。逐级关闭粗层贡献,检查光晕是否按预计收缩;把亮点缓慢移过纹素边界,观察降采样相位造成的变化。边缘亮点与动态分辨率单独检查。

test_post.py 检查零输入、软阈值中点、预曝光尺度不变性及不同层数的常量保持;正文提取与帐篷上采样函数通过DXC着色器模型6.0像素入口编译,编译封装见probe.hlsl。GPU纹理链、滤波伪影和性能尚未测量。

reference.py是Python3标准库的完整标量图像参考实现,展开盒平均、边界钳位、双线性与帐篷重建和尺度混合。test_reference.py检查常量场、16亮点、尺度权重、奇数尺寸拒绝和粗层扩散范围。命令为python -m unittest discover -s 知识库/examples/mechanism_depth/bloom -p test_reference.py。它验证本文数值模型,不模拟GPU纹理精度与真实镜头。

相关主题

参考资料