騰訊DeepOcean原創文章:dopro.io/webgl-filte…javascript
上個章節中,咱們主要從如何繪製圖片和如何添加濾鏡以及動態控制濾鏡效果兩方面入手,輔助以灰度濾鏡和對比度濾鏡的案例,讓你們對webgl濾鏡開發有了初步的認識,也見識到了glsl語言的一些特性。若是你以爲上面兩個濾鏡太簡單,不夠硬,那麼,本章節咱們將會以抖音故障特效爲例,爲你們詳細講解如何讓特效動起來,以及如何實現一個複雜特效。html
先貼出咱們的目標效果圖前端
1. 由靜轉動java
2. 圖片位移和rgb色彩通道分離web
3. 隨機片斷切割算法
廢話很少說,咱們直接來上代碼,這裏咱們繼續在第一章的基礎上進行改造,若是你對webgl濾鏡尚未任何經驗,建議先看第一篇,《半小時輕鬆玩轉WebGL濾鏡技術系列(一)》編程
// ...
// 片元着色器
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
因爲轉爲了gif,因此效果可能不是很好,建議仍是代碼體驗微信
下面咱們來分析一下爲了實現這樣的效果咱們作了什麼
首先在着色器中,咱們用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區間的值,那麼也就實現了圖中的閃爍狀況
繪製圖像環節,咱們其實也主要是實現了兩個事情,一是初始化time和speed兩個變量,二是在requestAnimationFrame
的時候傳入最新的時間而且重繪畫面,並提供UI組件可視化的變更speed參數
實現位移的方式並不複雜,一樣是在片斷着色器中
// ...
// 片元着色器
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)
來使圖像產生了錯位,可是從圖中咱們也看出一個問題,當錯位過多時會使圖像超出畫面,因此要想視覺能夠接受,位移值不能過大。
實現色彩通道分離的方式並不難,只要咱們將位移的圖像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);
}
`
// ...
複製代碼
結果如圖
上面兩個效果中咱們發現其實位移和色彩通道分離實現起來都並不複雜,可是如何讓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來對效果進行調整
着色器代碼
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);
}
複製代碼
效果以下
《The Book of Shaders》主要講解着色器的相關運用,《Shadertoy》主要集合了一些特效案例,webgl的出現爲視覺交互和用戶體驗帶來了無限的可能,咱們身處用戶體驗的最前端,更應快速吸取快速掌握。
one more thing
細心的同窗必定會發現,文末的效果跟開篇的效果雖然看起來很像,可是彷佛還有一點點差距,沒錯,其實開篇的效果中不只僅只有一種濾鏡,還疊加了電視線的濾鏡,那麼下一篇,咱們將會爲你們講解如何實現濾鏡疊加,以及電視線濾鏡的實現方法,敬請期待。