Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

WebGL(肆) 矩阵与着色器的内部工作

  在之前的几篇文章中,已经介绍了绘制顶点的方法。但是,只有顶点我们做不出什么有意义的效果来。WebGL是用来做3D的,所以不能局限于顶点,这就需要在着色器程序中使用矩阵。这篇文章咱就要来绘制一个每个面颜色不同的立方体,当然,还要让它旋转起来。
  之前的文章中有说到着色器内定义变量的关键字吧,我们已经知道了“attribute”的用法。现在,我们还需要用上“uniform”和“varying”这两个关键字。“uniform”是定义着色器外部变量的接口,这个貌似在之前说“attribute”的时候就说过了。如果非要把之前的“attribute”解释为属性的话就是每个顶点的属性,而“uniform”才是整个着色器的属性。我们需要对一个图形进行变换,也就是在一次绘制中对所用顶点做一个同样的操作,这是就可以使用“uniform”。下面是创建WebGL和准备着色器程序的代码,很多东西在之前的文章中就说过了,我就不注释的太详细了。
<canvas id="canvas" width="600" height="400"></canvas>
<script id="vs_s" type="text/x-glsl">
attribute vec3 po; //顶点数据源
attribute vec3 co; //颜色数据源
uniform mat4 pro; //投射矩阵
uniform mat4 rot; //旋转矩阵
uniform mat4 mov; //平移矩阵
varying vec3 co_v; //co_v管道入口
void main(){
  co_v=co; //把co放入co_v管道
  gl_Position=pro*mov*rot*vec4(po,1.0);
}
</script>
<script id="fs_s" type="text/x-glsl">
varying lowp vec3 co_v; //co_v管道出口
void main(){
  gl_FragColor=vec4(co_v,1);
}
</script>
<script>
var webgl,program,fs,vs,tmp,i,j;

//获取WebGL对象,并制作着色器程序
webgl=canvas.getContext("experimental-webgl");
program=webgl.createProgram();
vs=webgl.createShader(webgl.VERTEX_SHADER);
fs=webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vs,vs_s.textContent);
webgl.shaderSource(fs,fs_s.textContent);
webgl.compileShader(vs);
webgl.compileShader(fs);
webgl.attachShader(program,vs);
webgl.attachShader(program,fs);
webgl.linkProgram(program);
webgl.useProgram(program);
</script>
  由于这次的着色器比较复杂,用JavaScript的字符串写就不直观了也不如容易编辑。所以我们把它独立出来放在文本类型的SCRIPT标签中。在顶点着色器中,我们使用了两个数据源。一个是顶点数据源,另一个颜色数据源,它们分别描述每个顶点的坐标和颜色。之后又定义了三个矩阵,他们是使用uniform关键字定义的,所以他们是整个着色器程序的属性,也就是说所有顶点共用这些矩阵。之后,还有一个“varying”关键字定义的变量“co_v”。这个关键字定义的变量是顶点着色器和片段着色器通信的管道,在片段着色器的代码中也有这个变量的声明,而且变量名也是“co_v”。“管道”是个啥?感觉还是很混乱吧?因为有一个很关键的概念我还没说。
  在之前的文章中,我只说过传入多少个顶点,顶点着色器就会被执行多少次。但是片段着色器则不同。片段着色器是和图元相关的。顶点着色器处理完顶点之后到片段着色器开始调用之前,这期间还有一个步骤。为什么之前的例子中我给定三个点能画出三角形呢?这就是因为我们定义的图元是三角形。顶点着色器只是处理顶点的坐标而已,它并不理会图元怎么着,对它来说一切都只是顶点而已。这就需要一个步骤来处理,这个步骤称为“图元装配与光栅化(Primitive Assembly and Rasterization)”。这个步骤首先会根据给定的顶点来描绘图元(图元装配)。可是图元未必都是点,也有可能是一条线,或一个面(三角形),这种图元的区域我们称为“片段(fragment)”。在片段中的任何一点都有自己的坐标,这个坐标当然是需要计算出来的(光栅化)。计算出的这一大堆坐标才是片段着色器需要处理的东西,也就是说,顶点着色器的执行次数是顶点的个数,而片段着色器的执行次数是片段内点的个数。
  另外,“光栅化”的过程计算的并不仅仅是片段面的所有坐标!我们在顶点着色器中用“varying”这个关键字定义了一个管道吧?这个管道是连接到片段着色器的,而顶点着色器被执行的次数和片段着色器被执行的次数是不同的,如果只是传入这么一个变量,那在整个片段面上就无法使用。所以,“varying”关键字定义的管道变量也会被光栅化。这个以后说颜色变化的时候就很容易理解,这篇就不再详细解释了。
  由于我们要实现的是立方体的每个面拥有不同的颜色,所以需要把颜色也传入片段着色器中。这就使用“varying”管道传过去了。还要注意的一点,片段着色器中的浮点数变量需要设置一个精度。有“高(highp)”、“中(mediump)”、“低(lowp)”,可选择。这个东西很无厘头,具体看看看官方的GLSL文档(4.7章)。
  接着,我们为程序指定数据源吧。
//数据源相关
var po,co,dat,buf;

//获取接口位置,并开启数组模式
po=webgl.getAttribLocation(program,"po");
co=webgl.getAttribLocation(program,"co");
webgl.enableVertexAttribArray(po);
webgl.enableVertexAttribArray(co);

