跳到主要内容

全屏滤镜与后处理

后处理是 场景色纹理上的全屏三角形,不是 CSS filter: blur(),也不是 2D ctx.filter。场景图仍按世界单位画进 FBO;滤镜只吃这张投影。UI chrome 不要进场景 FBO,否则导出去不掉。

1. 场景怎么切

CPU 场景 + 相机 viewProj
↓ 画进 scene FBO(RGBA8 / 半浮点)
ping-pong:blur / LUT / 辉光
↓ 全屏三角形 blit 到默认 FB
合成到 <canvas>

preserveDrawingBuffer 默认 false,默认缓冲合成后可空。中间结果必须在 自己的 FBO。清的是当前绑定目标,绑错就黑屏或残影,见 帧缓冲

export type PingPong = {
read: WebGLFramebuffer;
write: WebGLFramebuffer;
readTex: WebGLTexture;
writeTex: WebGLTexture;
};

export function swapPingPong(pp: PingPong): void {
const read = pp.read;
pp.read = pp.write;
pp.write = read;
const tex = pp.readTex;
pp.readTex = pp.writeTex;
pp.writeTex = tex;
}

export function blitFullscreen(
gl: WebGL2RenderingContext,
src: WebGLTexture,
dest: WebGLFramebuffer | null,
width: number,
height: number,
drawQuad: (gl: WebGL2RenderingContext) => void,
): void {
gl.bindFramebuffer(gl.FRAMEBUFFER, dest);
gl.viewport(0, 0, width, height);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, src);
drawQuad(gl);
}

离屏尺寸跟 drawingBufferWidth/Height,不要跟 CSS。resize 重建附件,见 DPR。读写同一张纹理是未定义行为,必须 ping-pong。

适用:LUT、全屏扭曲、分离高斯辉光、tone map。
不适用:给一颗按钮做圆角阴影(DOM/CSS);给矢量图标做模糊(SVG 滤镜);热路径上「滤完再 readPixels 当直方图」。

2. 哪层必须 CPU,哪层进 GPU

归属
开哪些 pass、阈值、LUT 字节CPU 参数
场景物体、相机CPU 文档 + viewProj
场景色 / 深度 / 速度纹理GPU
卷积、下采样链GPU;结果不要读回

辉光:先画场景 → 阈值缩小 → 横向/纵向分离模糊 → 加回。不要在全分辨率上 9×9 暴力核。深度采样必须是深度 纹理,renderbuffer 不能当后处理源。

CSS filter 作用在合成层,模糊的是整张 canvas 含 overlay,和相机、MSAA、导出无关。产品要的是「场景发光、UI 不发光」,只能 FBO。

MRT 一次写出颜色+速度可以,但两个无关滤镜不要硬塞进 MRT。WebGPU 后处理是 render pass 换 attachment;没有探测 adapter 就不要为滤镜切栈。

禁止:滤完每帧全屏 readPixels 做调色;用 toDataURL 当预览。需要像素统计走 计算着色

3. 失败形态

症状原因
花屏 / 反馈噪声读写同一张纹理
辉光发黑clear 时 FBO 绑反,或 LDR 8bit 累加饱和
导出带工具条chrome 画进了场景 FBO
全屏滤镜糊 UI用了 CSS filter 冒充后处理
一开 bloom 掉半帧全分辨率大核,没分离、没下采样

权威资料

核对日期:2026-08-26