半小時輕鬆玩轉WebGL濾鏡技術系列(二)
上個章節中,我們主要從如何繪製圖片和如何新增濾鏡以及動態控制濾鏡效果兩方面入手,輔助以灰度濾鏡和對比度濾鏡的案例,讓大家對webgl濾鏡開發有了初步的認識,也見識到了glsl語言的一些特性。如果你覺得上面兩個濾鏡太簡單,不夠硬,那麼,本章節我們將會以抖音故障特效為例,為大家詳細講解如何讓特效動起來,以及如何實現一個複雜特效。
先貼出我們的目標效果圖
效果分析
1. 由靜轉動
2. 圖片位移和rgb色彩通道分離
3. 隨機片段切割
由靜轉動
如果你小時候也玩過這樣的翻頁動畫,那麼這裡就很容易理解,動畫其實就是將一張張靜止的圖按順序和一定的時間間隔連續播放。那麼在webgl中,我們其實只需要做兩點,首先,將時間戳作為片段著色器中的一個變數傳遞進去參與繪圖計算,然後,通過定時器(或類似功能)來不斷的傳入最新的時間戳並且重繪整個圖形。
廢話不多說,我們直接來上程式碼,這裡我們繼續在第一章的基礎上進行改造,如果你對webgl濾鏡還沒有任何經驗,建議先看第一篇, ofollow,noindex">《半小時輕鬆玩轉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) })
如果一切順利,那麼你將會看到一幅閃瞎眼的畫面
這時如果我們把右上角的speed一路拉滿到1.0那麼,畫面將會是這樣的
由於轉為了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)); } ` // ...
對比原圖來看


我們通過`v_TexCoord – vec2(0.3)`來使影象產生了錯位,但是從圖中我們也看出一個問題,當錯位過多時會使影象超出畫面,所以要想視覺可以接受,位移值不能過大。
2. 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); } ` // ...
結果如圖
3. 隨機
上面兩個效果中我們發現其實位移和色彩通道分離實現起來都並不複雜,但是如何讓`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來對效果進行調整
隨機片段切割
最後,我們還需要實現最後一個效果,就是隨機片段的切割,如果你看過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); }
效果如下,通過引數調整我們可以找到自認為最理想的狀態
效果融合
當我們分別實現了單獨的效果後,那肯定是希望將他們融合起來啦,廢話不多說,直接上程式碼和效果圖
著色器程式碼
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
細心的同學一定會發現,文末的效果跟開篇的效果雖然看起來很像,但是似乎還有一點點差距,沒錯,其實開篇的效果中不僅僅只有一種濾鏡,還疊加了電視線的濾鏡,那麼下一篇,我們將會為大家講解如何實現濾鏡疊加,以及電視線濾鏡的實現方法,敬請期待。