視覺化學習:如何用WebGL繪製3D物體

beckyye發表於2024-07-10

在之前的文章中,我們使用WebGL繪製了很多二維的圖形和影像,在學習2D繪圖的時候,我們提過很多次關於GPU的高效渲染,但是2D圖形的繪製只展示了WebGL部分的能力,WebGL更強大的地方在於,它可以繪製各種3D圖形,而3D圖形能夠極大地增強視覺化的表現能力。

相信很多小夥伴都對此有所耳聞,也有不少人學習WebGL,就是衝著它的3D繪圖能力。

接下來,我就用一個簡單的正立方體的例子來演示在WebGL中如何繪製3D物體。

本文可配合影片食用

從二維到三維

首先,我們先來繪製一個熟悉的2D圖形,正方形。

// vertex
attribute vec2 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = vec4(a_vertexPosition, 1, 1);
}

// fragment
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}
// ...
renderer.setMeshData([{
  positions: [
    [-0.5, -0.5],
    [-0.5, 0.5],
    [0.5, 0.5],
    [0.5, -0.5]
  ],
  attributes: {
    color: [
      [1, 0, 0, 1],
      [1, 0, 0, 1],
      [1, 0, 0, 1],
      [1, 0, 0, 1],
    ]
  },
  cells: [[0, 1, 2], [2, 0, 3]]
}]);
// ...

上述這些程式碼比較簡單,我就不過多解釋了。

在畫布上我們看到,繪製了一個紅色的正方形,它是一個平面圖形。

視覺化學習:如何用WebGL繪製3D物體

接下來,我們就在這個圖形的基礎上,將它擴充為3D的正立方體。

要想把2維圖形擴充為3維幾何體,第一步就是要把頂點擴充套件到3維。也就是把vec2擴充套件為vec3。

// vertex
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = vec4(a_vertexPosition, 1);
}

當然僅僅修改Shader是不夠的,因為資料是從JavaScript傳遞過來的,所以我們需要在JavaScript中計算立方體的頂點資料,然後再傳遞給Shader。

一個立方體有8個頂點,能組成6個面。在WebGL中需要用12個三角形來繪製它。

視覺化學習:如何用WebGL繪製3D物體

如果6個面的屬性相同的話,我們可以複用8個頂點來繪製;

但如果屬性不完全相同,比如每個面要繪製成不同的顏色,或者新增不同的紋理圖片,就得把每個面的頂點分開。這樣的話,就需要24個頂點來分別處理6個面。

為了方便使用,我們可以定義一個JavaScript函式,用來生成立方體6個面的24個頂點,以及12個三角形的索引,並且定義每個面的顏色。

/**
 * 生成立方體6個面的24個頂點,12個三角形的索引,定義每個面的顏色資訊
 * @param size
 * @param colors
 * @returns {{cells: *[], color: *[], positions: *[]}}
 */
export function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
    const h = 0.5 * size;
    const vertices = [
        [-h, -h, -h],
        [-h, h, -h],
        [h, h, -h],
        [h, -h, -h],
        [-h, -h, h],
        [-h, h, h],
        [h, h, h],
        [h, -h, h]
    ];

    const positions = [];
    const color = [];
    const cells = [];

    let colorIdx = 0;
    let cellsIdx = 0;
    const colorLen = colors.length;

    function quad(a, b, c, d) {
        [a, b, c, d].forEach(item => {
            positions.push(vertices[item]);
            color.push(colors[colorIdx % colorLen]);
        });
        cells.push(
            [0, 1, 2].map(i => i + cellsIdx),
            [0, 2, 3].map(i => i + cellsIdx)
        );
        colorIdx ++;
        cellsIdx += 4;
    }

    quad(1, 0, 3, 2); // 內
    quad(4, 5, 6, 7); // 外
    quad(2, 3, 7, 6); // 右
    quad(5, 4, 0, 1); // 左
    quad(3, 0, 4, 7); // 下
    quad(6, 5, 1, 2); // 上

    return {positions, color, cells};
}

現在我們就可以透過呼叫cube這個函式,構建出立方體的頂點資訊。

const geometry = cube(1.0, [
    [1, 0, 0, 1],   // 紅
    [0, 0.5, 0, 1], // 綠
    [0, 0, 1, 1]    // 藍
]);

透過這段程式碼,我們就能建立出一個稜長為1的立方體,並且六個面的顏色分別是“紅、綠、藍、紅、綠、藍”。

接下來我們就要把這個立方體的頂點資訊傳遞給Shader。

在傳遞資料之前,我們需要先了解一個知識點,是關於繪製3D圖形與2D圖形存在的一點不同,那就是繪製3D圖形時,必須要開啟深度檢測和啟用深度緩衝區。

在WebGL中,我們可以透過gl.enable(gl.DEPTH_TEST);這段程式碼來開啟深度檢測;在清空畫布的時候,也要用gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);這段程式碼來同時清空顏色緩衝區和深度緩衝區。

啟動和清空深度檢測和深度緩衝區這兩個步驟,非常重要。但是一般情況下,我們幾乎不會用原生的方式來編寫程式碼,所以瞭解一下即可。為了方便使用,在本文演示的例子中,我們還是直接使用gl-renderer這個庫,它封裝了深度檢測,我們在使用時,在建立renderer的時候配置一個引數depth: true就可以了。

