Transformaciones de WebGL

Gregg Tavares
Gregg Tavares

Traducción de WebGL 2D

Antes de pasar al 3D, quedémonos con el 2D durante un poco más. Espera conmigo, por favor. Puede que este artículo parezca extremadamente obvio para algunos, pero llegaré a cierto punto en algunos.

Este artículo es la continuación de una serie que comienza con Conceptos básicos de WebGL. Si no lo has leído, te sugiero que leas al menos el primer capítulo y, luego, regresas aquí. Translation es un nombre matemático sofisticado que básicamente significa "mover". Supongo que mover una oración del inglés al japonés también sirve, pero en este caso estamos hablando del movimiento de geometría. Con el código de muestra que terminamos en la primera publicación, podrías traducir fácilmente nuestro rectángulo con solo cambiar los valores pasados a setRectangle, ¿verdad? Esta es una muestra basada en nuestra muestra anterior.

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

Todo bien por ahora. Pero ahora imagina que queríamos hacer lo mismo con una forma más complicada. Supongamos que queremos dibujar una "F" que consta de 6 triángulos como este.

Letra F

A continuación, se muestra el código actual, tendríamos que cambiar setRectangle a algo más parecido a esto.

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

Es de esperar que no vaya a escalar bien. Si queremos dibujar una geometría muy compleja con cientos o miles de líneas, tendríamos que escribir un código bastante complejo. Además, cada vez que dibujamos JavaScript, debemos actualizar todos los puntos. Hay una manera más sencilla. Solo debes subir la geometría y hacer la traducción en el sombreador. Este es el nuevo sombreador

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

y reestructuraremos un poco el código. En el caso de uno, solo necesitamos establecer la geometría una vez.

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

Luego, solo debemos actualizar u_translation antes de dibujar con la traducción que deseamos.

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

Ten en cuenta que se llama a setGeometry solo una vez. Ya no está dentro de drawScene.

Ahora, cuando dibujamos WebGL, se hace prácticamente todo. Todo lo que estamos haciendo es preparar una traducción y pedirle que dibuje. Incluso si nuestra geometría tuviera decenas de miles de puntos, el código principal seguiría siendo el mismo.

Rotación de WebGL 2D

Lo admitiré desde el principio. No tengo idea de cómo explicar esto tendrá sentido, pero ¿qué diablos podría probar?

En primer lugar, quiero presentarte lo que se conoce como el "círculo de la unidad". Si recuerdas las matemáticas de tu escuela secundaria (¡no te duermas conmigo!), un círculo tiene un radio. El radio de un círculo es la distancia desde el centro del círculo hasta el borde. Un círculo unitario es un círculo con un radio de 1.0.

Si recuerdas las matemáticas básicas de 3er grado, cuando multiplicas algo por 1, el valor sigue siendo igual. Entonces, 123 * 1 = 123. Es bastante básico, ¿verdad? Un círculo unitario, un círculo con un radio de 1.0 también es una forma de 1. Es un 1 que rota. Puedes multiplicar algo por este círculo de la unidad y, de una forma similar a multiplicarlo por 1, excepto que sucede magia y las cosas rotan. Vamos a tomar ese valor de X e Y de cualquier punto en el círculo unitario y multiplicaremos nuestra geometría por ellos de nuestra muestra anterior. Estas son las actualizaciones de nuestro sombreador.

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

Y actualizamos JavaScript para poder pasar esos 2 valores.

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

¿Por qué funciona? Bueno, mira los cálculos.

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;

Dejemos que tengas un rectángulo y quieras rotarlo. Antes de comenzar a rotarlo, la esquina superior derecha está en 3.0, 9.0. Elijamos un punto en el círculo de la unidad 30 grados en el sentido de las manecillas del reloj desde las 12.

Rotación de 30 grados

La posición en el círculo es 0.50 y 0.87.

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

Ahí es exactamente donde necesitamos que esté

Dibujo de rotación

Lo mismo para 60 grados en el sentido de las manecillas del reloj

Rotación de 60 grados

La posición en el círculo es 0.87 y 0.50.

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

Puedes ver que, a medida que rotamos ese punto en el sentido de las manecillas del reloj hacia la derecha, el valor X aumenta y el Y se reduce. Si se continúa yendo más allá de los 90 grados, X comenzaría a volverse más pequeño nuevamente e Y empezaría a crecer. Ese patrón nos da la rotación. Los puntos de un círculo de unidad tienen otro nombre. Se llaman seno y coseno. Así que, para cualquier ángulo dado, podemos buscar el seno y el coseno de esta manera.

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

Si copias y pegas el código en tu consola de JavaScript y escribes printSineAndCosignForAngle(30), verás que se imprime s = 0.49 c= 0.87 (nota: Redondeé los números). Si combinas todo, puedes rotar tu geometría a cualquier ángulo que desees. Solo debes establecer la rotación en el seno y el coseno del ángulo al que desees rotar.

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

