后期处理:给渲染结果加滤镜
本教程共 40 篇 · 第 36 篇 · 更新于 2026-08-14 · 约 9 分钟阅读
本节目标:理解后期处理的渲染管线,掌握 EffectComposer + RenderPass 的最小结构,会用 UnrealBloomPass 做泛光,认识常用 Pass 及参数。
前面所有章节,渲染器画完直接显示。这一章在”画完”和”显示”之间插一道工序:把画面当图片再加工一遍。泛光、景深、描边、胶片颗粒、故障特效,全是这么来的。这个工序叫后期处理(Post Processing)。 后期处理的经典应用:泛光、景深、色调映射、色差、暗角、胶片颗粒。游戏里”电影感”的画面,一大半来自后期而不是 3D 本身。
从渲染管线说起
后期处理的核心思路:不把场景直接画到屏幕,先画到一张离屏纹理上。
完整的管线是这样:
- 场景渲染到离屏纹理(render target),得到”原始画面”;
- 原始画面经过一个个处理单元(Pass)加工:模糊、调色、叠加效果;
- 前一个 Pass 的输出是后一个 Pass 的输入;
- 最后一个 Pass 把结果画到屏幕。
每个 Pass 本质上是一个全屏着色器:把上一张图当纹理采样,算完输出新图。所以后期处理的效果上限,取决于你写全屏着色器的能力——第 34 章的 GLSL 在这里直接派上用场。
EffectComposer:Pass 的管理者
three.js 用 EffectComposer 管理这条 Pass 链。它负责离屏纹理的创建、Pass 的调度、结果的输出。它是扩展模块,要从 addons 引入:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
最小结构三步:
new EffectComposer(renderer)——传入渲染器,内部自动建好离屏纹理;- 依次
composer.addPass(...)添加 Pass,顺序就是执行顺序; - 渲染循环里调用
composer.render(),替代原来的renderer.render(scene, camera)。 EffectComposer 内部维护两张离屏纹理:readBuffer 和 writeBuffer。每个 Pass 从 readBuffer 读上一张图,把结果写进 writeBuffer,然后两张交换,下一个 Pass 继续。这就是”流水线”的物理实现。Pass 链越长,图被搬运的次数越多,能用两个 Pass 解决的,别用三个。
完整示例:泛光(Bloom)
泛光是最有代表性的后期效果:亮的地方向四周溢光,霓虹灯、发光体立刻高级起来。three.js 用 UnrealBloomPass 实现。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>36 后期处理</title>
<style>body { margin: 0; overflow: hidden; background: #000; }</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer();
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// 一圈自发光的小球
const geometry = new THREE.SphereGeometry(0.4, 24, 24);
for (let i = 0; i < 12; i++) {
const hue = i / 12;
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(hue, 0.8, 0.6),
emissive: new THREE.Color().setHSL(hue, 0.8, 0.4),
emissiveIntensity: 1.5
});
const mesh = new THREE.Mesh(geometry, material);
const angle = (i / 12) * Math.PI * 2;
mesh.position.set(Math.cos(angle) * 2.5, Math.sin(i * 1.7) * 1.2, Math.sin(angle) * 2.5);
scene.add(mesh);
}
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(3, 5, 4);
scene.add(light);
// 后期管线:渲染 → 泛光 → 输出
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight), // 分辨率
1.2, // strength 强度
0.6, // radius 半径
0.6 // threshold 阈值
);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
composer.setSize(innerWidth, innerHeight); // 离屏纹理也要跟着变
});
renderer.setAnimationLoop(() => {
controls.update();
composer.render(); // 替代 renderer.render()
});
</script>
</body>
</html>
和普通渲染的区别集中在 composer 三行:
new RenderPass(scene, camera)——第一个 Pass,把场景渲染进离屏纹理,必须最先添加;new UnrealBloomPass(resolution, strength, radius, threshold)——泛光;new OutputPass()——最后输出到屏幕,处理色彩空间和色调映射。
UnrealBloomPass 的三个参数
泛光效果好不好,全看三个参数:
strength:发光强度,越大光晕越亮越夸张,常用 0.5~1.5;radius:光晕扩散半径,范围 0~1,越大越柔和;threshold:亮度阈值,只有超过这个亮度的像素才发光。0 表示整个画面都发光,0.6 表示只有亮部发光。
Tipthreshold 是泛光的灵魂。场景里想突出霓虹灯,把阈值调到只有灯管亮度能超过;想让整体朦胧,阈值调低。小球示例用了 emissive 自发光,亮度超过阈值,所以只有小球发光,背景保持干净。
泛光的原理也顺带讲清:UnrealBloomPass 先把超过阈值的亮部抠出来,生成一组逐级缩小的 mip 纹理,每级做一次模糊,再按权重叠加回原图。大范围光晕在高分辨率上模糊很贵,它选择在小图上模糊再放大,这就是它又快又好的原因。
OutputPass 不能省
r152 起 three.js 默认开启色彩管理(第 5 章讲过),渲染结果要先从线性空间转换到 sRGB,还要应用渲染器的色调映射。这个转换原来由渲染器直接完成,但走 EffectComposer 后,画面经过了离屏纹理,最后的转换必须由 OutputPass 补上。
Note忘记加 OutputPass 的典型症状:画面发灰、发暗,颜色不对。任何 EffectComposer 管线都以 OutputPass 结尾,这是 r185 的标准配置。
常用 Pass 一览
three.js 的 addons 里预置了几十个 Pass,都在 three/addons/postprocessing/ 下:
| Pass | 效果 | 备注 |
|---|---|---|
| RenderPass | 渲染场景 | 管线第一个 |
| OutputPass | 输出到屏幕 | 管线最后一个 |
| UnrealBloomPass | 泛光 | 最常用 |
| ShaderPass | 套用任意着色器 | 配合 shaders/ 下的现成 Shader |
| OutlinePass | 物体描边 | 参数 edgeStrength 等 |
| BokehPass | 景深虚化 | 参数 focus、aperture |
| FXAAPass / SMAAPass | 抗锯齿 | 后期管线里替代 antialias |
| GlitchPass | 故障抖动 | 赛博朋克风 |
| FilmPass | 胶片颗粒 | 复古风 |
| SSAOPass | 环境光遮蔽 | 接触阴影 |
| AfterimagePass | 残影拖尾 | 运动感 |
| HalftonePass | 半调网点 | 打印风 |
| LUTPass | 颜色查找表 | 电影调色 |
OutlinePass 和 BokehPass 的参数值得记一下。OutlinePass 常用 edgeStrength 描边强度、edgeGlow 发光、edgeThickness 粗细,还有 visibleEdgeColor 可见边颜色;描边对象要收集进它的 selectedObjects 数组。BokehPass 用 focus 对焦距离和 aperture 光圈大小控制虚化,模拟真实镜头。 ShaderPass 值得单独说:它把一个现成 Shader 包成 Pass,是最灵活的通用件。比如 RGBShiftShader(色彩偏移)、DotScreenShader(网点)、SobelOperatorShader(边缘检测)。第 33、34 章学的 GLSL 可以直接写进自定义 Shader,再交给 ShaderPass:
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { RGBShiftShader } from 'three/addons/shaders/RGBShiftShader.js';
const rgbShift = new ShaderPass(RGBShiftShader);
rgbShift.uniforms['amount'].value = 0.0015;
composer.addPass(rgbShift);
性能与踩坑
后期处理有代价:每个 Pass 都是一次全屏绘制。Pass 越多,像素被处理的次数越多:
- 移动端尽量少用 Pass,或调低内部分辨率;
- 初始化分辨率用
new THREE.Vector2(innerWidth, innerHeight),注意离屏纹理尺寸,太大会很卡; renderer.setSize()之后必须composer.setSize(),否则纹理尺寸跟不上窗口;- 开了 EffectComposer,渲染器的 antialias 对中间纹理不生效,需要抗锯齿就用 FXAAPass 或 SMAAPass。
- 高分屏上 devicePixelRatio 会让离屏纹理尺寸暴涨,移动端尤其明显。composer 会按渲染器的像素比创建纹理,性能吃紧时把 pixelRatio 限制在 2 以内。
Tip排查后期问题先做减法:只留 RenderPass + OutputPass,画面正常说明管线没问题,再逐个加 Pass 二分定位。这招比盯着几十行 shader 报错快得多。
另一条路线:TSL 后期
第 35 章讲过 TSL。r185 里后期处理有了新写法:pass(scene, camera) 返回一个 Pass 节点,直接和 TSL 节点组合:
import { pass, grayscale } from 'three/tsl';
const scenePass = pass(scene, camera);
const output = grayscale(scenePass); // 转灰度
这段代码用 WebGLRenderer 或 WebGPURenderer 都能跑。EffectComposer 是成熟稳定的老路线,示例多、资料多;TSL 是新方向,代码更简洁。入门推荐先用 EffectComposer 理解管线概念,TSL 后期等熟悉节点体系后再玩。