鍍金池/ 教程/ HTML/ WebGL 2D 矩陣
WebGL 文本 HTML
WebGL 文本 Canvas 2D
WebGL 2D 圖像旋轉(zhuǎn)
WebGL 圖像處理(續(xù))
WebGL 2D 矩陣
WebGL 繪制多個(gè)東西
WebGL 圖像處理
WebGL 2D 圖像轉(zhuǎn)換
WebGL 3D 透視
WebGL 是如何工作的
WebGL 文本 紋理
WebGL 2D 圖像伸縮
WebGL 場(chǎng)景圖
WebGL 3D 攝像機(jī)
WebGL 文本 使用字符紋理
WebGL 正交 3D
WebGL 基本原理
WebGL - 更少的代碼,更多的樂(lè)趣
WebGL 著色器和 GLSL

WebGL 2D 矩陣

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

http://wiki.jikexueyuan.com/project/webgl/images/transformation1.png" alt="transformation1" />

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

http://wiki.jikexueyuan.com/project/webgl/images/transformation2.png" alt="transformation2" />

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

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

http://wiki.jikexueyuan.com/project/webgl/images/nine_boxes.png" alt="9 boxes" />

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

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

http://wiki.jikexueyuan.com/project/webgl/images/matrix_transform.png" alt="matrix transformation" />

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

http://wiki.jikexueyuan.com/project/webgl/images/matrix.png" alt="matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/multiply_matrix.png" alt="multiply matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/simplify_result.png" alt="simplify result" />

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

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

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

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

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

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

http://wiki.jikexueyuan.com/project/webgl/images/rotate_matrix.png" alt="rotate_matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/rotate_apply_matrix.png" alt="rotate_apply_matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/rotate_matrix_result.png" alt="rotate_matrix_result" />

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

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

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

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

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

http://wiki.jikexueyuan.com/project/webgl/images/scale_matrix.png" alt="scale_matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/scale_apply_matrix.png" alt="scale apply matrix" />

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

http://wiki.jikexueyuan.com/project/webgl/images/scale_matrix_result.png" alt="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é)果。

為了讓上面的做法更清楚,于是編寫(xiě)如下的函數(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ì)問(wèn),之后了?這個(gè)看起來(lái)并沒(méi)有方便多少。然而,此時(shí)如果你想改變執(zhí)行的順序,就不再需要編寫(xiě)一個(gè)新的渲染器了。我們僅僅只需要改變數(shù)序公式。

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

如下是新版本:

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

  // 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è),我們要編寫(xiě)自己的函數(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)槲覀兡軌蚴褂镁仃?,那么就可以選擇變化的順序,可以在執(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)或者伸縮。現(xiàn)在你就明白了 Photoshop 或者 Flash 中實(shí)現(xiàn)繞某點(diǎn)旋轉(zhuǎn)的原理。

讓我們學(xué)習(xí)更深入點(diǎn)。如果你回到本系列的第一篇文章 WebGL 基本原理,你也許還記得我們編寫(xiě)的渲染器的代碼中將像素轉(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)在反過(guò)來(lái)看下每一步,第一步,“將像素變換成 0.0 變成 1.0”,其實(shí)是一個(gè)伸縮操作。第二步同樣是伸縮變換。接下來(lái)是平移變換,并且 Y 的伸縮因子是 -1。我們可以通過(guò)將該矩陣傳給渲染器實(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é)是很容易的,我們僅僅只需編寫(xiě)一個(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è)置分辨率的代碼。最后一步,通過(guò)使用數(shù)學(xué)矩陣就將原先需要 6-7 步操作復(fù)雜的渲染器變成僅僅只需要 1 步操作的更簡(jiǎn)單的渲染器。

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