WebGL绘制多个几何体

  利用WebGL重现三维场景的时候,往往不仅仅有一个几何体,比如像机器人是由多个子零件构成的装配体,可能同一个零件出现在不同的位置,可能两个零件的形状一样颜色不一样,也有颜色形状完全不同的零件。 阅读下面的讲解内容,一方面可以学习如何利用WebGL创建多几何体场景,同时借助实际的问题进一步加深对渲染管线这个硬件黑箱的认知,只有更好的认知渲染管线才能更好的应用与它紧密关联的API接口, 学习精力是有限的,毕竟GPU的数字电路不是每个人都有时间学习明白,只能通过简单的案例程序学习如何利用WebGL API操作渲染管线的相关功能。

  绘制多个几何体你首先会想到应该是每个几何体都有自己的顶点数据,要创建多个几何体自然要先利用类型数据创建这些顶点数据,除此外还要学习绘制函数的多次调用与帧缓存的知识,学习如何切换使用多组着色器程序。

绘制两个相同的立方体

方式一

源码下载

  最简单思路,增加一个几何体就把几何体的顶点添加到顶点数据中,在《光照立方体》代码的基础上更改即可,把立方体原来数据复制一份,为了把两个立方体的位置错开,可以使用for循环批量修改顶点数据,把某个坐标值加或减。

77       /**
78        创建顶点位置数据数组data,Javascript中小数点前面的0可以省略
79        **/
80       var data=new Float32Array([
81           .3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,.3,      //面1
82           .3,.3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,      //面2
83           .3,.3,.3,.3,.3,-.3,-.3,.3,-.3,.3,.3,.3,-.3,.3,-.3,-.3,.3,.3,      //面3
84           -.3,.3,.3,-.3,.3,-.3,-.3,-.3,-.3,-.3,.3,.3,-.3,-.3,-.3,-.3,-.3,.3,//面4
85           -.3,-.3,-.3,.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,.3,//面3
86           .3,-.3,-.3,-.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,.3,-.3, //面6
87           //立方体2的顶点坐标数据
88           .3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,.3,      //面1
89           .3,.3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,      //面2
90           .3,.3,.3,.3,.3,-.3,-.3,.3,-.3,.3,.3,.3,-.3,.3,-.3,-.3,.3,.3,      //面3
91           -.3,.3,.3,-.3,.3,-.3,-.3,-.3,-.3,-.3,.3,.3,-.3,-.3,-.3,-.3,-.3,.3,//面4
92           -.3,-.3,-.3,.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,.3,//面3
93           .3,-.3,-.3,-.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,.3,-.3 //面6
94       ]);
95       //立方体1顶点数据x坐标批量加0.5
96       for(var i = 0;i<36*3;i += 3 ){
97           data[i] += 0.5;
98       }
99       //立方体2顶点数据x坐标批量减0.5
100      for(var i = 36*3;i<72*3;i += 3 ){
101          data[i] -= 0.5;
102      }

  顶点的法向量数据、颜色数据重新复制一份即可,和原来相同。

103      /**
104       创建顶点颜色数组colorData
105       **/
106      var colorData = new Float32Array([
107          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面1
108          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面2
109          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面3
110          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面4
111          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面5
112          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, //红色——面6
113          //立方体2的顶点颜色数据
114          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面1
115          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面2
116          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面3
117          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面4
118          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面5
119          1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0 //红色——面6
120      ]);
121      /**
122       *顶点法向量数组normalData
123       **/
124      var normalData = new Float32Array([
125          0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
126          1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
127          0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
128          -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
129          0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
130          0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,//z轴负方向——面6
131          //立方体2的顶点法向量数据
132          0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
133          1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
134          0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
135          -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
136          0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
137          0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1//z轴负方向——面6
138      ]);

  绘制的顶点数量增加了,drawArrays方法的参数要从36变为72.

