单层高斯模糊缺陷#
单层高斯模糊无论半径多大只有单一尺度,这会导致:
- 亮部边缘形成均匀厚度的“光圈”,缺乏真实镜头因色散和散射产生的层次过渡。
- 大半径模糊会吞噬高亮区域的纹理细节;小半径模糊则无法产生铺满屏幕的恢弘光晕,导致细节丢失
我们的分层策略便是:利用图像金字塔(Image Pyramid),在低分辨率处理大尺度光晕(低频),在高分辨率处理小尺度光晕(高频),最后重组成连续的光晕分布。
图像金字塔#
下采样链(Downsample Chain)#
这里其实就是 Mipmap 做法,我们从**全分辨率(设为 Level 0)**开始,不断将分辨率减半(1/2, 1/4, 1/8 … 直到 1/32 或 1/64),每一层都需要独立存储为一张纹理(或Mipmap切片)
WARNING下采样时必须进行滤波(通常用双线性或高斯滤波),而不能直接取整数点,否则会产生严重的摩尔纹和锯齿。
13-tap 高光提取降采样#
常规的双线性下采样会对所有亮度一视同仁,容易让高光在降采样的第一步就被“平均”掉。因此,行业内更偏爱带亮度倾向性的 13-tap 降采样核。
该核是覆盖了 5x5 虚拟网格的 13 个特定点(9 个整数偏移点 + 4 个半像素偏移点),并分成了 5 个 2x2 小组,赋予了完全不同的组权重:
1 X = -1.0 X = -0.5 X = 0.0 X = 0.5 X = 1.02
3Y = 1.0 [ k ] [ ] [ l ] [ ] [ m ]4
5Y = 0.5 [ ] [ i ] [ ] [ j ] [ ]6
7Y = 0.0 [ f ] [ ] [ g ] [ ] [ h ]8
9Y = -0.5 [ ] [ d ] [ ] [ e ] [ ]10
11Y = -1.0 [ a ] [ ] [ b ] [ ] [ c ]| 小组 | 组成点 | 组总权重 | 单点均摊权重 |
|---|---|---|---|
| 中心组 | d, e, i, j | 0.125 | 0.03125 |
| 外围组1 | a, b, g, f | 0.03125 | 0.0078125 |
| 外围组2 | b, c, h, g | 0.03125 | 0.0078125 |
| 外围组3 | f, g, l, k | 0.03125 | 0.0078125 |
| 外围组4 | g, h, m, l | 0.03125 | 0.0078125 |
为什么这样能抗闪烁并保细节?
- 传统 4-tap Box 滤波: 高光点如果在 2x2 采样框的边界上,一旦画面晃动或摄像机微移,高光在下一级 Mip 里会时而出现、时而消失,导致严重的亮度闪烁
- 13-tap 算法: 通过高权重中心点 g 锁死当前像素亮度,同时因为有了 4 个半偏移点和 4 个边中点,即使高光点有微小位移,它依然会被周围至少 3~4 个采样点捕捉到,
为什么总权重只有 0.25? 降采样不只是缩小,它同时模拟着光线散射能量损失,确保在上采样后画面能量守恒。
采样函数的具体实现
1vec3 downsampleBox13(vec2 uv) {2 vec2 t = srcTexelSize;3 vec3 a = texture(srcTex, uv + t * vec2(-1.0, -1.0)).rgb;4 vec3 b = texture(srcTex, uv + t * vec2( 0.0, -1.0)).rgb;5 vec3 c = texture(srcTex, uv + t * vec2( 1.0, -1.0)).rgb;6 vec3 d = texture(srcTex, uv + t * vec2(-0.5, -0.5)).rgb;7 vec3 e = texture(srcTex, uv + t * vec2( 0.5, -0.5)).rgb;8 vec3 f = texture(srcTex, uv + t * vec2(-1.0, 0.0)).rgb;9 vec3 g = texture(srcTex, uv).rgb;10 vec3 h = texture(srcTex, uv + t * vec2( 1.0, 0.0)).rgb;11 vec3 i = texture(srcTex, uv + t * vec2(-0.5, 0.5)).rgb;12 vec3 j = texture(srcTex, uv + t * vec2( 0.5, 0.5)).rgb;13 vec3 k = texture(srcTex, uv + t * vec2(-1.0, 1.0)).rgb;14 vec3 l = texture(srcTex, uv + t * vec2( 0.0, 1.0)).rgb;15 vec3 m = texture(srcTex, uv + t * vec2( 1.0, 1.0)).rgb;16
17 // 中心2x2权重最高(0.125*4),外围四个2x2区块权重较低(0.03125*4),18 // 这个权重分布是COD论文里验证过的,能有效避免降采样时的"高光闪烁"19 vec3 result = (d + e + i + j) * 0.125;20 result += (a + b + g + f) * 0.03125;21 result += (b + c + h + g) * 0.03125;22 result += (f + g + l + k) * 0.03125;23 result += (g + h + m + l) * 0.03125;24 return result;25}上采样链(Upsample Chain)#
模糊处理后,需要把小图还原回屏幕大小。通常采用双线性插值(硬件原生支持)或双三次插值(质量略高但开销大)。
3x3 tent filter升采样#
下图是 upsampleTent 在屏幕/纹理空间中,9次采样最终落到的实际物理位置
1 X 方 向 (左 <--------- 右)2 -------------------------------------------3 X偏移 = -1.0 X偏移 = 0.0 X偏移 = +1.04 (o.z = -dx) (o.w = 0) (o.x = +dx)5
6 Y方向 [ 左上角 ] [ 正上方 ] [ 右上角 ]7 (上) Y+ uv + o.zy uv + o.wy uv + o.xy8 ↑ 权重: 1 权重: 2 权重: 19
10 Y方向 [ 正左方 ] [ 正中心 ] [ 正右方 ]11 (中) 0 uv + o.zw uv uv + o.xw12 权重: 2 权重: 4 权重: 213
14 Y方向 [ 左下角 ] [ 正下方 ] [ 右下角 ]15 (下) Y- uv - o.xy uv - o.wy uv - o.zy16 权重: 1 权重: 2 权重: 1对低分辨率采样函数
1vec3 upsampleTent(vec2 uv) {2 vec4 o = srcTexelSize.xyxy * vec4(1.0, 1.0, -1.0, 0.0) * bloomRadius;3
4 vec3 result = texture(srcTex, uv - o.xy).rgb;5 result += texture(srcTex, uv - o.wy).rgb * 2.0;6 result += texture(srcTex, uv - o.zy).rgb;7 result += texture(srcTex, uv + o.zw).rgb * 2.0;8 result += texture(srcTex, uv ).rgb * 4.0;9 result += texture(srcTex, uv + o.xw).rgb * 2.0;10 result += texture(srcTex, uv + o.zy).rgb;11 result += texture(srcTex, uv + o.wy).rgb * 2.0;12 result += texture(srcTex, uv + o.xy).rgb;13
14 return result / 16.0;15}o 是一个偏移向量:
-
o.x = dx * 1.0= 向右 1 个像素 -
o.y = dy * 1.0= 向上 1 个像素 -
o.z = dx * (-1.0)= 向左 1 个像素 -
o.w = dy * 0.0= 垂直方向 0
多频段模糊#
固定核半径#
在提取完亮部过后,我们要对每一层进行单独的高斯模糊,每一层都使用一个极小的固定核,一般取 3 * 3,5 * 5,因为降低了分辨率,使用极小的核就可以覆盖大范围,极大节约了性能,同时还能实现大光晕。
模糊迭代#
对于最深的那一层,为了获得极致的柔和拖尾,通常会对该层重复执行 2~3 次可分离高斯模糊(即水平+垂直算一次,再做一次水平+垂直)。这能让光晕边缘呈现自然的“高斯钟形”衰减。
分层合成#
所有层级独立上采样回全分辨率,存储为独立的临时纹理(或利用Mipmap逐级上采样写入)。最终合成公式为一个加权累加和:
BloomFinal(x,y)=i=0∑N(UpSample(Blurredi)×Wi)其中 Wi 是每一层独立的控制参数。
我们在CPU端可以这样实现,这里是基于opengl实现
1// ---------- 阶段3: 逐级降采样,构建mip链 ----------2 downsampleProgram.use();3 GLuint srcTex = brightTexture;4 int srcW = SCR_WIDTH, srcH = SCR_HEIGHT;5
6 for (int level = 1; level <= NUM_BLOOM_MIPS; level++) {7 glActiveTexture(GL_TEXTURE0);8 glBindTexture(GL_TEXTURE_2D, srcTex);9 downsampleProgram.setInt("srcTex", 0);10 downsampleProgram.setVec2("srcTexelSize", 1.0f / srcW, 1.0f / srcH);11 glBindImageTexture(0, bloomMips[level], 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F);12
13 GLuint gx = (mipWidths[level] + 15) / 16;14 GLuint gy = (mipHeights[level] + 15) / 16;15 glDispatchCompute(gx, gy, 1);16 // 注意:这里barrier要同时包含TEXTURE_FETCH,因为下一轮要用sampler2D采样这次写入的结果,17 // 光有IMAGE_ACCESS_BARRIER只保证image2D读写顺序,不保证texture()采样能看到最新数据18 glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT);19
20 srcTex = bloomMips[level];21 srcW = mipWidths[level];22 srcH = mipHeights[level];23 }24
25 // ---------- 阶段4: 逐级升采样叠加,从最小一级往回叠加,最终叠回brightTexture本身 ----------26 upsampleProgram.use();27 for (int level = NUM_BLOOM_MIPS; level >= 1; level--) {28 GLuint dstTex = (level == 1) ? brightTexture : bloomMips[level - 1];29 int dstW = (level == 1) ? SCR_WIDTH : mipWidths[level - 1];30 int dstH = (level == 1) ? SCR_HEIGHT : mipHeights[level - 1];31
32 glActiveTexture(GL_TEXTURE0);33 glBindTexture(GL_TEXTURE_2D, bloomMips[level]);34 upsampleProgram.setInt("srcTex", 0);35 upsampleProgram.setVec2("srcTexelSize", 1.0f / mipWidths[level], 1.0f / mipHeights[level]);36 upsampleProgram.setFloat("bloomRadius", 1.0f); // 想要更柔和的大范围光晕可以调到1.5~2.0试试37
38 glBindImageTexture(0, dstTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);39
40 GLuint gx = (dstW + 15) / 16;41 GLuint gy = (dstH + 15) / 16;42 glDispatchCompute(gx, gy, 1);43 glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT);44 }合成中的参数把握#
强度衰减曲线#
- 在高斯模糊中的
bloomIntensity可以根据分层自行调整,靠近高分辨率的层级强度设定较低(避免过度锐利刺眼),靠近低分辨率的层级强度设定较高(制造恢弘氛围)。 - 也可以自行设计 S 型曲线
色调偏移与色差#
1- 真实镜头的折射率随波长变化(红光偏外,蓝光偏内),对最深层增加红色和绿色的权重,让光晕外围呈现暖黄色;对内层增加蓝色权重。尺寸缩放#
- 即使层级固定(如 1/16),合成时也可以乘以一个缩放系数。如果调大某一层的权重或拉长其曝光时间,等效于物理上的“过曝膨胀”。
更好的混合策略#
频域交叉淡入(Cross-fading)#
在相邻层之间加入平滑的权重过渡。即并不是简单地分频,而是让高频层也带有一点低频成分,低频层也保留一点高频边缘。这通常通过对上采样后的图像进行重叠加权平均实现。
高亮保留(Specular Preservation)#
在合成最终颜色时,不要直接把模糊结果加到原图上。先进的做法是:只叠加模糊层的增量部分,且与源图像的亮度进行相对比较。这样做能避免暗部区域被Bloom染脏,保持画面通透。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
