Traducción de WebGL 2D
Antes de pasar a las 3D, sigamos con las 2D un poco más. Tenme paciencia, por favor. Este artículo puede parecer demasiado obvio para algunos, pero llegaré a un punto en unos pocos artículos.
Este artículo es la continuación de una serie que comienza con Conceptos básicos de WebGL. Si aún no lo has leído, te sugiero que leas al menos el primer capítulo y, luego, vuelvas aquí. La traslación es un nombre matemático elegante que básicamente significa "mover" algo. Supongo que mover una oración del inglés al japonés también encaja, pero en este caso estamos hablando de mover geometría. Con el código de muestra que obtuvimos en la primera publicación, podrías traducir fácilmente nuestro rectángulo con solo cambiar los valores que se pasan a setRectangle, ¿verdad? Este es un ejemplo basado en nuestro ejemplo 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);
}
Hasta ahora, todo bien. Pero ahora imagina que queremos hacer lo mismo con una forma más complicada. Supongamos que queremos dibujar una "F" que consta de 6 triángulos como este.
Bueno, siguiendo 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);
}
Como puedes ver, esto no va 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 tiene que actualizar todos los puntos. Hay una forma más sencilla. Solo sube la geometría y realiza la traslació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. Por un lado, 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 traslació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);
}
Observa que se llama a setGeometry solo una vez. Ya no está dentro de drawScene.
Ahora, cuando dibujamos, WebGL hace prácticamente todo. Lo único que hacemos es establecer una traslació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 en 2D de WebGL
Admito que no sé si mi explicación tendrá sentido, pero vale la pena intentarlo.
Primero, quiero presentarte lo que se llama un "círculo unitario". Si recuerdas las matemáticas de la escuela secundaria (¡no te duermas!), 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, si multiplicas algo por 1, se mantiene igual. Por lo tanto, 123 * 1 = 123. Bastante básico, ¿verdad? Bueno, un círculo unitario, un círculo con un radio de 1.0, también es una forma de 1. Es un 1 giratorio. Así que puedes multiplicar algo por este círculo unitario y, en cierto modo, es como multiplicar por 1, excepto que ocurre la magia y las cosas rotan. Tomaremos ese valor de X e Y de cualquier punto del 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 el 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, miremos las matemáticas.
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;
Supongamos que tienes un rectángulo y quieres rotarlo. Antes de comenzar a rotarlo, la esquina superior derecha está en 3.0, 9.0. Elijamos un punto en el círculo unitario a 30 grados en el sentido de las manecillas del reloj desde las 12 en punto.
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é.
Lo mismo para 60 grados en el sentido de las manecillas del reloj
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 de X aumenta y el de Y disminuye. Si continuáramos más allá de los 90 grados, X comenzaría a disminuir de nuevo, y Y comenzaría a aumentar. Ese patrón nos da rotación. Hay otro nombre para los puntos de un círculo unitario. Se llaman seno y coseno. Por lo tanto, 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 la geometría al ángulo que desees. Solo debes establecer la rotación en el seno y el coseno del ángulo al que deseas rotar.
...
var angleInRadians = angleInDegrees * Math.PI / 180;
rotation[0] = Math.sin(angleInRadians);
rotation[1] = Math.cos(angleInRadians);
Espero que esto haya tenido sentido. A continuación, una más sencilla. Escala.
¿Qué son los radianes?
Los radianes son una unidad de medida que se usa con círculos, rotaciones y ángulos. Así como podemos medir la distancia en pulgadas, yardas, metros, etc., podemos medir los á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 pasar de pulgadas a pies, dividimos por 12. Para pasar de pulgadas a yardas, dividimos por 36. No sé ustedes, pero yo no puedo dividir por 36 mentalmente. Con el sistema métrico, es mucho más fácil. Para pasar de milímetros a centímetros, dividimos por 10. Para pasar de milímetros a metros, dividimos por 1,000. Puedo dividir por 1,000 mentalmente.
Los radianes y los grados son similares. Los grados dificultan los cálculos. Los radianes facilitan los cálculos matemáticos. Hay 360 grados en un círculo, pero solo hay 2π radianes. Por lo tanto, un giro completo es de 2π radianes. Media vuelta es π radianes. Un giro de 1/4, es decir, 90 grados, es π/2 radianes. Por lo tanto, si quieres rotar algo 90 grados, solo usa Math.PI * 0.5. Si quieres rotarla 45 grados, usa Math.PI * 0.25, etc.
Casi todas las matemáticas que involucran ángulos, círculos o rotación funcionan de manera muy simple si comienzas a pensar en radianes. Así que pruébalo. Usa radianes, no grados, excepto en las pantallas de la IU.
Escala 2D de WebGL
El escalamiento es tan fácil como la traducción.
Multiplicamos la posición por la escala deseada. Estos son los cambios con respecto a 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 código JavaScript necesario para establecer 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);
}
Un aspecto que se debe tener en cuenta es que, si se escala por un valor negativo, se invierte la geometría. Espero que estos últimos 3 capítulos te hayan ayudado a comprender la traslación, la rotación y la escala. A continuación, veremos la magia de las matrices que combinan estos 3 elementos 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í no es importante. Lo importante es que puedas distinguir su orientación desde cualquier dirección. Si usáramos un corazón ♥ o un triángulo △, por ejemplo, no podríamos saber si se volteó horizontalmente. Un círculo ○ sería aún peor. Un rectángulo de color podría funcionar con diferentes colores en cada esquina, pero tendrías que recordar qué esquina es cuál. La orientación de una F se reconoce al instante.
Cualquier forma de la que puedas distinguir la orientación funcionaría, yo solo usé la "F" desde que me presentaron la idea por primera vez.
Matrices 2D de WebGL
En los últimos 3 capítulos, vimos cómo trasladar, rotar y escalar la geometría. La traslación, la rotación y la escala se consideran tipos 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, una rotación del 30% y una traslación de 100, 0.
Y aquí hay una traslación de 100,0, una rotación de 30% y una escala de 2, 1
Los resultados son completamente diferentes. Peor aún, si necesitáramos 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 se puede hacer lo mismo con las matemáticas de matrices. Para 2D, usamos una matriz de 3 x 3. Una matriz de 3 x 3 es como una cuadrícula con 9 casillas.
| 1.0 | 2.0 | 3.0 |
| 4.0 | 5.0 | 6.0 |
| 7.0 | 8.0 | 9.0 |
Para hacer el cálculo, multiplicamos la posición por las columnas de la matriz y sumamos los resultados. Nuestras posiciones solo tienen 2 valores, x e y, pero para hacer este cálculo necesitamos 3 valores, por lo que usaremos 1 para el tercer valor. En este caso, nuestro 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 mirando eso y pensando "¿CUÁL ES EL PUNTO?". Supongamos que tenemos una traducción. Llamaremos tx y ty a la cantidad que queremos trasladar. Creemos una matriz como esta
| 1.0 | 0.0 | 0.0 |
| 0.0 | 1.0 | 0.0 |
| tx | ty | 1.0 |
Y ahora, mira esto:
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 tus conocimientos de álgebra, podemos borrar cualquier lugar que se multiplique por cero. Multiplicar por 1 no hace nada, así que simplifiquemos para ver qué sucede
newX = x + tx;
newY = y + ty;
Y no nos importa mucho el extra. 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 la publicación sobre la rotación, solo necesitamos el seno y el coseno del ángulo en el que queremos rotar.
s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);
Y creamos una matriz como esta
| c | -s | 0.0 |
| s | c | 0.0 |
| 0.0 | 0.0 | 1.0 |
Si aplicamos la matriz, obtenemos lo siguiente:
newX = x * c + y * s + 1 * 0
newY = x * -s + y * c + 1 * 0
extra = x * 0.0 + y * 0.0 + 1 * 1
Si tachamos todas las multiplicaciones por 0 y 1, obtenemos lo siguiente:
newX = x * c + y * s;
newY = x * -s + y * c;
que es exactamente lo que teníamos en nuestra muestra de rotación. Por último, la escala. Llamaremos a nuestros 2 factores de escala sx y sy, y crearemos una matriz como esta.
| sx | 0.0 | 0.0 |
| 0.0 | sy | 0.0 |
| 0.0 | 0.0 | 1.0 |
Si aplicamos la matriz, obtenemos lo siguiente:
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 muy
newX = x * sx;
newY = y * sy;
que es igual a nuestro ejemplo de escalamiento.
Ahora, estoy seguro de que todavía estás pensando. ¿Qué significa? ¿Qué sentido tiene? Parece mucho trabajo solo para hacer lo mismo que ya estábamos haciendo.
Aquí es donde ocurre 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 devuelve el resultado.
Para que todo quede más claro, crearemos funciones que generen matrices para la traslación, la rotación y el escalamiento.
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 antiguo 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 sencillo.
<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);
}
Aun así, tal vez te preguntes: ¿Y qué? No parece un gran beneficio . Pero, ahora, si queremos cambiar el orden, no tenemos que escribir un sombreador nuevo. Solo 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 los brazos en un cuerpo, las lunas en un planeta alrededor de un sol o las ramas en un árbol. Para ver un ejemplo simple de animación jerárquica, dibujemos nuestra "F" 5 veces, pero cada vez comencemos 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, tuvimos que introducir la función makeIdentity, que crea una matriz de identidad. Una matriz de identidad es una matriz que representa efectivamente 1.0, de modo que si multiplicas por la identidad, no pasa nada. Al igual que
X * 1 = X
así 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 ejemplo hasta ahora nuestra 'F' rota alrededor de su esquina superior izquierda. Esto se debe a que las matemáticas que usamos siempre rotan alrededor del origen y la esquina superior izquierda de nuestra "F" está en el origen (0, 0). Pero ahora, como podemos hacer operaciones matemáticas de matrices y podemos 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 escalar desde cualquier punto. Ahora ya sabes cómo Photoshop o Flash te permiten mover el punto de rotación. Hagamos algo aún más loco. Si vuelves al primer artículo sobre los conceptos básicos de WebGL, es posible que recuerdes que tenemos código en el sombreador para convertir de píxeles a 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, el primero, "convertir de píxeles a 0.0 a 1.0", es en realidad una operación de escalamiento. La segunda también es una operación de escalamiento. 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 crear 2 matrices de escala, una para escalar por 1.0/resolución, otra para escalar por 2.0, una tercera para trasladar por -1.0, -1.0 y una cuarta para escalar Y por -1 y, luego, multiplicarlas todas juntas, pero, en cambio, como las matemáticas son simples, crearemos una función que cree una matriz de "proyección" para una resolución determinada 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 completo.
<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 o 7 pasos a uno muy simple con solo 1 paso, todo gracias a la magia de las matemáticas de matrices.
Espero que este artículo te haya ayudado a desmitificar las matemáticas de matrices. A continuación, pasaré a las imágenes en 3D. En 3D, las matemáticas de matrices siguen los mismos principios y usos. Comencé con 2D para que sea fácil de entender.