WebGL——粒子化图片

4,677 阅读4分钟

项目地址

思路

最近在学习webGL原生,给的3d项目又不是很多,就想到了其实2d的项目也可以拿webGL练习,在接到这个项目之后,脑子里蹦出个要做出粒子的效果,大致思路已经有了,就是获取图片的有效像素作为point的xy坐标,z轴的不同来实现3d粒子效果,顺着思路往下想,透视投影的话那我生成xy坐标还得和z做一个透视得逆转变,使他们在透视投影得时候还是看到的是正常的图片,但发现有两个问题,一是我这个数学浆糊来回倒腾逆矩阵太迷糊,二就是在透视投影中z轴不同尺寸不同,意思就是远小近大,导致每个粒子得大小也就不同,所以我换了种思路,简单易懂那就是正交投影切透视投影

想到正交投影得特性完全就可以满足我把粒子排列成图案,又想到透视投影可以把粒子打散,我得思路就有了:

| 生成坐标 —— 正交透视切换

效果:

实现

1.生成坐标

这里的想法是运用canvas得getImageData 读取有效像素(alpha!=0),生成x,y以及随机得z坐标,并结合像素颜色生成buffer供webGL引用。

getImageData 也有一些小技巧,例如可以实现碰撞检测

代码如下:

// image 已经准备好了的
const data = [];
const canvas = <HTMLCanvasElement>document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image,0,0);
// 获取像素数据
const imageData = ctx.getImageData(0,0,canvas.width,canvas.height);
//以中心点定位
const offsetX = imageData.width/2;
const offsetY = imageData.height/2;
for(let x = 0;x<imageData.width;x++){
    for(let y = 0;y<imageData.height;y++){
        const r = x*4+y*imageData.width*4;
        //过滤无效像素 减小数据量
        if(imageData.data[r+3]){
            //buffer中记录了坐标轴和颜色值
            data.push(
                x-offsetX,                  //x
                -(y-offsetY),               //y
                (Math.random()-.5)*offsetX, //z
                imageData.data[r+0]/255,    //r
                imageData.data[r+1]/255,    //g
                imageData.data[r+2]/255,    //b
                imageData.data[r+3]/255,    //a
            );
        }
    }
}

接下来用这些数据传给webGL(常规的创建shader,program不做赘述,参照手撸3d贺卡):

    //获取attribute
    aPosition = gl.getAttribLocation(program,'aPosition');
    gl.enableVertexAttribArray(aPosition);
    aColor = gl.getAttribLocation(program,'aColor');
    gl.enableVertexAttribArray(aColor);
    
    //用作世界坐标转成齐次坐标
    uViewPort = gl.getUniformLocation(program,'uViewPort');
    gl.uniform2f(uViewPort,window.innerWidth,window.innerHeight);
    
    //为gl.ARRAY_BUFFER写入上述数据data,以及设置attribute读取数据
    gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);
    gl.vertexAttribPointer(aPosition,3,gl.FLOAT,false,7*4,0);
    gl.vertexAttribPointer(aColor,3,gl.FLOAT,false,7*4,3*4);
    gl.drawArrays(
        gl.POINTS,
        0,
        data.length/7   //除以7是因为每个顶点我写的七个数据(xyz,rgba)
    );

这时的着色器很简单(因为是gl.POINTS模式所以要设置gl_PointSize):

//___________________顶点着色器_________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform vec2 uViewPort;
    varying vec4 vColor;
    void main(void) {
        gl_Position = vec4(aPosition/uViewPort.xyx, 1);
        gl_PointSize = 1.0;         //须设置
        vColor = aColor;
    }
//___________________片元着色器_________________________
    varying vec4 vColor;
    void main(void) {
        gl_FragColor = vColor;
    }

到这里的效果其实就是一张静态图片的样子了,但是本质上是每个像素其实是3d的一个点,不做截图了。

2.正交矩阵切透视矩阵

在1中我没有写代码意义上的正交矩阵,其实不写的话本身就可以看成一个 x,y,z从(-1,1)的正交投影,所以没写,在这一步中要用到透视投影,所以为上述代码做了些调整,把上述代码中的uViewPort替换成矩阵即可,剩下的就是对矩阵做修改,修改如下:

//___________________TypeScript_________________________
    import * as Matrix from 'gl-mat4';
    const ortho = Matrix.create();
    Matrix.ortho(
        ortho,
        -canvas.width/2,
        canvas.width/2,
        -canvas.height/2,
        canvas.height/2,
        -canvas.width/2,
        canvas.width/2
    );
    worldViewProjection = gl.getUniformLocation(program,'worldViewProjection');
    gl.uniformMatrix4fv(worldViewProjection,false,ortho);
    
//___________________顶点着色器_________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform mat4 worldViewProjection;
    varying vec4 vColor;
    void main(void) {
        gl_Position = worldViewProjection*vec4(aPosition, 1);
        gl_PointSize = 1.0;         //须设置
        vColor = aColor;
    }

根据openGL透视除法的特性,我在每次渲染的时候只需要动态调整ortho[11]就能调整透视程度,当且仅当ortho[11]==0的时候此投影为正交投影。

render();
function render(time){
    //更新ortho
    ortho[11] = (Math.cos(time/1000)+1)/50;
    gl.uniformMatrix4fv(worldViewProjection,false,ortho);
    //清空
    gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
    //渲染
    gl.drawArrays(gl.POINTS,0,data.length/7);
    requestAnimationFrame(render);
}

效果如下

剩下的就自由发挥了,因为z轴是随机的,还有投影矩阵中[11]也是有规律的变化,在顶点着色器中可以根据这两个参数进行相应的简单计算。

我的计算是:

//___________________顶点着色器_________________________
    precision mediump float;
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform mat4 worldViewProjection;
    varying vec4 vColor;
    varying float vAlpha;
    void main(void) {
        gl_Position = worldViewProjection * vec4(aPosition, 1.0);
        vAlpha = 1.-pow(worldViewProjection[3].b*50.,2.);
        gl_Position.xy += gl_Position.z*pow((1.-vAlpha)*gl_Position.w,gl_Position.w);
        gl_PointSize = 1.0;
        vColor = aColor;
    }
//___________________片元着色器_________________________
    varying vec4 vColor;
    varying float vAlpha;
    void main(void) {
        gl_FragColor = vColor*vAlpha;
    }

最终形成的效果有向左下发射的效果

3.芝士店 —— 透视除法

透视除法只是将齐次坐标中的 W 分量转换为1的专用名词

在渲染管线中处于光栅化阶段内,所以是自动执行的,为了找透视除法的执行位置我也是搜了好久。。

相关渲染管线请查看opengl图形管线,文中所述的为openGL es3,不过大致是差不多的,可以参考下

下图中左侧的投影矩阵是我在这个项目中的模型,主要是矩阵中第四行第三个位置由0~other值切换,就会导致原有坐标的w分量不为1,不为1之后经过光栅化阶段中的透视除法后,其余xyz相应分量会得到改变,从而实现透视的效果~