经纬志
首页 / luma.gl / 教程

用离屏渲染做通用计算

先看结论

WebGL2 没有计算着色器,通用的 GPU 计算只能借渲染管线完成: 把数据放进纹理,用全屏三角形逐像素算,结果再写进另一张纹理。 这套做法通常叫 GPGPU,也是早期 WebGL 做粒子与流体模拟的方式。

最后验证 2026-11-03 · luma.gl 9.4 · 更新 2026-10-29 · 引用本文

结构:两张纹理加一条管线

计算的关键约束是不能在同一帧里读写同一张纹理。因此准备两张同规格的纹理, 每轮从 A 读、往 B 写,下一轮交换角色,这就是乒乓。

function createField(width, height) {
  return device.createTexture({
    width: width,
    height: height,
    format: 'rgba32float',
    usage: luma.Texture.SAMPLE | luma.Texture.RENDER | luma.Texture.COPY_DST,
    sampler: { minFilter: 'nearest', magFilter: 'nearest', mipmapFilter: 'none' }
  });
}

const SIZE = 512;
let src = createField(SIZE, SIZE);
let dst = createField(SIZE, SIZE);

const fboA = device.createFramebuffer({
  width: SIZE,
  height: SIZE,
  colorAttachments: [src]
});

const fboB = device.createFramebuffer({
  width: SIZE,
  height: SIZE,
  colorAttachments: [dst]
});

let writeFbo = fboB;   // 本轮写入目标,初始写进 B

const sim = new luma.Model(device, {
  vs: fullscreenVS,
  fs: simFS,
  attributes: { positions: quadBuffer },
  bufferLayout: [
    { name: 'positions', stepMode: 'vertex', format: 'float32x2' }
  ],
  bindings: { uState: src },
  vertexCount: 3,          // 全屏三角形
  topology: 'triangle-list'
});

用三角形而不是四边形覆盖全屏,省掉两个顶点与一次对角线上的重复光栅化。 顶点坐标写成 [-1, -1]、[3, -1]、[-1, 3] 即可。

逐帧迭代

每轮重新绑定源纹理、切换到另一个帧缓冲,绘制一次,然后同时交换纹理与帧缓冲的引用。 两张纹理各配一个固定的帧缓冲,运行期不需要改动附件。

function step() {
  const pass = device.beginRenderPass({
    framebuffer: writeFbo,
    clearColor: false        // 全屏覆盖,不需要清除
  });

  sim.setBindings({ uState: src });
  sim.draw(pass);
  pass.end();

  // 交换:本轮结果成为下一轮输入
  const texTmp = src;
  src = dst;
  dst = texTmp;

  writeFbo = (writeFbo === fboA) ? fboB : fboA;
}

const loop = new luma.AnimationLoop({
  device: device,
  onRender: function () { step(); }
});

clearColor: false 省掉一次全屏清除。全屏三角形覆盖每个像素, 清除在这里没有意义,还会多一次写带宽。

格式与精度

格式精度可用条件
rgba32float单精度浮点,每通道 32 位需要浮点渲染支持,isTextureFormatSupported 判断
rgba16float半精度,范围有限支持面更广,适合颜色与光照累积
rgba8unorm8 位定点,0 到 1一定能用,只适合可视化结果
function pickFormat() {
  if (device.isTextureFormatSupported('rgba32float')) return 'rgba32float';
  if (device.isTextureFormatSupported('rgba16float')) return 'rgba16float';
  return 'rgba8unorm';
}

const format = pickFormat();
console.log('计算精度', format);
console.log(device.limits);   // 纹理尺寸等上限

浮点纹理的线性过滤在 WebGL 里还需要额外支持。计算阶段一律用 nearest,只有展示最终结果时才考虑 linear, 否则采样器可能被静默降级。

读回结果

计算完成后的结果仍在 GPU 上。需要交给 JavaScript 时, 先把纹理内容写进缓冲,再异步取出。

const readback = device.createBuffer({
  byteLength: SIZE * SIZE * 16,      // 4 通道 × 4 字节
  usage: luma.Buffer.COPY_DST | luma.Buffer.COPY_SRC
});

src.readBuffer({}, readback);
const bytes = await readback.readAsync(0, readback.byteLength);

const floats = new Float32Array(bytes.buffer);
console.log(floats[0], floats[1], floats[2], floats[3]);

注意事项

反馈回路

把当前帧缓冲的颜色附件同时绑成采样源,是 WebGL 明确禁止的用法。 乒乓的意义就在这里:读的和写的永远不是同一张纹理。

浮点渲染不是必然可用

WebGL2 需要 EXT_color_buffer_float 才能把浮点纹理当渲染目标。 没有扩展时创建会失败,代码应回退到半精度或 8 位, 而不是直接让整条渲染循环抛错。

迭代轮数越多误差越大

半精度浮点的有效位数少,几百轮迭代后累积误差会让画面出现块状伪影。 长时间模拟改用 32 位浮点,或定期用 CPU 侧的结果重置状态。

引用本文

经纬志(2026)。《用离屏渲染做通用计算》。https://www.jingweizhi.com/lumagl/tutorial/gpgpu-fbo.html(访问日期 2026-11-03)。