Skip to main content

13. 后期处理艺术:屏幕后处理

到现在为止,我们所有的 Shader 都是直接作用在物体上的。 屏幕后处理 (Post-Processing) 打开了一扇新的大门:它把整个 3D 场景渲染完的结果当成一张图片,然后再对这张图片进行二次处理。

这就像是给照片加滤镜:黑白、模糊、发光 (Bloom)、老电影效果等。


1. 核心原理:帧缓冲 (FBO)

在原生 WebGL 中,这叫 Framebuffer Object (FBO)。 在 Three.js 中,这通常由 EffectComposer 管理,但理解其底层原理至关重要。

流程对比

  • 普通渲染

    • 场景 -> 默认帧缓冲(屏幕)
  • 后处理渲染

    • Pass 1: 场景 -> 离屏帧缓冲 (Texture) (你看不见)
    • Pass 2: 画一个铺满屏幕的大四边形,把上面生成的 Texture 贴上去,并应用滤镜 Shader -> 屏幕

2. 实战:搭建一个简单的后处理 Shader

我们跳过繁琐的 FBO 设置(假设你使用 Three.js 的 ShaderPass 或原生 FBO 已经拿到了纹理),只关注 Fragment Shader 的写法。

输入变量

  • tDiffuse: 上一步渲染出来的场景图 (Texture)。
  • vUv: 屏幕坐标 (0,0 到 1,1)。

2.1 灰度滤镜 (Grayscale)

最入门的效果。

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(vec3(gray), 1.0);
}

2.2 晕影 (Vignette)

让屏幕四个角变暗,模拟老式相机镜头,能极大地增强"电影感"。

uniform sampler2D tDiffuse;
varying vec2 vUv;

void main() {
vec4 color = texture2D(tDiffuse, vUv);

// 计算当前像素距离屏幕中心的距离
// vUv 范围是 0~1,中心是 0.5
vec2 center = vec2(0.5);
float dist = distance(vUv, center);

// 简单的线性变暗:距离越远,系数越小
// smoothstep 用于控制暗角的范围和柔和度
// 0.8 开始变暗,1.5 完全黑 (虽然 dist 最大也就 0.707)
float vignette = 1.0 - smoothstep(0.4, 1.0, dist);

gl_FragColor = vec4(color.rgb * vignette, 1.0);
}

2.3 像素化 / 马赛克 (Pixelation)

通过修改 UV 坐标来降低分辨率。

原理:把连续的 UV 坐标"对齐"到离散的网格上。

uniform sampler2D tDiffuse;
varying vec2 vUv;
uniform float uPixels; // 比如 64.0 (即横向分割成64个格子)

void main() {
// 1. 放大 UV
vec2 p = vUv * uPixels;

// 2. 向下取整 (丢弃小数部分)
p = floor(p);

// 3. 缩小回 0~1
p = p / uPixels;

// 4. 加上半个像素偏移 (可选,采样中心点)
p += 0.5 / uPixels;

gl_FragColor = texture2D(tDiffuse, p);
}

2.4 色差 / 故障风 (Chromatic Aberration)

让 R、G、B 三个通道使用稍微不同的 UV 坐标进行采样。

uniform sampler2D tDiffuse;
varying vec2 vUv;

void main() {
// 红色通道稍微向左移
float r = texture2D(tDiffuse, vUv + vec2(0.005, 0.0)).r;

// 绿色通道原地不动
float g = texture2D(tDiffuse, vUv).g;

// 蓝色通道稍微向右移
float b = texture2D(tDiffuse, vUv - vec2(0.005, 0.0)).b;

gl_FragColor = vec4(r, g, b, 1.0);
}

3. 组合使用

后处理的强大之处在于链式组合。 你可以搞一个数组: [场景] -> [像素化 Pass] -> [RGB分离 Pass] -> [晕影 Pass] -> [屏幕]

在 Three.js 中,只需简单地 composer.addPass(new ShaderPass(...)) 即可实现。


📝 编程作业

难度:⭐⭐⭐ 困难 | 预计时间:90 分钟

任务要求

使用 Three.js 的 EffectComposer 实现多个屏幕后处理效果。

具体要求

实现以下四个后处理效果(每个效果单独完成):

  1. 灰度效果:将场景转换为黑白
  2. 晕影效果:边缘变暗
  3. 像素化效果:降低分辨率
  4. 色差效果:RGB 通道偏移

代码框架

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';

// 1. 创建场景和渲染器
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
// ... 添加物体到场景

// 2. 创建 EffectComposer
const composer = new EffectComposer(renderer);

// 3. 添加 RenderPass(渲染场景)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// 4. 创建自定义 ShaderPass
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);
// TODO: 实现灰度转换
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), 1.0);
}
`
};

const grayscalePass = new ShaderPass(grayscaleShader);
composer.addPass(grayscalePass);

// 5. 渲染循环中使用 composer
function animate() {
composer.render(); // 使用 composer 而不是 renderer.render()
}

效果实现提示

效果 1:灰度

float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), 1.0);

效果 2:晕影

vec2 center = vec2(0.5);
float dist = distance(vUv, center);
float vignette = 1.0 - smoothstep(0.4, 1.0, dist);
gl_FragColor = vec4(color.rgb * vignette, 1.0);

效果 3:像素化

uniform float uPixels;
vec2 p = floor(vUv * uPixels) / uPixels;
p += 0.5 / uPixels;
gl_FragColor = texture2D(tDiffuse, p);

效果 4:色差

float r = texture2D(tDiffuse, vUv + vec2(0.005, 0.0)).r;
float g = texture2D(tDiffuse, vUv).g;
float b = texture2D(tDiffuse, vUv - vec2(0.005, 0.0)).b;
gl_FragColor = vec4(r, g, b, 1.0);

检查清单

  • 每个效果都可以单独开启/关闭
  • 效果参数可以通过 uniform 调节
  • 多个效果可以组合使用
  • 代码结构清晰,易于扩展

扩展挑战

  • 🌟 添加 GUI 控制面板(如 dat.GUI)来实时调整参数
  • 🌟 实现更多效果:模糊、发光、老电影效果
  • 🌟 优化性能:注意 Pass 的顺序和复杂度

参考资源