167      /**执行绘制命令**/
168      gl.drawArrays(gl.TRIANGLES,0,72);
167      /**执行绘制命令**/
168      gl.drawArrays(gl.TRIANGLES,0,36);

方式二(重用数据)

源码下载

  两个几何体颜色、形状完全相同,没有必要在创建一组顶点数据,可以使用drawArrays方法多次调用同一组顶点数据,执行平移变换即可,第一次调用drawArrays方法把立方体整体向右平移(x轴正方向),第二次调用立方体整体向左平移(x轴负方向)。

  在着色器声明旋转矩阵mx、my和平移矩阵Tx,旋转矩阵传入一次数据不再改变,平移矩阵的数据每次调用方法drawArrays,都会重新传入着色器新的数据,在新的位置渲染出来几何体。

21           'uniform mat4 mx;'+//绕x轴旋转矩阵
22           'uniform mat4 my;'+//绕y轴旋转矩阵
23           'uniform mat4 Tx;'+//沿着x轴平移矩阵

  在着色器声明旋转矩阵mx、my和平移矩阵Tx,旋转矩阵传入一次数据不再改变,平移矩阵的数据每次调用方法drawArrays,都会重新传入着色器新的数据,在新的位置渲染出来几何体。

56       /**从program对象获得旋转矩阵变量mx、my和平移矩阵变量Tx地址**/
57       var mx = gl.getUniformLocation(program,'mx');
58       var my = gl.getUniformLocation(program,'my');
59       var Tx = gl.getUniformLocation(program,'Tx');

  通过方法uniformMatrix4fv把类型数据创建的旋转矩阵数据传递给着色器。

126  /**
127   * 传入旋转矩阵数据
128   ***/
129  var angle = Math.PI/4;//旋转角度
130  var sin = Math.sin(angle);
131  var cos = Math.sin(angle);
132  //旋转矩阵数据
133  var mxArr = new Float32Array([1,0,0,0,  0,cos,-sin,0,  0,sin,cos,0,  0,0,0,1]);
134  var myArr = new Float32Array([cos,0,-sin,0,  0,1,0,0,  sin,0,cos,0,  0,0,0,1]);
135  //类型数组传入矩阵
136  gl.uniformMatrix4fv(mx, false, mxArr);
137  gl.uniformMatrix4fv(my, false, myArr); 

  声明一个绘制函数draw(x),参数x是平移矩阵的一个元素,表示沿x轴平移距离,每次对用draw()函数都会传入新的x值。调用draw函数,执行gl.drawArrays(gl.TRIANGLES,0,36);语句的时候,渲染管线会生成立方体图像的像素值,像素值存储在帧缓冲区的颜色缓冲区中,你可以把帧缓冲区当成一个RGB像素值仓库, 每执行一次gl.drawArrays(gl.TRIANGLES,0,36);生成一组RGB值,这些数据会被送进帧缓冲去中,默认不会覆盖前面的RGB数据,显示系统会不停的循环扫描帧缓冲区中的颜色数据显示在屏幕上。你可以解除第146行注释掉的代码,然后刷新浏览器可以法线,网页上只有一个立方的图像, 这句代码gl.clear(gl.COLOR_BUFFER_BIT);的作用就是清除帧缓冲区中颜色缓冲区存储的颜色数据,clear()是一个WebGL方法,参数COLOR_BUFFER_BIT就是颜色缓冲区,当然你也可以清除其它显存区域中的数据,比如参数是gl.DEPTH_BUFFER_BIT就表示深度缓冲区像素深度数据,深度缓冲区和颜色缓冲区一样都是帧缓存的子缓冲区。 立方体的所有面的像素值都存储在颜色缓冲区中,这些像素的深度值都保存在深度缓冲区中,深度缓冲区中数据表征像素距离人眼睛的长度,执行gl.enable(gl.DEPTH_TEST);可以开启深度检测功能,离眼睛近的像素才会显示出来,离眼睛远的像素值会被覆盖,对应生活中就是正常情况下你的眼睛不可能不可能看到立方体的背面,你只能看到离你眼睛近的正面。

