WebGPU 存储纹理(Storage Textures)
英文原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-textures.html
在前面的系列文章中,我们了解了向着色器传递数据的多种方式:阶段间变量、统一变量、存储缓冲区、顶点缓冲区、常规纹理等。本文聚焦于 存储纹理(Storage Textures)——一种可以直接在着色器中写入的纹理,而无需通过传统的渲染管线由 GPU 间接更新。
什么是存储纹理?
存储纹理并不是一种特殊的纹理类型,它只是通过 createTexture 创建的普通纹理,但额外添加了 STORAGE_BINDING 使用标志(usage flag)。这样,该纹理就可以在着色器中作为存储纹理使用,同时仍保留原有的其他用途(例如作为常规纹理绑定、渲染附件等)。
从概念上讲,存储纹理类似于将存储缓冲区当作二维数组来使用。下面是一个对比示例:
使用存储缓冲区模拟二维数组:
@group(0) @binding(0)
`var<storage>` buf: `array<f32>`;
fn loadValueFromBuffer(pos: vec2u) -> f32 {
return buffer[pos.y * width + pos.x];
}
fn storeValueToBuffer(pos: vec2u, v: f32) {
buffer[pos.y * width + pos.x] = v;
}
// ...
let pos = vec2u(2, 3);
var v = loadValueFromBuffer(pos);
storeValueToBuffer(pos, v * 2.0);
使用存储纹理实现同样功能:
@group(0) @binding(0)
var tex: `texture_storage_2d<r32float, read_write>`;
// ...
let pos = vec2u(2, 3);
let mipLevel = 0;
var v = textureLoad(tex, pos, mipLevel);
textureStore(tex, pos, mipLevel, v * 2);
存储纹理相比存储缓冲区的优势
-
存储纹理仍然是一个纹理
你可以在一个着色器里把它当作存储纹理使用,而在另一个着色器里把它当作普通纹理使用(配合采样器、mipmap 等),无需额外转换。 -
存储纹理具有格式解释(format interpretation)
存储缓冲区只是原始字节,没有格式概念。而存储纹理在加载时会根据其纹理格式自动进行数据转换。例如:@group(0) @binding(0) var tex: `texture_storage_2d<rgba8unorm, read>`; @group(0) @binding(1) var buf: `array<f32>`; // ... let t = textureLoad(tex, pos, 0); // 自动将 4 字节解包为 0~1 的 vec4f let b = buffer[pos.y * bufferWidth + pos.x]; // 仅得到一个 f32上面
textureLoad从rgba8unorm纹理中加载 4 个字节,并自动转换成 4 个介于 0 到 1 的浮点值返回为vec4f。如果使用缓冲区,则需要手动拆分字节并转换。 -
存储纹理具有内建维度
缓冲区只有长度(用户需要自己传递宽度来映射二维坐标),而存储纹理可以通过textureDimensions直接获取宽高,无需额外传入尺寸。
存储纹理的限制
存储纹理虽然强大,但有以下限制:
-
读写格式限制
只有r32float、r32sint、r32uint这三种格式支持read_write(读写)。其他支持的格式在同一着色器中只能声明为read或write。 -
可作为存储纹理的格式有限
以下格式家族支持存储纹理:rgba8(unorm/snorm/sint/uint)rgba16(float/sint/uint)rg32(float/sint/uint)rgba32(float/sint/uint)
注意:常见的
bgra8unorm格式默认不支持作为存储纹理,需要启用特性'bgra8unorm-storage'(下文详述)。 -
不能使用采样器
当纹理作为STORAGE_BINDING使用时,不能调用textureSample等依赖采样器的函数,只能使用textureLoad和textureStore来读写单个纹素(texel)。
将画布(Canvas)用作存储纹理
我们可以将 WebGPU 画布的当前纹理配置为存储纹理,以便直接在计算着色器中写入画布内容。
要启用画布纹理的存储能力,需要在 context.configure 中添加 STORAGE_BINDING 使用标志:
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.STORAGE_BINDING,
});
这里 TEXTURE_BINDING 是浏览器将纹理显示到页面所必需的,而 STORAGE_BINDING 让我们可以在着色器中将画布纹理作为存储纹理写入。如果还需要通过渲染通道绘制到画布,则需要额外添加 RENDER_ATTACHMENT 标志。
但是有一个复杂点:getPreferredCanvasFormat 可能返回 rgba8unorm 或 bgra8unorm(取决于平台性能)。而 bgra8unorm 默认不能作为存储纹理。解决方案是检测并启用 'bgra8unorm-storage' 特性;如果该特性不可用,则强制使用 rgba8unorm 作为画布格式。
const adapter = await navigator.gpu?.requestAdapter();
const hasBGRA8unormStorage = adapter.features.has('bgra8unorm-storage');
const device = await adapter?.requestDevice({
requiredFeatures: hasBGRA8unormStorage
? ['bgra8unorm-storage']
: [],
});
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = hasBGRA8unormStorage
? navigator.gpu.getPreferredCanvasFormat()
: 'rgba8unorm';
context.configure({
device,
format: presentationFormat,
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.STORAGE_BINDING,
});
示例:用计算着色器在存储纹理中绘制同心圆
下面是一个完整的计算着色器示例,直接在存储纹理上绘制同心圆条纹。注意存储纹理的格式在着色器中必须明确指定。
const module = device.createShaderModule({
label: 'circles in storage texture',
code: /* wgsl */ `
@group(0) @binding(0)
var tex: `texture_storage_2d<${presentationFormat}, write>`;
@compute @workgroup_size(1) fn cs(
@builtin(global_invocation_id) id : vec3u
) {
let size = textureDimensions(tex);
let center = vec2f(size) / 2.0;
// 当前要写入的像素坐标
let pos = id.xy;
// 计算到纹理中心的距离
let dist = distance(vec2f(pos), center);
// 基于距离计算条纹
let stripe = dist / 32.0 % 2.0;
let red = vec4f(1, 0, 0, 1);
let cyan = vec4f(0, 1, 1, 1);
let color = select(red, cyan, stripe < 1.0);
// 将颜色写入纹理
textureStore(tex, pos, color);
}
`,
});
创建计算管线与派发工作组:
const pipeline = device.createComputePipeline({
label: 'circles in storage texture',
layout: 'auto',
compute: {
module,
},
});
function render() {
const texture = context.getCurrentTexture();
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: texture },
],
});
const encoder = device.createCommandEncoder({ label: 'our encoder' });
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(texture.width, texture.height);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
渲染结果是一个红/青相间的同心圆图案(点击下方链接可查看实际效果)。此示例直接向画布存储纹理写入,无需任何渲染通道。
核心优化要点(提炼)
-
利用存储纹理的格式解释与维度能力,替代手工缓冲区索引
存储纹理自动处理像素格式转换(如rgba8unorm→vec4f)并提供textureDimensions,相比存储缓冲区可减少手动字节操作和额外参数传递,提升代码清晰度和可维护性。 -
合理使用画布存储纹理并处理格式兼容性
将画布配置为存储纹理时,务必检测'bgra8unorm-storage'特性。最佳实践:优先使用getPreferredCanvasFormat并启用相应特性;若特性不可用,回退到rgba8unorm,避免运行时错误。 -
优化计算着色器的工作派发并避免数据竞争
示例中每个像素派发一个工作组(@workgroup_size(1)且dispatchWorkgroups(texture.width, texture.height))是低效的,实际生产代码应使用更大的工作组和分块策略(参考图像直方图文章)。同时,存储纹理的随意写入可能引发数据竞争,必须通过textureBarrier或设计无冲突的写入模式来保证正确性。
术语检查(GLSL/WebGL 相关)
虽然本文涉及 WebGPU/WGSL,但某些概念与 WebGL/GLSL 有对应关系。以下术语翻译与说明供参考:
- Storage Texture → 存储纹理(WGSL 中
texture_storage_2d) - read_write / read / write → 读写 / 只读 / 只写(访问限定符)
- textureLoad / textureStore → 纹理加载 / 纹理存储(类似 GLSL 的
imageLoad/imageStore,但有格式限制) - Compute Shader → 计算着色器(与 GLSL 计算着色器概念相似)
- Workgroup → 工作组(相当于 GLSL 中的 local workgroup)
- Dispatch Workgroups → 派发工作组(类似
glDispatchCompute) - Usage Flags → 用途标志(如
STORAGE_BINDING、TEXTURE_BINDING、RENDER_ATTACHMENT) - Bind Group → 绑定组(对应 WebGL 中的 uniform/texture 绑定集合)
- Render Pass → 渲染通道
- Texture Format → 纹理格式(如
rgba8unorm、bgra8unorm、r32float) - Sampler → 采样器(存储纹理不可使用)
以上术语在 WebGPU 生态中均已标准化,本文翻译遵循中文技术社区常用译法。
本文为深度解读,原文请参考链接。
