You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

657 lines
25 KiB
Markdown

2 years ago
# 17 | 如何使用后期处理通道增强图像效果?
你好,我是月影。
前面几节课,我们学习了利用向量和矩阵公式,来处理像素和生成纹理的技巧,但是这些技巧都有一定的局限性:每个像素是彼此独立的,不能共享信息。
为什么这么说呢因为GPU是并行渲染的所以在着色器的执行中每个像素的着色都是同时进行的。这样一来我们就不能获得某一个像素坐标周围坐标点的颜色信息也不能获得要渲染图像的全局信息。
这会导致什么问题呢?如果我们要实现与周围像素点联动的效果,比如给生成的纹理添加平滑效果滤镜,就不能直接通过着色器的运算来实现了。
因此在WebGL中像这样不能直接通过着色器运算来实现的效果我们需要使用其他的办法来实现其中一种办法就是使用**后期处理通道**。所谓后期处理通道是指将渲染出来的图像作为纹理输入给新着色器处理是一种二次加工的手段。这么一来虽然我们不能从当前渲染中获取周围的像素信息却可以从纹理中获取任意uv坐标下的像素信息也就相当于可以获取任意位置的像素信息了。
使用后期处理通道的一般过程是我们先正常地将数据送入缓冲区然后执行WebGLProgram。只不过在执行了WebGLProgram之后我们要将输出的结果再作为纹理送入另一个WebGLProgram进行处理这个过程可以进行一次也可以循环多次。最后经过两次WebGLProgram处理之后我们再输出结果。
![](https://static001.geekbang.org/resource/image/a9/75/a9b5a5f90e2c3e465a0a6d2b7070ef75.jpg "后期通道处理的一般过程示意图")
你可以先仔细看看这张流程总结图加深一下印象。接下来我会结合这个过程说说怎么用后期处理通道来实现Blur滤镜、辉光效果和烟雾效果这样你就能理解得更深刻了。
首先我们来实现Blur滤镜。
## 如何用后期处理通道实现Blur滤镜
其实在第11节课中我们已经在Canvas2D中实现了Bblur滤镜高斯模糊的平滑效果滤镜但Canvas2D实现滤镜的性能不佳尤其是在图片较大需要大量计算的时候。
而在WebGL中我们可以通过后期处理来实现高性能的Blur滤镜。下面我就以给随机三角形图案加Blur滤镜为例来说说具体的操作。
首先,我们实现一个绘制随机三角形图案的着色器。代码如下:
```
#ifdef GL_ES
precision highp float;
#endif
float line_distance(in vec2 st, in vec2 a, in vec2 b) {
vec3 ab = vec3(b - a, 0);
vec3 p = vec3(st - a, 0);
float l = length(ab);
return cross(p, normalize(ab)).z;
}
float seg_distance(in vec2 st, in vec2 a, in vec2 b) {
vec3 ab = vec3(b - a, 0);
vec3 p = vec3(st - a, 0);
float l = length(ab);
float d = abs(cross(p, normalize(ab)).z);
float proj = dot(p, ab) / l;
if(proj >= 0.0 && proj <= l) return d;
return min(distance(st, a), distance(st, b));
}
float triangle_distance(in vec2 st, in vec2 a, in vec2 b, in vec2 c) {
float d1 = line_distance(st, a, b);
float d2 = line_distance(st, b, c);
float d3 = line_distance(st, c, a);
if(d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0 || d1 <= 0.0 && d2 <= 0.0 && d3 <= 0.0) {
return -min(abs(d1), min(abs(d2), abs(d3))); // 内部距离为负
}
return min(seg_distance(st, a, b), min(seg_distance(st, b, c), seg_distance(st, c, a))); // 外部为正
}
float random (vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))*
43758.5453123);
}
vec3 hsb2rgb(vec3 c){
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
varying vec2 vUv;
void main() {
vec2 st = vUv;
st *= 10.0;
vec2 i_st = floor(st);
vec2 f_st = 2.0 * fract(st) - vec2(1);
float r = random(i_st);
float sign = 2.0 * step(0.5, r) - 1.0;
float d = triangle_distance(f_st, vec2(-1), vec2(1), sign * vec2(1, -1));
gl_FragColor.rgb = (smoothstep(-0.85, -0.8, d) - smoothstep(0.0, 0.05, d)) * hsb2rgb(vec3(r + 1.2, 0.5, r));
gl_FragColor.a = 1.0;
}
```
这个着色器绘制出的效果如下图:
![](https://static001.geekbang.org/resource/image/bc/5b/bce2b0fbe3954c9a690a46f422de4f5b.jpeg)
接着就是重点了,我们要使用后期处理通道对它进行高斯模糊。
首先我们需要准备另一个着色器blurFragment。通过它我们能将第一次渲染后生成的纹理tMap内容给显示出来。
```
#ifdef GL_ES
precision highp float;
#endif
varying vec2 vUv;
uniform sampler2D tMap;
void main() {
vec4 color = texture2D(tMap, vUv);
gl_FragColor.rgb = color.rgb;
gl_FragColor.a = color.a;
}
```
然后我们要修改JavaScript代码把渲染分为两次。第一次渲染时我们启用program程序但不直接把图形输出到画布上而是输出到一个帧缓冲对象Frame Buffer Object上。第二次渲染时我们再启用blurProgram程序将第一次渲染完成的纹理fbo.texture作为blurFragment的tMap变量这次的输出绘制到画布上。代码如下
```
...
renderer.useProgram(program);
renderer.setMeshData([{
positions: [
[-1, -1],
[-1, 1],
[1, 1],
[1, -1],
],
attributes: {
uv: [
[0, 0],
[0, 1],
[1, 1],
[1, 0],
],
},
cells: [[0, 1, 2], [2, 0, 3]],
}]);
const fbo = renderer.createFBO();
renderer.bindFBO(fbo);
renderer.render();
renderer.bindFBO(null);
const blurProgram = renderer.compileSync(blurFragment, vertex);
renderer.useProgram(blurProgram);
renderer.setMeshData(program.meshData);
renderer.uniforms.tMap = fbo.texture;
renderer.render();
```
其中renderer.createFBO是创建帧缓冲对象bindFBO是绑定帧缓冲对象。为了方便调用我在这里通过gl-renderer做了一层简单的封装。
那经过两次渲染之后,我们运行程序输出的结果和之前输出的并不会有什么区别。因为第二次渲染只不过是将第一次渲染到帧缓冲的结果原封不动地输出到画布上了。
接下来我们修改blurFragment的代码在其中添加高斯模糊的代码。代码如下
```
#ifdef GL_ES
precision highp float;
#endif
varying vec2 vUv;
uniform sampler2D tMap;
uniform int axis;
void main() {
vec4 color = texture2D(tMap, vUv);
// 高斯矩阵的权重值
float weight[5];
weight[0] = 0.227027;
weight[1] = 0.1945946;
weight[2] = 0.1216216;
weight[3] = 0.054054;
weight[4] = 0.016216;
// 每一个相邻像素的坐标间隔这里的512可以用实际的Canvas像素宽代替
float tex_offset = 1.0 / 512.0;
vec3 result = color.rgb;
result *= weight[0];
for(int i = 1; i < 5; ++i) {
float f = float(i);
if(axis == 0) { // x轴的高斯模糊
result += texture2D(tMap, vUv + vec2(tex_offset * f, 0.0)).rgb * weight[i];
result += texture2D(tMap, vUv - vec2(tex_offset * f, 0.0)).rgb * weight[i];
} else { // y轴的高斯模糊
result += texture2D(tMap, vUv + vec2(0.0, tex_offset * f)).rgb * weight[i];
result += texture2D(tMap, vUv - vec2(0.0, tex_offset * f)).rgb * weight[i];
}
}
gl_FragColor.rgb = result.rgb;
gl_FragColor.a = color.a;
}
```
因为高斯模糊有两个方向x和y方向所以我们至少要执行两次渲染一次对x轴另一次对y轴。如果想要达到更好的效果我们还可以执行多次渲染。
那我们就以分别对x轴和y轴执行2次渲染为例修改后的JavaScript代码如下
```
// 创建两个FBO对象交替使用
const fbo1 = renderer.createFBO();
const fbo2 = renderer.createFBO();
// 第一次,渲染原始图形
renderer.bindFBO(fbo1);
renderer.render();
// 第二次对x轴高斯模糊
renderer.useProgram(blurProgram);
renderer.setMeshData(program.meshData);
renderer.bindFBO(fbo2);
renderer.uniforms.tMap = fbo1.texture;
renderer.uniforms.axis = 0;
renderer.render();
// 第三次对y轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(fbo1);
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.render();
// 第四次对x轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(fbo2);
renderer.uniforms.tMap = fbo1.texture;
renderer.uniforms.axis = 0;
renderer.render();
// 第五次对y轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(null);
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.render();
```
在上面的代码中我们创建了两个FBO对象然后将它们交替使用。我们一共进行5次绘制先对原始图片执行1次渲染再进行4次后期处理。
这里啊我还要告诉你一个小技巧。本来啊在执行的这5次绘制中前四次都是输出到帧缓冲对象所以我们至少需要4个FBO对象。但是由于我们可以交替使用FBO对象也就是可以把用过的对象重复使用。因此无论需要绘制多少次我们都只要创建两个对象就可以也就节约了内存。
最终我们就能通过后期处理通道实现Blur滤镜给三角形图案加上模糊的效果了。渲染结果如下
![](https://static001.geekbang.org/resource/image/0d/91/0d14fea4328158fe9d32f09181a22f91.jpeg)
## 如何用后期处理通道实现辉光效果?
在上面这个例子中,我们是对所有元素进行高斯模糊的。那除此之外,我们还可以对特定元素进行高斯模糊。在可视化和游戏开发中,就常用这种技巧来实现元素的“辉光”效果。比如,下面这张图就是用辉光效果实现的浩瀚宇宙背景。
[![](https://static001.geekbang.org/resource/image/e9/4a/e9b5daeefdde2111cb955d2b91986b4a.jpeg "全局辉光效果图片来源于知乎H光大小姐")](https://zhuanlan.zhihu.com/p/44131797)
那类似这样的辉光效果该怎么实现呢我们可以在前面添加高斯模糊例子的基础上进行修改。首先我们给blurFragment加了一个关于亮度的滤镜将颜色亮度大于filter值的三角形过滤出来添加高斯模糊。修改后的代码如下。
```
uniform float filter;
void main() {
vec4 color = texture2D(tMap, vUv);
float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
brightness = step(filter, brightness);
// 高斯矩阵的权重值
float weight[5];
weight[0] = 0.227027;
weight[1] = 0.1945946;
weight[2] = 0.1216216;
weight[3] = 0.054054;
weight[4] = 0.016216;
// 每一个相邻像素的坐标间隔这里的512可以用实际的Canvas像素宽代替
float tex_offset = 1.0 / 512.0;
vec3 result = color.rgb;
result *= weight[0];
for(int i = 1; i < 5; ++i) {
float f = float(i);
if(axis == 0) { // x轴的高斯模糊
result += texture2D(tMap, vUv + vec2(tex_offset * f, 0.0)).rgb * weight[i];
result += texture2D(tMap, vUv - vec2(tex_offset * f, 0.0)).rgb * weight[i];
} else { // y轴的高斯模糊
result += texture2D(tMap, vUv + vec2(0.0, tex_offset * f)).rgb * weight[i];
result += texture2D(tMap, vUv - vec2(0.0, tex_offset * f)).rgb * weight[i];
}
}
gl_FragColor.rgb = brightness * result.rgb;
gl_FragColor.a = color.a;
}
```
然后我们再增加一个bloomFragment着色器用来做最后的效果混合。这里我们会用到一个叫做[**Tone Mapping**](https://zh.wikipedia.org/wiki/%E8%89%B2%E8%B0%83%E6%98%A0%E5%B0%84)(色调映射)的方法。这个方法就比较复杂了,在这里,你只要知道它可以将对比度过大的图像色调映射到合理的范围内就可以了,其他的内容你可以在课后看一下我给出的参考链接。
这个着色器的代码如下:
```
#ifdef GL_ES
precision highp float;
#endif
uniform sampler2D tMap;
uniform sampler2D tSource;
varying vec2 vUv;
void main() {
vec3 color = texture2D(tSource, vUv).rgb;
vec3 bloomColor = texture2D(tMap, vUv).rgb;
color += bloomColor;
// tone mapping
float exposure = 2.0;
float gamma = 1.3;
vec3 result = vec3(1.0) - exp(-color * exposure);
// also gamma correct while we're at it
if(length(bloomColor) > 0.0) {
result = pow(result, vec3(1.0 / gamma));
}
gl_FragColor.rgb = result;
gl_FragColor.a = 1.0;
}
```
最后我们修改JavaScript渲染的逻辑添加新的后期处理规则。这里我们要使用三个FBO对象因为第一个FBO对象在渲染原始图形之后还要在混合效果时使用后两个对象是用来交替使用完成高斯模糊的。最后我们再将原始图形和高斯模糊的结果进行效果混合就可以了。修改后的代码如下
```
// 创建三个FBO对象fbo1和fbo2交替使用
const fbo0 = renderer.createFBO();
const fbo1 = renderer.createFBO();
const fbo2 = renderer.createFBO();
// 第一次,渲染原始图形
renderer.bindFBO(fbo0);
renderer.render();
// 第二次对x轴高斯模糊
renderer.useProgram(blurProgram);
renderer.setMeshData(program.meshData);
renderer.bindFBO(fbo2);
renderer.uniforms.tMap = fbo0.texture;
renderer.uniforms.axis = 0;
renderer.uniforms.filter = 0.7;
renderer.render();
// 第三次对y轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(fbo1);
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.uniforms.filter = 0;
renderer.render();
// 第四次对x轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(fbo2);
renderer.uniforms.tMap = .texture;
renderer.uniforms.axis = 0;
renderer.uniforms.filter = 0;
renderer.render();
// 第五次对y轴高斯模糊
renderer.useProgram(blurProgram);
renderer.bindFBO(fbo1);
renderer.uniforms.tMap = fbo2.texture;
renderer.uniforms.axis = 1;
renderer.uniforms.filter = 0;
renderer.render();
// 第六次,叠加辉光
renderer.useProgram(bloomProgram);
renderer.setMeshData(program.meshData);
renderer.bindFBO(null);
renderer.uniforms.tSource = fbo0.texture;
renderer.uniforms.tMap = fbo1.texture;
renderer.uniforms.axis = 1;
renderer.uniforms.filter = 0;
renderer.render();
```
这样渲染之后,我就能让三角形图案中几个比较亮的三角形,产生一种微微发光的效果。渲染效果如下:
![](https://static001.geekbang.org/resource/image/69/c5/6905c7b0ff6b8b8d903e0eaeefcbfbc5.jpeg "局部辉光效果示意图")
这样,我们就实现了最终的局部辉光效果。实现它的关键,就是在高斯模糊原理的基础上,将局部高斯模糊的图像与原始图像叠加。
## 如何用后期处理通道实现烟雾效果?
除了模糊和辉光效果之外后期处理通道还经常用来实现烟雾效果。接下来我们就实现一个小圆的烟雾效果。具体的实现过程主要分为两步第一步和前面两个例子一样我们通过创建一个shader画出一个简单的圆。第二步我们对这个圆进行后期处理不过这次的处理方法就和实现辉光不同了。
下面,我们就一起来看。
首先我们创建一个简单的shader也就是使用距离场在画布上画一个圆。这个shader我们非常熟悉具体的代码如下
```
#ifdef GL_ES
precision highp float;
#endif
varying vec2 vUv;
void main() {
vec2 st = vUv - vec2(0.5);
float d = length(st);
gl_FragColor.rgb = vec3(1.0 - smoothstep(0.05, 0.055, d));
gl_FragColor.a = 1.0;
}
```
![](https://static001.geekbang.org/resource/image/72/77/72d1816d29d1401636e0099016f80477.jpeg)
接着我们修改一下shader代码增加uTime、tMap这两个变量代码如下所示。其中uTime用来控制图像随时间变化而tMap是我们用来做后期处理的变量。
```
#ifdef GL_ES
precision highp float;
#endif
varying vec2 vUv;
uniform sampler2D tMap;
uniform float uTime;
void main() {
vec3 smoke = vec3(0);
if(uTime <= 0.0) {
vec2 st = vUv - vec2(0.5);
float d = length(st);
smoke = vec3(1.0 - smoothstep(0.05, 0.055, d));
}
vec3 diffuse = texture2D(tMap, vUv).rgb;
gl_FragColor.rgb = diffuse + smoke;
gl_FragColor.a = 1.0;
}
```
然后我们依然创建两个FBO用它们交替进行绘制。最后我们把绘制的内容输出到画布上。这里我使用了一个if语句根据绘制过程判断初始绘制还是后续的叠加过程就能把着色器合并成一个。这样一来不管是输出到画布还是FBO我们使用同一个program就可以了。
```
const fbo = {
readFBO: renderer.createFBO(),
writeFBO: renderer.createFBO(),
get texture() {
return this.readFBO.texture;
},
swap() {
const tmp = this.writeFBO;
this.writeFBO = this.readFBO;
this.readFBO = tmp;
},
};
function update(t) {
// 输出到画布
renderer.bindFBO(null);
renderer.uniforms.uTime = t / 1000;
renderer.uniforms.tMap = fbo.texture;
renderer.render();
// 同时输出到FBO
renderer.bindFBO(fbo.writeFBO);
renderer.uniforms.tMap = fbo.texture;
// 交换读写缓冲以便下一次写入
fbo.swap();
renderer.render();
requestAnimationFrame(update);
}
update(0);
```
你会发现上面的代码执行以后输出的画面并没有什么变化。这是为什么呢因为我们第一次渲染时也就是当uTime为0的时候我们直接画了一个圆。而当我们从上一次绘制的纹理中获取信息重新渲染时因为每次获取的纹理图案都是不变的所以现在的画面依然是静止的圆。
如果我们想让这个图动起来比如说让它向上升那么我们只要在每次绘制的时候改变一下采样的y坐标就是每次从tMap取样时取当前纹理坐标稍微下方一点的像素点就可以了。具体的操作代码如下
```
void main() {
vec3 smoke = vec3(0);
if(uTime <= 0.0) {
vec2 st = vUv - vec2(0.5);
float d = length(st);
smoke = vec3(1.0 - smoothstep(0.05, 0.055, d));
}
vec2 st = vUv;
st.y -= 0.001;
vec3 diffuse = texture2D(tMap, st).rgb;
gl_FragColor.rgb = diffuse + smoke;
gl_FragColor.a = 1.0;
}
```
![](https://static001.geekbang.org/resource/image/ec/44/ec596ee38eabbbyy06061fe638a3df44.gif)
不过,由于纹理采样精度的问题,我们得到的上升圆还会有一个扩散的效果。不过这没有关系,它不影响我们接下来要实现的烟雾效果。
接下来我们需要构建一个烟雾的扩散模型也就是以某个像素位置以及周边像素的纹理颜色来计算新的颜色值。为了方便你理解我就以一个5\*5的画布为例来详细说说。假设这个画布只有中心五个位置的颜色是纯白1.0),周围都是黑色,如下图所示。
![](https://static001.geekbang.org/resource/image/8e/6b/8ed93106a13428492bd5bc2eyy8d676b.jpeg)
在这个扩散模型中每个格子到下一时刻的颜色变化量等于它周围四个格子的颜色值之和减去它自身颜色值的4倍乘以扩散系数。
假设扩散系数是常量0.1,那么第一轮每一格的颜色值我在表格上标出来了,如下图所示。
![](https://static001.geekbang.org/resource/image/6f/44/6f46f3354b470d217110764cfc3f0344.jpg)
可以看到,上图中有三种颜色的格子,分别是红色、蓝色和绿色。下面,我们直接来看颜色值的计算过程。
首先是中间红色的那个格子。因为它四周的格子颜色都是1.0所以它的颜色变化量是0.1 \* ((1.0 + 1.0 + 1.0 + 1.0) - 4 \* 1.0) = 0那么下一帧的颜色值还是1.0不变。
其次红格子周围的四个蓝色格子。它们下一帧的颜色变化量为0.1 \* ((1.0 + 0 + 0 + 0)- 4 \* 1.0) = -0.3那么它们下一帧的颜色值都要减去0.3 就是 0.7。
最后,在计算绿色格子下一帧的颜色值时,要分为两种情况。
第一种当要计算的绿色格子和两个蓝色格子相邻的时候颜色变化量为0.1 \* ((1.0 + 1.0 + 0 + 0) - 4 \* 0) = 0.2所以绿格子下一帧的颜色值变为0.2。
第二种当这个绿色格子只和一个蓝色格子相邻的时候颜色变化量为0.1那么绿格子下一帧的颜色值就变为0.1。
就这样我们把每一帧颜色按照这个规则不断迭代下去就能得到一个烟雾扩散效果了。那我们下一步就是把它实现到Shader中不过在Fragment Shader中添加扩散模型的时候为了让这个烟雾效果能上升得更明显我稍稍修改了一下扩散公式的权重让它向上的幅度比较大。
```
#ifdef GL_ES
precision highp float;
#endif
varying vec2 vUv;
uniform sampler2D tMap;
uniform float uTime;
void main() {
vec3 smoke = vec3(0);
if(uTime <= 0.0) {
vec2 st = vUv - vec2(0.5);
float d = length(st);
smoke = vec3(step(d, 0.05));
// smoke = vec3(1.0 - smoothstep(0.05, 0.055, d));
}
vec2 st = vUv;
float offset = 1.0 / 256.0;
vec3 diffuse = texture2D(tMap, st).rgb;
vec4 left = texture2D(tMap, st + vec2(-offset, 0.0));
vec4 right = texture2D(tMap, st + vec2(offset, 0.0));
vec4 up = texture2D(tMap, st + vec2(0.0, -offset));
vec4 down = texture2D(tMap, st + vec2(0.0, offset));
float diff = 8.0 * 0.016 * (
left.r +
right.r +
down.r +
2.0 * up.r -
5.0 * diffuse.r
);
gl_FragColor.rgb = (diffuse + diff) + smoke;
gl_FragColor.a = 1.0;
}
```
![](https://static001.geekbang.org/resource/image/81/27/816c8798dbe1bd0999c225fa068d8e27.gif)
你会发现,这个效果还不是特别真实。那为了达到更真实的烟雾效果,我们还可以在扩散函数上增加一些噪声,代码如下:
```
void main() {
vec3 smoke = vec3(0);
if(uTime <= 0.0) {
vec2 st = vUv - vec2(0.5);
float d = length(st);
smoke = vec3(step(d, 0.05));
// smoke = vec3(1.0 - smoothstep(0.05, 0.055, d));
}
vec2 st = vUv;
float offset = 1.0 / 256.0;
vec3 diffuse = texture2D(tMap, st).rgb;
vec4 left = texture2D(tMap, st + vec2(-offset, 0.0));
vec4 right = texture2D(tMap, st + vec2(offset, 0.0));
vec4 up = texture2D(tMap, st + vec2(0.0, -offset));
vec4 down = texture2D(tMap, st + vec2(0.0, offset));
float rand = noise(st + 5.0 * uTime);
float diff = 8.0 * 0.016 * (
(1.0 + rand) * left.r +
(1.0 - rand) * right.r +
down.r +
2.0 * up.r -
5.0 * diffuse.r
);
gl_FragColor.rgb = (diffuse + diff) + smoke;
gl_FragColor.a = 1.0;
}
```
这样,我们最终实现的效果看起来就会更真实一些:
![](https://static001.geekbang.org/resource/image/aa/1c/aa617ae27b63fc4687279a17298a721c.gif)
## 要点总结
今天我们学习了怎么在WebGL中使用后期处理通道来增强图像的视觉效果。我们讲了两个比较常用的效果一个是Blur滤镜以及基于它实现辉光效果,另一个是烟雾效果。
那后期处理通道实现这些效果的核心原理其实都是一样的都是把第一次渲染后的内容输出到帧缓冲对象FBO中然后把这个对象的内容作为纹理图片再进行下一次渲染这个渲染的过程可以重复若干次。最后我们再把结果输出到屏幕上就可以了。
到这里,视觉基础篇的内容,我们就全部讲完了。在这个模块里,我们围绕处理图像的细节,系统地学习了怎么表示颜色,怎么生成重复图案,怎么构造和使用滤镜处理图像,怎么进行纹理造型,还有怎么使用不同的坐标系绘图,怎么使用噪声和网格噪声生成复杂纹理、以及今天学习的怎么使用后期处理通道增强图像。我把核心的内容总结成了一张脑图,你可以借助它,来复习巩固这一模块的内容,查缺补漏。
![](https://static001.geekbang.org/resource/image/0b/48/0bcbeb9b391a1b7392b69f9ceaa66548.jpg)
## 小试牛刀
第一题,今天,我们实现的烟雾扩散效果还不够完善,也不够有趣,你能试着改进它,让它变得更有趣吗?你可以参考我给出的两个建议,也可以试试其他的效果。
1.给定一个向量,表示风向和风速,让烟雾随着这个风扩散,这个风可以随着时间慢慢变化(你可以使用前面学过的噪声来实现风的变化),看看能够做出什么效果。
2.尝试让烟雾跟随着鼠标移动轨迹扩散,达到类似下面的效果。
![](https://static001.geekbang.org/resource/image/f1/89/f180f7ca5c2yy5a9304b504893cf8289.gif)
如果你觉得难以实现,这里有[一篇文章](https://gamedevelopment.tutsplus.com/tutorials/how-to-write-a-smoke-shader--cms-25587)详细讲了烟雾生成的方法,你可以仔细研究一下。
第二题实际上后期处理通道可不止是实现Blur滤镜、辉光或者烟雾效果那么简单它还可以实现很多不同的功能。比如SpriteJS官网上那种类似于[探照灯]((https://spritejs.org/demo/#/shader_and_pass/pass))的效果就是用后期处理通道实现的。你可以用gl-renderer来实现这类效果吗
![](https://static001.geekbang.org/resource/image/5d/14/5df356fdb628efe1173089e90238e814.gif)
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课再见!
* * *
## 源码
完整的示例代码见[GitHub仓库](https://github.com/akira-cn/graphics/tree/master/pass)
## 推荐阅读
\[1\] [原始FBO对象的创建和使用方法](https://blog.csdn.net/xufeng0991/article/details/76736971)
\[2\] [How to Write a Smoke Shader](https://gamedevelopment.tutsplus.com/tutorials/how-to-write-a-smoke-shader--cms-25587)