news 2026/10/29 15:36:49

WebGPU 后期处理管线深度实现:手写 WGSL 屏幕空间全屏泛光(Bloom)着色器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebGPU 后期处理管线深度实现:手写 WGSL 屏幕空间全屏泛光(Bloom)着色器

在次世代 Web 3D 场景、数字孪生以及赛博朋克风格的交互界面中,全屏泛光(Bloom Effect)是赋予发光材质、霓虹光带与超强光源“真实物理光芒”的灵魂级后期处理技术。在物理光学世界中,当进入人眼或摄影机镜头的光子通量远超感光芯片的动态范围时,镜头内部的镜片微小瑕疵与眼球玻璃体会导致强烈光线向周围扩散漫溢(Light Bleeding)。

在 WebGL 时代,实现高质量 Bloom 往往受制于繁琐的多 Pass 切换开销与移动端显存带宽瓶颈。很多开发者为了省事,只做一次简单的全屏高斯模糊,结果导致低亮度区域发灰、高光区域边缘生硬失真。

进入 WebGPU 时代,得益于现代显式管线(Explicit Pipeline)的高性能调度以及 WGSL(WebGPU Shading Language)对硬件级双线性过滤的高效支持,我们可以构建媲美《使命召唤》等 3A 端游质量的物理级双向金字塔 Bloom 管线(Dual Kawase / Mip-chain Downsample & Upsample)。

本文将从物理光学逻辑、降采样金字塔拓扑到手写 WGSL 着色器,深入拆解这一高性能泛光管线的实现。


现代 Bloom 管线架构:降采样与升采样金字塔

直接在全分辨率屏幕(如 4K 视网膜屏)上执行大半径模糊,其像素采样开销将彻底摧毁 GPU。现代工业级 Bloom 算法(由 Jorge Jimenez 在 SIGGRAPH 2014 提出并广泛应用于现代游戏引擎)采用了类似拉普拉斯金字塔的多级迭代流程:

[原始 HDR 渲染画面] │ ▼ (Pass 1: 亮度阈值提取 Threshold Pass) [提取的高亮纹理 Level 0 (全分辨率)] │ ▼ (Pass 2: 13-tap 降采样) [高光纹理 Level 1 (1/2 分辨率)] │ ▼ (Pass 3: 13-tap 降采样) [高光纹理 Level 2 (1/4 分辨率)] │ ▼ (Pass 4: 13-tap 降采样) [高光纹理 Level 3 (1/8 分辨率)] │ │ (渐进升采样与帐篷滤波 Tent Filter 逐级混合) ▼ (Pass 5 ~ 7: 9-tap 升采样混合) [最终大光圈泛光贴图] │ ▼ (Final Pass: 与原图合成 + ACES 色调映射) [屏幕最终输出]

为什么必须分级降采样?

在 $1/8$ 分辨率的贴图上采样 1 个像素,相当于在原始全屏画面上覆盖了巨大的物理物理光圈半径。通过从高分辨率到低分辨率逐级降采样(Downsample),再从低分辨率逐级升采样(Upsample)并与上一级叠加混合,我们能以极小的采样次数,合成出兼具“中心高锐度辉光”与“外围宽广柔和光晕”的物理真实光芒。


WGSL 着色器实现一:高光阈值提取(Threshold Extract)

在第一阶段,我们需要将画面中普通亮度的物体过滤掉,只保留超过 HDR 阈值(如亮度 $> 1.0$)的发光像素,并提供平滑软阈值(Soft Knee),避免明暗交界处产生阶跃闪烁(Fireflies)。

// bloom_threshold.wgsl struct BloomThresholdUniforms { threshold: f32, // 基础阈值 (如 1.0) knee: f32, // 软拐点宽度 (如 0.5) }; @group(0) @binding(0) var<uniform> uniforms: BloomThresholdUniforms; @group(0) @binding(1) var inputTexture: texture_2d<f32>; @group(0) @binding(2) var textureSampler: sampler; struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>, }; // 全屏三角顶点着色器 (无需顶点缓冲区) @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var out: VertexOutput; let uv = vec2<f32>( f32((vertexIndex << 1u) & 2u), f32(vertexIndex & 2u) ); out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0); out.uv = vec2<f32>(uv.x, 1.0 - uv.y); return out; } @fragment fn fs_threshold(in: VertexOutput) -> @location(0) vec4<f32> { let color = textureSample(inputTexture, textureSampler, in.uv).rgb; // 计算感知亮度 (Rec. 709 亮度系数) let brightness = max(color.r, max(color.g, color.b)); // 软阈值过渡计算 (Quadratic Knee) let soft = clamp(brightness - uniforms.threshold + uniforms.knee, 0.0, 2.0 * uniforms.knee); let softFactor = (soft * soft) / (4.0 * uniforms.knee + 1e-4); let contrib = max(softFactor, brightness - uniforms.threshold) / max(brightness, 1e-4); let filteredColor = color * max(0.0, contrib); return vec4<f32>(filteredColor, 1.0); }

