Skip to main content

第 8 节:帧缓冲与后处理

🎯 学习目标

  • 理解帧缓冲(FBO)的作用
  • 掌握离屏渲染技术
  • 实现后处理效果
  • 对比原生 WebGL 和 Three.js 的后处理系统

📖 理论:帧缓冲基础

帧缓冲(Framebuffer)是用于离屏渲染的缓冲区。

帧缓冲的组成

  1. 颜色附件:存储颜色信息
  2. 深度附件:存储深度信息
  3. 模板附件:存储模板信息

后处理流程

场景渲染 → 帧缓冲 → 后处理 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>

关键点

  1. 创建帧缓冲gl.createFramebuffer()
  2. 附加纹理gl.framebufferTexture2D()
  3. 附加深度缓冲gl.framebufferRenderbuffer()
  4. 检查状态gl.checkFramebufferStatus()
  5. 切换目标:绑定不同的帧缓冲

🎨 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 的优势

  1. EffectComposer:自动管理帧缓冲链
  2. RenderPass:自动渲染场景到纹理
  3. ShaderPass:简化后处理 Shader 的使用
  4. 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 简化使用
  • 自动处理全屏四边形

📝 编程作业

基础作业(⭐)

任务:实现灰度后处理效果

要求

  1. 创建一个简单的场景
  2. 渲染到帧缓冲
  3. 应用灰度后处理
  4. 使用原生 WebGL 和 Three.js 两种方式实现

检查清单

  • 场景可以正常渲染
  • 后处理效果正确
  • 理解帧缓冲的作用

进阶作业(⭐⭐)

任务:实现多个后处理效果

要求

  1. 实现灰度、模糊、色差三种效果
  2. 可以组合使用
  3. 可以实时切换效果

提示

// 模糊(简单版本)
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;

检查清单

  • 三种效果都正确
  • 可以组合使用
  • 可以实时切换

挑战作业(⭐⭐⭐)

任务:实现完整的后处理系统

要求

  1. 支持多种后处理效果
  2. 支持效果参数调节
  3. 支持效果预设保存和加载
  4. 优化性能(减少不必要的渲染)

扩展功能

  • 实现效果预览
  • 实现效果动画
  • 实现自定义效果编辑器

🎓 本节总结

关键概念

  1. 帧缓冲(FBO):用于离屏渲染的缓冲区
  2. 后处理:对渲染结果进行二次处理
  3. Pass 链:多个后处理效果可以串联
  4. 全屏四边形:后处理使用全屏四边形

原生 WebGL vs Three.js

方面原生 WebGLThree.js
帧缓冲管理手动创建自动管理
后处理链手动管理EffectComposer
代码量
灵活性完全控制受限于 API

最佳实践

  1. 减少 Pass 数量:每个 Pass 都有性能开销
  2. 合并效果:尽量在一个 Pass 中实现多个效果
  3. 降低分辨率:后处理可以使用较低分辨率
  4. 使用 Ping-Pong:多个效果需要多个帧缓冲时使用

下一步

完成作业后,进入下一节:第 10 节:性能优化


📚 参考资源