腾讯DeepOcean原创文章:dopro.io/webgl-filte…javascript
滤镜技术一直在咱们的生活中有着普遍的应用,不论是各式各样的美图软件,仍是最近大热的短视频app,其中都将滤镜效果做为产品的重要卖点,有些甚至成为了产品的标志,好比本文的封面是否是让你忽然想到了某款短视频app,无疑,滤镜效果有着重要的商业价值,那么咱们可否将这种价值引入web平台呢,答案是确定的,接下来咱们将经过系列文章为你们逐步讲解如何利用WebGL开发滤镜效果。
html
1. 理解如何绘制图片前端
2. 理解如何添加滤镜及动态控制滤镜效果java
let imageSrc = '...' // 待加载图片路径 let oImage = await loadImage(imageSrc) // 辅助函数见文末
htmlweb
<canvas id="canvas"></canvas>
算法
javascriptcanvas
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) // 辅助函数见文末
复制代码
initVertexBuffers(gl) // 辅助函数见文末
api
initTexture(gl, oImage) // 辅助函数见文末
bash
// 设置canvas背景色
gl.clearColor(0, 0, 0, 0)
// 清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) // 此处的4表明咱们将要绘制的图像是正方形
复制代码
恭喜你,到了这一步,你应该已经看到图片被绘制在了canvas中微信
如下的例子咱们都用该图像做为原始图像
...
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);
}
...
复制代码
效果以下
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;
,而这行就是对控制对比度的参数声明,直接刷新后页面会报错,由于咱们并未传入相应的对比度值,那么,应该如何传入呢,方法以下。
let u_Contrast = gl.getUniformLocation(gl.program, 'u_Contrast') // 字符串名称要与shader中的变量名一致
复制代码
// 此处用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)
})
复制代码
效果以下
除去以上两点,其实滤镜方面还有视频滤镜,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)
}
复制代码