无需FBO,如何在WebGPU中实现bloom效果?
在WebGPU中实现Bloom效果
WebGPU没有直接对应WebGL中FBO的组件,而是将其职责拆分为纹理(Texture)、纹理视图(TextureView)和渲染通道(RenderPass)。Bloom的核心逻辑(亮区提取→多轮降采样模糊→多轮上采样模糊→原画面混合)不变,只是需要用WebGPU的API重新实现每一步:
1. 创建中间渲染纹理
首先要创建一系列用于存储中间结果的纹理,这些纹理需要同时支持作为渲染目标和采样纹理,所以创建时要指定usage包含RENDER_ATTACHMENT和TEXTURE_BINDING:
function createRenderTexture(device, width, height) { return device.createTexture({ size: { width, height, depthOrArrayLayers: 1 }, format: 'rgba16float', // 浮点格式保留亮区细节,避免精度丢失 usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, sampleCount: 1, }); } // 根据需求创建原场景纹理、亮区纹理、多组降采样/上采样纹理 const sceneTexture = createRenderTexture(device, canvas.width, canvas.height); const brightPassTexture = createRenderTexture(device, canvas.width, canvas.height); // 降采样纹理数组,尺寸每次减半 const downSampleTextures = []; let currentWidth = canvas.width; let currentHeight = canvas.height; for (let i = 0; i < 4; i++) { // 示例做4轮降采样 currentWidth = Math.floor(currentWidth / 2); currentHeight = Math.floor(currentHeight / 2); downSampleTextures.push(createRenderTexture(device, currentWidth, currentHeight)); } // 上采样纹理数组,对应降采样的逆过程 const upSampleTextures = [...downSampleTextures].reverse().slice(1); upSampleTextures.push(createRenderTexture(device, canvas.width, canvas.height));
2. 提取场景亮区
编写亮区提取的片元着色器,将原场景中亮度超过阈值的像素保留,其余置黑:
@group(0) @binding(0) var sceneTexture: texture_2d<f32>; @group(0) @binding(1) var sceneSampler: sampler; struct VertexOutput { @location(0) uv: vec2f, }; @fragment fn fs(input: VertexOutput) -> @location(0) vec4f { let color = textureSample(sceneTexture, sceneSampler, input.uv); // 用标准亮度公式计算像素亮度 let brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); let threshold = 1.0; // 可根据需求调整阈值 // 只保留亮度超过阈值的部分 return vec4(color.rgb * step(threshold, brightness), 1.0); }
然后创建RenderPass,将输出目标绑定到brightPassTexture的视图,执行渲染得到亮区纹理。
3. 多轮降采样高斯模糊
对亮区纹理进行多次降采样,每次将纹理尺寸减半,同时应用高斯模糊:
- 每次降采样都需要创建对应的RenderPass,绑定上一级纹理作为输入,当前级更小的纹理作为输出目标。
- 降采样着色器结合高斯模糊,示例如下:
@group(0) @binding(0) var inputTexture: texture_2d<f32>; @group(0) @binding(1) var inputSampler: sampler; @group(0) @binding(2) var textureSize: vec2f; // 当前输入纹理的尺寸 struct VertexOutput { @location(0) uv: vec2f, }; @fragment fn fs(input: VertexOutput) -> @location(0) vec4f { let texelSize = 1.0 / textureSize; // 9邻域高斯采样权重 let weights = vec4(0.227027, 0.1945946, 0.1216216, 0.054054); let offsets = vec2(0.0, 1.38461538) * texelSize; var color = textureSample(inputTexture, inputSampler, input.uv) * weights.x; color += textureSample(inputTexture, inputSampler, input.uv + offsets) * weights.y; color += textureSample(inputTexture, inputSampler, input.uv - offsets) * weights.y; color += textureSample(inputTexture, inputSampler, input.uv + offsets * 2.0) * weights.z; color += textureSample(inputTexture, inputSampler, input.uv - offsets * 2.0) * weights.z; color += textureSample(inputTexture, inputSampler, input.uv + offsets * 3.0) * weights.w; color += textureSample(inputTexture, inputSampler, input.uv - offsets * 3.0) * weights.w; return color; }
循环执行每一轮降采样,将上一步的输出作为当前步的输入。
4. 多轮上采样并混合
从最小的降采样纹理开始,逐步上采样到原尺寸,每次上采样后和上一级的降采样纹理混合(保留细节):
- 上采样着色器同样应用高斯模糊,并且在采样时考虑尺寸缩放后的UV偏移。
- 每一轮上采样的RenderPass,绑定当前要上采样的纹理和对应的上一级降采样纹理作为输入,输出到更大尺寸的纹理。
5. 混合原场景与Bloom纹理
最后创建RenderPass,将原场景纹理和最终的上采样模糊纹理作为输入,在片元着色器中按比例混合:
@group(0) @binding(0) var sceneTexture: texture_2d<f32>; @group(0) @binding(1) var bloomTexture: texture_2d<f32>; @group(0) @binding(2) var sampler: sampler; struct VertexOutput { @location(0) uv: vec2f, }; @fragment fn fs(input: VertexOutput) -> @location(0) vec4f { let sceneColor = textureSample(sceneTexture, sampler, input.uv); let bloomColor = textureSample(bloomTexture, sampler, input.uv); // 直接叠加或根据需求调整混合比例 return vec4(sceneColor.rgb + bloomColor.rgb, 1.0); }
执行该RenderPass,将结果输出到画布的纹理视图,完成Bloom效果渲染。
关键注意事项
- 纹理格式:优先选择浮点格式(如
rgba16float)存储中间结果,避免亮区细节因精度丢失被截断。 - 采样器设置:降采样和上采样时使用线性采样(
FilterMode.Linear),让模糊过渡更平滑。 - 循环复用逻辑:降采样和上采样的RenderPass可以通过循环批量创建,减少重复代码。
内容的提问来源于stack exchange,提问作者jiyinyiyong
相关产品推荐
相关产品推荐

