Chuyển đổi WebGL

Gregg Tavares
Gregg Tavares

Dịch 2D WebGL

Trước khi chuyển sang 3D, hãy giữ lại 2D lâu hơn một chút. Làm ơn kiên nhẫn chờ tôi. Bài viết này có vẻ cực kỳ rõ ràng đối với một số người, nhưng tôi sẽ muốn nhấn mạnh thêm vào một điểm trong một vài bài viết.

Bài viết này là phần tiếp theo của loạt bài viết bắt đầu với Kiến thức cơ bản về WebGL. Nếu bạn chưa đọc, tôi khuyên bạn nên đọc ít nhất chương đầu tiên rồi quay lại đây. Dịch là một số tên toán học lạ mắt, về cơ bản có nghĩa là "để di chuyển" một cái gì đó. Tôi cho rằng việc di chuyển câu từ tiếng Anh sang tiếng Nhật cũng phù hợp nhưng trong trường hợp này, chúng ta đang nói về việc di chuyển hình học. Bằng cách sử dụng mã mẫu mà chúng tôi đã kết thúc trong bài đăng đầu tiên, bạn có thể dễ dàng dịch hình chữ nhật chỉ bằng cách thay đổi các giá trị được truyền thành setHình chữ nhật, đúng không? Đây là một mẫu dựa trên mẫu trước của chúng tôi.

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

Đến giờ thì mọi thứ vẫn ổn! Nhưng bây giờ, hãy tưởng tượng chúng ta muốn làm điều tương tự với một hình dạng phức tạp hơn. Giả sử chúng ta muốn vẽ một chữ 'F' bao gồm 6 hình tam giác như thế này.

Chữ F

Chà, sau đây là mã hiện tại, chúng ta sẽ phải thay đổi setóng thành một kiểu tương tự hơn.

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

Hy vọng là bạn có thể thấy rằng việc mở rộng quy mô vẫn chưa hiệu quả. Nếu muốn vẽ một số hình học rất phức tạp với hàng trăm hoặc hàng nghìn đường, chúng ta sẽ phải viết một số mã khá phức tạp. Trên hết, mỗi lần chúng ta vẽ JavaScript phải cập nhật tất cả các điểm. Có một cách đơn giản hơn. Chỉ cần tải hình học lên và dịch trong chương trình đổ bóng. Đây là chương trình đổ bóng mới

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

và chúng ta sẽ điều chỉnh cấu trúc mã một chút. Đối với một chương trình, chúng ta chỉ cần đặt hình học một lần.

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

Sau đó, chúng ta chỉ cần cập nhật u_translation trước khi vẽ bằng bản dịch mong muốn.

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

Lưu ý rằng setGeometry chỉ được gọi một lần. Khung này không còn nằm trong drawScene.

Giờ đây, khi chúng ta vẽ WebGL thực sự đang làm mọi thứ. Tất cả những gì chúng tôi làm là thiết lập bản dịch và yêu cầu bản dịch đó vẽ. Ngay cả khi hình học của chúng ta có hàng chục nghìn điểm thì mã chính sẽ vẫn giữ nguyên.

Xoay 2D WebGL

Tôi xin thừa nhận trước rằng tôi không biết tôi giải thích như thế nào có hợp lý hay không, nhưng tôi có thể thử xem sao.

Trước tiên, tôi muốn giới thiệu với bạn khái niệm "vòng tròn đơn vị". Nếu bạn còn nhớ toán học trung học cơ sở (đừng ngủ với tôi!) một vòng tròn có bán kính. Bán kính của một hình tròn là khoảng cách từ tâm đường tròn đến cạnh. Một hình tròn đơn vị là một hình tròn có bán kính 1,0.

