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

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

上個章節中,咱們主要從如何繪製圖片和如何添加濾鏡以及動態控制濾鏡效果兩方面入手,輔助以灰度濾鏡和對比度濾鏡的案例,讓你們對webgl濾鏡開發有了初步的認識,也見識到了glsl語言的一些特性。若是你以爲上面兩個濾鏡太簡單,不夠硬,那麼,本章節咱們將會以抖音故障特效爲例,爲你們詳細講解如何讓特效動起來,以及如何實現一個複雜特效。html

先貼出咱們的目標效果圖前端

picture

效果分析

1. 由靜轉動java

2. 圖片位移和rgb色彩通道分離web

3. 隨機片斷切割算法

由靜轉動

若是你小時候也玩過這樣的翻頁動畫,那麼這裏就很容易理解,動畫其實就是將一張張靜止的圖按順序和必定的時間間隔連續播放。那麼在webgl中,咱們其實只須要作兩點,首先,將時間戳做爲片斷着色器中的一個變量傳遞進去參與繪圖計算,而後,經過定時器(或相似功能)來不斷的傳入最新的時間戳而且重繪整個圖形。 how-to-animate

廢話很少說,咱們直接來上代碼,這裏咱們繼續在第一章的基礎上進行改造,若是你對webgl濾鏡尚未任何經驗,建議先看第一篇,《半小時輕鬆玩轉WebGL濾鏡技術系列(一)》編程

初始化着色器階段

javascript
// ...
// 片元着色器
FSHADER_SOURCE: `
precision highp float;
uniform sampler2D u_Sampler;
uniform float speed; // 控制速度
uniform float time; // 傳入時間
varying vec2 v_TexCoord;
void main () {
	// 經過速度和時間值來肯定最終的時間變量
    float cTime = floor(time * speed * 50.0);
    // gl_FragColor = texture2D(u_Sampler, v_TexCoord);
	// 這裏爲了測試,咱們選擇用sin函數把時間轉化爲0.0-1.0之間的隨機值
	gl_FragColor = vec4(vec3(sin(cTime)), 1.0);
}
`
// ...
複製代碼

繪製圖像

// 以當日早上0點爲基準
let todayDateObj = (() => {
    let oDate = new Date()
    oDate.setHours(0, 0, 0, 0)
    return oDate
})()
// 獲取time位置
let uTime = gl.getUniformLocation(gl.program, 'time')
// 獲取speed位置
let uSpeed = gl.getUniformLocation(gl.program, 'speed')
// 計算差值時間傳入
let diffTime = (new Date().getTime() - todayDateObj.getTime()) / 1000 // 以秒傳入,保留毫秒以實現速度變化
// 獲取speed位置
gl.uniform1f(uTime, diffTime)
// 傳入默認的speed,0.3
gl.uniform1f(uSpeed, 0.3)
// 設置canvas背景色
gl.clearColor(0, 0, 0, 0)
// 清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT) 
// 繪製 
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)  
// 定時循環傳入最新的時間以及從新繪製
let loop = () => {
    requestAnimationFrame(() => {
        diffTime = (new Date().getTime() - todayDateObj.getTime()) / 1000 // 以秒傳入,保留毫秒以實現速度變化
        gl.uniform1f(uTime, diffTime)
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
        loop()
    })
}
loop()
// 利用GUI生成控制speed的進度條
let speedController = gui.add({speed: 0.3}, 'speed', 0, 1, 0.01)
speedController.onChange(val => {
    gl.uniform1f(uSpeed, val)
})
複製代碼

若是一切順利,那麼你將會看到一幅閃瞎眼的畫面canvas

動態測試

這時若是咱們把右上角的speed一路拉滿到1.0那麼,畫面將會是這樣的bash

動態測試2

因爲轉爲了gif,因此效果可能不是很好,建議仍是代碼體驗微信