WGSL 着色器实现二:13-Tap 抗走样降采样(Downsample)

传统的单像素双线性采样在大幅度缩小图像时会丢失关键的高光孤岛,导致画面在镜头移动时高光疯狂闪烁。这里我们采用 Jimenez 提出的13 点加权盒状采样法,通过交错采样覆盖 36 个屏幕像素的几何重心:

// bloom_downsample.wgsl struct DownsampleUniforms { texelSize: vec2<f32>, // 1.0 / 上一级纹理分辨率 }; @group(0) @binding(0) var<uniform> uniforms: DownsampleUniforms; @group(0) @binding(1) var inputTexture: texture_2d<f32>; @group(0) @binding(2) var linearSampler: sampler; @fragment fn fs_downsample(in: VertexOutput) -> @location(0) vec4<f32> { let uv = in.uv; let d = uniforms.texelSize; // 13 个关键采样点采样 (4 个内圈角点 + 4 个外圈角点 + 4 个边缘点 + 1 个中心点) let a = textureSample(inputTexture, linearSampler, uv + vec2<f32>(-2.0 * d.x, 2.0 * d.y)).rgb; let b = textureSample(inputTexture, linearSampler, uv + vec2<f32>(0.0, 2.0 * d.y)).rgb; let c = textureSample(inputTexture, linearSampler, uv + vec2<f32>(2.0 * d.x, 2.0 * d.y)).rgb; let dCorner = textureSample(inputTexture, linearSampler, uv + vec2<f32>(-2.0 * d.x, 0.0)).rgb; let e = textureSample(inputTexture, linearSampler, uv).rgb; let f = textureSample(inputTexture, linearSampler, uv + vec2<f32>(2.0 * d.x, 0.0)).rgb; let g = textureSample(inputTexture, linearSampler, uv + vec2<f32>(-2.0 * d.x, -2.0 * d.y)).rgb; let h = textureSample(inputTexture, linearSampler, uv + vec2<f32>(0.0, -2.0 * d.y)).rgb; let i = textureSample(inputTexture, linearSampler, uv + vec2<f32>(2.0 * d.x, -2.0 * d.y)).rgb; let j = textureSample(inputTexture, linearSampler, uv + vec2<f32>(-d.x, d.y)).rgb; let k = textureSample(inputTexture, linearSampler, uv + vec2<f32>(d.x, d.y)).rgb; let l = textureSample(inputTexture, linearSampler, uv + vec2<f32>(-d.x, -d.y)).rgb; let m = textureSample(inputTexture, linearSampler, uv + vec2<f32>(d.x, -d.y)).rgb; // 加权平均 (Jimenez 13-tap 权重分布) var downsampleColor = e * 0.125; downsampleColor += (a + c + g + i) * 0.03125; downsampleColor += (b + dCorner + f + h) * 0.0625; downsampleColor += (j + k + l + m) * 0.125; return vec4<f32>(downsampleColor, 1.0); }

WGSL 着色器实现三:9-Tap 升采样与帐篷滤波混合(Upsample)

在升采样放大的过程中,如果仅依靠硬件默认的双线性插值,放大的色块会呈现菱形走样。我们使用 9-Tap 帐篷滤波核(Tent Filter),并将其以加法混合(Additive Blending)的方式逐级累加回高一级别的纹理缓冲区中:

// bloom_upsample.wgsl struct UpsampleUniforms { filterRadius: vec2<f32>, // 滤波扩散半径 (通常为 0.005) }; @group(0) @binding(0) var<uniform> uniforms: UpsampleUniforms; @group(0) @binding(1) var currentLevelTexture: texture_2d<f32>; @group(0) @binding(2) var linearSampler: sampler; @fragment fn fs_upsample(in: VertexOutput) -> @location(0) vec4<f32> { let uv = in.uv; let r = uniforms.filterRadius; // 9-Tap 帐篷滤波采样核 (3x3 高斯逼近) let a = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(-r.x, r.y)).rgb; let b = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(0.0, r.y)).rgb; let c = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(r.x, r.y)).rgb; let d = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(-r.x, 0.0)).rgb; let e = textureSample(currentLevelTexture, linearSampler, uv).rgb; let f = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(r.x, 0.0)).rgb; let g = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(-r.x, -r.y)).rgb; let h = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(0.0, -r.y)).rgb; let i = textureSample(currentLevelTexture, linearSampler, uv + vec2<f32>(r.x, -r.y)).rgb; // 帐篷加权分布 var upsampleColor = e * 4.0; upsampleColor += (b + d + f + h) * 2.0; upsampleColor += (a + c + g + i) * 1.0; upsampleColor *= (1.0 / 16.0); return vec4<f32>(upsampleColor, 1.0); }

