第 10 课:后处理
从物体表面走到整张画面
前九课的片元 Shader 给单个物体表面上色。色彩校正、暗角、模糊、Bloom 和屏幕故障效果通常需要处理已经渲染完成的整张图像。Three.js 先把场景画进离屏纹理,再用覆盖屏幕的两个三角形读取这张纹理。此时 vUv 对应屏幕图像的 0–1 范围;同一公式会作用于所有物体及背景。
本课用 EffectComposer 管理 Pass 链:RenderPass(scene,camera) → 一个或多个 ShaderPass → OutputPass。每个 Pass 从上一张纹理的 tDiffuse 采样,把结果写入另一张 render target;Composer 在两张目标之间交换读写角色。最后的 OutputPass 负责 Three.js 输出颜色空间转换。运行代码在 exercises/post-runtime.js,每题自己的 shader.frag 保留要学习的公式。
数据流是:JS 创建场景与材质 → 几何体 attribute 经物体顶点着色器 → 插值与普通片元着色器形成原始图像 → RenderPass 写入纹理 → 全屏顶点 Shader 传递 vUv → 后处理片元 Shader 采样 tDiffuse → 输出到屏幕。全屏 Pass 不知道原场景的模型坐标或法线;如果需要深度和法线,必须另行提供缓冲。
第一类:每像素独立的颜色变换
反色 1.0 - color、灰度 dot(color, vec3(0.2126,0.7152,0.0722)) 只读当前 UV 的一个像素,通常比邻域滤镜便宜。权重对应线性 RGB 的近似亮度感受;对 sRGB 编码值直接套线性亮度公式会产生不同结果。本课 RenderPass 的颜色在后处理链内按线性值使用,输出时由 OutputPass 转成显示颜色。
暗角根据 length(vUv - 0.5) 计算中心距离,再用 smoothstep 形成平滑衰减。其范围取决于画布宽高比:这里使用归一化 UV,非正方形画布上暗角的屏幕像素形状可能拉伸。需要圆形暗角时,把 UV 的横向距离乘以宽高比。色差把 R、G、B 分别从略微错位的 UV 读取,边缘产生彩边,代价是额外纹理采样。
第二类:读取相邻像素
模糊对相邻采样加权平均。uTexelSize = 1.0 / vec2(textureWidth,textureHeight) 表示一个纹理像素的 UV 距离;因此 uv + uTexelSize*vec2(1,0) 是右邻像素。单方向五次采样使横向边缘变软。二维 5×5 核需要 25 次采样;若核权重可分离,先横向五次,再纵向五次,只需约十次,但需要额外一个 Pass 和目标纹理。阅读第 4、5 题时,注意采样次数和中间纹理内存都属于成本。
Bloom 先保留亮部,再模糊并加回原图。本课第 6 题使用 Three.js 的 UnrealBloomPass 完成这组步骤;自写的 shader.frag 先提高亮部,便于观察阈值。strength 加大提高光晕贡献,threshold 提高则减少参与发光的区域。Bloom 是亮度扩散,不是场景中真实的光线传播;高分辨率下通常需要降采样或多尺度模糊。
第三类:改变采样位置与风格
CRT 先把 vUv 变成以中心为零的坐标,再按半径轻微放大采样位置,形成桶形弯曲;超出 0–1 的区域用黑色显示。扫描线按纹理像素行计算亮度,因此改动分辨率会改变线密度。故障效果把 UV 的 Y 划成条带,结合离散时间生成稳定随机数,只让少数条带水平偏移;若每个像素都使用独立随机数,画面会成为杂乱噪声。
水彩题先做两个方向的小半径模糊,再用 floor(color*levels+0.5)/levels 减少色阶。这是便于学习的风格化近似,没有纸张纤维、真实颜料扩散或边缘积色。想深化效果,可以把噪声、边缘检测或纸纹加入新的 Pass,同时监测纹理采样与显存占用。
日常开发取舍与排查
多个效果可串成 Pass 链,但每增加一层通常意味着一次全屏绘制、至少一次纹理读取,以及中间 render target 的内存。移动端先减少画布像素比、模糊半径或 Pass 数;简单色调变换可以合并到一个 Pass。水彩和 Bloom 的中间纹理不要在每帧重新创建。窗口缩放时要更新 renderer 和 Composer 尺寸,页面离开时调用 composer.dispose()、材质和几何体的 dispose()。
画面全黑时,先确认 RenderPass 在最前、OutputPass 在最后,并把自定义 Pass 暂时改为 gl_FragColor = texture2D(tDiffuse, vUv)。模糊方向错误时检查 uTexelSize 的宽高顺序;彩边过宽时按真实纹理像素调整偏移;Bloom 不亮时先确认输入亮度和 threshold。后处理测得的 draw call 会增加,但 draw call 数本身不能代替 GPU 时间。
练习
| 难度 | 题目 | 可见验收 |
|---|---|---|
| ⭐ | EffectComposer 全屏反色 | 场景整体反色 |
| ⭐ | 亮度权重灰度 | 灰度且层次保留 |
| ⭐⭐ | 径向暗角 | 四周渐暗 |
| ⭐⭐ | 五次采样的横向模糊 | 横向边缘变软 |
| ⭐⭐ | 横向加纵向的可分离模糊 | 双方向模糊 |
| ⭐⭐⭐ | 亮部提取与 Bloom | 可调亮部光晕 |
| ⭐⭐⭐ | RGB 通道色差 | 边缘彩色分离 |
| ⭐⭐⭐ | CRT 弯曲与扫描线 | 弯曲与细密条纹 |
| ⭐⭐⭐⭐ | 分块错位故障效果 | 局部条带闪动 |
| ⭐⭐⭐⭐ | 水彩式模糊与色调分离 | 软边和色阶 |