138      function draw(x) {//x变量表示沿着x轴的平移距离
139          //旋转矩阵传入矩阵
140          var TxArr = new Float32Array([1,0,0,0,  0,1,0,0,  0,0,1,0,  x,0,0,1]);
141          gl.uniformMatrix4fv(Tx, false, TxArr);
142          /**执行绘制命令**/
143          gl.drawArrays(gl.TRIANGLES,0,36);
144      }
145      draw(0.5);//x轴正方向平移0.5
146  //    gl.clear(gl.COLOR_BUFFER_BIT);//清空画布上一帧图像
147      draw(-0.5);//x轴负方向平移0.5

WebGL坐标系

  第一个WebGL程序创建了两个立方体的顶点数据,两个立方体一左一右,然后执行了绕x轴和y轴两个旋转变换,查看效果图你可以知道绕y旋转的时候是WebGL默认坐标系的y轴, 不是过立方体几何中心的的y轴,查看立方体的顶点坐标可以看出来,两个立方体自身的几何中心是在x轴上的,所以立方体绕WebGL坐标系x轴的旋转相当于绕自身过几何中心的x轴旋转。复杂的场景往往牵扯到各种各样抽象出来的坐标系, 比如视图坐标系、世界坐标系等,他们都是基于WebGL默认的坐标系抽象出来的,这里不再多说,先有一个简单的印象,一个几何体变换是相对于谁而言。

  对比上面两个WebGL程序,你可以发现发现他们稍有区别。第二个WebGL程序的运行结果和第一个之所以不一样是因为平移和旋转的先后顺序不一样,第一个WebGL程序中虽然没有平移变换,但是两个几何体一左一右关于y轴对称,相当于从中间平移过去, 然后经过两次旋转。查看下面第二个WebGL程序的着色器程序矩阵相乘的顺序,可以知道着色器处理器先对顶点进行旋转变换再进行平移变换。

26   // 平移矩阵Tx、旋转矩阵mx、旋转矩阵my连乘
27   '   gl_Position = Tx*mx*my*apos;' +

  矩阵的乘法运算中,矩阵的左乘和右乘是不一样的,是有顺序性的,离apos列向量近的矩阵就是先执行的变换,如果把平移矩阵Tx靠近apos列向量,运行程序你会发现和第一个WebGL程序的运算结果是一样的。

26   // 平移矩阵Tx、旋转矩阵mx、旋转矩阵my连乘
27   '   gl_Position = mx*my*Tx*apos;' +

给两个立方体添加旋转动画

源码下载

  下面的代码作用是绘制出两个立方体并实现旋转动画,对于解决实际的问题是有帮助的,比如机器人运动的时候每个零件都有自己的运动。综合《立方体动画》和《绘制放个立方体》这两节课的内容,基本不用写多少代码,把两节课代码进行简单组合。

  两个立方体顶点数据和旋转矩阵数据都是相同,绘制两个立方体是利用平移矩阵多次绘制和帧缓存颜色缓冲区的RGB数据默认不删除保存特点实现。基本思路是利用requestAnimationFrame方法循环调用绘制函数draw(),在绘制函数中更新立方体的旋转矩阵数据, 同时连续两次调用立方体绘制函数drawTx()绘制两个立方体。

时间概念

  主要深入思考几个时间的数量级问题,CPU、GPU执行一条指令的时间是纳秒ns级,动画的刷新要求一般30~60FPS,也就是说两帧图像的间隔是16.7ms~33.3ms,只要程序安排的合理, 在这个时间长度内CPU执行完所有的Javascript程序,GPU执行处理完所有的顶点数据生成RGB像素值是没有问题的,尤其像下面短小的绘制函数更是没有问题。