渲染通道与离屏
通道决定这一批绘制画到哪里。画到画布是最常见的情况, 画到帧缓冲则开启离屏渲染,它是后期处理、拾取与多通道合成的共同基础。
通道的创建参数
| 参数 | 作用 | 说明 |
|---|---|---|
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)。