現在我們就把這個三維立方體用gl-renderer渲染出來。

// ...
renderer = new GlRenderer(glRef.value, {
  depth: true // 開啟深度檢測
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([{
  positions: geometry.positions,
  attributes: {
    color: geometry.color
  },
  cells: geometry.cells
}]);
renderer.render();

現在我們在畫布上看到的是一個紅色正方形,這是因為其他面被遮擋住了。

視覺化學習:如何用WebGL繪製3D物體

投影矩陣:變換WebGL座標系

但是,等等,為什麼我們看到的是紅色的一面呢?按照我們所編寫的程式碼,預期看到的應該是綠色的一面,也就是說我們預期Z軸是向外的,因為規範的直角座標系是右手座標系。所以按照現在的繪製結果,我們發現WebGL的座標系其實是左手系的?

但一般來說,不管什麼圖形庫或者圖形框架,在繪圖的時候,都會預設將座標系從左手系轉換為右手系,因為這更符合我們的使用習慣。所以這裡,我們也去把WebGL的座標系從左手系轉換為右手系,簡單來說,就是將Z軸座標方向反轉。關於座標轉換,可以透過齊次矩陣來完成。對座標轉換不熟悉的小夥伴,可以參考我之前的一篇關於仿射變換的文章。

將Z軸座標方向反轉,對應的齊次矩陣是這樣的:

[
	1, 0, 0, 0,
	0, 1, 0, 0,
	0, 0, -1, 0,
	0, 0, 0, 1
]

這種轉換座標的齊次矩陣,也被稱為投影矩陣,ProjectionMatrix。

現在我們修改一下頂點著色器,把這個投影矩陣新增進去。

// vertex
attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴充套件到vec3
attribute vec4 color; // 四維向量

varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換座標系

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = projectionMatrix * vec4(a_vertexPosition, 1.0);
}

現在我們就能看到畫布上顯示的是綠色的正方形了。

視覺化學習:如何用WebGL繪製3D物體

模型矩陣:讓立方體旋轉起來

現在我們只能看到立方體的一個面,因為Z軸是垂直於螢幕的,這樣子從視覺上看好像和2維圖形沒什麼區別,沒法讓人很直觀地聯想、感受到這是一個三維的幾何體,為了將其他的面露出來,我們可以去旋轉立方體。

要想旋轉立方體,我們同樣可以透過矩陣運算來實現。這個矩陣叫做模型矩陣,ModelMatrix,它定義了被繪製的物體變換。

把模型矩陣加入到頂點著色器中,將它與投影矩陣相乘,再乘上齊次座標,就得到最終的頂點座標了。

attribute vec3 a_vertexPosition; // 1:把頂點從vec2擴充套件到vec3
attribute vec4 color; // 四維向量

varying vec4 vColor;
uniform mat4 projectionMatrix; // 2:投影矩陣-變換座標系
uniform mat4 modelMatrix; // 3:模型矩陣-使幾何體旋轉

void main() {
  gl_PointSize = 1.0;
  vColor = color;
  gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}

現在我們定義一個JavaScript函式,用立方體沿x、y、z軸的旋轉來生成模型矩陣。

以x、y、z三個方向的旋轉得到三個齊次矩陣,然後將它們相乘,就能得到最終的模型矩陣。

import { multiply } from 'ogl/src/math/functions/Mat4Func.js';
// ...
export function fromRotation(rotationX, rotationY, rotationZ) {
    let c = Math.cos(rotationX);
    let s = Math.sin(rotationX);
    const rx = [
        1,  0, 0, 0, // 繞X軸旋轉
        0,  c, s, 0,
        0, -s, c, 0,
        0,  0, 0, 1
    ];

    c = Math.cos(rotationY);
    s = Math.sin(rotationY);
    const ry = [
        c,  0, s, 0,
        0,  1, 0, 0, // 繞Y軸旋轉
        -s, 0, c, 0,
        0,  0, 0, 1
    ];

    c = Math.cos(rotationZ);
    s = Math.sin(rotationZ);
    const rz = [
        c,  s, 0, 0,
        -s, c, 0, 0,
        0,  0, 1, 0, // 繞Z軸旋轉
        0,  0, 0, 1
    ];

    const ret = [];
    multiply(ret, rx, ry);
    multiply(ret, ret, rz);
    return ret;
}

我們把模型矩陣傳給頂點著色器,不斷更新三個旋轉角度,就能實現立方體旋轉的效果。

// ...
let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
  rotationX += 0.003;
  rotationY += 0.005;
  rotationZ += 0.007;
  renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
  requestAnimationFrame(update);
}
update();
// ...

現在我們就能在旋轉中看到立方體的其他幾個面了,能更直觀地感受到這是一個三維物體。

視覺化學習:如何用WebGL繪製3D物體

總結

至此,我們就實現了正立方體的繪製。在3D物體的繪製中,正立方體屬於是比較簡單的一類,螢幕前的小夥伴們都可以來動手嘗試下,感興趣的小夥伴,還可以嘗試去實現圓柱體、正四面體等等這些幾何體的繪製。

參考程式碼

效果預覽

相關文章