腾讯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
细心的同窗必定会发现,文末的效果跟开篇的效果虽然看起来很像,可是彷佛还有一点点差距,没错,其实开篇的效果中不只仅只有一种滤镜,还叠加了电视线的滤镜,那么下一篇,咱们将会为你们讲解如何实现滤镜叠加,以及电视线滤镜的实现方法,敬请期待。