下面咱們來分析一下爲了實現這樣的效果咱們作了什麼

  1. 首先在着色器中,咱們用float cTime = floor(time * speed * 50.0);這樣的一段代碼肯定了最終的時間變量,那麼來分析一下,time咱們傳入是以秒爲單位的,可是保留了三位毫秒變量,若是speed是一個較小值,那麼speed * 50.0能夠看做是無限接近於1,那麼通過floor後time * speed * 50.0幾乎是等於time,也就是時間變量1000毫秒變一次,可是若是speed不斷增大,當speed爲0.2時,能夠認爲時間變量每100毫秒就要變一次,繼續增大,speed爲1.0時就是20毫秒變一次,能夠看出毫秒間隔隨着speed的增大不斷減小,也就實現了咱們對速度變化的要求,須要注意的是,即便speed繼續增大,若是間隔超過了requestAnimationFrame的間隔值也是無效的。gl_FragColor = vec4(vec3(abs(sin(cTime)), 1.0);這段函數其實就很好理解了,咱們經過abs(sin(cTime))將cTime轉化爲不斷變化的0.0-0.1區間的值,那麼也就實現了圖中的閃爍狀況

  2. 繪製圖像環節,咱們其實也主要是實現了兩個事情,一是初始化time和speed兩個變量,二是在requestAnimationFrame的時候傳入最新的時間而且重繪畫面,並提供UI組件可視化的變更speed參數

圖片位移和rgb色彩通道分離

將效果圖導入ps中逐幀分析,咱們發現,其實整個畫面在隨着時間不停地進行隨機位移,且每次位移還伴隨着色彩通道的變化,那麼咱們一個一個來看
  1. 位移

實現位移的方式並不複雜,一樣是在片斷着色器中

// ...
// 片元着色器
FSHADER_SOURCE: `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main () {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord - vec2(0.3));
}
`
// ...
複製代碼

對比原圖來看

位移測試0 位移測試1

咱們經過v_TexCoord - vec2(0.3)來使圖像產生了錯位,可是從圖中咱們也看出一個問題,當錯位過多時會使圖像超出畫面,因此要想視覺能夠接受,位移值不能過大。

  1. rgb色彩通道分離

實現色彩通道分離的方式並不難,只要咱們將位移的圖像rgb中任意一值與原圖疊加便可,一樣是片斷着色器

// ...
// 片元着色器
FSHADER_SOURCE: `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main () {
	// 原圖
	vec3 color = texture2D(u_Sampler, v_TexCoord).rgb;
	// 以通道r舉例
	color.r = texture2D(u_Sampler, v_TexCoord - vec2(0.1)).r;
    gl_FragColor = vec4(color, 1.0);
}
`
// ...
複製代碼

結果如圖

通道分離1
  1. 隨機

上面兩個效果中咱們發現其實位移和色彩通道分離實現起來都並不複雜,可是如何讓v_TexCoord - vec2(0.1)中的變量和color.r = texture2D(u_Sampler, v_TexCoord - vec2(0.1)).r;中的通道選擇可以隨着時間產生隨機變化是咱們要考慮的重點,那麼就須要用到隨機函數,這裏給你們介紹一種隨機函數。

float random (vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
複製代碼

上述是一個實現隨機的方法,你能夠很輕易的在網上各類複雜效果中看到這個方法,該方法接收一個vec2類型的變量,最終能夠生成一個均勻分佈在0.0-1.0區間的值,這裏咱們直接拿來使用,有興趣的同窗能夠私下了解一下隨機算法相關內容。下面是咱們簡單的演示

// ...
// 片元着色器
FSHADER_SOURCE: `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
float random (vec2 st) {
	return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
}
void main () {
    float rnd = random( v_TexCoord );
    gl_FragColor = vec4(vec3(rnd),1.0);
}
`
// ...
複製代碼

效果以下圖

隨機測試

分析完了三種效果,那麼咱們如何將他們結合起來呢,首先來看位移部分,要想實現必定區間內的隨機位移,那麼咱們就引入第三個變量offset來控制位移距離,經過offset來肯定位移的區間,再利用隨機函數產生區間內隨機變化的值來肯定最終位移值,而後是rgb通道分離,咱們能夠經過隨機函數產生一個0.0-1.0的隨機值,經過三等份來肯定rgb各自的區間,將上述疊加起來,理論上就可以實現咱們要的效果,那麼咱們來嘗試一下。

再次擴展繪圖函數

// 獲取offset位置
let uOffset = gl.getUniformLocation(gl.program, 'offset')
// 傳入默認的offset,0.3
gl.uniform1f(uOffset, 0.3)
// 設置canvas背景色
gl.clearColor(0, 0, 0, 0)
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 繪製
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) // 此處的4表明咱們將要繪製的圖像是正方形
// 利用GUI生成控制offset的進度條
let offsetController = gui.add({speed: 0.3}, 'offset', 0, 1, 0.01)
offsetController.onChange(val => {
    gl.uniform1f(uOffset, val)
})
複製代碼

着色器代碼

precision highp float;
uniform sampler2D u_Sampler;
uniform float offset;
uniform float speed;
uniform float time;
varying vec2 v_TexCoord;
// 隨機方法
float random (vec2 st) {
	return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
}
// 範圍隨機
float randomRange (vec2 standard ,float min, float max) {
	return min + random(standard) * (max - min);
}
void main () {
    // 原圖
    vec3 color = texture2D(u_Sampler, v_TexCoord).rgb;
    // 位移值放縮 0.0-0.5
    float maxOffset = offset / 6.0;
    // 時間計算
    float cTime = floor(time * speed * 50.0);
    vec2 texOffset = vec2(randomRange(vec2(cTime + maxOffset, 9999.0), -maxOffset, maxOffset), randomRange(vec2(cTime, 9999.0), -maxOffset, maxOffset));
    vec2 uvOff = fract(v_TexCoord + texOffset);
    // rgb隨機分離
    float rnd = random(vec2(cTime, 9999.0));
    if (rnd < 0.33){
    	color.r = texture2D(u_Sampler, uvOff).r;
    }else if (rnd < 0.66){
    	color.g = texture2D(u_Sampler, uvOff).g;
    } else{
    	color.b = texture2D(u_Sampler, uvOff).b;
    }
    gl_FragColor = vec4(color, 1.0);
}
複製代碼

效果以下,固然,你也能夠試着改變speed和offset來對效果進行調整

初步結果1

隨機片斷切割

最後,咱們還須要實現最後一個效果,就是隨機片斷的切割,若是你看過ps實現glitcher的效果,那應該很容易知道,切割效果的實現就是在圖片中切割出必定數量的寬100%,高度隨機的長條,而後使其發生橫向位移,那麼咱們來實現一下。

着色器代碼

precision highp float;
uniform sampler2D u_Sampler;
uniform float offset;
uniform float speed;
uniform float time;
varying vec2 v_TexCoord;
// 隨機方法
float random (vec2 st) {
	return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
}
// 範圍隨機
float randomRange (vec2 standard ,float min, float max) {
	return min + random(standard) * (max - min);
}
void main () {
    // 原圖
    vec3 color = texture2D(u_Sampler, v_TexCoord).rgb;
    // 時間計算
    float cTime = floor(time * speed * 50.0);
    // 切割圖片的最大位移值
    float maxSplitOffset = offset / 3.0;
    // 這裏咱們選擇切割10次
    for (float i = 0.0; i < 10.0; i += 1.0) { // 切割縱向座標 float sliceY = random(vec2(cTime + offset, 1999.0 + float(i))); // 切割高度 float sliceH = random(vec2(cTime + offset, 9999.0 + float(i))) * 0.25; // 計算隨機橫向偏移值 float hOffset = randomRange(vec2(cTime + offset, 9625.0 + float(i)), -maxSplitOffset, maxSplitOffset); // 計算最終座標 vec2 splitOff = v_TexCoord; splitOff.x += hOffset; splitOff = fract(splitOff); // 片斷若是在切割區間,就偏移區內圖像 if (v_TexCoord.y > sliceY && v_TexCoord.y < fract(sliceY+sliceH)) {
        	color = texture2D(u_Sampler, splitOff).rgb;
        }
    }
    gl_FragColor = vec4(color, 1.0);
}
複製代碼

