半小时轻松玩转WebGL滤镜技术系列(三)多滤镜叠加

beepress-weixin-zhihu-jianshu-toutiao-plugin-1556550472

在上个章节中,我们以抖音故障特效为例,为大家详细讲解了如何让特效动起来,以及如何实现一个较复杂特效。那么本章我们继续为大家讲解webgl滤镜技术中的多滤镜叠加技术,这一技术常应用于各大美颜软件,滤镜相机中,我们可以通过多层滤镜混合来实现更酷炫的效果,如下图中就混合了抖音故障特效和电影特效。


半小时轻松玩转WebGL滤镜技术系列(三)多滤镜叠加

帧缓冲

在开始正式实现该效果之前,我们先要向大家普及一个概念,帧缓冲,它便是实现多滤镜叠加的核心技术点。

帧缓冲简介

帧缓冲(Framebuffer Object),简称 FBO,它是用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲的结合,之前的操作我们都是在默认帧缓冲的渲染缓冲中进行,也就是我们的窗口。除此之外,OpenGL允许我们定义我们自己的帧缓冲,也就是说我们能够定义我们自己的颜色缓冲,甚至是深度缓冲和模板缓冲(本次滤镜场景暂不涉及后两种缓冲类型),用来作为绘制的载体,当在自己的 FBO 上绘制好了之后,可以再把绘制内容显示到屏幕上,实现更多酷炫的效果。

帧缓冲的基本应用

  1. // 创建帧缓冲区

  2. var fbo = gl.createFramebuffer();

  3. // 绑定帧缓冲区

  4. gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  5. // 创建纹理对象并设置其参数

  6. var texture = gl.createTexture()

  7. // 绑定纹理对象

  8. gl.bindTexture(gl.TEXTURE_2D, texture)

  9. // 设置纹理参数

  10. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)

  11. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)

  12. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

  13. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)

  14. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

  15. // 关联纹理对象

  16. gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)

这里需要注意,上述代码并不是帧缓冲的所有api实现,这里只是针对2d图像渲染场景,帧缓冲的应用场景十分丰富,有兴趣的同学可以单独查阅资料了解。

多滤镜叠加

介绍完了帧缓冲,那么我们就开始正式讲解它的多滤镜叠加中的具体应用,对于webgl来说,多滤镜叠加其实就是利用帧缓冲离屏渲染的能力,通过循环多个shader程序对纹理进行反复渲染,最终达到所有shader渲染完成后再上屏的操作。

接下来的讲解会大量与系列文章第一篇重合,主要差别会在流程中标记*。这里建议大家,如果你对webgl还没有一定的认识,建议可以先阅读系列文章的第一篇和第二篇,更有利于本章的理解。

半小时轻松玩转WebGL滤镜技术系列(一)如何绘制图像以及基本滤镜实现介绍

半小时轻松玩转WebGL滤镜技术系列(二)以抖音故障特效为例解析复杂特效

前期准备

首先我们将要叠加的滤镜数据准备好

start.js

  1. import vshader from '@/assets/js/filter/vshader.glsl'

  2. import glitcher from '@/assets/js/filter/glitcher.glsl'

  3. import film from '@/assets/js/filter/film.glsl'

  4. let filterArr = [

  5. {

  6. name: 'glitcher',

  7. zh: '故障特效',

  8. glsl: {

  9. vshader: vshader,

  10. fshader: glitcher

  11. }

  12. },

  13. {

  14. name: 'film',

  15. zh: '电影特效',

  16. glsl: {

  17. vshader: vshader,

  18. fshader: film

  19. }

  20. }

  21. ]

加载纹理图片

半小时轻松玩转WebGL滤镜技术系列(三)多滤镜叠加

start.js

  1. let imageSrc = '...' // 待加载图片路径

  2. let oImage = await loadImage(imageSrc) // 辅助函数见文末

创建canvas,获取WebGL绘图上下文

index.html

  1. <canvas id="canvas"></canvas>

start.js

  1. let oCanvas = document.getElementById('canvas')

  2. oCanvas.width = oImage.width // 初始化canvas宽高

  3. oCanvas.height = oImage.height

  4. let gl = getWebGLContext(oCanvas) // 辅助函数见文末

*初始化着色器

start.js

  1. import initShaders from '@/assets/js/utils/initShaders'

  2. // ...

  3. filterArr.forEach(item => {

  4. initShaders(gl, item.glsl)

  5. })

