说明
【跟月影学可视化】学习笔记。
为什么需要后期处理通道?
由于 GPU 是并行渲染的,所以在着色器的执行中,每个像素的着色都是同时进行的,彼此独立的,不能共享信息,就不能获得某一个像素坐标周围坐标点的颜色信息,也不能获得要渲染图像的全局信息。
什么是后期处理通道?
所谓后期处理通道,是指将渲染出来的图像作为纹理输入给新着色器处理,是一种二次加工的手段。可以从纹理中获取任意 uv 坐标下的像素信息,也就相当于可以获取任意位置的像素信息。
后期处理通道的一般过程
- 将数据送入缓冲区
- 然后执行 WebGLProgram
- 将输出的结果再作为纹理,送入另一个 WebGLProgram 进行处理
- 最后输出结果
如何用后期处理通道实现 Blur 滤镜?
下面实现一个绘制随机三角形图案的着色器,然后使用后期处理通道对它进行高斯模糊。
这是没有进行高斯模糊的效果:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>如何用后期处理通道实现 Blur 滤镜</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #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; } `; // 通过blurFragment,能将第一次渲染后生成的纹理 tMap 内容给显示出来。 const 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; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); 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], ], }, ]); // 高斯模糊有两个方向,下面分别对 x 轴和 y 轴执行 2 次渲染为例 const blurProgram = renderer.compileSync(blurFragment, vertex); // 创建两个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(); </script> </body> </html>
高斯模糊之后,效果如下:
如何用后期处理通道实现辉光效果?
实现它的关键,就是在高斯模糊原理的基础上,将局部高斯模糊的图像与原始图像叠加,就实现了最终的局部辉光效果。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>如何用后期处理通道实现辉光效果?</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #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; } `; /** * 通过blurFragment,能将第一次渲染后生成的纹理 tMap 内容给显示出来。 * 给 blurFragment 加了一个关于亮度的滤镜,将颜色亮度大于 filter 值的三角形过滤出来添加高斯模糊。 * */ const blurFragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; uniform sampler2D tMap; uniform int axis; 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 着色器,用来做最后的效果混合。 const bloomFragment = ` #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; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); 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], ], }, ]); // 高斯模糊有两个方向,下面分别对 x 轴和 y 轴执行 2 次渲染为例 const blurProgram = renderer.compileSync(blurFragment, vertex); const bloomProgram = renderer.compileSync(bloomFragment, vertex); // 创建三个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 = fbo1.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(); </script> </body> </html>
如何用后期处理通道实现烟雾效果?
先构建一个烟雾的扩散模型:每个格子到下一时刻的颜色变化量,等于它周围四个格子的颜色值之和减去它自身颜色值的 4 倍,乘以扩散系数。
假设扩散系数是常量 0.1,第一轮每一格的颜色值如下:
按照这个规则不断迭代下去,修改扩散公式的权重以及加入噪声就能得到一个简单的烟雾扩散效果。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>17.如何用后期处理通道实现烟雾效果?</title> <style> canvas { border: 1px dashed salmon; } </style> </head> <body> <canvas width="512" height="512"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const vertex = ` attribute vec2 a_vertexPosition; attribute vec2 uv; varying vec2 vUv; void main() { gl_PointSize = 1.0; vUv = uv; gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec2 vUv; uniform sampler2D tMap; uniform float uTime; vec2 random2(vec2 st){ st = vec2( dot(st,vec2(127.1,311.7)), dot(st,vec2(269.5,183.3)) ); return -1.0 + 2.0 * fract(sin(st) * 43758.5453123); } // Gradient Noise by Inigo Quilez - iq/2013 // https://www.shadertoy.com/view/XdXGW8 float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); vec2 u = f * f * (3.0 - 2.0 * f); return mix( mix( dot( random2(i + vec2(0.0,0.0) ), f - vec2(0.0,0.0) ), dot( random2(i + vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x), mix( dot( random2(i + vec2(0.0,1.0) ), f - vec2(0.0,1.0) ), dot( random2(i + vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y ); } 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; } `; const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const program = renderer.compileSync(fragment, vertex); 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], ], }, ]); // 创建两个fbo对象 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(); renderer.bindFBO(fbo.writeFBO); renderer.uniforms.tMap = fbo.texture; fbo.swap(); renderer.render(); requestAnimationFrame(update); } update(0); </script> </body> </html>