WebGL——粒子化图片

项目地址html

思路

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

想到正交投影得特性彻底就能够知足我把粒子排列成图案,又想到透视投影能够把粒子打散,我得思路就有了:canvas

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

效果: markdown

实现

1.生成坐标

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

getImageData 也有一些小技巧,例如能够实现碰撞检测
复制代码

代码以下:oop

// 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贺卡):post

//获取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的一个点,不作截图了。spa

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相应份量会获得改变,从而实现透视的效果~

相关文章
相关标签/搜索