Espero que esta información haya sido útil. Ahora sigue una más sencilla. Escala.

¿Qué son los radianes?

Los radianes son una unidad de medida que se usa con círculos, rotación y ángulos. Así como podemos medir la distancia en pulgadas, yardas, metros, etc., podemos medir ángulos en grados o radianes.

Probablemente sepas que las matemáticas con medidas métricas son más fáciles que las matemáticas con medidas imperiales. Para ir de pulgadas a pies, dividimos por 12. Para ir de pulgadas a yardas, dividimos por 36. No sé tú, pero no puedo dividir por 36 en mi cabeza. Con las métricas, es mucho más fácil. Para pasar de milímetros a centímetros, dividimos por 10. Para de milímetros a metros, dividimos por 1000. Puedo dividir mi cabeza por 1,000.

Los radianes y los grados son similares. Los grados dificultan las matemáticas. Los radianes facilitan los cálculos. Un círculo tiene 360 grados, pero solo tiene 2π radianes. Por lo tanto, un giro completo equivale a 2π radianes. Un medio giro equivale a π radianes. Un giro de 1/4, es decir, una regresión de 90, equivale a π/2 radianes. Por lo tanto, si deseas rotar algo 90 grados, solo usa Math.PI * 0.5. Si deseas rotarla 45 grados, usa Math.PI * 0.25, etcétera.

Casi todas las matemáticas que involucran ángulos, círculos o rotación funcionan de forma muy simple si se empieza a pensar en radianes. Pruébalo. Usa radianes, no grados, excepto en pantallas de IU.

Escala 2D WebGL

El escalamiento es tan sencillo como la traducción.

Multiplicamos la posición por la escala deseada. Estos son los cambios en nuestro ejemplo anterior.

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

y agregamos el JavaScript necesario para configurar la escala cuando dibujamos.

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

Cabe observar que el ajuste de tamaño mediante un valor negativo cambia nuestra geometría. Espero que estos 3 capítulos te hayan resultado útiles para comprender la traslación, la rotación y la escala. Luego, repasaremos la magia de las matrices que combinan las 3 en una forma mucho más simple y, a menudo, más útil.

¿Por qué una F?

La primera vez que vi a alguien usar una 'F' fue en una textura. La 'F' en sí misma no es importante. Lo importante es que puedas distinguir su orientación desde cualquier dirección. Si usábamos un corazón ♥ o un triángulo △, por ejemplo, no pudimos saber si se giró de forma horizontal. Un círculo ○ sería aún peor. Podría decirse que un rectángulo de color funcionaría con diferentes colores en cada esquina, pero luego tendrías que recordar cuál es cada esquina. La orientación de una F se puede reconocer al instante.

Orientación F

Cualquier forma que se pueda dar a la orientación funcionaría. Acabo de usar 'F' desde que llegué a conocer la idea.

Matrices 2D de WebGL

En los últimos 3 capítulos, explicamos cómo traducir geometrías, rotarlas y ajustarlas a escala. La traslación, la rotación y la escala se consideran un tipo de “transformación”. Cada una de estas transformaciones requería cambios en el sombreador y cada una de las 3 transformaciones dependía del orden.

Por ejemplo, aquí hay una escala de 2, 1, rotación del 30% y traslación de 100, 0.

Rotación y traslación F

Y aquí hay una traslación de 100.0, una rotación del 30% y una escala de 2, 1.

Rotación F y escala

Los resultados son completamente diferentes. Peor aún, si necesitábamos el segundo ejemplo, tendríamos que escribir un sombreador diferente que aplicara la traslación, la rotación y la escala en el nuevo orden deseado. Bueno, algunas personas mucho más inteligentes que yo descubrieron que puedes hacer todo lo mismo con las matemáticas de matrices. Para 2d, usamos una matriz 3x3. Una matriz de 3x3 es como una cuadrícula con 9 cuadros.

1.0 2.0 3.0
4.0 5.0 6.0
7.0 8.0 9.0

Para hacer los cálculos matemáticos, multiplicamos la posición hacia abajo en las columnas de la matriz y sumamos los resultados. Nuestras posiciones solo tienen 2 valores: "x" e "y", pero para hacer esta matemática, se necesitan 3 valores. Por eso, usaremos 1 para el tercer valor. En este caso, el resultado sería

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

Probablemente estés viendo eso y pensando "¿CUÁL ES EL PUNTO?". Bueno, supongamos que tenemos una traducción. El importe que queremos traducir será "tx" y "ty". Hagamos una matriz como esta

1.00.00.0
0.01.00.0
txty1.0

Y ahora, echa un vistazo

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