initShader.js

  1. let loadShader = function (gl, type, source) {

  2. var shader = gl.createShader(type)

  3. if (shader == null) {

  4. console.log('无法创建着色器')

  5. return null

  6. }

  7. gl.shaderSource(shader, source)

  8. gl.compileShader(shader)

  9. var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS)

  10. if (!compiled) {

  11. var error = gl.getShaderInfoLog(shader)

  12. console.log('Failed to compile shader: ' + error)

  13. gl.deleteShader(shader)

  14. return null

  15. }

  16. return shader

  17. }

  18. let createProgram = function (gl, vshader, fshader) {

  19. var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader)

  20. var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader)

  21. if (!vertexShader || !fragmentShader) {

  22. return null

  23. }

  24. var program = gl.createProgram()

  25. if (!program) {

  26. return null

  27. }

  28. gl.attachShader(program, vertexShader)

  29. gl.attachShader(program, fragmentShader)

  30. gl.linkProgram(program)

  31. var linked = gl.getProgramParameter(program, gl.LINK_STATUS)

  32. if (!linked) {

  33. var error = gl.getProgramInfoLog(program)

  34. console.log('无法连接程序对象: ' + error)

  35. gl.deleteProgram(program)

  36. gl.deleteShader(fragmentShader)

  37. gl.deleteShader(vertexShader)

  38. return null

  39. }

  40. return program

  41. }

  42. // 重点看这里

  43. export default function (gl, shaderObj) {

  44. let program = shaderObj.program

  45. if (!program) {

  46. program = createProgram(gl, shaderObj.vshader, shaderObj.fshader)

  47. }

  48. if (!program) {

  49. console.log('无法创建程序对象')

  50. return false

  51. }

  52. shaderObj.program = program;

  53. return true;

  54. }

创建shader程序的逻辑与之前单一shader程序相同,这里我们重点关注最后几行,由于在第一章中我们是单一的shader程序,所以我们会直接使用shader程序并保存在gl的属性中

  1. gl.useProgram(program)

  2. gl.program = program

而在多shader中,我们的使用操作要滞后,program也要保存在相应的shader对象中

  1. // gl.useProgram(program);

  2. shaderObj.program = program;

*设置顶点位置

start.js

  1. import initVertexBuffers from '@/assets/js/utils/initVertexBuffers'

  2. // ...

  3. initVertexBuffers.common()

  4. filterArr.forEach(item => {

  5. initShaders(gl, item.glsl)

  6. initVertexBuffers.single(item.glsl.program)

  7. })

initVertexBuffers.js

  1. export default {

  2. common (gl) {

  3. const vertices = new Float32Array([

  4. -1, 1, 0.0, 1.0,

  5. -1, -1, 0.0, 0.0,

  6. 1, 1, 1.0, 1.0,

  7. 1, -1, 1.0, 0.0

  8. ])

  9. let vertexBuffer = gl.createBuffer()

  10. gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)

  11. gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)

  12. },

  13. single (gl, program) {

  14. let a_Position = gl.getAttribLocation(program, 'a_Position')

  15. const FSIZE = Float32Array.BYTES_PER_ELEMENT

  16. gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)

  17. gl.enableVertexAttribArray(a_Position)

  18. }

  19. }

在设置顶点位置的流程中,我们可以看到,原有的一个函数被拆分为了两个,这是因为各个shader程序需要独立的传入顶点坐标,这里需要注意,我们只传入了顶点坐标,并没有传入纹理坐标,这部分是因为我们将纹理坐标的计算融合进了顶点着色器中,文末会给出具体的着色器代码。

*配置图像纹理

start.js

  1. import initVertexBuffers from '@/assets/js/utils/initVertexBuffers'

  2. // ...

  3. initVertexBuffers.common()

  4. filterArr.forEach(item => {

  5. initShaders(gl, item.glsl)

  6. initVertexBuffers.single(item.glsl.program)

  7. })

  8. // 加载图像纹理

  9. let imgTexture = initTexture(gl, image1)

initTexture.js

  1. export default function (gl, image) {

  2. let texture = gl.createTexture()

  3. // let u_Sampler = gl.getUniformLocation(program, 'texture')

  4. // 对纹理图像进行y轴翻转

  5. gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)

  6. // 开启0号纹理单元

  7. // gl.activeTexture(gl.TEXTURE0)

  8. // 绑定纹理对象

  9. gl.bindTexture(gl.TEXTURE_2D, texture)

  10. // 配置纹理参数

  11. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)

  12. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

  13. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)

  14. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

  15. // 配置纹理图像

  16. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)

  17. // 将0号纹理传递给着色器的取样器变量

  18. // gl.uniform1i(u_Sampler, 0)

  19. return texture

  20. }

