使用 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.

let colors = [];

for (const c of faceColors) {
  // 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.

  // prettier-ignore
  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
function initBuffers(gl) {
  // …

  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.uniformMatrix4fv 调用之后、包含 gl.drawArrays() 行的块,用以下块替换

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() 函数中,替换调用 drawScene() 并更新 squareRotation 的代码,改用传递和更新 cubeRotation

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

此时,我们已经有了一个动画立方体在旋转,它的六个面颜色非常鲜艳。

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