滤镜技术一直在我们的生活中有着广泛的应用, 不管是各式各样的美图软件, 还是最近大热的短视频 App, 其中都将滤镜效果作为产品的重要卖点, 有些甚至成为了产品的标志, 比如本文的封面是不是让你突然想到了某款短视频 App, 无疑, 滤镜效果有着重要的商业价值, 那么我们能否将这种价值引入 web 平台呢, 答案是肯定的, 接下来我们将通过系列文章为大家逐步讲解如何利用 WebGL 开发滤镜效果.
要想做到封面中的效果, 我们需要掌握大量的 WebGL 知识和图像算法, 作为系列的第一篇, 我希望通过本文先让大家对滤镜有一个初步的认识, 能够做到以下两点.
1. 理解如何绘制图片
2. 理解如何添加滤镜及动态控制滤镜效果
如何绘制图片
注意: 以下流程中的辅助函数均会在文末给出
加载想要绘制的图片文件
- let imageSrc = '...' // 待加载图片路径
- let oImage = await loadImage(imageSrc) // 辅助函数见文末
创建 canvas, 获取 WebGL 绘图上下文
- html
- <canvas id="canvas"></canvas>
- JavaScript
- oCanvas.width = oImage.width // 初始化 canvas 宽高
- oCanvas.height = oImage.height
- let gl = getWebGLContext(oCanvas) // 辅助函数见文末
初始化着色器
- // 顶点着色器
- 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) // 辅助函数见文末
配置图像纹理
initTexture(gl, oImage) // 辅助函数见文末
绘制图像
- // 设置 canvas 背景色
- gl.clearColor(0, 0, 0, 0)
- // 清空 & lt;canvas>
- gl.clear(gl.COLOR_BUFFER_BIT)
- // 绘制
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) // 此处的 4 代表我们将要绘制的图像是正方形
恭喜你, 到了这一步, 你应该已经看到图片被绘制在了 canvas 中
如何添加滤镜及动态控制滤镜效果
以下的例子我们都用该图像作为原始图像
添加滤镜
添加滤镜的关键点在于 shader(着色器), 在片元着色器中我们可以看到这样一段代码
- ...
- void main () {
- gl_FragColor = texture2D(u_Sampler, v_TexCoord);
- }
- ...
这里 texture2D(u_Sampler, v_TexCoord) 代表着图像解析后的 rgba 值, 当我们直接赋值给 gl_FragColor 时则原图输出, 那么, 滤镜的核心也就在这里, 我们需要对其进行改写, 下面我们先从最简单的灰度滤镜效果做例子, 从 rgb 色转为灰度色的算法我们可以轻易从网上找出, 这里取其中一种 Gray = R0.299 + G0.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);
- }
- ...
效果如下
动态控制滤镜
生活中我们的滤镜大多数并不会像灰度滤镜这么简单, 举个例子, 我们经常看到图像处理 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;, 而这行就是对控制对比度的参数声明, 直接刷新后页面会报错, 因为我们并未传入相应的对比度值, 那么, 应该如何传入呢, 方法如下.
在 initShader 后的任意步骤处添加如下代码
let u_Contrast = gl.getUniformLocation(gl.program, 'u_Contrast') // 字符串名称要与 shader 中的变量名一致
在 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)
- })
效果如下
总结
如果你有耐心看完以上部分并实践了其中的代码, 那么到了此处, 你应该已经能够试着对一些图片进行较为简单的滤镜处理, 但是应该还有几个疑惑
即使看完了代码并实践了代码, 但却并不能完全理解其中每段代码的意义. 这类同学建议先学习 WebGL 基础和 GLSL 基础, 对相应的 API, 变量类型等有所掌握.
此处只举例了灰度滤镜和对比度滤镜, 与封面上的效果相去甚远.介于篇幅, 系列的第一篇更多的是入门, 至于滤镜的效果, 其实当你看过了灰度滤镜和对比度滤镜, 就会发现其实不同的滤镜都只是在片段着色器中对颜色进行不同的算法处理, 有心的同学可以在 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)
- }
来源: https://juejin.im/post/5bcf5163f265da0a972e5c26