Nếu bạn còn nhớ từ toán học lớp 3 cơ bản, nếu bạn nhân một nội dung nào đó với 1 thì kết quả không thay đổi. Vậy 123 * 1 = 123. Khá cơ bản phải không? Chà, hình tròn đơn vị, hình tròn có bán kính 1,0 cũng là dạng 1. Tôi đang xoay số 1. Vì vậy, bạn có thể nhân một thứ gì đó với vòng tròn đơn vị này và theo cách đó, giống như nhân với 1, ngoại trừ điều kỳ diệu xảy ra và mọi thứ xoay vòng. Chúng ta sẽ lấy giá trị X và Y đó từ bất kỳ điểm nào trên vòng tròn đơn vị, sau đó nhân hình học của mình với các giá trị đó trong mẫu trước. Sau đây là những điểm cập nhật về chương trình đổ bóng của chúng tô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;

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;

Và chúng tôi cập nhật JavaScript để có thể truyền 2 giá trị đó vào.

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

Vì sao việc này hiệu quả? Chà, hãy xem phép toán.

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;

Giữ nguyên một hình chữ nhật và bạn muốn xoay hình chữ nhật đó. Trước khi bạn bắt đầu xoay, góc trên cùng bên phải là 3.0, 9.0. Hãy chọn một điểm trên vòng tròn đơn vị 30 độ theo chiều kim đồng hồ từ 12 giờ.

Xoay 30 độ

Vị trí trên đường tròn có 0,50 và 0,87

3.0 * 0.87 + 9.0 * 0.50 = 7.1
9.0 * 0.87 - 3.0 * 0.50 = 6.3

Đó chính xác là nơi chúng tôi cần

Bản vẽ xoay

Tương tự như vậy với góc 60 độ theo chiều kim đồng hồ

Xoay 60 độ

Vị trí trên đường tròn có 0,87 và 0,50

3.0 * 0.50 + 9.0 * 0.87 = 9.3
9.0 * 0.50 - 3.0 * 0.87 = 1.9

Bạn có thể thấy rằng khi chúng ta xoay điểm đó theo chiều kim đồng hồ sang phải, giá trị X sẽ lớn hơn và Y nhỏ hơn. Nếu tiếp tục vượt quá 90 độ, thì X sẽ bắt đầu nhỏ lại và Y sẽ bắt đầu to hơn. Mẫu đó cho phép chúng ta xoay vòng. Có một tên khác cho các điểm trên một vòng tròn đơn vị. Đó là sin và cosin. Vì vậy, với bất kỳ góc nào cho trước, chúng ta chỉ cần tra cứu sin và cos như sau.

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

Nếu sao chép và dán mã vào bảng điều khiển JavaScript rồi nhập printSineAndCosignForAngle(30), bạn sẽ thấy mã in ra s = 0.49 c= 0.87 (lưu ý: Tôi đã làm tròn các con số). Nếu bạn ghép tất cả lại với nhau, bạn có thể xoay hình học đến bất kỳ góc nào bạn muốn. Chỉ cần đặt chế độ xoay theo sin và cosin của góc bạn muốn xoay tới.

  ...
  var angleInRadians = angleInDegrees * Math.PI / 180;
  rotation[0] = Math.sin(angleInRadians);
  rotation[1] = Math.cos(angleInRadians);

Tôi hy vọng thông tin này giúp ích cho bạn. Hãy chuyển sang một giải pháp đơn giản hơn. Tỷ lệ.

radian là gì?

Radian là một đơn vị đo lường được sử dụng với đường tròn, vòng quay và góc. Giống như chúng ta có thể đo khoảng cách bằng inch, yard, mét, v.v., chúng ta có thể đo góc theo độ hoặc radian.

Có lẽ bạn biết rằng toán học có phép đo hệ mét dễ hơn toán học bằng phép đo hệ đo lường Anh. Để đi từ inch sang feet, chúng ta chia cho 12. Để tính từ inch sang yard, chúng ta chia cho 36. Tôi không biết về bạn nhưng tôi không thể chia cho 36 trong đầu. Nếu có chỉ số, việc này sẽ dễ dàng hơn nhiều. Để chuyển từ milimét sang cm, chúng ta chia cho 10. Để từ milimét sang mét, chúng ta chia cho 1000. Tôi có thể chia cho 1000 trong đầu.

