第 8 节:帧缓冲与后处理
🎯 学习目标
- 理解帧缓冲(FBO)的作用
- 掌握离屏渲染技术
- 实现后处理效果
- 对比原生 WebGL 和 Three.js 的后处理系统
📖 理论:帧缓冲基础
帧缓冲(Framebuffer)是用于离屏渲染的缓冲区。
帧缓冲的组成
- 颜色附件:存储颜色信息
- 深度附件:存储深度信息
- 模板附件:存储模板信息
后处理流程
场景渲染 → 帧缓冲 → 后处理 Shader → 屏幕
常见后处理效果
- 灰度:将颜色转换为灰度
- 模糊:高斯模糊
- 色差:RGB 通道偏移
- 晕影:边缘变暗
- 像素化:降低分辨率
💻 原生 WebGL 实现
实现一个简单的灰度后处理效果:
<!DOCTYPE html>
<html>
<head>
<title>帧缓冲与后处理 - 原生 WebGL</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.3/gl-matrix-min.js"></script>
<script>
const canvas = document.getElementById("glcanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gl = canvas.getContext("webgl");
if (!gl) {
alert("无法初始化 WebGL");
}
const mat4 = glMatrix.mat4;
// 场景渲染 Shader
const sceneVertexShader = `
attribute vec4 aVertexPosition;
uniform mat4 uModelViewProjection;
void main() {
gl_Position = uModelViewProjection * aVertexPosition;
}
`;
const sceneFragmentShader = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}
`;
// 后处理 Shader
const postVertexShader = `
attribute vec2 aPosition;
varying vec2 vTexCoord;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
vTexCoord = aPosition * 0.5 + 0.5;
}
`;
const postFragmentShader = `
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 灰度效果
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(gray, gray, gray, color.a);
}
`;
// 编译函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("编译错误:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("链接错误:", gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
// 创建场景渲染程序
const sceneVS = createShader(gl, gl.VERTEX_SHADER, sceneVertexShader);
const sceneFS = createShader(gl, gl.FRAGMENT_SHADER, sceneFragmentShader);
const sceneProgram = createProgram(gl, sceneVS, sceneFS);
// 创建后处理程序
const postVS = createShader(gl, gl.VERTEX_SHADER, postVertexShader);
const postFS = createShader(gl, gl.FRAGMENT_SHADER, postFragmentShader);
const postProgram = createProgram(gl, postVS, postFS);
// 创建帧缓冲
function createFramebuffer(gl, width, height) {
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// 创建纹理作为颜色附件
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 创建深度缓冲
const depthBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
// 附加到帧缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
// 检查帧缓冲状态
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
console.error("帧缓冲不完整");
return null;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { framebuffer, texture, depthBuffer };
}
const fbo = createFramebuffer(gl, canvas.width, canvas.height);
// 场景几何体(三角形)
const sceneVertices = new Float32Array([
-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0,
]);
const sceneBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sceneBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sceneVertices, gl.STATIC_DRAW);
// 后处理全屏四边形
const postVertices = new Float32Array([
-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1,
]);
const postBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, postBuffer);
gl.bufferData(gl.ARRAY_BUFFER, postVertices, gl.STATIC_DRAW);
// 渲染函数
function render() {
// 第一步:渲染到帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.framebuffer);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(sceneProgram);
const scenePosLoc = gl.getAttribLocation(sceneProgram, "aVertexPosition");
const sceneMvpLoc = gl.getUniformLocation(sceneProgram, "uModelViewProjection");
const mvp = mat4.create();
mat4.identity(mvp);
gl.uniformMatrix4fv(sceneMvpLoc, false, mvp);
gl.bindBuffer(gl.ARRAY_BUFFER, sceneBuffer);
gl.enableVertexAttribArray(scenePosLoc);
gl.vertexAttribPointer(scenePosLoc, 3, gl.FLOAT, false, 0, 0);
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 第二步:后处理到屏幕
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(postProgram);
const postPosLoc = gl.getAttribLocation(postProgram, "aPosition");
const postTexLoc = gl.getUniformLocation(postProgram, "uTexture");
gl.bindBuffer(gl.ARRAY_BUFFER, postBuffer);
gl.enableVertexAttribArray(postPosLoc);
gl.vertexAttribPointer(postPosLoc, 2, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.texture);
gl.uniform1i(postTexLoc, 0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
render();
</script>
</body>
</html>
关键点
- 创建帧缓冲:
gl.createFramebuffer() - 附加纹理:
gl.framebufferTexture2D() - 附加深度缓冲:
gl.framebufferRenderbuffer() - 检查状态:
gl.checkFramebufferStatus() - 切换目标:绑定不同的帧缓冲
🎨 Three.js 对比实现
Three.js 使用 EffectComposer 简化后处理:
<!DOCTYPE html>
<html>
<head>
<title>帧缓冲与后处理 - Three.js</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("canvas"),
});
renderer.setSize(window.innerWidth, window.innerHeight);
// 创建场景
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xff8800 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 创建后处理链
const composer = new EffectComposer(renderer);
// 渲染场景到纹理
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 灰度效果
const grayscaleShader = {
uniforms: {
tDiffuse: { value: null },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(gray, gray, gray, color.a);
}
`,
};
const grayscalePass = new ShaderPass(grayscaleShader);
composer.addPass(grayscalePass);
// 渲染循环
function animate() {
requestAnimationFrame(animate);
mesh.rotation.y += 0.02;
// 使用 composer 渲染
composer.render();
}
animate();
</script>
</body>
</html>
Three.js 的优势
- EffectComposer:自动管理帧缓冲链
- RenderPass:自动渲染场景到纹理
- ShaderPass:简化后处理 Shader 的使用
- Pass 链:可以轻松组合多个效果
🔍 原理对比分析
1. 帧缓冲管理
原生 WebGL:
// 需要手动创建和管理
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(...);
Three.js:
// 自动管理
const composer = new EffectComposer(renderer);
// 内部自动创建和管理帧缓冲
2. 后处理链
原生 WebGL:
- 需要手动管理多个帧缓冲
- 需要手动切换渲染目标
Three.js:
- 自动管理后处理链
- 自动处理 Pass 之间的传递
3. Shader 管理
原生 WebGL:
- 需要手动创建后处理 Shader
- 需要手动管理全屏四边形
Three.js:
- 提供 ShaderPass 简化使用
- 自动处理全屏四边形
📝 编程作业
基础作业(⭐)
任务:实现灰度后处理效果
要求:
- 创建一个简单的场景
- 渲染到帧缓冲
- 应用灰度后处理
- 使用原生 WebGL 和 Three.js 两种方式实现
检查清单:
- 场景可以正常渲染
- 后处理效果正确
- 理解帧缓冲的作用
进阶作业(⭐⭐)
任务:实现多个后处理效果
要求:
- 实现灰度、模糊、色差三种效果
- 可以组合使用
- 可以实时切换效果
提示:
// 模糊(简单版本)
vec4 color = texture2D(uTexture, vTexCoord);
color += texture2D(uTexture, vTexCoord + vec2(0.001, 0.0));
color += texture2D(uTexture, vTexCoord - vec2(0.001, 0.0));
color /= 3.0;
// 色差
float r = texture2D(uTexture, vTexCoord + vec2(0.002, 0.0)).r;
float g = texture2D(uTexture, vTexCoord).g;
float b = texture2D(uTexture, vTexCoord - vec2(0.002, 0.0)).b;
检查清单:
- 三种效果都正确
- 可以组合使用
- 可以实时切换
挑战作业(⭐⭐⭐)
任务:实现完整的后处理系统
要求:
- 支持多种后处理效果
- 支持效果参数调节
- 支持效果预设保存和加载
- 优化性能(减少不必要的渲染)
扩展功能:
- 实现效果预览
- 实现效果动画
- 实现自定义效果编辑器
🎓 本节总结
关键概念
- 帧缓冲(FBO):用于离屏渲染的缓冲区
- 后处理:对渲染结果进行二次处理
- Pass 链:多个后处理效果可以串联
- 全屏四边形:后处理使用全屏四边形
原生 WebGL vs Three.js
| 方面 | 原生 WebGL | Three.js |
|---|---|---|
| 帧缓冲管理 | 手动创建 | 自动管理 |
| 后处理链 | 手动管理 | EffectComposer |
| 代码量 | 多 | 少 |
| 灵活性 | 完全控制 | 受限于 API |
最佳实践
- 减少 Pass 数量:每个 Pass 都有性能开销
- 合并效果:尽量在一个 Pass 中实现多个效果
- 降低分辨率:后处理可以使用较低分辨率
- 使用 Ping-Pong:多个效果需要多个帧缓冲时使用
下一步
完成作业后,进入下一节:第 10 节:性能优化