使用 WebGL 创建 3D 对象

让我们通过添加五个面将正方形平面扩展到三维空间,以创建一个立方体。为了高效地做到这一点,我们将从直接使用顶点绘制(通过调用 gl.drawArrays() 方法)切换到使用顶点数组作为表格,并通过调用 gl.drawElements() 在该表格中引用各个顶点以定义每个面的顶点位置。

考虑:每个面需要四个顶点来定义它,但每个顶点由三个面共享。我们可以通过构建一个包含所有 24 个顶点的数组,然后通过其在该数组中的索引引用每个顶点(而不是移动整个坐标集)来传递更少的数据。如果你想知道为什么我们需要 24 个顶点,而不是仅仅 8 个,那是因为每个角都属于三个不同颜色的面,而单个顶点需要具有一个特定的颜色;因此我们将为每个面创建每个顶点的三个副本,每个副本都具有不同的颜色。

定义立方体顶点的坐标

首先,让我们通过更新 initBuffers() 中的代码来构建立方体的顶点位置缓冲区。这与正方形平面几乎相同,但由于有 24 个顶点(每边 4 个),因此代码会更长一些。

注意: 在你的 "init-buffers.js" 模块的 initPositionBuffer() 函数中,用以下代码替换 positions 声明

js
const positions = [
  // 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,
];

由于我们已向顶点添加了 z 坐标,因此需要将 vertexPosition 属性的 numComponents 更新为 3。

注意: 在你的 "draw-scene.js" 模块的 setPositionAttribute() 函数中,将 numComponents 常量从 2 更改为 3

js
const numComponents = 3;

定义顶点的颜色

我们还需要构建一个包含所有 24 个顶点的颜色数组。此代码首先为每个面定义一个颜色,然后使用循环来组装包含所有顶点颜色数组。

注意: 在你的 "init-buffers.js" 模块的 initColorBuffer() 函数中,用以下代码替换 colors 声明

js
const faceColors = [
  [1.0, 1.0, 1.0, 1.0], // Front face: white
  [1.0, 0.0, 0.0, 1.0], // Back face: red
  [0.0, 1.0, 0.0, 1.0], // Top face: green
  [0.0, 0.0, 1.0, 1.0], // Bottom face: blue
  [1.0, 1.0, 0.0, 1.0], // Right face: yellow
  [1.0, 0.0, 1.0, 1.0], // Left face: purple
];

// Convert the array of colors into a table for all the vertices.

var colors = [];

for (var j = 0; j < faceColors.length; ++j) {
  const c = faceColors[j];
  // Repeat each color four times for the four vertices of the face
  colors = colors.concat(c, c, c, c);
}

定义元素数组

一旦顶点数组生成,我们就需要构建元素数组。

注意: 在你的 "init-buffer.js" 模块中,添加以下函数

js
function initIndexBuffer(gl) {
  const indexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

  // This array defines each face as two triangles, using the
  // indices into the vertex array to specify each triangle's
  // position.

  const indices = [
    0,
    1,
    2,
    0,
    2,
    3, // front
    4,
    5,
    6,
    4,
    6,
    7, // back
    8,
    9,
    10,
    8,
    10,
    11, // top
    12,
    13,
    14,
    12,
    14,
    15, // bottom
    16,
    17,
    18,
    16,
    18,
    19, // right
    20,
    21,
    22,
    20,
    22,
    23, // left
  ];

  // Now send the element array to GL

  gl.bufferData(
    gl.ELEMENT_ARRAY_BUFFER,
    new Uint16Array(indices),
    gl.STATIC_DRAW,
  );

  return indexBuffer;
}

indices 数组将每个面定义为一对三角形,并将每个三角形的顶点指定为立方体顶点数组的索引。因此,立方体被描述为 12 个三角形的集合。

接下来,你需要从 initBuffers() 中调用这个新函数,并返回它创建的缓冲区。

注意: 在你的 "init-buffers.js" 模块的 initBuffers() 函数末尾,添加以下代码,并替换现有的 return 语句

js
const indexBuffer = initIndexBuffer(gl);

return {
  position: positionBuffer,
  color: colorBuffer,
  indices: indexBuffer,
};

绘制立方体

接下来,我们需要在 drawScene() 函数中添加代码,以便使用立方体的索引缓冲区进行绘制,并添加新的 gl.bindBuffer()gl.drawElements() 调用。

注意: 在你的 drawScene() 函数中,在 gl.useProgram 行之前添加以下代码

js
// Tell WebGL which indices to use to index the vertices
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);

注意: 在你的 "draw-scene.js" 模块的 drawScene() 函数中,用以下代码块替换包含 gl.drawArrays() 行的两个 gl.uniformMatrix4fv 调用之后的代码块

js
{
  const vertexCount = 36;
  const type = gl.UNSIGNED_SHORT;
  const offset = 0;
  gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}

由于立方体的每个面都由两个三角形组成,因此每个面有 6 个顶点,或者立方体共有 36 个顶点,即使其中许多是重复的。

最后,让我们将变量 squareRotation 替换为 cubeRotation,并添加绕 x 轴的第二个旋转。

注意: 在你的 "webgl-demo.js" 文件的开头,用以下代码行替换 squareRotation 声明

js
let cubeRotation = 0.0;

注意: 在你的 drawScene() 函数声明中,将 squareRotation 替换为 cubeRotation

js
function drawScene(gl, programInfo, buffers, cubeRotation) {

注意: 在你的 drawScene() 函数中,用以下代码替换 mat4.rotate 调用

js
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation, // amount to rotate in radians
  [0, 0, 1],
); // axis to rotate around (Z)
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation * 0.7, // amount to rotate in radians
  [0, 1, 0],
); // axis to rotate around (Y)
mat4.rotate(
  modelViewMatrix, // destination matrix
  modelViewMatrix, // matrix to rotate
  cubeRotation * 0.3, // amount to rotate in radians
  [1, 0, 0],
); // axis to rotate around (X)

注意: 在你的 main() 函数中,用传递并更新 cubeRotation 而不是 squareRotation 的代码替换调用 drawScene() 并更新 squareRotation 的代码

js
drawScene(gl, programInfo, buffers, cubeRotation);
cubeRotation += deltaTime;

在这一点上,我们现在拥有一个动画立方体,它正在旋转,六个面以非常鲜明的颜色呈现。

查看完整代码 | 在新页面中打开此演示