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 的圓形。
如果你記得第 3 年級基礎數學中將某個項目乘以 1,會保持不變。所以 123 * 1 = 123。基本原則單位圓形、半徑 1.0 的圓形也都是 1 形式。是旋轉 1。你可以將這個單位圓形乘上 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 D 未知數為 π/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,000,旋轉 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 個值,因此針對第三個值,我們使用 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 | 十 | 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
如果你記得代數,可以刪除任何乘法乘以 0 的位置。以 1 相乘效果不會真的沒有問題,讓我們簡化以瞭解實際情況
newX = x + tx;
newY = y + ty;
還有一點我們完全不在意這看起來有點出乎意料,與翻譯範例中的翻譯程式碼相似。同樣地,我們一起旋轉就像我們向旋轉貼文說的例子,只需要使用要旋轉的角度正弦和餘弦。
s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);
我們要建構像這樣的矩陣
c | - 秒 | 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 和 1s
newX = x * c + y * s;
newY = x * -s + y * c;
這正是輪替樣本中的確切值。最後就能擴大規模我們將 2 個縮放比例係數 Sx, Sy 然後建構像這樣的矩陣
六 | 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
我也知道
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,另一個乘以 -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 設計,希望大家能保持簡單易懂。