半小时轻松玩转WebGL滤镜技术系列(一)

滤镜技术一直在我们的生活中有着广泛的应用,不管是各式各样的美图软件,还是最近大热的短视频app,其中都将滤镜效果作为产品的重要卖点,有些甚至成为了产品的标志,比如本文的封面是不是让你突然想到了某款短视频app,无疑,滤镜效果有着重要的商业价值,那么我们能否将这种价值引入web平台呢,答案是肯定的,接下来我们将通过系列文章为大家逐步讲解如何利用WebGL开发滤镜效果。

要想做到封面中的效果,我们需要掌握大量的WebGL知识和图像算法,作为系列的第一篇,我希望通过本文先让大家对滤镜有一个初步的认识,能够做到以下两点。

1. 理解如何绘制图片

2. 理解如何添加滤镜及动态控制滤镜效果

如何绘制图片

注意:以下流程中的辅助函数均会在文末给出

1. 加载想要绘制的图片文件

let imageSrc = '...' // 待加载图片路径
let oImage = await loadImage(imageSrc) // 辅助函数见文末

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

html

<canvas id="canvas"></canvas>

javascript

let oCanvas = document.getElementById('canvas') 
oCanvas.width = oImage.width // 初始化canvas宽高
oCanvas.height = oImage.height 
let gl = getWebGLContext(oCanvas) // 辅助函数见文末

3. 初始化着色器

let fragmentSource = {
    // 顶点着色器
    VSHADER_SOURCE: `
    attribute vec4 a_Position;
    attribute vec2 a_TexCoord;
    varying vec2 v_TexCoord;
    void main () {
        gl_Position = a_Position;
        v_TexCoord = a_TexCoord;
    }
    `,
    // 片元着色器
   FSHADER_SOURCE: `
    precision highp float;
    uniform sampler2D u_Sampler;
    varying vec2 v_TexCoord;
    void main () {
    	gl_FragColor = texture2D(u_Sampler, v_TexCoord);
    }
    `
}
initShaders(gl,fragmentSource.VSHADER_SOURCE,fragmentSource.FSHADER_SOURCE) // 辅助函数见文末

4. 设置顶点位置

initVertexBuffers(gl) // 辅助函数见文末

5. 配置图像纹理

initTexture(gl, oImage) // 辅助函数见文末

6. 绘制图像

// 设置canvas背景色
gl.clearColor(0, 0, 0, 0)
// 清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) // 此处的4代表我们将要绘制的图像是正方形

恭喜你,到了这一步,你应该已经看到图片被绘制在了canvas中

如何添加滤镜及动态控制滤镜效果

demo-original

以下的例子我们都用该图像作为原始图像

添加滤镜

添加滤镜的关键点在于shader(着色器),在片元着色器中我们可以看到这样一段代码