Si recuerdas tu álgebra, podemos borrar cualquier lugar que se multiplica por cero. La multiplicación por 1 no hace nada de manera efectiva, así que simplifiquemos para ver qué sucede

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

Y otra cosa que no nos importa. Eso se parece sorprendentemente al código de traducción de nuestro ejemplo de traducción. Del mismo modo, hagamos la rotación. Como señalamos en el post de rotación, solo necesitamos el seno y el coseno del ángulo al que queremos rotar.

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

Y construimos una matriz como esta

c-s0.0
sc0.0
0.00.01.0

Si aplicamos la matriz, obtenemos esto

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

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

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

Atenuar todo multiplicando por 0 y 1, obtenemos

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

que es exactamente lo que teníamos en nuestra muestra de rotación. Y, por último, escala. Llamaremos a nuestros 2 factores de escala sx y sy. Construimos una matriz como esta

sx0.00.0
0.0sy0.0
0.00.01.0

Si aplicamos la matriz, obtenemos esto

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

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

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

que es realmente

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

que es lo mismo que nuestro ejemplo de escalamiento. Estoy segura de que aún estás pensando. Entonces, ¿qué sucede? ¿Cuál es el punto? Pareciera que implicaba mucho trabajo para hacer lo mismo que de costumbre. Aquí es donde entra en juego la magia. Resulta que podemos multiplicar matrices y aplicar todas las transformaciones a la vez. Supongamos que tenemos la función matrixMultiply, que toma dos matrices, las multiplica y muestra el resultado. Para que quede más claro, crearemos funciones destinadas a crear matrices para traslación, rotación y escala.

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

Ahora, cambiemos nuestro sombreador. El sombreador anterior se veía así

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

Nuestro nuevo sombreador será mucho más simple.

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

Y así es como lo usamos

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

De todas formas, te preguntarás: ¿y entonces? Eso no parece ser un gran beneficio . Sin embargo, si queremos cambiar el orden, no es necesario que escribamos un sombreador nuevo. Podemos cambiar las matemáticas.

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

Poder aplicar matrices como esta es especialmente importante para la animación jerárquica, como brazos en un cuerpo, lunas en un planeta alrededor de un sol o ramas en un árbol. Para un ejemplo simple de animación jerárquica, dibujemos nuestra 'F' 5 veces, pero cada vez vamos a empezar con la matriz de la 'F' anterior.

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

Para ello, introdujimos la función makeIdentity, que forma una matriz de identidad. Una matriz identidad es una matriz que efectivamente representa 1.0 para que, si se multiplica por la identidad, no suceda nada. Al igual que

X * 1 = X

también

matrixX * identity = matrixX

Este es el código para crear una matriz de identidad.

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

Un ejemplo más: En cada muestra hasta ahora, nuestra 'F' rota sobre su esquina superior izquierda. Esto se debe a que la matemática que usamos siempre rota alrededor del origen y la esquina superior izquierda de nuestra “F” está en el origen, (0, 0). Pero ahora, como podemos hacer cálculos de matrices y elegir el orden en que se aplican las transformaciones, podemos mover el origen antes de que se aplique el resto de las transformaciones.

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

Con esa técnica, puedes rotar o ajustar la escala desde cualquier punto. Ahora sabes cómo Photoshop o Flash te permiten mover el punto de rotación. Vamos aún más loco. Si regresas al primer artículo sobre Conceptos básicos de WebGL, quizás recuerdes que tenemos código en el sombreador para convertir píxeles a un espacio de recorte que se ve de la siguiente manera.

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

Si observas cada uno de esos pasos por separado, el primer paso, "convertir de píxeles a 0.0 a 1.0", en realidad es una operación de escala. La segunda también es una operación de escala. La siguiente es una traslación y la última escala Y en -1. De hecho, podemos hacer todo eso en la matriz que pasamos al sombreador. Podríamos hacer 2 matrices de escala, una para escalar por 1.0/resolución, otra para escalar por 2.0, una tercera para traducir por -1.0, -1.0 y un 4 para escalar Y por -1 y luego multiplicarlas todas juntas, pero en cambio, como las matemáticas son simples, solo haremos una función que haga una matriz de "proyección" para una resolución dada directamente.

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

Ahora podemos simplificar aún más el sombreador. Este es el nuevo sombreador de vértices.

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

En JavaScript, debemos multiplicar por la matriz de proyección.

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

También quitamos el código que establecía la resolución. Con este último paso, pasamos de un sombreador bastante complicado con 6 a 7 pasos a uno muy sencillo con solo 1 paso, todo gracias a la magia de la matemática de matrices.

Espero que este artículo haya ayudado a desmitificar la matemática matricial. Ahora pasaré a la etapa 3D. En las matrices 3D, las matemáticas siguen los mismos principios y usos. Comencé con 2D para que sea fácil de entender.