WebGL 2D 矩陣

2018-10-03 11:26 更新

WebGL 2D 矩陣

在前面三篇文章中我們講解了如何平移幾何圖形,如何旋轉(zhuǎn)幾何圖形,如何伸縮變換圖形。平移,旋轉(zhuǎn)和伸縮都被認(rèn)為是一種變化類型。每一種變化都需要改變渲染器,而且他們依賴于操作的順序。在前面的例子中我們進(jìn)行了伸縮,旋轉(zhuǎn)和平移操作。如果他們執(zhí)行操作的順序改變將會(huì)得到不同的結(jié)果。例如 XY 伸縮變換為 2,1,旋轉(zhuǎn) 30%,接著平移變換 100,0。

transformation1

如下是平移 100,0,旋轉(zhuǎn) 30%,接著伸縮變換 2,1。

transformation2

結(jié)果是完全不同的。更糟糕的是,如果我們需要得到的是第二個(gè)示例的效果,就必須編寫一個(gè)不同的渲染器,按照我們想要的執(zhí)行順序進(jìn)行平移,旋轉(zhuǎn)和伸縮變換。

然而,有些比我聰明的人利用數(shù)學(xué)中的矩陣能夠解決上面這個(gè)問題。對(duì)于 2d 圖形,使用一個(gè) 3X3 的矩陣。3X3 的矩陣類似了 9 宮格。

9 boxes

數(shù)學(xué)中的操作是與列相乘然后把結(jié)果加在一起。一個(gè)位置有兩個(gè)值,用 x 和 y 表示。但是為了實(shí)現(xiàn)這個(gè)需要三個(gè)值,因?yàn)槲覀儗?duì)第三個(gè)值設(shè)為 1。

在上面的例子中就變成了:

matrix transformation

對(duì)于上面的處理你也許會(huì)想“這樣處理的原因在哪里”。假設(shè)要執(zhí)行平移變換。我們將想要執(zhí)行的平移的總量為 tx 和 ty。構(gòu)造如下的矩陣:

matrix

接著進(jìn)行計(jì)算:

multiply matrix

如果你還記得代數(shù)學(xué),就可以那些乘積結(jié)果為零的位置。乘以 1 的效果相當(dāng)于什么都沒做,那么將計(jì)算簡(jiǎn)化看看發(fā)生了什么:

simplify result

或者更簡(jiǎn)潔的方式:

newX = x + tx;
newY = y + ty;

extra 變量我們并不用在意。這個(gè)處理和我們?cè)谄揭浦芯帉懙拇a驚奇的相似。

同樣地,讓我們看看旋轉(zhuǎn)。正如在旋轉(zhuǎn)那篇中指出當(dāng)我們想要進(jìn)行旋轉(zhuǎn)的時(shí)候,我們只需要角度的 sine 和 cosine 值。

s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);

構(gòu)造如下的矩陣:

rotate_matrix

執(zhí)行上面的矩形操作:

rotate_apply_matrix

將得到 0 和 1 結(jié)果部分用黑色塊表示了。

rotate_matrix_result

同樣可以簡(jiǎn)化計(jì)算:

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

上面處理的結(jié)果剛好和旋轉(zhuǎn)例子效果一樣。

最后是伸縮變換。稱兩個(gè)伸縮變換因子為 sx 和 sy。

構(gòu)造如下的矩陣:

scale_matrix

進(jìn)行矩陣操作會(huì)得到如下:

scale apply matrix

實(shí)際需要計(jì)算:

scale matrix result

簡(jiǎn)化為:

newX = x * sx;
newY = y * sy;

和我們以前講解的伸縮示例是一樣的。

到了這里,我堅(jiān)信你仍然在思考,這樣處理之后了?有什么意義。看起來(lái)好象它只是做了和我們以前一樣的事。

接下來(lái)就是魔幻的地方。已經(jīng)被證明了我們可以將多個(gè)矩陣乘在一起,接著一次執(zhí)行完所有的變換。假設(shè)有函數(shù) matrixMultiply,它帶兩個(gè)矩陣做參數(shù),將他們倆相乘,返回乘積結(jié)果。

為了讓上面的做法更清楚,于是編寫如下的函數(shù)構(gòu)建一個(gè)用來(lái)平移,旋轉(zhuǎn)和伸縮的矩陣:

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
  ];
}

接下來(lái),修改渲染器。以往的渲染器是如下的形式:

<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;
...

新的渲染器將會(huì)變得更簡(jiǎn)單:

<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);
  }

如下是使用新的代碼的示例。平移,旋轉(zhuǎn)和伸縮滑動(dòng)條是一樣的。但是他們?cè)阡秩酒魃蠎?yīng)用的更簡(jiǎn)單。