效果以下,經過參數調整咱們能夠找到自認爲最理想的狀態

切割1

效果融合

當咱們分別實現了單獨的效果後,那確定是但願將他們融合起來啦,廢話很少說,直接上代碼和效果圖

着色器代碼

precision highp float;
uniform sampler2D u_Sampler;
uniform float offset;
uniform float speed;
uniform float time;
varying vec2 v_TexCoord;
float random (vec2 st) {
	return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
}
float randomRange (vec2 standard ,float min, float max) {
	return min + random(standard) * (max - min);
}
void main () {
  // 原圖
	vec3 color = texture2D(u_Sampler, v_TexCoord).rgb;
  // 位移值放縮 0.0-0.5
  float maxOffset = offset / 6.0;
  // 時間計算
  float cTime = floor(time * speed * 50.0);
  // 切割圖片的最大位移值
  float maxSplitOffset = offset / 2.0;
  // 這裏咱們選擇切割10次
  for (float i = 0.0; i < 10.0; i += 1.0) { // 切割縱向座標 float sliceY = random(vec2(cTime + offset, 1999.0 + float(i))); // 切割高度 float sliceH = random(vec2(cTime + offset, 9999.0 + float(i))) * 0.25; // 計算隨機橫向偏移值 float hOffset = randomRange(vec2(cTime + offset, 9625.0 + float(i)), -maxSplitOffset, maxSplitOffset); // 計算最終座標 vec2 splitOff = v_TexCoord; splitOff.x += hOffset; splitOff = fract(splitOff); // 片斷若是在切割區間,就偏移區內圖像 if (v_TexCoord.y > sliceY && v_TexCoord.y < fract(sliceY+sliceH)) {
        color = texture2D(u_Sampler, splitOff).rgb;
      }
  }
  vec2 texOffset = vec2(randomRange(vec2(cTime + maxOffset, 9999.0), -maxOffset, maxOffset), randomRange(vec2(cTime, 9999.0), -maxOffset, maxOffset));
  vec2 uvOff = fract(v_TexCoord + texOffset);
  // rgb隨機分離
  float rnd = random(vec2(cTime, 9999.0));
  if (rnd < 0.33){
    color.r = texture2D(u_Sampler, uvOff).r;
  }else if (rnd < 0.66){
    color.g = texture2D(u_Sampler, uvOff).g;
  } else{
    color.b = texture2D(u_Sampler, uvOff).b;
  }
  gl_FragColor = vec4(color, 1.0);
}
複製代碼

