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 個旋轉函式。由於我們實際上只會繞著 Z 軸旋轉,因此在 2D 中只需要一個。不過,為了進行 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 空間像素單位。我會傳入與 width
相似的深度值,因此我們的空間寬度會是 0 到寬度像素,高度會是 0 到高度像素,但深度會是 -depth / 2 到 +depth / 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 drawing」的另一種說法。請注意,就 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,
這已經比較接近,但仍有另一個問題。即使所有三角形都朝向正確方向,且背面朝向的三角形已淘汰,仍有部分地方會出現應在後方繪製的三角形,而非應在前方繪製的三角形。輸入深度緩衝區。深度緩衝區 (有時稱為 Z 緩衝區) 是 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 大小為 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,並需要 z 為 1,但由於 z 的預設值為 0,因此我們必須明確提供 1。不過,對於 3D 而言,即使我們未提供 'w',預設值仍為 1,這正是矩陣運算所需的值。