Radian và độ là tương tự nhau. Bằng cấp khiến toán học trở nên khó khăn. Rađa giúp giải toán trở nên thật dễ dàng. Có 360 độ trong một vòng tròn nhưng chỉ có 2π radian. Vì vậy, một vòng quay đầy đủ là 2π radian. Nửa vòng là π radian. Một lượt 1/4, tức là 90 suy giảm là π/2 radian. Vì vậy, nếu bạn muốn xoay 90 độ, chỉ cần sử dụng Math.PI * 0.5. Nếu bạn muốn xoay 45 độ, hãy sử dụng Math.PI * 0.25, v.v.

Hầu như mọi phép toán liên quan đến góc, vòng tròn hoặc phép xoay đều hoạt động rất đơn giản nếu bạn bắt đầu tư duy bằng radian. Vì vậy, hãy thử. Sử dụng radian, không phải độ ngoại trừ trong màn hình giao diện người dùng.

Tỷ lệ WebGL 2D

Mở rộng quy mô cũng dễ dàng như dịch.

Chúng tôi nhân vị trí với quy mô mong muốn. Sau đây là những thay đổi so với mẫu trước.

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

và thêm JavaScript cần thiết để đặt tỷ lệ khi vẽ.

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

Một điều cần lưu ý là việc chia tỷ lệ theo giá trị âm sẽ lật hình học của chúng ta. Tôi hy vọng 3 chương cuối cùng này giúp ích cho bạn trong việc hiểu về bản dịch, việc xoay vòng và quy mô. Tiếp theo, chúng ta sẽ tìm hiểu điều kỳ diệu là ma trận kết hợp cả 3 thành phần đơn giản và thường hữu ích hơn nhiều.

Tại sao lại là chữ "F"?

Lần đầu tiên tôi thấy ai đó sử dụng chữ "F" là trên hoạ tiết. Chữ 'F' không quan trọng. Điều quan trọng là bạn phải biết được hướng của thiết bị từ bất kỳ hướng nào. Nếu chúng ta sử dụng một trái tim DD hoặc hình tam giác △ chẳng hạn, chúng ta không thể phân biệt được liệu nó có được lật theo chiều ngang hay không. Hình tròn ○ sẽ còn tệ hơn. Một hình chữ nhật tô màu có thể sẽ tương thích với các màu khác nhau trên mỗi góc nhưng sau đó bạn phải nhớ tương ứng với góc nào. Hướng của F có thể nhận ra ngay lập tức.

Hướng F

Bạn có thể biết được hướng bất kỳ hình dạng nào cũng được. Tôi chỉ dùng từ "F" từ khi còn "F 'irst" được giới thiệu về ý tưởng này.

Ma trận WebGL 2D

Trong 3 chương trước, chúng ta đã tìm hiểu cách dịch hình học, xoay hình học và điều chỉnh tỷ lệ hình học. Các phép dịch, xoay và tỷ lệ đều được xem là một loại "biến đổi". Mỗi phép biến đổi này đòi hỏi phải có những thay đổi đối với chương trình đổ bóng và cả 3 phép biến đổi này đều phụ thuộc vào thứ tự.

Ví dụ ở đây là tỷ lệ 2, 1, xoay vòng 30% và bản dịch 100, 0.

Xoay F và biên dịch

Và đây là bản dịch 100,0, xoay 30% và thang điểm 2, 1

Xoay và điều chỉnh tỷ lệ F

Kết quả hoàn toàn khác nhau. Thậm chí tệ hơn, nếu cần ví dụ thứ hai, chúng tôi sẽ phải viết một chương trình đổ bóng khác áp dụng việc dịch, xoay và điều chỉnh theo tỷ lệ theo thứ tự mong muốn mới. Chà, một số người thông minh hơn tôi rất nhiều, đã phát hiện ra rằng bạn có thể làm mọi thứ tương tự với toán ma trận. Đối với 2d chúng tôi sử dụng ma trận 3x3. Ma trận 3x3 giống như lưới có 9 ô.

