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

此時,我們已經有了一個動畫立方體在旋轉,它的六個面顏色非常鮮豔。

檢視完整程式碼 | 在新頁面開啟此演示