此時(shí),你仍然會(huì)問,之后了?這個(gè)看起來(lái)并沒有方便多少。然而,此時(shí)如果你想改變執(zhí)行的順序,就不再需要編寫一個(gè)新的渲染器了。我們僅僅只需要改變數(shù)序公式。

  ...
    // Multiply the matrices.
    var matrix = matrixMultiply(translationMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, scaleMatrix);
    ...

如下是新版本:

能夠按照這種方式執(zhí)行矩陣操作是特別重要的,特別是對(duì)于層級(jí)動(dòng)畫的實(shí)現(xiàn)比如身體上手臂的,在一個(gè)星球上看月球同時(shí)在圍繞著太陽(yáng)旋轉(zhuǎn),或者數(shù)上的樹枝等都是很重要的。舉一個(gè)簡(jiǎn)單的層級(jí)動(dòng)畫例子,現(xiàn)在想要繪制 5 次 ‘F’,但是每次繪制是從上一個(gè) ‘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);
    }
  }

為了實(shí)現(xiàn)這個(gè),我們要編寫自己的函數(shù) makeIdentity,這個(gè)函數(shù)返回單位矩陣。單位矩陣實(shí)際上表示的類似于 1.0 的矩陣,如果一個(gè)矩陣乘以單位矩陣,那么得到的還是原先那個(gè)矩陣。就如:

X*1 = X

同樣:

matrixX*identity = matrixX

如下是構(gòu)造單位矩陣的代碼:

function makeIdentity() {
  return [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
  ];
}

如下是 5 個(gè) F:

再來(lái)一個(gè)示例,在前面示例中,‘F’ 旋轉(zhuǎn)總是繞坐上角。這是因?yàn)槲覀兪褂玫臄?shù)學(xué)方法總是圍著源點(diǎn)旋轉(zhuǎn),并且 ‘F’ 的左上角就是原點(diǎn),(0,0)。

但是現(xiàn)在,因?yàn)槲覀兡軌蚴褂镁仃嚕敲淳涂梢赃x擇變化的順序,可以在執(zhí)行其他的變換之前先移動(dòng)原點(diǎn)。

 // 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);

如下所示,注意 F 可以圍著中心點(diǎn)進(jìn)行旋轉(zhuǎn)和伸縮。

使用如上的方法,你可以圍著任何點(diǎn)進(jìn)行旋轉(zhuǎn)或者伸縮?,F(xiàn)在你就明白了 Photoshop 或者 Flash 中實(shí)現(xiàn)繞某點(diǎn)旋轉(zhuǎn)的原理。

讓我們學(xué)習(xí)更深入點(diǎn)。如果你回到本系列的第一篇文章 WebGL 基本原理,你也許還記得我們編寫的渲染器的代碼中將像素轉(zhuǎn)換成投影空間,如下所示:

  ...
  // 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);

如果你現(xiàn)在反過來(lái)看下每一步,第一步,“將像素變換成 0.0 變成 1.0”,其實(shí)是一個(gè)伸縮操作。第二步同樣是伸縮變換。接下來(lái)是平移變換,并且 Y 的伸縮因子是 -1。我們可以通過將該矩陣傳給渲染器實(shí)現(xiàn)上面的所有操作。可以構(gòu)造二維伸縮矩陣,其中一個(gè)伸縮因子設(shè)置為 1.0/分辨率,另外一個(gè)伸縮因子設(shè)置為 2.0,第三個(gè)使用 -1.0,-1.0 來(lái)進(jìn)行移動(dòng),并且第四個(gè)設(shè)置伸縮因子 Y 為 -1,接著將他們乘在一起,然而,因?yàn)閿?shù)學(xué)是很容易的,我們僅僅只需編寫一個(gè)函數(shù),能夠直接將給定的分辨率轉(zhuǎn)換成投影矩陣。

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
  ];
}

現(xiàn)在我們能進(jìn)一步簡(jiǎn)化渲染器。如下是完整的頂點(diǎn)渲染器。

<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.clientWidth, canvas.clientHeight);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);
    matrix = matrixMultiply(matrix, projectionMatrix);
    ...
  }

我們也移出了設(shè)置分辨率的代碼。最后一步,通過使用數(shù)學(xué)矩陣就將原先需要 6-7 步操作復(fù)雜的渲染器變成僅僅只需要 1 步操作的更簡(jiǎn)單的渲染器。

希望這篇文章能夠讓你理解矩陣數(shù)學(xué)。接下來(lái)會(huì)講解 3D 空間的知識(shí)。在 3D 中矩陣數(shù)學(xué)遵循同樣的規(guī)律和使用方式。從 2D 開始講解是希望讓知識(shí)簡(jiǎn)單易懂。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)