wandres.dev
MATEMÁTICAS DEL 3D II · Matrices y transformaciones

Qué es una transformación lineal y por qué se escribe como matriz

La intuición geométrica que convierte una tabla de números en una operación con sentido: las columnas de una matriz son las imágenes de los ejes.

⏱ 18 min

Una matriz no es una tabla de números que hay que memorizar cómo multiplicar. Es la descripción completa de una transformación del espacio, escrita de la forma más compacta posible: basta con decir a dónde van a parar los tres ejes. Con esa idea, la multiplicación de matriz por vector deja de ser un algoritmo y pasa a ser evidente, y todo lo que viene después —composición, inversas, la matriz normal— se vuelve deducible en lugar de memorizable.

🎯 Al terminar esta lección sabrás
  • Definir transformación lineal por sus dos propiedades y traducirlas a consecuencias geométricas.
  • Explicar por qué las columnas de una matriz son las imágenes de los vectores base.
  • Interpretar el determinante como factor de volumen y su signo como cambio de orientación.
  • Manejar el almacenamiento por columnas de Matrix4 sin confundirlo con la escritura por filas.

Dos propiedades y todo lo que implican

Una transformación es lineal si cumple dos condiciones: transformar una suma es lo mismo que sumar las transformaciones, y transformar un vector escalado es lo mismo que escalar el vector transformado. Escrito así parece una definición de libro sin consecuencias. Tiene tres, y son visuales.

Las rectas siguen siendo rectas. Y además siguen siendo paralelas si lo eran, y siguen estando igualmente espaciadas.

El origen no se mueve. Se deduce directamente: transformar el vector cero es lo mismo que transformar cualquier vector escalado por cero, y eso da cero.

La rejilla se deforma de manera uniforme. No hay curvatura, no hay pliegues, no hay zonas que se comporten distinto de otras. Una transformación lineal hace lo mismo en todas partes.

Las rotaciones, los escalados, las reflexiones y los cizallamientos son lineales. La traslación no lo es, porque mueve el origen, y ese hecho aparentemente menor obliga a toda la maquinaria de la lección siguiente.

Las columnas son las imágenes de los ejes

Aquí está la idea que convierte la matriz en algo comprensible.

Cualquier vector se puede escribir como combinación de los tres ejes unitarios: (3, 0, 4) es tres veces el eje X más cero veces el eje Y más cuatro veces el eje Z. Como la transformación es lineal, transformar esa combinación es combinar las transformaciones con los mismos coeficientes. Es decir: si sé a dónde van a parar los tres ejes, sé a dónde va a parar cualquier vector.

Y a dónde van a parar los tres ejes son nueve números para tres dimensiones: tres coordenadas por cada eje transformado. Esos nueve números son la matriz, colocados de forma que cada columna es la imagen de un eje. La primera columna es dónde acaba el eje X, la segunda dónde acaba el eje Y, la tercera dónde acaba el eje Z.

Con eso, la multiplicación de matriz por vector deja de ser un algoritmo con índices cruzados y pasa a ser una frase: combinar las columnas de la matriz con los pesos que da el vector. Multiplicar la primera columna por la primera componente, la segunda por la segunda, la tercera por la tercera, y sumar.

Léelo al revés y tendrás el atajo más útil para depurar: si abres una matriz y quieres saber qué hace, mira sus columnas. Si la primera columna es (0, 1, 0), esa matriz manda el eje X a donde estaba el eje Y, es decir, contiene una rotación de noventa grados en ese plano. Si las columnas son (2,0,0), (0,2,0) y (0,0,2), es un escalado uniforme al doble. Una matriz de rotación tiene sus tres columnas unitarias y mutuamente perpendiculares, porque una rotación no deforma los ejes, solo los gira.

import * as THREE from 'three';

// Construir una transformacion diciendo a donde van los ejes.
const nuevoX = new THREE.Vector3(0, 1, 0);   // el eje X acaba donde estaba Y
const nuevoY = new THREE.Vector3(-1, 0, 0);  // el eje Y acaba en la X negativa
const nuevoZ = new THREE.Vector3(0, 0, 1);   // el eje Z no se mueve

const giro = new THREE.Matrix4().makeBasis(nuevoX, nuevoY, nuevoZ);

const v = new THREE.Vector3(3, 0, 4);
console.log('transformado:', v.clone().applyMatrix4(giro).toArray());  // (0, 3, 4)

// Es exactamente una rotacion de 90 grados alrededor del eje Z.
const referencia = new THREE.Matrix4().makeRotationZ(Math.PI / 2);
console.log('coinciden:', giro.equals(referencia) || 'salvo redondeo');

// El determinante mide cuanto cambia el volumen.
console.log('determinante de una rotacion:', giro.determinant().toFixed(6));   // 1

const escalado = new THREE.Matrix4().makeScale(2, 3, 1);
console.log('determinante del escalado:', escalado.determinant());             // 6

const espejo = new THREE.Matrix4().makeScale(-1, 1, 1);
console.log('determinante del espejo:', espejo.determinant());                 // -1

El determinante: volumen y orientación

El determinante de una matriz es el factor por el que multiplica los volúmenes. Un escalado de dos por tres por uno multiplica el volumen por seis, y su determinante es seis. Una rotación no cambia volúmenes y su determinante es uno.

