WebGL 2D 轉譯
在進入 3D 之前,我們先繼續討論 2D。請稍候。這篇文章對某些人來說可能顯而易見,但我會在幾篇文章中逐步說明。
本文是 WebGL 基礎知識系列文章的續篇。如果您尚未閱讀,建議您至少先閱讀第一章,然後再回到這裡。「平移」是數學術語,基本上就是「移動」的意思。將英文句子翻譯成日文也算是一種轉換,但我們這裡討論的是幾何圖形的轉換。使用第一篇文章中最後的程式碼範例,只要變更傳遞至 setRectangle 的值,就能輕鬆平移矩形,對吧?以下是根據先前範例製作的範例。
// First lets make some variables
// to hold the translation of the rectangle
var translation = [0, 0];
// then let's make a function to
// re-draw everything. We can call this
// function after we update the translation.
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Setup a rectangle
setRectangle(gl, translation[0], translation[1], width, height);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
到目前為止都很順利。但現在假設我們想對更複雜的形狀執行相同操作。假設我們要繪製由 6 個三角形組成的「F」,如下所示。
根據目前的程式碼,我們必須將 setRectangle 變更為類似以下內容。
// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl, x, y) {
var width = 100;
var height = 150;
var thickness = 30;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// left column
x, y,
x + thickness, y,
x, y + height,
x, y + height,
x + thickness, y,
x + thickness, y + height,
// top rung
x + thickness, y,
x + width, y,
x + thickness, y + thickness,
x + thickness, y + thickness,
x + width, y,
x + width, y + thickness,
// middle rung
x + thickness, y + thickness * 2,
x + width * 2 / 3, y + thickness * 2,
x + thickness, y + thickness * 3,
x + thickness, y + thickness * 3,
x + width * 2 / 3, y + thickness * 2,
x + width * 2 / 3, y + thickness * 3]),
gl.STATIC_DRAW);
}
您應該可以發現,這種做法無法有效擴充。如果想繪製非常複雜的幾何圖形,可能需要數百或數千行程式碼,這會非常複雜。此外,每次繪製時,JavaScript 都必須更新所有點。其實有更簡單的方法。只要上傳幾何圖形,並在著色器中進行平移即可。這是新的著色器
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
void main() {
// Add in the translation.
vec2 position = a_position + u_translation;
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = position / u_resolution;
...
並稍微重組程式碼。首先,我們只需要設定幾何圖形一次。
// 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,
30, 0,
0, 150,
0, 150,
30, 0,
30, 150,
// top rung
30, 0,
100, 0,
30, 30,
30, 30,
100, 0,
100, 30,
// middle rung
30, 60,
67, 60,
30, 90,
30, 90,
67, 60,
67, 90]),
gl.STATIC_DRAW);
}
接著,只要在繪製前,使用所需的平移值更新 u_translation 即可。
...
var translationLocation = gl.getUniformLocation(
program, "u_translation");
...
// Set Geometry.
setGeometry(gl);
..
// Draw scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
請注意,系統只會呼叫 setGeometry 一次。它不再位於 drawScene 內。
現在,WebGL 幾乎包辦了所有繪圖工作。我們只是設定平移並要求繪製。即使幾何圖形有數萬個點,主要程式碼仍會保持不變。
WebGL 2D 旋轉
我先承認,我不知道我的解釋是否合理,但管他的,試試看也無妨。
首先,我想介紹「單位圓」的概念。如果你還記得國中數學 (別睡著了!),圓形有半徑。圓形的半徑是指從圓心到圓周的距離。單位圓是半徑為 1.0 的圓。
如果你還記得小學三年級的基礎數學,任何數字乘以 1 都會保持不變。因此 123 * 1 = 123。很簡單吧?半徑為 1.0 的單位圓也是 1 的一種形式。這是一台兩用捲髮器。因此,你可以將某個值乘以這個單位圓,在某種程度上,這就像是乘以 1,但神奇的是,事物會旋轉。 我們將從單位圓上的任何一點取得 X 和 Y 值,並將先前範例中的幾何圖形乘以這些值。以下是著色器的更新內容。
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
void main() {
// Rotate the position
vec2 rotatedPosition = vec2(
a_position.x * u_rotation.y + a_position.y * u_rotation.x,
a_position.y * u_rotation.y - a_position.x * u_rotation.x);
// Add in the translation.
vec2 position = rotatedPosition + u_translation;
接著更新 JavaScript,以便傳入這 2 個值。
...
var rotationLocation = gl.getUniformLocation(program, "u_rotation");
...
var rotation = [0, 1];
..
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Set the rotation.
gl.uniform2fv(rotationLocation, rotation);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
為什麼有效?我們來算算看。
rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;
假設您有一個矩形,並想旋轉它。在開始旋轉之前,右上角位於 3.0、9.0。我們在單位圓上選取一個點,從 12 點鐘方向順時針旋轉 30 度。
圓圈上的位置是 0.50 和 0.87
3.0 * 0.87 + 9.0 * 0.50 = 7.1
9.0 * 0.87 - 3.0 * 0.50 = 6.3
這正是我們需要的位置
順時針旋轉 60 度也一樣
圓圈上的位置是 0.87 和 0.50
3.0 * 0.50 + 9.0 * 0.87 = 9.3
9.0 * 0.50 - 3.0 * 0.87 = 1.9
可以看到,當我們將該點順時針向右旋轉時,X 值會變大,Y 值會變小。如果超過 90 度,X 會再次變小,Y 則會變大。這個模式會產生旋轉。單位圓上的點還有另一個名稱。分別是正弦和餘弦。因此,只要知道角度,就能像這樣查出正弦和餘弦值。
function printSineAndCosineForAnAngle(angleInDegrees) {
var angleInRadians = angleInDegrees * Math.PI / 180;
var s = Math.sin(angleInRadians);
var c = Math.cos(angleInRadians);
console.log("s = " + s + " c = " + c);
}
如果將程式碼複製並貼到 JavaScript 控制台,然後輸入 printSineAndCosignForAngle(30),就會看到系統輸出 s = 0.49 c= 0.87 (注意:我將數字四捨五入)。綜合以上步驟,您就能將幾何圖形旋轉至任何角度。只要將旋轉角度設為所需旋轉角度的正弦和餘弦即可。
...
var angleInRadians = angleInDegrees * Math.PI / 180;
rotation[0] = Math.sin(angleInRadians);
rotation[1] = Math.cos(angleInRadians);
希望以上說明對您有幫助。接下來是更簡單的例子。擴充
什麼是弧度?
弧度是用於圓形、旋轉和角度的測量單位。就像我們可以用英吋、碼、公尺等單位測量距離一樣,我們也可以用度或弧度測量角度。
你可能知道,公制單位的數學運算比英制單位更簡單。如要將英寸換算成英尺,請除以 12。如要將英寸換算成碼,請除以 36。我不知道你能不能,但我無法在腦中除以 36。公制單位就簡單多了。如要將公釐換算成公分,請除以 10。將公釐除以 1000 即可換算成公尺。我可以在腦中除以 1000。
弧度和角度類似。角度會讓數學計算變得困難。弧度可簡化數學運算。圓形有 360 度,但只有 2π 弧度。因此,完整一圈是 2π 弧度。半圈是 π 弧度。1/4 圈 (即 90 度) 為 π/2 弧度。因此,如要將某個項目旋轉 90 度,只要使用 Math.PI * 0.5 即可。如要旋轉 45 度,請使用 Math.PI * 0.25 等。
如果開始以弧度思考,幾乎所有涉及角度、圓形或旋轉的數學問題都會變得非常簡單。歡迎試用。請使用弧度,而非度數 (UI 顯示除外)。
WebGL 2D 縮放
縮放操作和翻譯一樣簡單。
將位置乘以所需比例。以下是與先前範例不同的地方。
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;
void main() {
// Scale the positon
vec2 scaledPosition = a_position * u_scale;
// Rotate the position
vec2 rotatedPosition = vec2(
scaledPosition.x * u_rotation.y +
scaledPosition.y * u_rotation.x,
scaledPosition.y * u_rotation.y -
scaledPosition.x * u_rotation.x);
// Add in the translation.
vec2 position = rotatedPosition + u_translation;
並加入繪製時設定縮放比例所需的 JavaScript。
...
var scaleLocation = gl.getUniformLocation(program, "u_scale");
...
var scale = [1, 1];
...
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Set the rotation.
gl.uniform2fv(rotationLocation, rotation);
// Set the scale.
gl.uniform2fv(scaleLocation, scale);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
請注意,如果縮放值為負數,幾何圖形會翻轉。希望最後 3 章能幫助你瞭解平移、旋轉和縮放。接下來,我們將介紹矩陣的神奇之處,矩陣能將這 3 種轉換結合為更簡單、通常也更實用的形式。
為什麼是「F」?
我第一次看到有人使用「F」是在材質上。「F」本身並不重要。重點是,無論從哪個方向看,都能辨識出方向。如果我們使用愛心 ♥ 或三角形 △,就無法判斷是否水平翻轉。圓圈 ○ 則更不理想。彩色矩形或許可以為每個角落設定不同顏色,但你必須記住每個角落代表的意義。F 的方向可以立即辨識。
只要能看出方向,任何形狀都可以。我從第一次接觸這個概念起,就一直使用「F」。
WebGL 2D 矩陣
在過去 3 個章節中,我們介紹了如何平移、旋轉和縮放幾何圖形。平移、旋轉和縮放都屬於「轉換」類型。每項轉換都需要變更著色器,且 3 項轉換都必須按照順序進行。
舉例來說,以下是縮放比例 2、1、旋轉 30%,以及平移 100、0。
平移 100,0、旋轉 30% 和縮放 2, 1
結果完全不同。更糟的是,如果需要第二個範例,我們就必須編寫不同的著色器,按照新的所需順序套用平移、旋轉和縮放。 有些比我聰明得多的人發現,你可以用矩陣數學完成所有相同的工作。2D 則使用 3x3 矩陣。3x3 矩陣就像有 9 個方格的網格。
| 1.0 | 2.0 | 3.0 |
| 4.0 | 5.0 | 6.0 |
| 7.0 | 8.0 | 9.0 |
計算方式是將矩陣中各欄的位置相乘,然後將結果相加。位置只有 2 個值 (x 和 y),但進行這項運算需要 3 個值,因此第 3 個值會使用 1。 在本例中,結果會是
newX = x * 1.0 + y * 4.0 + 1 * 7.0
newY = x * 2.0 + y * 5.0 + 1 * 8.0
extra = x * 3.0 + y * 6.0 + 1 * 9.0
你可能會想:「這有什麼意義?」假設我們有翻譯。我們將要平移的量稱為 tx 和 ty。我們來建立一個像這樣的矩陣
| 1.0 | 0.0 | 0.0 |
| 0.0 | 1.0 | 0.0 |
| tx | ty | 1.0 |
現在來看看
newX = x * 1.0 + y * 0.0 + 1 * tx
newY = x * 0.0 + y * 1.0 + 1 * ty
extra = x * 0.0 + y * 0.0 + 1 * 1
根據代數原理,我們可以刪除任何乘以零的位置。乘以 1 實際上不會有任何作用,因此我們簡化一下,看看會發生什麼事
newX = x + tx;
newY = y + ty;
我們不太在乎額外的部分。這與翻譯範例中的翻譯程式碼非常相似。同樣地,我們來旋轉。正如我們在旋轉貼文中指出的,我們只需要旋轉角度的正弦和餘弦。
s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);
我們建立這樣的矩陣
| c | -s | 0.0 |
| 秒 | c | 0.0 |
| 0.0 | 0.0 | 1.0 |
套用矩陣後,會得到
newX = x * c + y * s + 1 * 0
newY = x * -s + y * c + 1 * 0
extra = x * 0.0 + y * 0.0 + 1 * 1
將所有乘以 0 和 1 的部分塗黑,
newX = x * c + y * s;
newY = x * -s + y * c;
這與旋轉範例中的結果完全相同。最後是擴充。我們將 2 個縮放比例係數稱為 sx 和 sy,並建立如下矩陣
| sx | 0.0 | 0.0 |
| 0.0 | sy | 0.0 |
| 0.0 | 0.0 | 1.0 |
套用矩陣後,會得到
newX = x * sx + y * 0 + 1 * 0
newY = x * 0 + y * sy + 1 * 0
extra = x * 0.0 + y * 0.0 + 1 * 1
這項功能
newX = x * sx;
newY = y * sy;
這與我們的縮放範例相同。現在,您可能還在思考。那又怎樣?有什麼意義?這似乎需要花費大量精力,才能完成我們已經在做的事情?這時,神奇之處就來了。原來我們可以將矩陣相乘,一次套用所有轉換。假設我們有一個函式 matrixMultiply,會接收兩個矩陣,將兩者相乘並傳回結果。為了讓內容更清楚,我們來建立函式,建構平移、旋轉和縮放矩陣。
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
];
}
現在來變更著色器。舊著色器看起來像這樣
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;
void main() {
// Scale the positon
vec2 scaledPosition = a_position * u_scale;
// Rotate the position
vec2 rotatedPosition = vec2(
scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);
// Add in the translation.
vec2 position = rotatedPosition + u_translation;
...
我們的新著色器會簡單得多。
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform mat3 u_matrix;
void main() {
// Multiply the position by the matrix.
vec2 position = (u_matrix * vec3(a_position, 1)).xy;
...
以下說明我們如何使用這項功能
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Compute the matrices
var translationMatrix =
makeTranslation(translation[0], translation[1]);
var rotationMatrix = makeRotation(angleInRadians);
var scaleMatrix = makeScale(scale[0], scale[1]);
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
// Set the matrix.
gl.uniformMatrix3fv(matrixLocation, false, matrix);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
不過,您可能會想問,那又怎樣?這似乎沒什麼好處。但現在如果想變更順序,就不必編寫新的著色器。只要修改數學公式即可。
...
// Multiply the matrices.
var matrix = matrixMultiply(translationMatrix, rotationMatrix);
matrix = matrixMultiply(matrix, scaleMatrix);
...
能夠套用這類矩陣,對於階層式動畫 (例如身體上的手臂、圍繞太陽的行星上的月亮,或樹上的樹枝) 尤其重要。以下是階層式動畫的簡單範例,我們繪製「F」5 次,但每次都從前一個「F」的矩陣開始。
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Compute the matrices
var translationMatrix = makeTranslation(translation[0], translation[1]);
var rotationMatrix = makeRotation(angleInRadians);
var scaleMatrix = makeScale(scale[0], scale[1]);
// Starting Matrix.
var matrix = makeIdentity();
for (var i = 0; i < 5; ++i) {
// Multiply the matrices.
matrix = matrixMultiply(matrix, scaleMatrix);
matrix = matrixMultiply(matrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
// Set the matrix.
gl.uniformMatrix3fv(matrixLocation, false, matrix);
// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
}
為此,我們必須導入 makeIdentity 函式,建立單位矩陣。單位矩陣是有效代表 1.0 的矩陣,因此乘以單位矩陣不會產生任何效果。就像
X * 1 = X
so too
matrixX * identity = matrixX
以下是建立單位矩陣的程式碼。
function makeIdentity() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1
];
}
再舉一個例子,到目前為止,在每個範例中,「F」都是以左上角為中心旋轉。這是因為我們使用的數學運算一律會繞著原點旋轉,而「F」的左上角位於原點 (0, 0)。 但現在,由於我們可以進行矩陣數學運算,並選擇套用轉換的順序,因此可以在套用其餘轉換之前移動原點。
// make a matrix that will move the origin of the 'F' to
// its center.
var moveOriginMatrix = makeTranslation(-50, -75);
...
// Multiply the matrices.
var matrix = matrixMultiply(moveOriginMatrix, scaleMatrix);
matrix = matrixMultiply(matrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
使用這項技術,您就能從任何點旋轉或縮放。現在您知道 Photoshop 或 Flash 如何讓您移動旋轉點。我們來做更瘋狂的事。如果回顧第一篇 WebGL 基本概念文章,您可能會記得著色器中有程式碼,可將像素轉換為裁剪空間,如下所示。
...
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
如果依序查看每個步驟,第一個步驟「將像素轉換為 0.0 到 1.0」實際上是縮放作業。第二個也是擴充作業。接著是平移,最後將 Y 縮放 -1。實際上,我們可以在傳遞至著色器的矩陣中完成所有操作。我們可以建立 2 個縮放矩陣,一個縮放 1.0/解析度,另一個縮放 2.0,第 3 個平移 -1.0、-1.0,第 4 個將 Y 縮放 -1,然後將它們全部相乘,但由於數學很簡單,我們將建立一個函式,直接為給定的解析度建立「投影」矩陣。
function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so that 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}
現在,我們可以進一步簡化著色器。以下是完整的全新頂點著色器。
<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>
在 JavaScript 中,我們需要將投影矩陣
// Draw the scene.
function drawScene() {
...
// Compute the matrices
var projectionMatrix =
make2DProjection(canvas.width, canvas.height);
...
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);
...
}
我們也移除了設定解析度的程式碼。最後這個步驟,我們從相當複雜的 6 到 7 個步驟著色器,變成只有 1 個步驟的簡單著色器,這一切都歸功於矩陣數學的魔力。
希望這篇文章能幫助您瞭解矩陣數學。接下來,我會介紹 3D。3D 矩陣數學遵循相同的原則和用法。我先從 2D 開始,希望簡單易懂。