全屏滤镜与后处理
后处理是 场景色纹理上的全屏三角形,不是 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