最终合成与 ACES 色调映射(Tone Mapping)

升采样迭代完成后,我们得到了一张纯粹记录漫溢光晕的高精度浮点纹理。在最后一个 Composite Pass 中,将原始渲染画面与 Bloom 贴图按强度系数相加,并执行电影工业级ACES Filmic Tone Mapping,将可能超过 1.0 的 HDR 能量平滑映射回 LDR 显示器所能呈现的[0, 1]范围:

// 电影级 ACES 胶片色调映射曲线 fn acesToneMapping(color: vec3<f32>) -> vec3<f32> { let a = 2.51; let b = 0.03; let c = 2.43; let d = 0.59; let e = 0.14; return clamp((color * (a * color + b)) / (color * (c * color + d) + e), vec3<f32>(0.0), vec3<f32>(1.0)); }

显存复用与移动端极致能效

在 WebGPU 中调度多级金字塔时,最核心的性能铁律是:严禁在每一帧重复创建纹理或 RenderPassDescriptor 对象。

  • 纹理复用池:在引擎初始化阶段,预先分配一套固定层级(通常 5 层,从 1/2 分辨率一直到 1/32 分辨率)的双向纹理链(Mip-Chain),整个渲染生命周期保持常驻;
  • 利用 RenderPass 的清晰语义:在升采样的每个阶段,将混合模式配置为blend: { color: { operation: 'add', srcFactor: 'one', dstFactor: 'one' } },直接让 GPU 硬件固定功能管线执行纹理累加,节省昂贵的片段着色器手动双纹理读取带宽。

通过手写完整的 WGSL 双向金字塔 Bloom 管线,我们在现代浏览器中复现了端游级绚烂、深邃、呼吸感十足的光影漫溢效果。每一束穿透黑夜的高光,都在精确的算力调度下,绽放出震撼人心的光学之美。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 18:08:37

ISO 26262 ASIL 评级:为什么 D 只有 S3+E4+C3 这一个组合

ISO 26262 ASIL 评级&#xff1a;为什么 D 只有 S3E4C3 这一个组合 先说结论 ISO 26262-3 Table 4 一共 36 格&#xff0c;只有 S3E4C3 得出 ASIL D。S3E4C2、S3E3C3、S2E4C3 都只比 D 差一格&#xff0c;但结果全是 C。S0、E0、C0 不在表里——这三个等级无需分配 ASIL。三个最…

作者头像 李华
网站建设 2026/10/15 23:15:29

Python上机实验2|随机模拟与算法效率

实验简介&#xff1a; 本次上机实验围绕 random 随机库 交互式猜数字游戏&#xff0c;增加输入校验、记录猜测过程、连玩3局求平均次数&#xff1b; 实现思路&#xff1a; 交互式猜数字游戏 使用 random.randint(1,100) 生成1~100随机答案。用 try-except 捕获非整数输入&#…

作者头像 李华
网站建设 2026/10/19 22:54:41

中秋手抄报、手工灯笼要做了?材料先备这 3 样,不返工

中秋节快到了&#xff0c;很多学校会给孩子布置"中秋作业"&#xff1a;低年级做手工灯笼、画月亮&#xff0c;中年级画手抄报、写中秋主题的小作文。老师一声令下&#xff0c;家长群里就开始"求素材、求模板"了。别等节前两三天再抓瞎&#xff0c;材料先备…

作者头像 李华
网站建设 2026/10/15 20:34:01

NLP 基础到高级 09:序列到序列模型

序列到序列模型 两个 RNN 假装是翻译器。它们遇到的瓶颈正是注意力机制存在的原因。 类型: 构建 语言: Python 前置条件: Phase 5 08(CNN + RNN 处理文本),Phase 3 11(PyTorch 简介) 预计用时: ~75 分钟 问题 分类将可变长度序列映射到单个标签。翻译将可变长度序…

作者头像 李华
网站建设 2026/10/29 12:39:13

海口吊顶和柜体怎么接缝,两种做法的差别

吊顶与柜体的接缝有两种常见做法&#xff1a;柜体顶到吊顶的下沿&#xff0c;或者柜体穿过吊顶顶到楼板。两种做法在收口位置、可调节余量和后期检修上都不一样。选哪一种&#xff0c;取决于吊顶是否留了检修口、以及柜体到顶后的观感要求。落到工序上&#xff0c;海口整家定制…

作者头像 李华