半小时轻松玩转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等领域相关的原创优质技术文章:

相关文章
相关标签/搜索