需要注意的是这里我们删除了跟具体shader程序相关的部分,具体原因在下一步说明

*绘制图像

start.js

  1. let imgTexture = initTexture(gl, image1)

  2. let tempFramebuffers = []

  3. let currentFramebufferIndex = 0

  4. const initFramebufferObject = (gl, width, height) => {

  5. var fbo = gl.createFramebuffer();

  6. gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  7. var texture = gl.createTexture()

  8. gl.bindTexture(gl.TEXTURE_2D, texture)

  9. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)

  10. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)

  11. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

  12. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)

  13. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

  14. gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)

  15. return {

  16. fbo,

  17. texture

  18. }

  19. }

  20. const getTempFramebuffer = function (index) {

  21. tempFramebuffers[index] = tempFramebuffers[index] || initFramebufferObject(image1.width, image1.height)

  22. return tempFramebuffers[index]

  23. }

  24. const drawScene = (program, index) => {

  25. let source = null

  26. let target = null

  27. // 第一次渲染时使用图片纹理

  28. if (index === 0) {

  29. // 注意这里是在配置图像纹理中注释掉的步骤

  30. let u_Sampler = gl.getUniformLocation(program, 'texture')

  31. gl.activeTexture(gl.TEXTURE0)

  32. gl.uniform1i(u_Sampler, 0)

  33. source = imgTexture

  34. } else {

  35. // 后续渲染都使用上一次在缓冲中存储的纹理

  36. source = getTempFramebuffer(currentFramebufferIndex).texture

  37. }

  38. if (index === filterArr.length - 1) {

  39. target = null

  40. } else {

  41. currentFramebufferIndex = (currentFramebufferIndex + 1) % 2

  42. target = getTempFramebuffer(currentFramebufferIndex).fbo

  43. }

  44. gl.bindTexture(gl.TEXTURE_2D, source)

  45. gl.bindFramebuffer(gl.FRAMEBUFFER, target)

  46. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)

  47. }

  48. filterArr.forEach((item, index) => {

  49. // 每次渲染都使用当前的shader程序

  50. gl.useProgram(item.glsl.program)

  51. drawScene(item.glsl.program, index)

  52. })

最终实现效果如下

半小时轻松玩转WebGL滤镜技术系列(三)多滤镜叠加

这里我们可以看出已经跟单shader有较大的区别,首先我们循环当前滤镜数组,对每一个shader对象进行一次绘制,在 drawScene函数中,当我们进行第一次渲染的时候,我们将 source设置为原始的图片纹理,将target设置为新创建的帧缓冲对象,这里需要注意的是,我们在配置图像纹理步骤中省略掉的传递纹理的步骤挪到了这里,在第一次渲染的时候激活了0号图像纹理并且将0号纹理传递给着色器的取样器变量(其实在单一图像纹理时我们可以不主动声明,默认会取0号图像纹理并激活传入),在后续渲染时,source取上一次渲染好的帧缓冲纹理,而target进则是设置为存储好的fbo对象,这样进行反复的shader程序处理,直到最后一次,我们将target设置为null,代表我们要将反复处理的帧缓冲当做图像纹理绘制到屏幕上,由此便实现了多滤镜叠加的效果。

总结

在本章,我们通过帧缓冲的特性来实现了多滤镜叠加的效果,同样,通过多滤镜叠加这一特效,也使我们对帧缓冲这一特性有了更高的认识。当然,多滤镜叠加只是帧缓冲技术点的一个落地场景,更多的运用,更多的可能,我们或在探索,或在积累。技术始终还是要落地于实际应用场景,而强大的技术则能够为我们的应用带来无限可能。在后续,我们依然会为大家带来更加丰富实用的webgl相关知识,更多精彩,敬请期待。

最后附上我们本次使用的shader着色器代码

顶点着色器

  1. attribute vec4 a_Position;

  2. varying vec2 texCoord;

  3. void main () {

  4. gl_Position = vec4(vec2(a_Position),0.0,1.0);

  5. texCoord = vec2(0.5, 0.5) * (vec2(a_Position)+vec2(1.0, 1.0));

  6. }

注意:纹理坐标我们直接通过顶点坐标进行计算