...
void main () {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
...

这里texture2D(u_Sampler, v_TexCoord)代表着图像解析后的rgba值,当我们直接赋值给gl_FragColor时则原图输出,那么,滤镜的核心也就在这里,我们需要对其进行改写,下面我们先从最简单的灰度滤镜效果做例子,从rgb色转为灰度色的算法我们可以轻易从网上找出,这里取其中一种Gray = R*0.299 + G*0.587 + B*0.114,实际运用如下

...
void main () {
    vec4 color = texture2D(u_Sampler, v_TexCoord);
    float gray = 0.2989*color.r+0.5870*color.g+0.1140*color.b;
    gl_FragColor = vec4(gray,gray,gray , color.a);
}
...

效果如下

demo-grey

动态控制滤镜

生活中我们的滤镜大多数并不会像灰度滤镜这么简单,举个例子,我们经常看到图像处理app中对比度的调整都是一个滑动条,这个时候我们就需要动态的传入参数来控制显示效果,注意下面对比度的着色器代码

`
precision highp float;
uniform sampler2D u_Sampler;
uniform float u_Contrast;
varying vec2 v_TexCoord;
void main () {
    vec4 textureColor = texture2D(u_Sampler, v_TexCoord);
    if (u_Contrast > 0.0) {
        textureColor.rgb = (textureColor.rgb - 0.5) / (1.0 - u_Contrast) + 0.5;
    } else {
        textureColor.rgb = (textureColor.rgb - 0.5) * (1.0 + u_Contrast) + 0.5;
    }
    gl_FragColor = textureColor;
}
`

可以看到,相比于灰度处理中,除了main()方法中算法不一样,而且多出来了一行uniform float u_Contrast;,而这行就是对控制对比度的参数声明,直接刷新后页面会报错,因为我们并未传入相应的对比度值,那么,应该如何传入呢,方法如下。
1. 在initShader后的任意步骤处添加如下代码

let u_Contrast = gl.getUniformLocation(gl.program, 'u_Contrast') // 字符串名称要与shader中的变量名一致

2. 在slider或者其他控制对比度的组件中将值传入并重新绘制图形

// 此处用dat.gui组件做变量控制
import * as dat from 'dat.gui'
const gui = new dat.GUI()
let contrastController = gui.add({u_Contrast: 0}, 'u_Contrast', -1, 1, 0.01)
contrastController.onChange(val => {
    gl.uniform1f(u_Contrast, val)
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
})

效果如下

demo-contrast

总结

如果你有耐心看完以上部分并实践了其中的代码,那么到了此处,你应该已经能够试着对一些图片进行较为简单的滤镜处理,但是应该还有几个疑惑

1. 即使看完了代码并实践了代码,但却并不能完全理解其中每段代码的意义。这类同学建议先学习WebGL基础和GLSL基础,对相应的api,变量类型等有所掌握。
2. 此处只举例了灰度滤镜和对比度滤镜,与封面上的效果相去甚远。介于篇幅,系列的第一篇更多的是入门,至于滤镜的效果,其实当你看过了灰度滤镜和对比度滤镜,就会发现其实不同的滤镜都只是在片段着色器中对颜色进行不同的算法处理,有心的同学可以在google或百度中找到较多的着色器代码进行实践,当然,如果效果过于定制化,则还是需要自己来写,所以,对于glsl语言的掌握也尤其重要。

除去以上两点,其实滤镜方面还有视频滤镜,web camera滤镜,多图像纹理,多滤镜混合等等一些特性没有讲到,下篇文章,我们将会重点教大家实现封面中的抖音风格滤镜,敬请期待!

PS:辅助函数
loadImage.js

export default function (imgSrc) {
    return new Promise((resolve, reject) => {
	let oImage = new Image()
	oImage.onload = () => {
	    resolve(oImage)
	}
	oImage.onerror = () => {
	    reject(new Error('load error'))
	}
	oImage.src = imgSrc
    })
}

getWebGLContext.js

export default function (canvas) {
    let gl;
    let glContextNames = ['webgl', 'experimental-webgl'];
    for (let i = 0; i < glContextNames.length; i ++) {
      try {
        gl = canvas.getContext(glContextNames[i],{
        });
      } catch (e) {
      }
    }
    if (gl) {
      gl.clearColor(0, 0, 0, 0)
      gl.clear(gl.COLOR_BUFFER_BIT)
    }
    return gl
}

initShaders.js

let loadShader = function (gl, type, source) {
    // 创建着色器对象
    let shader = gl.createShader(type);
    if (shader == null) {
        console.log('无法创建着色器');
        return null;
    }
    // 设置着色器源代码
    gl.shaderSource(shader, source);
    // 编译着色器
    gl.compileShader(shader);
    // 检查着色器的编译状态
    let compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
        let error = gl.getShaderInfoLog(shader);
        console.log('Failed to compile shader: ' + error);
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

let createProgram = function (gl, vshader, fshader) {
    // 创建着色器对象
    let vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    let fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
        return null;
    }
    // 创建程序对象
    let program = gl.createProgram();
    if (!program) {
        return null;
    }
    // 为程序对象分配顶点着色器和片元着色器
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    // 连接着色器
    gl.linkProgram(program);
    // 检查连接
    let linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
        let error = gl.getProgramInfoLog(program);
        console.log('无法连接程序对象: ' + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
    }
    return program;
}
export default function (gl, vshader, fshader) {
    var program = createProgram(gl, vshader, fshader);
    if (!program) {
        console.log('无法创建程序对象');
        return false;
    }

    gl.useProgram(program);
    gl.program = program;

    return true;
}

initVertexBuffers.js

export default function (gl) {
    // 顶点着色器的坐标与纹理坐标的映射
    const vertices = new Float32Array([
	-1, 1, 0.0, 1.0,
	-1, -1, 0.0, 0.0,
	1, 1, 1.0, 1.0,
	1, -1, 1.0, 0.0
    ])
    // 创建缓冲区对象
    let vertexBuffer = gl.createBuffer()
    // 绑定buffer到缓冲对象上
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    // 向缓冲对象写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
    const FSIZE = Float32Array.BYTES_PER_ELEMENT
    // 将缓冲区对象分配给a_Position变量
    let a_Position = gl.getAttribLocation(gl.program, 'a_Position')
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0)
    // 连接a_Position变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position)
    // 将缓冲区对象分配给a_TexCoord变量
    let a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord')
    gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2)
    // 使用缓冲数据建立程序代码到着色器代码的联系
    gl.enableVertexAttribArray(a_TexCoord)
}

initTexture.js

export default function (gl, image) {
    let texture = gl.createTexture()
    let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
    // 对纹理图像进行y轴翻转
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    // 开启0号纹理单元
    gl.activeTexture(gl.TEXTURE0)
    // 绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture)
    // 配置纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    // 配置纹理图像
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
    //将0号纹理传递给着色器的取样器变量
    gl.uniform1i(u_Sampler, 0)
}

发表评论