1 2 3
4 5 6.0
7.0 8.0 9.0

Để làm toán, chúng ta nhân vị trí theo cột của ma trận rồi cộng kết quả với nhau. Vị trí của chúng ta chỉ có 2 giá trị, x và y nhưng để thực hiện phép toán này, chúng ta cần 3 giá trị để sử dụng 1 cho giá trị thứ ba. Trong trường hợp này, kết quả của chúng ta sẽ là

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

Có lẽ bạn đang nhìn vào thông tin đó và nghĩ "Ý LÀ GÌ". Giả sử chúng ta có bản dịch. Chúng tôi sẽ gọi số tiền chúng tôi muốn dịch tx và ty. Hãy tạo một ma trận như thế này

10,00,0
0,010,0
txty1

Và bây giờ, hãy khám phá

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

Nếu bạn nhớ đại số của mình, chúng ta có thể xoá bất kỳ điểm nào nhân với 0. Nhân 1 với 1 là không có tác dụng gì cả, vì vậy, hãy đơn giản hoá để xem điều gì đang xảy ra

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

Ngoài ra, chúng tôi không thực sự quan tâm đến những vấn đề khác. Mã này trông khá giống mã dịch trong ví dụ về bản dịch của chúng tôi. Tương tự, hãy thực hiện xoay vòng. Như chúng ta đã chỉ ra trong bài đăng xoay, chúng ta chỉ cần sin và cosin của góc mà chúng ta muốn xoay.

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

Và chúng tôi tạo một ma trận như thế này

c-giây0,0
giâyc0,0
0,00,01

Áp dụng ma trận, chúng ta sẽ nhận được

newX = x * c + y * s + 1 * 0

newY = x * -s + y * c + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

Bôi đen tất cả các giá trị nhân với 0 và 1 ta được

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

Đó chính xác là những gì chúng tôi đã có trong mẫu xoay vòng. Và cuối cùng là mở rộng quy mô. Chúng tôi sẽ gọi 2 hệ số tỷ lệ là sx và sy Và chúng tôi tạo một ma trận như thế này

sx0,00,0
0,0sy0,0
0,00,01

Áp dụng ma trận, chúng ta sẽ nhận được

newX = x * sx + y * 0 + 1 * 0

newY = x * 0 + y * sy + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

thực sự

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

Điều này cũng giống với mẫu điều chỉnh theo tỷ lệ của chúng tôi. Giờ tôi chắc là bạn vẫn còn đang suy nghĩ. Vậy thì sao? Vấn đề là gì? Có vẻ như có rất nhiều việc chúng tôi vẫn làm vốn đã cũ? Đây là lúc phép màu phát huy. Hoá ra chúng ta có thể nhân các ma trận với nhau rồi áp dụng mọi phép biến đổi cùng một lúc. Giả sử chúng ta có hàm matrixMultiply. Hàm này nhận hai ma trận, nhân các ma trận đó rồi trả về kết quả. Để làm rõ hơn, hãy tạo các hàm xây dựng ma trận để dịch chuyển, xoay và chuyển tỷ lệ.

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

Bây giờ, hãy thay đổi chương trình đổ bóng. Chương trình đổ bóng cũ có dạng như sau

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

Chương trình đổ bóng mới của chúng ta sẽ đơn giản hơn nhiều.

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

Và sau đây là cách chúng tôi sử dụng

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

Vẫn có thể bạn muốn hỏi, vậy thì sao? Điều đó có vẻ không đem lại nhiều lợi ích . Nhưng giờ đây, nếu muốn thay đổi thứ tự thì không cần phải viết chương trình đổ bóng mới. Chúng ta chỉ cần thay đổi cách tính toán.

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

