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);

存储纹理相比存储缓冲区的优势

  1. 存储纹理仍然是一个纹理
    你可以在一个着色器里把它当作存储纹理使用,而在另一个着色器里把它当作普通纹理使用(配合采样器、mipmap 等),无需额外转换。

  2. 存储纹理具有格式解释(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。如果使用缓冲区,则需要手动拆分字节并转换。

  3. 存储纹理具有内建维度
    缓冲区只有长度(用户需要自己传递宽度来映射二维坐标),而存储纹理可以通过 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]);
}

渲染结果是一个红/青相间的同心圆图案(点击下方链接可查看实际效果)。此示例直接向画布存储纹理写入,无需任何渲染通道。

核心优化要点(提炼)

  1. 利用存储纹理的格式解释与维度能力,替代手工缓冲区索引
    存储纹理自动处理像素格式转换(如 rgba8unorm → vec4f)并提供 textureDimensions,相比存储缓冲区可减少手动字节操作和额外参数传递,提升代码清晰度和可维护性。

  2. 合理使用画布存储纹理并处理格式兼容性
    将画布配置为存储纹理时,务必检测 'bgra8unorm-storage' 特性。最佳实践:优先使用 getPreferredCanvasFormat 并启用相应特性;若特性不可用,回退到 rgba8unorm,避免运行时错误。

  3. 优化计算着色器的工作派发并避免数据竞争
    示例中每个像素派发一个工作组(@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 生态中均已标准化,本文翻译遵循中文技术社区常用译法。


本文为深度解读,原文请参考链接。