项目地址html
最近在学习webGL原生,给的3d项目又不是不少,就想到了其实2d的项目也能够拿webGL练习,在接到这个项目以后,脑子里蹦出个要作出粒子的效果,大体思路已经有了,就是获取图片的有效像素做为point的xy坐标,z轴的不一样来实现3d粒子效果,顺着思路往下想,透视投影的话那我生成xy坐标还得和z作一个透视得逆转变,使他们在透视投影得时候仍是看到的是正常的图片,但发现有两个问题,一是我这个数学浆糊来回倒腾逆矩阵太迷糊,二就是在透视投影中z轴不一样尺寸不一样,意思就是远小近大,致使每一个粒子得大小也就不一样,因此我换了种思路,简单易懂那就是正交投影切透视投影。web
想到正交投影得特性彻底就能够知足我把粒子排列成图案,又想到透视投影能够把粒子打散,我得思路就有了:canvas
| 生成坐标 —— 正交透视切换bash
效果: markdown
这里的想法是运用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
在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; } 复制代码
最终造成的效果有向左下发射的效果
透视除法只是将齐次坐标中的 W 份量转换为1的专用名词
在渲染管线中处于光栅化阶段内,因此是自动执行的,为了找透视除法的执行位置我也是搜了很久。。
相关渲染管线请查看opengl图形管线,文中所述的为openGL es3,不过大体是差很少的,能够参考下
下图中左侧的投影矩阵是我在这个项目中的模型,主要是矩阵中第四行第三个位置由0~other值切换,就会致使原有坐标的w份量不为1,不为1以后通过光栅化阶段中的透视除法后,其他xyz相应份量会获得改变,从而实现透视的效果~