片段着色器    

  • glitcher.glsl

  1. precision mediump float;

  2. uniform sampler2D texture;

  3. varying vec2 texCoord;

  4. //uniform float offset;

  5. //uniform float speed;

  6. //uniform float time;


  7. float offset = 0.071;

  8. float speed = 0.0;

  9. float time = 0.0;

  10. float random (vec2 st) {

  11. return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);

  12. }

  13. float randomRange (vec2 standard ,float min, float max) {

  14. return min + random(standard) * (max - min);

  15. }

  16. void main () {

  17. vec3 color = texture2D(texture, texCoord).rgb;

  18. float maxOffset = offset / 6.0;

  19. float cTime = floor(time * speed * 50.0);

  20. float maxSplitOffset = offset / 2.0;

  21. for (float i = 0.0; i < 10.0; i += 1.0) {

  22. float sliceY = random(vec2(cTime + offset, 1999.0 + float(i)));

  23. float sliceH = random(vec2(cTime + offset, 9999.0 + float(i))) * 0.25;

  24. float hOffset = randomRange(vec2(cTime + offset, 9625.0 + float(i)), -maxSplitOffset, maxSplitOffset);

  25. vec2 splitOff = texCoord;

  26. splitOff.x += hOffset;

  27. splitOff = fract(splitOff);

  28. if (texCoord.y > sliceY && texCoord.y < fract(sliceY+sliceH)) {

  29. color = texture2D(texture, splitOff).rgb;

  30. }

  31. }

  32. vec2 texOffset = vec2(randomRange(vec2(cTime + maxOffset, 9999.0), -maxOffset, maxOffset), randomRange(vec2(cTime, 9999.0), -maxOffset, maxOffset));

  33. vec2 uvOff = fract(texCoord + texOffset);

  34. // 注意这里相比第二章做了简单的优化,保证时间为0时也会有值进入随机

  35. float rnd = random(vec2(cTime + offset, 9999.0));

  36. if (rnd < 0.33){

  37. color.r = texture2D(texture, uvOff).r;

  38. }else if (rnd < 0.66){

  39. color.g = texture2D(texture, uvOff).g;

  40. } else{

  41. color.b = texture2D(texture, uvOff).b;

  42. }

  43. gl_FragColor = vec4(color, 1.0);

  44. }

故障特效在第二章中已经有较为详细的讲解,这里需要注意在最后计算随机值得时候,为了避免时间为0的情况,我们加上offset便宜量得到一个较小值参与随机计算。

  • film.glsl

  1. precision mediump float;

  2. uniform sampler2D texture;

  3. varying vec2 texCoord;


  4. //uniform float time;

  5. //uniform float nIntensity; // [0, 2]

  6. //uniform float sIntensity;// [0, 2]

  7. //uniform float sCount; // [0, 1000]

  8. //uniform bool grayscale;


  9. bool grayscale = false;

  10. float time = 1000.0;

  11. float nIntensity = 0.4;

  12. float sIntensity = 0.9;

  13. float sCount = 800.0;


  14. void main() {

  15. vec4 cTextureScreen = texture2D( texture, texCoord );

  16. float x = texCoord.x * texCoord.y * time * 1000.0;

  17. x = mod( x, 13.0 ) * mod( x, 123.0 );

  18. float dx = mod( x, 0.01 );

  19. vec3 cResult = cTextureScreen.rgb + cTextureScreen.rgb * clamp( 0.1 + dx * 10.0, 0.0, 1.0 );

  20. vec2 sc = vec2( sin( texCoord.y * sCount ), cos( texCoord.y * sCount ) );

  21. cResult += cTextureScreen.rgb * vec3( sc.x, sc.y, sc.x ) * sIntensity;

  22. cResult = cTextureScreen.rgb + clamp( nIntensity, 0.0,1.0 ) * ( cResult - cTextureScreen.rgb );

  23. if(grayscale) {

  24. cResult = vec3( cResult.r * 0.3 + cResult.g * 0.59 + cResult.b * 0.11 );

  25. }

  26. gl_FragColor = vec4( cResult, cTextureScreen.a );

  27. }

注: 该部分参考部分开源shader

电影效果主要包含的就是噪点和扫描线的生成,通过上一节对于故障特效的分析,同学们可以试着分析一下电影效果其中的奥妙。

注意:两个shader中均有注释的部分,如果你已经看过系列前两章,应该会比较清楚,注释掉的就是动态传参的部分,我们在这里为了突出重点直接写死了其值。当你实现了本章的效果后,你可能会想,借助第二章的原理我能不能让图片动起来呢,那么这些参数就派上用场了,这里也给大家留下一些深入的空间,如何动态给多滤镜程序传参呢?这里给大家一个小tip,注意传参时区分program.

始发于微信公众号: 腾讯DeepOcean

发表评论