效果以下

最終

總結

當你實現了文章最後的效果時,相信你已經可以自行去改寫一些效果了,其實,本文的特效還有更大的擴展空間,例如分割線的區間數量,是否也能夠經過傳參數來控制呢,包括縱向切割高度,也是同樣,甚至你想再增長一些額外的效果,也都是沒問題的,固然,前提是你對glsl足夠熟悉和熟練。

本章內容的主題雖然是故障特效,但在實踐過程當中其實也用到了一些通用的特效處理方法,例如隨機函數的運用,偏移的運用等等。另外,文中也大量運用了一些glsl的經常使用基本類型(vec2,vec3,vec4)及內置函數(fract),要想快速實現濾鏡效果,對於glsl基本的語法必定要作到爛熟於心,看到函數即能想到效果。這裏爲你們推薦幾個學習途徑,首先是《WebGL編程指南》,可以幫你快速創建基礎,《The Book of Shaders》主要講解着色器的相關運用,《Shadertoy》主要集合了一些特效案例,webgl的出現爲視覺交互和用戶體驗帶來了無限的可能,咱們身處用戶體驗的最前端,更應快速吸取快速掌握。

one more thing

細心的同窗必定會發現,文末的效果跟開篇的效果雖然看起來很像,可是彷佛還有一點點差距,沒錯,其實開篇的效果中不只僅只有一種濾鏡,還疊加了電視線的濾鏡,那麼下一篇,咱們將會爲你們講解如何實現濾鏡疊加,以及電視線濾鏡的實現方法,敬請期待。

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

相關文章
相關標籤/搜索