El signo añade una información que en gráficos importa mucho: un determinante negativo significa que la transformación ha invertido la orientación del espacio. Es lo que hace un espejo. Y tiene una consecuencia que se ve en pantalla: si aplicas una escala negativa a un objeto, el orden en que sus triángulos aparecen recorridos se invierte, y todos ellos pasan a considerarse caras traseras. Un objeto reflejado con una escala negativa desaparece si su material solo dibuja caras frontales.

Three.js compensa este caso concreto: el renderer comprueba el signo del determinante de la matriz de mundo de cada malla y, si es negativo, invierte el criterio de cara frontal para ese objeto. Es la razón de que puedas escalar por menos uno para hacer un espejo de un modelo y funcione sin más. Conviene saber que ese arreglo existe, porque cuando escribes tu propio pipeline de dibujado no lo tienes.

El determinante cero significa que la transformación aplasta el espacio: manda todo a un plano, a una recta o a un punto. Y aplastar no se puede deshacer, así que una matriz con determinante cero no tiene inversa. En la práctica esto aparece cuando alguien pone una escala en cero para «esconder» un objeto: la matriz deja de ser invertible, y cualquier cálculo que necesite la inversa —empezando por la matriz normal— produce valores sin sentido. Para ocultar algo está visible, que además ahorra el dibujado entero.

Matrix4 se escribe por filas y se almacena por columnas, y esa asimetría es intencionada

Cuando llamas a matriz.set(...) pasas los dieciséis números en orden de filas, tal como los escribirías en un papel. Cuando lees matriz.elements, los encuentras en orden de columnas. No es una incoherencia: set está pensado para que el código se parezca a la matriz que tienes en la cabeza, y elements está en el orden que exigen OpenGL y WebGL para subir la matriz a la GPU sin transponerla. La consecuencia práctica es la que hay que memorizar: en elements, la traslación está en los índices 12, 13 y 14, no en 3, 7 y 11 como sugeriría la escritura por filas. Ese es el dato con el que se puede leer una matriz volcada en la consola de un vistazo, y el que explica por qué tantos fragmentos de código que copian matrices entre bibliotecas producen objetos en posiciones absurdas: la otra biblioteca guardaba por filas. Hay una prueba de diagnóstico infalible cuando sospechas de una transposición: aplica la matriz a un punto conocido y mira si el resultado es el que esperas o su reflejo respecto a la diagonal. Y hay una regla de higiene: no construyas matrices a mano con set salvo que sepas exactamente lo que haces; usa makeRotationX, makeScale, makeTranslation, makeBasis y compose, que no admiten este error.

Las transformaciones básicas y qué columnas tienen

Vale la pena tener el catálogo en la cabeza, porque reconocerlo al leer una matriz volcada en consola ahorra mucho tiempo.

Escalado. Diagonal: cada eje se estira sin cambiar de dirección. Los valores fuera de la diagonal son cero.

Rotación. Columnas unitarias y mutuamente perpendiculares, determinante uno. Su inversa es su traspuesta, propiedad que se usa constantemente porque transponer es gratis e invertir no.

Cizallamiento. Un eje se inclina en la dirección de otro. Aparece un valor fuera de la diagonal. En gráficos casi nunca se busca a propósito, pero aparece solo, sin que nadie lo pida, al componer escalas no uniformes con rotaciones. Ese detalle tiene consecuencias que se ven en la lección de composición.

Reflexión. Como un escalado con algún valor negativo. Determinante negativo.

Proyección. Aplasta una dimensión. Determinante cero, no invertible. La proyección en perspectiva no es de este tipo, y por qué no lo es tiene su propia lección.

import * as THREE from 'three';

function describir(nombre, m) {
  const e = m.elements;
  const columnaX = new THREE.Vector3(e[0], e[1], e[2]);
  const columnaY = new THREE.Vector3(e[4], e[5], e[6]);
  const columnaZ = new THREE.Vector3(e[8], e[9], e[10]);
  const traslacion = new THREE.Vector3(e[12], e[13], e[14]);

  console.log(nombre);
  console.log('  X va a', columnaX.toArray().map((n) => +n.toFixed(3)));
  console.log('  Y va a', columnaY.toArray().map((n) => +n.toFixed(3)));
  console.log('  Z va a', columnaZ.toArray().map((n) => +n.toFixed(3)));
  console.log('  traslacion', traslacion.toArray());
  console.log('  determinante', +m.determinant().toFixed(3));
  console.log('  es rotacion pura:', Math.abs(columnaX.length() - 1) < 1e-6
    && Math.abs(columnaX.dot(columnaY)) < 1e-6);
}

describir('rotacion en Y de 45 grados', new THREE.Matrix4().makeRotationY(Math.PI / 4));
describir('escalado no uniforme', new THREE.Matrix4().makeScale(2, 0.5, 1));
describir('cizalla', new THREE.Matrix4().set(
  1, 0.5, 0, 0,
  0, 1,   0, 0,
  0, 0,   1, 0,
  0, 0,   0, 1
));

Esa función describir es una herramienta de depuración real, no un ejercicio. Cuando un objeto aparece deformado y no sabes por qué, volcar las columnas de su matriz de mundo con esos cuatro datos —a dónde va cada eje, cuál es la traslación, cuál el determinante y si sigue siendo una rotación pura— identifica el problema en segundos. Guárdala.