Việc có thể áp dụng các ma trận như thế này đặc biệt quan trọng đối với ảnh động phân cấp như cánh tay trên cơ thể, mặt trăng trên hành tinh xung quanh mặt trời hoặc cành cây. Đối với ví dụ đơn giản về ảnh động phân cấp, hãy vẽ 'F' của chúng ta 5 lần nhưng mỗi lần cho phép bắt đầu bằng ma trận từ 'F' trước đó.

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

Để làm điều này, chúng tôi giới thiệu hàm makeIdentity. Hàm này tạo ra một ma trận nhận dạng. Ma trận nhận dạng là một ma trận biểu diễn hiệu quả 1,0 sao cho nếu bạn nhân với danh tính thì sẽ không có gì xảy ra. Tương tự

X * 1 = X

cũng thế

matrixX * identity = matrixX

Dưới đây là mã để tạo ma trận nhận dạng.

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

Một ví dụ nữa, trong mỗi mẫu cho đến thời điểm hiện tại, chữ 'F' xoay quanh góc trên cùng bên trái. Lý do là phép toán chúng ta dùng luôn xoay quanh điểm gốc và góc trên cùng bên trái của chữ "F" là tại gốc toạ độ (0, 0) Nhưng giờ đây, vì chúng ta có thể thực hiện phép toán ma trận và có thể chọn thứ tự áp dụng các phép biến đổi, nên chúng ta có thể di chuyển điểm gốc trước khi áp dụng các phép biến đổi còn lại.

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

Với kỹ thuật này, bạn có thể xoay hoặc điều chỉnh tỷ lệ từ bất kỳ điểm nào. Bây giờ, bạn đã biết cách Photoshop hoặc Flash cho phép bạn di chuyển điểm xoay. Hãy tiếp tục điên rồ hơn nữa. Nếu quay lại bài viết đầu tiên về Kiến thức cơ bản về WebGL, bạn có thể còn nhớ chúng ta có mã trong chương trình đổ bóng để chuyển đổi từ pixel sang không gian cắt giống như thế này.

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

Nếu bạn lần lượt xem từng bước đó, bước đầu tiên, "chuyển đổi từ pixel sang 0,0 sang 1,0", thực sự là một thao tác theo tỷ lệ. Thứ hai cũng là toán tử điều chỉnh theo tỷ lệ. Tiếp theo là bản dịch và tỷ lệ cuối cùng là -1 cho Y. Chúng ta thực sự có thể làm điều đó tất cả trong ma trận mà chúng ta truyền vào chương trình đổ bóng. Chúng ta có thể tạo 2 ma trận tỷ lệ, một để tỷ lệ theo 1,0/độ phân giải, một để tỷ lệ 2,0, một thứ 3 để dịch -1,0, -1,0 và một thứ 4 để tỷ lệ Y với -1 sau đó nhân tất cả chúng với nhau nhưng thay vào đó, vì toán học rất đơn giản, chúng ta sẽ chỉ tạo một hàm tạo một ma trận "phép chiếu" cho một độ phân giải nhất định trực tiếp.

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

Bây giờ, chúng ta có thể đơn giản hoá chương trình đổ bóng hơn nữa. Đây là toàn bộ chương trình đổ bóng đỉnh mới.

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

Và trong JavaScript, chúng ta cần nhân với ma trận chiếu

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

Chúng tôi cũng đã xoá mã đặt độ phân giải. Với bước cuối cùng này, chúng ta đã đi từ một chương trình đổ bóng khá phức tạp với 6-7 bước đến một chương trình đổ bóng rất đơn giản chỉ với 1 bước, tất cả đều làm nên điều kỳ diệu của toán học ma trận.

Tôi hy vọng bài viết này đã giúp bạn hiểu rõ về toán học ma trận. Tôi sẽ chuyển sang mô hình 3D trong phần tiếp theo. Trong toán học ma trận 3D tuân theo cùng một nguyên tắc và cách sử dụng. Tôi bắt đầu dùng mô hình 2D để hy vọng rằng mọi thứ đều dễ hiểu.