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

騰訊DeepOcean發表於2018-10-23

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

半小時輕鬆玩轉WebGL濾鏡技術系列(一)
濾鏡技術一直在我們的生活中有著廣泛的應用,不管是各式各樣的美圖軟體,還是最近大熱的短視訊app,其中都將濾鏡效果作為產品的重要賣點,有些甚至成為了產品的標誌,比如本文的封面是不是讓你突然想到了某款短視訊app,無疑,濾鏡效果有著重要的商業價值,那麼我們能否將這種價值引入web平臺呢,答案是肯定的,接下來我們將通過系列文章為大家逐步講解如何利用WebGL開發濾鏡效果。

要想做到封面中的效果,我們需要掌握大量的WebGL知識和影像演算法,作為系列的第一篇,我希望通過本文先讓大家對濾鏡有一個初步的認識,能夠做到以下兩點。

1. 理解如何繪製圖片

2. 理解如何新增濾鏡及動態控制濾鏡效果

如何繪製圖片

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

html

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

javascript

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) // 輔助函式見文末

  1. 配置影像紋理

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

  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等領域相關的原創優質技術文章:

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

相關文章