WebGL 圖像 3D

Gregg Tavares
Gregg Tavares

WebGL 方向圖形 3D

本文將延續我們與 WebGL 相關的一系列文章。 第一個從基礎概念開始,前一句是關於 2D 矩陣的 2D 矩陣。請先閱讀這些內容。在上一篇文章中,我們介紹了 2D 矩陣的運作方式。我們談到了平移、旋轉、縮放,甚至是從像素投影到裁剪空間,都可以透過 1 矩陣和一些魔法矩陣數學的方式完成。只要再執行一個小步驟,就能建立 3D 圖像。在先前的 2D 範例中,我們是將 2D 點 (x, y) 乘以 3x3 矩陣。如要執行 3D 模型,我們需要 3D 點 (x、y、z) 和 4x4 矩陣。讓我們用最後一個範例,改成 3D。我們會再使用 F,但這次是 3D「F」。 首先,我們必須變更頂點著色器來處理 3D。這是舊的著色器。

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

新內容上線

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

它變得更加簡單了!接著,我們需要提供 3D 資料。

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

接著,我們要將所有矩陣函式從 2D 變更為 3D 以下是 makeTranslation、MakeRotation 和 makeScale 的 2D 版本

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

你會看到更新後的 3D 版本

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

請注意,現在有 3 種旋轉函式。我們只需要 2D 圖像 只能有效沿著 Z 軸旋轉雖然現在是 3D 模型 我們也希望可以同時在 X 軸和 Y 軸上旋轉從檢視可以看出它們都很類似。如果我們完成這項工作,就能像之前一樣簡化

Z 軸旋轉

newX = x * c + y * s;
newY = x * -s + y * c;

Y 軸旋轉


newX = x * c + z * s;
newZ = x * -s + z * c;

X 軸旋轉

newY = y * c + z * s;
newZ = y * -s + z * c;

我們也須更新投影函式。以下是舊的播放內容

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

因此會從像素轉換成裁剪空間這是第一次嘗試將圖片展開成 3D 格式

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

就像我們要將 x 和 y 的像素轉換成裁剪空間一樣,若是 z,也需要進行相同操作。在這個範例中,我也要製作 Z Space 像素單位我會傳遞一些類似 width 的值,表示深度為 0 至寬度像素、高度為 0 至高度像素,但深度為 - 深度 / 2 至 + 深度 / 2。最後,我們必須更新計算矩陣的程式碼。

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

第一個問題是,我們的幾何圖形是一個扁平的 F,因此很難看到任何 3D。為了修正這個問題,我們會將幾何圖形展開為 3D。目前的 F 是由 3 個矩形和 2 個三角形組成。如要製作 3D 圖像 總共需要 16 個矩形這裡列出的功能可說是不少。 16 個矩形 x 每個矩形 2 個三角形 x 每個三角形有 3 個頂點,共 96 個頂點。如要查看範例中的所有來源,我們得繪製更多頂點

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

移動滑桿並不容易,因為這是 3D 畫面並不容易。讓我們試試為矩形設定不同顏色為此,我們需要為頂點著色器新增另一個屬性,並將各項屬性從頂點著色器傳遞至片段著色器。新版頂點著色器隆重登場

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

而我們要在片段著色器中使用該顏色

<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
gl_FragColor = v_color;
}
</script>

我們必須查詢位置來提供顏色,然後設定另一個緩衝區和屬性來提供顏色。

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

不好啦,那是什麼東西?這樣,就可以讓 3D「F」、正面、背面、側面等各種部分,依照幾何圖形中的顯示順序繪製。這樣並不會獲得所需的結果,因為有時後方的結果,會是在位於正面的結果之後繪製。WebGL 中的三角形具有前置和後置的概念。面向前方的三角形則其頂點往順時針方向。後方的三角形,其頂點朝逆時針方向。

旋轉三角形。

WebGL 只能繪製正向或後置三角形。我們可以

gl.enable(gl.CULL_FACE);

我們只要執行一次就好啟用這項功能後,WebGL 預設為「縮減」後方的三角形。在本例中,「Culling」是「not draw」的有趣單字。請注意,在 WebGL 尚無異議的情況下,無論三角形是否被視為順時針或逆時針,皆取決於裁剪空間中該三角形的頂點。換句話說,在您為頂點著色器中的頂點套用數學之後,WebGL 就會判斷三角形是正面還是背面。舉例來說,在 X 中以 -1 縮放的順時針三角形,會變成逆時針三角形;如果以 X 軸或 Y 軸為中心旋轉 180 度,就會變成逆時針三角形。因為我們已停用 CULL_FACE,因此我們可以看到順時針(前方) 和逆時針(返回) 三角形。既然由於縮放或旋轉等原因,使得正面的三角形在翻轉,現在只要開啟這項功能,WebGL 就不會繪製。這很不錯,因為您在 3D 圖像的轉折時,一般會希望將哪些三角形面向您視為正面。

嘿!三角形都去哪裡了?結果卻有很多人遭遇錯誤的方法只要旋轉手機或向另一側拍攝,你就會看見這些文字。幸好這個問題很容易解決。我們會看看哪些管道倒退,然後交換 2 個端點。舉例來說,如果一個後三角形

1,   2,   3,
40,  50,  60,
700, 800, 900,

我們只要翻轉最後 2 個端點

1,   2,   3,
700, 800, 900,
40,  50,  60,

這樣就差一點,但還有一個問題。即使所有三角形面朝正確的方向,且正朝向的三角形,我們仍有許多地方,其中還有一些應該位於後方的三角形,而是繪製在應位於前方的三角形。輸入「DEPTH BUFFER」。深度緩衝區 (有時稱為 Z-Buffer) 是 depth 像素的矩形,每個色彩像素各有一個深度像素。WebGL 繪製每個彩色像素時,也可以繪製深度像素。這項作業是根據我們從 Z 的頂點著色器傳回的值執行。就像我們必須轉換成 X 和 Y 的裁剪空間一樣,所以轉換成 Z 時位於裁剪空間或 (-1 到 +1)。接著將該值轉換為深度空間值 (0 到 +1)。 WebGL 繪製色彩像素之前,會檢查對應的深度像素。如果所繪製像素的深度值大於對應深度像素的值,WebGL 不會繪製新的色彩像素。否則,系統會用片段著色器的顏色繪製新的色彩像素,「且」會使用新的深度值繪製深度像素。也就是說,無法繪製在其他像素後方的像素。開啟這項功能時 就像使用

gl.enable(gl.DEPTH_TEST);

開始繪製前,我們也需要將深度緩衝區清除回 1.0。

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

下一則訊息將說明如何展現多元風格。

為什麼屬性是 vec4,但 gl.vertexAttribPointer size 3

有些人特別注重細節,可能已經發現我們將 2 個屬性定義為

attribute vec4 a_position;
attribute vec4 a_color;

兩者都是「vec4」,但當您告知 WebGL 該如何從先前使用的緩衝區中擷取資料

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

每個條件都「3」表示每個屬性只計算出 3 個值。 這項功能可正常運作,因為 頂點著色器 WebGL 會針對您未提供的屬性提供預設值。預設值為 0、0、0、1,其中 x = 0、y = 0、z = 0 和 w = 1。這就是為什麼舊版 2D 頂點著色器必須明確提供 1。我們傳入 x 和 y,需要 1 代表 z,但由於 z 的預設值為 0,因此我們必須明確提供 1。以 3D 來說,雖然我們未提供 'w',但其預設使用 1,以便進行矩陣數學運算。