//指定顶点坐标的数据源
dat=new Float32Array([
  -1,-1,1,  1,-1,1,   1,1,1,  -1,1,1,   //前面
  -1,-1,-1, 1,-1,-1,  1,1,-1, -1,1,-1,  //后面
  -1,-1,-1, -1,1,-1,  -1,1,1, -1,-1,1,  //左面
  1,-1,-1,  1,1,-1,   1,1,1,  1,-1,1,   //右面
  -1,1,-1,  1,1,-1,   1,1,1,  -1,1,1,   //上面
  -1,-1,-1, 1,-1,-1,  1,-1,1, -1,-1,1,  //下面
]);
buf=webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER,buf);
webgl.bufferData(webgl.ARRAY_BUFFER,dat,webgl.STATIC_DRAW);
webgl.vertexAttribPointer(po,3,webgl.FLOAT,false,0,0);

//指定顶点颜色的数据源
tmp=[[1,0,0],[0,1,0],[0,0,1],[1,1,0],[1,0,1],[0,1,1]]; //六种颜色
for(dat=[],i=0;i<tmp.length;i++)//每一种颜色的顶点有4个
  for(j=0;j<4;j++)dat.push.apply(dat,tmp[i]);
dat=new Float32Array(dat);
buf=webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER,buf);
webgl.bufferData(webgl.ARRAY_BUFFER,dat,webgl.STATIC_DRAW);
webgl.vertexAttribPointer(co,3,webgl.FLOAT,false,0,0);
  这个代码没啥难点吧,基本上之前的文章都说过了。我们最后是使用索引来绘制的,所以这个地方准备24个坐标点(每个面四个点)就够了。另外颜色那里可能要注意下,把每个颜色复制4个。总共有6种颜色(每个面一种),复制颜色后,颜色数组就是24个嘛,和上面的坐标数组一样大。接着是设置uniform的代码。
//uniform参数相关
var pro,rot,mov;

//获取uniform们的句柄
pro=webgl.getUniformLocation(program,"pro");
rot=webgl.getUniformLocation(program,"rot");
mov=webgl.getUniformLocation(program,"mov");

//设置投射矩阵
webgl.uniformMatrix4fv(
  pro,false,(function(a,r,n,f){
    //参数分别是:视角、区域宽高比、近平面、远平面
    a=1/Math.tan(a*Math.PI/360);
    return [
      a/r,0,0,0, 0,a,0,0, 0,0,-(f+n)/(f-n),-1, 0,0,-2*f*n/(f-n),0
    ];
  })(45,canvas.width/canvas.height,1,100)
);

//设置移动矩阵(向z方向移动-10)
webgl.uniformMatrix4fv(
  mov,false,[1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,-5,1]
);
  其实uniform的设置和attrbute差不多的,也是先取出location再设置上想要的值。不过uniform的location和attribute的不同,它是一个句柄,所以我们必须去获取,不能像attribute一样直接使用序号数字。另外,由于uniform是在绘制执行之前设置的值,并不像attribute一样是绘制过程多次调用。所以它可以直接接受JavaScript数组而不用我们自己去操作显存,这个是很方便的。但是uniform也有个坑爹的地方,你看uniform设置值的函数名完全就是把变量类型都带入了,所以uniform设置值的函数有一大坨。每一种不同类型的变量都用不同的函数来设置值的,我就感觉这样的做法很蛋疼。上面这段代码设置了投射矩阵和移动矩阵,投射矩阵在之前的文章中“投射矩阵(Projection Martrix)的推导”有推导过,移动矩阵这个就简单了,就是坐标的加减而已。还有一个旋转矩阵我没有放在这段代码中设置,因为我的最终效果是旋转的立方体,所以每次绘制时都有不同的旋转角度,所以这个放在最后。关于旋转矩阵,虽然我还没写过三维的旋转矩阵相关的文章,不过之前有一篇说“元素中心旋转”的文章中有二维的旋转矩阵,原理是一样的,可以根据那个很容易的扩展到三维上,这个以后有机会再说吧。
  最后是和绘制相关的代码
//构造索引
for(dat=[],i=0;i<24;i+=4)dat.push(i+0,i+1,i+3,i+3,i+2,i+1);
dat=new Uint16Array(dat);
buf=webgl.createBuffer();
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER,buf);
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER,dat,webgl.STATIC_DRAW);

//开启深度测试
webgl.enable(webgl.DEPTH_TEST);

//绘制过程
var a=0;
setInterval(function(){
  //设置旋转矩阵
  a-=0.02;
  var s=Math.sin(a),c=Math.cos(a);
  webgl.uniformMatrix4fv(
    rot,false,[c*c,-s,s*c,0, s*c,c,s*s,0, -s,0,c,0, 0,0,0,1]
  );
  //绘制
  webgl.drawElements(webgl.TRIANGLES,36,webgl.UNSIGNED_SHORT,0);
},16);

  由于每个面都是固定的排序,所以索引可以直接用一个循环来生成。这里还有一个开启深度测试的代码,这是很重要的东西,如果不开启深度测试,那么z坐标就基本作废了,因为无法判断片段的渲染先后顺序。这里我使用的旋转矩阵只是把绕x轴旋转的和绕y轴旋转的两个矩阵乘在了一起而已,看上去有些诡异。
  其他就没啥了吧?还有什么疑问就提出吧~ 下面是这个实例的代码。
  WebGL矩阵与着色器的内部工作
网名:
54.224.247.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^