webGL课程4:实现三维效果
欢迎来到WebGL教学系列的第四课。这次我们来显示一些3D物体。本课是基于NeHe OpenGL教学系列的第五课。
如果你的浏览器已经支持WebGL,请点击此处,你将看到本课WebGL的现场版;如果不支持,你从此处可以获取一个支持WebGL的浏览器。
本课代码与前一课代码的不同之处完全集中于initBuffers和drawScene这两个函数。如果你现在滚动到drawScene函数,你将在第一行的地方看到细微的改变:保留场景中两个物体当前旋转状态的变量被更名了,它们以前是rTri和 rSquare,现在是:
var rPyramid = 0;
var rCube = 0;
设置好代码之后,我们来看看如何绘制锥体。就像在前面的课程里对三角形所做的那样,我们让它围绕Y轴旋转:
mvRotate(rPyramid, [0, 1, 0]);
然后绘制它。在上一课里,绘制彩色三角形的代码与绘制同样漂亮的锥体的新代码之间的唯一区别在于后者有更为丰富的矢量和颜色,这一切将在 initbuffers函数(我们稍后将看到)中进行处理。这就意味着除了缓冲区名字不同之外,代码是完全相同的。
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);
很简单吧。现在让我们来看看关于立方体的代码。第一步先旋转它,这一次和仅仅绕x轴旋转不同,我们将绕一个(从观察者的角度看)向上、向右并朝向你的轴上旋转立方体:
mvRotate(rCube, [1, 1, 1]);
下一步绘制立方体。这里涉及的东西多一些,我们有三种方法来绘制一个立方体:
1. 使用一个单一的三角形带。如果整个立方体都是同一种颜色,这将相当容易——我们利用一直使用的顶点位置来绘制立方体的一个面,接着添加另外两个点绘制另一个面,然后再添加两个点绘制第三个面,以此类推。这非常有效。不幸的是,我们想要立方体的每个面有不同的颜色。因为每个顶点指定了立方体的一个内角,同时每个内角又被三个面所共有,我们需要指定每个顶点三次。这样做如此复杂,以至于我甚至不想试图去解释它……
2. 我们可以通过绘制六个独自的正方形来绘制立方体,它们中的每个面包含各自的顶点位置和颜色集合。本课的第一个版本就是这样做的,而且它运行得很好。然而,这并非最佳的实现方式,因为每次在你的场景中指示WebGL绘制另一个对象时会耗费大量的时间。如果能较少地去调用drawArrays函数的话,那它将是比较好的方法。
3. 最后的选择是将立方体指定为六个正方形,每一个正方形由两个三角形构成,不过要将所有这些图形送给WebGL让它一次绘制完成。这和我们处理三角形带的方法有点相似,但是由于现在我们每次都完整地定义三角形,而不是像以前那样简单地通过在前一个三角形上添加一个单独的点来定义一个三角形,因此很容易指定每条边的颜色。它还有一个好处就是编码方式最为干净优美。让我来介绍一个新函数drawElements——它就是我们要去实现的方法:-)首先,我们将包含立方体顶点位置的缓冲区和在initBuffers函数中用合适的属性产生的颜色缓冲区关联起来,这就像我们处理锥体那样:
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
接着我们绘制三角形。这里有点小问题。我们考虑一下立方体的正面;我们现在有正面的四个顶点位置,同时它们每一个都有一个与之相关的颜色。然而,它需要使用两个三角形来绘制。由于我们现在使用的是各自指定自身顶点的简单三角形,而不是共享顶点的三角形带,所以我们不得不为此总共指定六个顶点。但问题是我们只能从缓冲区中为它提供四个。
我们所需要做的是指定如下方法:“绘制一个由缓冲区数组中前三个顶点缓冲区构成的三角形,接着绘制另一个由数组中第一个、第三个和第四个顶点缓冲区构成的三角形”。这样我们就能绘制出立方体的正面;然后我们使用类似的方法来绘制余下的部分。这就是我们要做的。
在这里,我们使用“元素缓冲区数组”和一个新函数drawElements。就像我们一直使用的缓冲区数组那样,元素缓冲区数组将在 initBuffers函数中填充相应的值,同时使用基于零点索引顶点位置和颜色的数组方法来保留一份顶点集合的列表(稍后我们会看到)。
我们利用立方体的元素数组保存当前的缓冲区(WebGL可以保留不同的缓冲区数组和元素缓冲区数组,因此我们在调用gl.bindBuffer时必须指定我们需要绑定哪一个缓冲区),接着我们将模型视图矩阵传到图形卡上,最后调用drawElements函数绘制三角形。
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
这就是drawScene函数。余下的代码都在initBuffers函数中,浅显易懂。我们用新名字来定义缓冲区以反映正在处理新的一类物体,同时我们为立方体的顶点索引缓冲区新增一项:
var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubeVertexPositionBuffer;
var cubeVertexColorBuffer;
var cubeVertexIndexBuffer;
我们对锥体所有正面的顶点位置缓冲区填充数值,同时 numItems有相应的变化:
pyramidVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
var vertices = [ // Front face 0.0, 1.0, 0.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // Right face 0.0, 1.0, 0.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, // Back face 0.0, 1.0, 0.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, // Left face 0.0, 1.0, 0.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0 ];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);
pyramidVertexPositionBuffer.itemSize = 3;
pyramidVertexPositionBuffer.numItems = 12;…
锥体的顶点颜色缓冲区也如此:
pyramidVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
var colors = [
// Front face
1.0, 0.0, 0.0, 1.0,
0.0,1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
// Right face
1.0, 0.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 1.0, 0.0, 1.0,
// Back face
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
// Left face
1.0, 0.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 1.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(colors), gl.STATIC_DRAW);
pyramidVertexColorBuffer.itemSize = 4;
pyramidVertexColorBuffer.numItems = 12;…
对于立方体的顶点位置缓冲区的处理如下:
cubeVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
vertices = [
// Front face
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);
cubeVertexPositionBuffer.itemSize = 3;
cubeVertexPositionBuffer.numItems = 24;
颜色缓冲区稍微复杂些,因为我们使用一个循环来产生一个顶点颜色的列表,所以我们不得不指定每种颜色四次,每个顶点一次。
cubeVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
var colors = [
[1.0, 0.0, 0.0, 1.0],
// Front face
[1.0, 1.0, 0.0, 1.0],
// Back face
[0.0, 1.0, 0.0, 1.0],
// Top face
[1.0, 0.5, 0.5, 1.0],
// Bottom face
[1.0, 0.0, 1.0, 1.0],
// Right face
[0.0, 0.0, 1.0, 1.0],
// Left face
];
var unpackedColors = [] for (var i in colors)
{
var color = colors[i];
for (var j=0; j < 4; j++) {
unpackedColors = unpackedColors.concat(color); } }
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(unpackedColors), gl.STATIC_DRAW);
cubeVertexColorBuffer.itemSize = 4;
cubeVertexColorBuffer.numItems = 24;
最后,我们定义元素缓冲区数组(提示:gl.bindBuffer和gl.bufferData函数的第一个参数是不同的):
cubeVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
var cubeVertexIndices = [
0, 1, 2,
0, 2, 3,
// Front face
4, 5, 6,
4, 6, 7,
// Back face
8, 9, 10,
8, 10, 11,
// Top face
12, 13, 14,
12, 14, 15,
// Bottom face
16, 17, 18,
16, 18, 19,
// Right face
20, 21, 22,
20, 22, 23
// Left face
]
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new WebGLUnsignedShortArray(cubeVertexIndices), gl.STATIC_DRAW);
cubeVertexIndexBuffer.itemSize = 1;
cubeVertexIndexBuffer.numItems = 36;
记住,在这个缓冲区中的每个数字是顶点位置和颜色缓冲区的一个索引。因此,结合drawScene函数绘制三角形的指令,第一行代码意味着我们利用顶点0,1和2获得一个三角形。接着利用顶点 0,2和3获得另一个三角形。由于两个三角形颜色相同且相邻,于是利用顶点0,1,2和3就可以获得一个正方形。重复这样的操作就可以获得立方体的所有面,最后立方体也就绘制好了!