半小時輕鬆玩轉WebGL濾鏡技術系列(一)

騰訊DeepOcean原創文章:dopro.io/webgl-filte…javascript

濾鏡技術一直在咱們的生活中有着普遍的應用,不論是各式各樣的美圖軟件,仍是最近大熱的短視頻app,其中都將濾鏡效果做爲產品的重要賣點,有些甚至成爲了產品的標誌,好比本文的封面是否是讓你忽然想到了某款短視頻app,無疑,濾鏡效果有着重要的商業價值,那麼咱們可否將這種價值引入web平臺呢,答案是確定的,接下來咱們將經過系列文章爲你們逐步講解如何利用WebGL開發濾鏡效果。

要想作到封面中的效果,咱們須要掌握大量的WebGL知識和圖像算法,做爲系列的第一篇,我但願經過本文先讓你們對濾鏡有一個初步的認識,可以作到如下兩點。html

1. 理解如何繪製圖片前端

2. 理解如何添加濾鏡及動態控制濾鏡效果java

如何繪製圖片

注意:如下流程中的輔助函數均會在文末給出
  1. 加載想要繪製的圖片文件
let imageSrc = '...' // 待加載圖片路徑
let oImage = await loadImage(imageSrc) // 輔助函數見文末
  1. 建立canvas,獲取WebGL繪圖上下文

htmlweb

<canvas id="canvas"></canvas>算法

javascriptcanvas

oCanvas.width = oImage.width // 初始化canvas寬高
oCanvas.height = oImage.height 
let gl = getWebGLContext(oCanvas) // 輔助函數見文末
複製代碼
  1. 初始化着色器
// 頂點着色器
    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) // 輔助函數見文末api

  1. 配置圖像紋理

initTexture(gl, oImage) // 輔助函數見文末bash

  1. 繪製圖像
// 設置canvas背景色
gl.clearColor(0, 0, 0, 0)
// 清空&lt;canvas&gt;
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 = 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);
}
...
複製代碼

效果以下

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 &gt; 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中的變量名一致
複製代碼
  1. 在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 =&gt; {
    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) =&gt; {
	let oImage = new Image()
	oImage.onload = () =&gt; {
	    resolve(oImage)
	}
	oImage.onerror = () =&gt; {
	    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 &lt; 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)
}
複製代碼

歡迎關注"騰訊DeepOcean"微信公衆號,每週爲你推送前端、人工智能、SEO/ASO等領域相關的原創優質技術文章:

相關文章
相關標籤/搜索