WebGL 绘制多个几何体

优质
小牛编辑
132浏览
2023-12-01

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

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

为了绘制两个相同的立方体效果,下面给出了两种方式。

方式一

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

var data=new Float32Array([
    .3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,.3,      //面1
    .3,.3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,      //面2
    .3,.3,.3,.3,.3,-.3,-.3,.3,-.3,.3,.3,.3,-.3,.3,-.3,-.3,.3,.3,      //面3
    -.3,.3,.3,-.3,.3,-.3,-.3,-.3,-.3,-.3,.3,.3,-.3,-.3,-.3,-.3,-.3,.3,//面4
    -.3,-.3,-.3,.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,.3,//面3
    .3,-.3,-.3,-.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,.3,-.3, //面6
    //立方体2的顶点坐标数据
    .3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,.3,      //面1
    .3,.3,.3,.3,-.3,.3,.3,-.3,-.3,.3,.3,.3,.3,-.3,-.3,.3,.3,-.3,      //面2
    .3,.3,.3,.3,.3,-.3,-.3,.3,-.3,.3,.3,.3,-.3,.3,-.3,-.3,.3,.3,      //面3
    -.3,.3,.3,-.3,.3,-.3,-.3,-.3,-.3,-.3,.3,.3,-.3,-.3,-.3,-.3,-.3,.3,//面4
    -.3,-.3,-.3,.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,.3,//面3
    .3,-.3,-.3,-.3,-.3,-.3,-.3,.3,-.3,.3,-.3,-.3,-.3,.3,-.3,.3,.3,-.3 //面6
]);
//立方体1顶点数据x坐标批量加0.5
for(var i = 0;i<36*3;i += 3 ){
    data[i] += 0.5;
}
//立方体2顶点数据x坐标批量减0.5
for(var i = 36*3;i<72*3;i += 3 ){
    data[i] -= 0.5;
}

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

/**
 创建顶点颜色数组colorData
 **/
var colorData = new Float32Array([
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面1
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面2
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面3
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面4
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面5
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, //红色——面6
    //立方体2的顶点颜色数据
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面1
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面2
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面3
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面4
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0,//红色——面5
    1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0, 1,0,0 //红色——面6
]);
/**
 *顶点法向量数组normalData
 **/
var normalData = new Float32Array([
    0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
    1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
    0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
    -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
    0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
    0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,//z轴负方向——面6
    //立方体2的顶点法向量数据
    0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,//z轴正方向——面1
    1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,//x轴正方向——面2
    0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,//y轴正方向——面3
    -1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,//x轴负方向——面4
    0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,//y轴负方向——面5
    0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1//z轴负方向——面6
]);

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

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

方式二(重用数据)

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

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

/**uniform声明旋转矩阵变量mx、my,平移矩阵Tx**/
uniform mat4 mx;//绕x轴旋转矩阵
uniform mat4 my;//绕y轴旋转矩阵
uniform mat4 Tx;//沿着x轴平移矩阵

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

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

通过WebGL APIgl.uniformMatrix4fv()把类型数据创建的旋转矩阵数据传递给着色器。

/**
 * 传入旋转矩阵数据
 ***/
var angle = Math.PI/4;//旋转角度
var sin = Math.sin(angle);
var cos = Math.cos(angle);
//旋转矩阵数据
var mxArr = new Float32Array([1,0,0,0,  0,cos,-sin,0,  0,sin,cos,0,  0,0,0,1]);
var myArr = new Float32Array([cos,0,-sin,0,  0,1,0,0,  sin,0,cos,0,  0,0,0,1]);
//类型数组传入矩阵
gl.uniformMatrix4fv(mx, false, mxArr);
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数据,显示系统会不停的循环扫描帧缓冲区中的颜色数据显示在屏幕上。你可以执行注释掉的代码gl.clear(gl.COLOR_BUFFER_BIT);,然后刷新浏览器可以发现,网页上只有一个立方的图像, 这句代码gl.clear(gl.COLOR_BUFFER_BIT);的作用就是清除帧缓冲区中颜色缓冲区存储的颜色数据,clear()是一个WebGL API,参数COLOR_BUFFER_BIT表示帧缓冲区的颜色缓冲区,当然你也可以清除其它显存区域中的数据,比如参数是gl.DEPTH_BUFFER_BIT就表示帧缓冲区的深度缓冲区像素深度数据,深度缓冲区和颜色缓冲区一样都是帧缓存的子缓冲区。 立方体的所有面的像素值都存储在颜色缓冲区中,这些像素的深度值都保存在深度缓冲区中,深度缓冲区中数据表征像素距离人眼睛的长度,执行gl.enable(gl.DEPTH_TEST);可以开启深度检测功能,离眼睛近的像素才会显示出来,离眼睛远的像素值会被覆盖,对应生活中就是正常情况下你的眼睛不可能不可能看到立方体的背面,你只能看到离你眼睛近的正面。

WebGL坐标系

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

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

//平移矩阵Tx、旋转矩阵mx、旋转矩阵my连乘、顶点齐次坐标连乘
gl_Position = Tx*mx*my*apos;

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

//平移矩阵Tx、旋转矩阵mx、旋转矩阵my连乘、顶点齐次坐标连乘
gl_Position = mx*my*Tx*apos;