騰訊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)
}
複製代碼