思路
最近在学习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相应分量会得到改变,从而实现透视的效果~