跳到主要内容

截图与离屏导出

用户点「保存当前画面」时,GPU 表面不一定还在。默认 preserveDrawingBuffer: false:浏览器合成到屏幕后,可以把 drawing buffer 清掉。下一帧再 toBlob / readPixels,读到的是空的或未定义内容,不是你刚才看见的那帧。

1. 机制:三条合法路径

路径做什么代价
同帧读 FBO / color attachment场景本就画到纹理,截图读那张,不碰 canvas 交换链架构要对:主相机输出是离屏目标
preserveDrawingBuffer: true合成后保留默认缓冲,随后 toBlob 也能撞上每帧多占带宽 / 显存
blit 到 2D 离屏同帧 drawImage(glCanvas)convertToBlob多一次拷;Y 与颜色空间要自己对齐

适用: 产品需要导出、分享图、缩略图、测试金标。路径按「这帧是否本来就有离屏目标」选,不要三种叠着用。

不适用: 每帧截图做业务(直方图、拾取、自动对焦)。那是热路径读回,见 06。拾取用 ID 缓冲或 CPU 空间索引,不要 readPixels 整张色缓冲。

2. 同帧读 FBO(首选)

场景已经画进 FBO 时,截图是读那张纹理,和「交换链被不清掉」解耦:

export function readFboRgba(
gl: WebGL2RenderingContext,
fbo: WebGLFramebuffer,
width: number,
height: number,
): Uint8Array {
const pixels = new Uint8Array(width * height * 4);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return pixels;
}

export function flipRowsRgba(
pixels: Uint8Array,
width: number,
height: number,
): Uint8Array {
const stride = width * 4;
const out = new Uint8Array(pixels.byteLength);
for (let y = 0; y < height; y += 1) {
const src = (height - 1 - y) * stride;
out.set(pixels.subarray(src, src + stride), y * stride);
}
return out;
}

readPixels 原点在左下。要当 PNG / ImageData(左上)必须翻行。WebGPU:在 同一 submitcopyTextureToBuffer,再 mapAsyncgetCurrentTexture() 不能留到下一帧。

3. preserve 与 blit 到 2D

export function captureGlToBlob(glCanvas: HTMLCanvasElement): Promise<Blob> {
return new Promise((resolve, reject) => {
glCanvas.toBlob((blob) => {
if (!blob) {
reject(new Error("toBlob returned null"));
return;
}
resolve(blob);
}, "image/png");
});
}

export async function blitGlToOffscreen(
glCanvas: HTMLCanvasElement,
cssW: number,
cssH: number,
dpr: number,
): Promise<Blob> {
const dest = new OffscreenCanvas(
Math.max(1, Math.round(cssW * dpr)),
Math.max(1, Math.round(cssH * dpr)),
);
const ctx = dest.getContext("2d");
if (!ctx) throw new Error("2d unavailable");
ctx.drawImage(glCanvas, 0, 0, dest.width, dest.height);
return dest.convertToBlob({ type: "image/png" });
}

toBlob / drawImage(glCanvas) 必须发生在 本帧 GL 提交之后、合成清空之前。典型写法:requestAnimationFrame 里先 draw*,同一回调末尾导出。不要 setTimeout(0) 再截。

preserveDrawingBuffer: true 只在「必须随时截默认缓冲、又没有 FBO」时开。开启后移动端带宽和内存都涨。需要截图的产品优先 FBO,而不是全局 preserve。

toDataURL 同步编码 + base64,4K × DPR 会卡主线程。导出一律 toBlob / convertToBlob

GL canvas 若已 taint,两条路径都会 SecurityError,见 污染。2D 离屏也会被脏。

4. 离屏规格,不要借视口 canvas

导出封面、缩略图、打印图是 离线光栅:单独 FBO / OffscreenCanvas,相机 fit 到规格,不要借用正在交互的那张表面。多规格循环渲染,不要先导出一张再 CSS 拉伸。印刷尺寸用英寸 × dpi,dpr = 1,见 Canvas 缩略图

5. 失败形态

症状原因
截到黑图 / 透明下一帧才 toBlob,默认缓冲已清
上下颠倒readPixels 当 PNG 没翻行
导出卡 2 秒toDataURL 同步 4K
预览正常导出 SecurityError纹理跨源无 CORS
开了截图掉一半帧全局 preserveDrawingBuffer + 每帧读回
WebGPU 截到空getCurrentTexture() 用到了下一帧

权威资料

核对日期:2026-08-26