webgl实现故障效果

1,933 阅读1分钟

故障效果

此篇文章介绍如何使用webgl实现一个下面这样子的故障效果,因为屏幕录制的原因这个gif图片看起来不是那么清晰,感兴趣的同学可以看完文章后动手实践一下,亲自看一下效果。

第一步:将图片渲染出来

为了达到最后的这种故障效果,我们先要将图片渲染出来,也就是实现基本的shader功能(此部分不会着重介绍)

1.1 导入相关配置文件

<script src="./lib/cuon-matrix.js"></script>
<script src="./lib/cuon-utils.js"></script>
<script src="./lib/webgl-debug.js"></script>
<script src="./lib/webgl-utils.js"></script>

这四个文件是关于webgl的方法封装,数学运算以及debug的文件。

你可以在这里获取到它们

1.2 顶点着色器和片元着色器

// 顶点着色器
let VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute vec2 a_TexCoord;
    varying vec2 v_TexCoord;

    void main() {
      gl_Position = a_Position;
      v_TexCoord = a_TexCoord;
    }`

// 片元着色器
let FSHADER_SOURCE = `
    precision mediump float;
    uniform sampler2D u_Sampler;
    uniform float time;
    varying vec2 v_TexCoord;

    void main() {
        gl_FragColor = texture2D(u_Sampler, v_TexCoord);
    }`

在顶点着色器中我们定义了三个变量,a_Position是用作处理顶点坐标,a_TexCoord和v_TexCoord用来向片元着色器传递数据。

而在片元着色器中我们使用v_TexCoord接收顶点着色器传过来的数据,使用u_Sampler处理纹理数据,预留了一个time变量用作后面的动效变量。

1.3 初始化并渲染纹理

let canvas = this.$refs.webgl //获取canvas
		
let gl = getWebGLContext(canvas); //获取canvas的上下文

initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);  //初始化shader,引入的js文件中函数

let n = this.initVertexBuffers(gl);  //初始化buffer,执行函数可在全部代码中查看

this.inirTextures(gl, n);  //初始化textures,执行函数可在全部代码中查看

let u_time = gl.getUniformLocation(gl.program, "time");  //获取片元着色器中时间的存储地址

let newTime = 0.1;  //时间变量的起始值
let draw = function(){
    newTime = newTime + 0.1;  //时间变量增值
    gl.uniform1f(u_time, newTime);  //进行赋值
    gl.clearColor(0.0, 0.0, 0.0, 1.0); //清空背景
    gl.clear(gl.COLOR_BUFFER_BIT);  //清空buffer
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);  //开始绘制
    requestAnimationFrame(draw);  //不断的执行动画
}

draw() //开始执行

上面的代码中主要涉及到了初始化wegl的操作,其中的initShaders是1.1引入的js文件中的函数,而initVertexBuffers和inirTextures函数是我们自定义的,具体的内容可以在最后面的全部代码中看到,并且我们这里向片元着色器不断的传入newTime变量的值,用来作为变换参数。 当上面的操作完成以后我们就可以得到纹理正确渲染的样式了:

第二步:确定我们要使用的变换函数

我们知道在webgl中我们一个函数要操作的是整个图片上面的所有的像素,所以我们要选定一个合适的函数来整体操作图片,那么为了让你不那么烧脑,我们来一点点的理解:

我们首先使用dot函数对两个vec2做了点积操作,其函数图像就是上面这样,需要注意这不是一条直线,只是因为y值变化较小而已。

那么为了让y轴的变化明显,我们在外面添加了一个sin函数,这样函数图像就有一点意思了,但是密度还是不够。

为了增加函数的密度,我们在sin函数的后面乘上了43758.0(为什么是这个值?后面调出来的),但是新的问题来了,函数的值域过大。

为了减少值域的范围我们使用fract函数,将函数的值域控制在在-1.0到1.0之间。

最后我们再对函数做了调整,那么就成了这个样子是不是感觉只是很多随机的点,其实不是的,放大它的一部分来看一下。

经过上面一点点的添加,我们就得到了一个函数公式,接下来就是将这个函数公式应用在片元着色器中了。

第三步:片元着色器使用函数

在片元着色器中我们先定义两个函数来做第二步中的变换函数。

float rand(vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.0, 78.0))) * 43758.0) * 2.0 - 1.0;
}
float offset(float blocks, vec2 uv) {
    return rand(vec2(time, floor(uv.y * blocks)));
}

在offset函数中我们使用time这个变量来作为第二步函数中的第一个x值,使用输入uv的y值也就是纵坐标来作为第二步函数中的第二个x值(因为我们要做横向的故障效果),上面的函数定义完成以后就是将这个函数应用在texture中了。

void main() {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);

    gl_FragColor.r = texture2D(u_Sampler, v_TexCoord + vec2(offset(16.0, v_TexCoord) * 0.03, 0.0)).r;	
    gl_FragColor.g = texture2D(u_Sampler, v_TexCoord + vec2(offset(8.0, v_TexCoord) * 0.03 * 0.07, 0.0)).g;
    gl_FragColor.b = texture2D(u_Sampler, v_TexCoord + vec2(offset(8.0, v_TexCoord) * 0.03, 0.0)).b;
}

本身在main函数中只是存在gl_FragColor赋值的操作,但是在上面的代码中我们添加了对其RGB三个通道的变化操作,经过上面的一步步后,我们就可以在自己的浏览器中看到效果了。

在最后为了方便你查看效果我这边赋上所有的代码,但是需要你自己去引入1.1中的4个js文件,需要注意我这边的代码是写在vue中的。

<template>
  <div>
    <canvas id="glcanvas" ref="webgl" width="750" height="1334"></canvas>
  </div>
</template>

<script>
/* eslint-disable */
import testImg from './static/img/img1.jpeg' //引入自己的本地图片文件
export default {
  props: {
    msg: String
  },
  mounted() {
    let VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute vec2 a_TexCoord;
        varying vec2 v_TexCoord;

        void main() {
          gl_Position = a_Position;
          v_TexCoord = a_TexCoord;
        }`
    let FSHADER_SOURCE = `
        precision mediump float;
        uniform sampler2D u_Sampler;
        uniform float time;
        varying vec2 v_TexCoord;

        float rand(vec2 co){
            return fract(sin(dot(co.xy ,vec2(12.0, 78.0))) * 43758.0) * 2.0 - 1.0;
        }

        float offset(float blocks, vec2 uv) {
          return rand(vec2(time, floor(uv.y * blocks)));
        }
        void main() {
            gl_FragColor = texture2D(u_Sampler, v_TexCoord);

            gl_FragColor.r = texture2D(u_Sampler, v_TexCoord + vec2(offset(16.0, v_TexCoord) * 0.03, 0.0)).r;	
            gl_FragColor.g = texture2D(u_Sampler, v_TexCoord + vec2(offset(8.0, v_TexCoord) * 0.03 * 0.07, 0.0)).g;
            gl_FragColor.b = texture2D(u_Sampler, v_TexCoord + vec2(offset(8.0, v_TexCoord) * 0.03, 0.0)).b;
        }`

    let canvas = this.$refs.webgl
		
    let gl = getWebGLContext(canvas);
    
    initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
    
    let n = this.initVertexBuffers(gl);
    
    this.inirTextures(gl, n);

    let u_time = gl.getUniformLocation(gl.program, "time");

    let newTime = 0.1;
    let draw = function(){
        newTime = newTime + 0.1;
        gl.uniform1f(u_time, newTime);
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        requestAnimationFrame(draw);
    }

    draw()
    
  },
  methods: {
    initVertexBuffers(gl){
        var verticesTexCoords = new Float32Array([
            -1.0, 1.0, 0.0, 1.0,
            -1.0, -1.0, 0.0, 0.0,
            1.0, 1.0, 1.0, 1.0,
            1.0, -1.0, 1.0, 0.0,
        ]);
        var n = 4;
        var vertexCoordBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexCoordBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
        
        var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
        
        var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
        gl.enableVertexAttribArray(a_Position);
        
        var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
        gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
        gl.enableVertexAttribArray(a_TexCoord)
        return n;
    },
    inirTextures(gl, n){
        var texture = gl.createTexture();
        var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
        var image = new Image();
        image.onload = ()=>{this.loadTexture(gl, n, texture, u_Sampler, image);};
        image.crossOrigin = "anonymous";
        image.src = testImg
        return true;
    },
    loadTexture(gl, n, texture, u_Sampler, image){
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.uniform1i(u_Sampler, 0);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
    }
  }
}
</script>