跳到主要内容

帧缓冲MRT与深度

默认绘制目标是 canvas 的 drawing buffer。离屏 pass、后处理、ID 拾取、阴影,都要自己的 FBOgl.clear 清的是 当前绑定 的 framebuffer——绑着离屏却以为在清屏幕,是最常见的黑屏 / 残影来源。

1. 颜色 + 深度

export type Offscreen = {
fbo: WebGLFramebuffer;
color: WebGLTexture;
depth: WebGLTexture;
width: number;
height: number;
};

export function createOffscreen(
gl: WebGL2RenderingContext,
width: number,
height: number,
): Offscreen {
const color = gl.createTexture();
const depth = gl.createTexture();
const fbo = gl.createFramebuffer();
if (!color || !depth || !fbo) throw new Error("fbo alloc failed");
gl.bindTexture(gl.TEXTURE_2D, color);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, depth);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT24, width, height);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depth, 0);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
throw new Error(`framebuffer incomplete: ${status}`);
}
return { fbo, color, depth, width, height };
}

export function bindOffscreen(gl: WebGL2RenderingContext, target: Offscreen | null): void {
if (!target) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
return;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
gl.viewport(0, 0, target.width, target.height);
}

深度用 深度纹理 而不是 renderbuffer,后处理才能采样。只要深度、不采样,用 renderbufferStorage(DEPTH_COMPONENT24) 更省。不需要深度的 blit pass 创建时关掉,少一项附件。

2. MRT

多渲染目标:片元一次写出颜色 + ID / 法线 / 速度。

export function attachMrt(
gl: WebGL2RenderingContext,
fbo: WebGLFramebuffer,
colors: readonly WebGLTexture[],
): void {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
const bufs: number[] = [];
colors.forEach((tex, i) => {
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0 + i,
gl.TEXTURE_2D,
tex,
0,
);
bufs.push(gl.COLOR_ATTACHMENT0 + i);
});
gl.drawBuffers(bufs);
}

GLSL 300 es 用 layout(location = n) outMAX_DRAW_BUFFERS 必须探测;移动端常是 4。附件尺寸必须一致,否则 incomplete。

适用:延迟着色、一次画出场景色和拾取 ID。
不适用:两个无关 pass 硬塞进 MRT「少一次 draw」——带宽和带宽换不来的同步会更差。

3. 清错 FBO

gl.bindFramebuffer(gl.FRAMEBUFFER, offscreen.fbo);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// ... draw to offscreen ...
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

离开离屏 必须 绑回 null 再画 canvas。preserveDrawingBuffer: false 只影响默认缓冲合成后是否保留,不影响 你自己的 FBO 内容;但下一帧若不清 FBO,后处理会吃到旧 ID / 旧辉光。

WebGPU:passcolorAttachments[].view + depthStencilAttachmentloadOp: "clear" 写在 begin 时,没有全局 bindFramebuffer。MRT 就是多个 color attachment。

4. 失败形态

症状原因
屏幕残影、离屏全黑clear 时 FBO 还绑着 / 已经解绑反了
FRAMEBUFFER_INCOMPLETE尺寸不一致、缺深度格式、浮点附件没扩展
MRT 第二目标空drawBuffers,或 FS 没写 location = 1
后处理采深度全 1用了 renderbuffer 却当纹理绑
拾取读到屏幕色读的是默认 FB,且合成后默认缓冲已丢

权威资料

核对日期:2026-08-26