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

渲染通道与离屏

先看结论

通道决定这一批绘制画到哪里。画到画布是最常见的情况, 画到帧缓冲则开启离屏渲染,它是后期处理、拾取与多通道合成的共同基础。

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

通道的创建参数

参数作用说明
framebuffer渲染目标省略或传 null 时画到画布
clearColor颜色清除值传 false 保留上一帧内容
clearDepth深度清除值默认 1,传 false 不清除
parameters.viewport绘制区域省略时按目标尺寸自动设置
depthReadOnly禁止写入深度只做采样的深度附件用得上

视口默认跟随目标:画到帧缓冲用帧缓冲的宽高,画到画布用绘制缓冲的宽高。 手工写死像素值,页面缩放或修改设备像素比之后就会错位。

两趟渲染的骨架

第一趟把场景画进 256×256 的帧缓冲,第二趟把那张颜色附件当普通纹理采样到屏幕。 两个通道必须先后结束,不能嵌套。

const SIZE = 256;

const colorTex = device.createTexture({
  width: SIZE,
  height: SIZE,
  format: 'rgba8unorm',
  usage: luma.Texture.SAMPLE | luma.Texture.RENDER | luma.Texture.COPY_DST,
  sampler: { minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'none' }
});

const depthTex = device.createTexture({
  width: SIZE,
  height: SIZE,
  format: device.preferredDepthFormat,
  usage: luma.Texture.RENDER
});

const fbo = device.createFramebuffer({
  width: SIZE,
  height: SIZE,
  colorAttachments: [colorTex],
  depthStencilAttachment: depthTex
});

const compositor = new luma.Model(device, {
  vs: quadVS,
  fs: compositeFS,
  attributes: { positions: quadPositions, uvs: quadUVs },
  bufferLayout: [
    { name: 'positions', stepMode: 'vertex', format: 'float32x2' },
    { name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
  ],
  bindings: { uScene: colorTex },
  vertexCount: 6,
  topology: 'triangle-list'
});

function render() {
  const offscreen = device.beginRenderPass({
    framebuffer: fbo,
    clearColor: [0.02, 0.04, 0.07, 1],
    clearDepth: 1
  });
  sceneModel.draw(offscreen);
  offscreen.end();

  const screen = device.beginRenderPass({
    clearColor: [0.04, 0.06, 0.09, 1]
  });
  compositor.draw(screen);
  screen.end();
}

draw 返回布尔值。返回 false 通常意味着资源未就绪、 管线链接失败或绑定缺失,控制台里的 luma.gl 警告会直接指出缺了哪一项。

附件用途一次给全

颜色附件要同时带 SAMPLE 与 RENDER: 前者供下一趟采样,后者供本趟渲染。用途在创建时固定,事后无法追加, 漏写就只能重建纹理。

// 需要回读像素时再加 COPY_SRC
const pickTex = device.createTexture({
  width: width,
  height: height,
  format: 'rgba8unorm',
  usage: luma.Texture.SAMPLE | luma.Texture.RENDER |
         luma.Texture.COPY_SRC | luma.Texture.COPY_DST,
  sampler: { minFilter: 'nearest', magFilter: 'nearest' }
});

const pickFbo = device.createFramebuffer({
  width: width,
  height: height,
  colorAttachments: [pickTex],
  depthStencilAttachment: device.createTexture({
    width: width,
    height: height,
    format: device.preferredDepthFormat,
    usage: luma.Texture.RENDER
  })
});

回读像素

拾取、数值校验、导出图片都需要把 GPU 上的结果读回 CPU。 v9 的路径是先把纹理内容写进缓冲,再从缓冲异步读出; 缓冲区大小要与读取的范围一致。

const W = 256;
const H = 256;

const readback = device.createBuffer({
  byteLength: W * H * 4,
  usage: luma.Buffer.COPY_DST | luma.Buffer.COPY_SRC
});

colorTex.readBuffer({}, readback);              // 整张读入缓冲
const bytes = await readback.readAsync(0, readback.byteLength);

function pixelAt(x, y) {
  const i = (y * W + x) * 4;
  return [bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]];
}

console.log('(10, 20) 处的像素', pixelAt(10, 20));

注意事项

通道嵌套

beginRenderPass 内部会压入当前状态,end() 时恢复。 在前一个通道未结束时再开一个,状态会互相覆盖,表现为视口错乱或清除失效。

同一帧读写同一张纹理

把帧缓冲的颜色附件绑成采样源,又准备画回这张帧缓冲,形成反馈回路, WebGL 会直接判定为未定义行为。需要迭代时用两张纹理做乒乓切换。

忘记 end()

漏掉一次 end(),后续所有绘制都会带着旧的视口与清除设置。 这类问题不会抛错,只表现为画面位置逐渐偏掉,排查成本很高。

引用本文

经纬志(2026)。《渲染通道与离屏》。https://www.jingweizhi.com/lumagl/tutorial/render-passes.html(访问日期 2026-11-03)。