Ángulos de Euler: tres números y doce convenciones
Qué significa exactamente el orden de un Euler en Three.js, por qué la misma orientación admite infinitas ternas de ángulos, y qué gana y qué pierde esta representación.
Los ángulos de Euler son la representación de rotaciones que todo el mundo entiende sin explicación: inclina, gira, ladea. Esa legibilidad es su única ventaja, y es una ventaja enorme, porque es la que hace que las herramientas de modelado y de animación los expongan. El precio son tres problemas estructurales que conviene conocer antes de construir nada encima: dependen del orden, no son únicos, y tienen una singularidad.
- Interpretar el orden de un
Eulerde Three.js como un producto concreto de matrices. - Demostrar que dos ternas distintas pueden representar la misma orientación.
- Enumerar los tres problemas estructurales de la representación.
- Elegir el orden adecuado según el tipo de objeto que se orienta.
Tres rotaciones sucesivas, y el orden es parte del dato
Un ángulo de Euler no es una rotación: son tres rotaciones alrededor de tres ejes, aplicadas una detrás de otra. Y como componer rotaciones no es conmutativo, el orden forma parte de la definición. Una terna de ángulos sin un orden asociado no significa nada.
En Three.js, el orden es una propiedad del propio objeto Euler y por defecto vale XYZ. Ese código indica cómo se construye la matriz: para el orden XYZ, la matriz resultante es el producto de la rotación en X por la de Y por la de Z, en ese orden de escritura. Con la regla de derecha a izquierda de la composición, eso significa que sobre el vector actúa primero la rotación en Z, después la de Y y por último la de X.
Hay seis órdenes posibles con tres ejes distintos, y Three.js los soporta todos: XYZ, YXZ, ZXY, ZYX, YZX y XZY. En la literatura matemática existen otras seis convenciones que repiten el primer eje al final, como ZXZ; esas se llaman ángulos de Euler propios y en gráficos no se usan.
La elección de orden no es indiferente y responde a un criterio práctico: se coloca en el centro el eje sobre el que menos se va a girar, porque el eje central es el que provoca la singularidad. Para una cámara en primera persona, el orden habitual es YXZ: giro horizontal en Y, elevación en X, inclinación en Z. Con ese orden, la elevación queda en el centro y el bloqueo aparece al mirar completamente arriba o abajo, que es un caso que se puede limitar. Con el orden por defecto XYZ, la singularidad aparecería en una orientación mucho más frecuente.
import * as THREE from 'three';
const grados = THREE.MathUtils.degToRad;
// La misma terna con dos ordenes produce orientaciones distintas.
const a = new THREE.Euler(grados(30), grados(45), grados(60), 'XYZ');
const b = new THREE.Euler(grados(30), grados(45), grados(60), 'ZYX');
const qa = new THREE.Quaternion().setFromEuler(a);
const qb = new THREE.Quaternion().setFromEuler(b);
console.log('angulo entre ambas:', THREE.MathUtils.radToDeg(qa.angleTo(qb)).toFixed(2));
// reorder recalcula los angulos para conservar la orientacion.
const c = a.clone().reorder('ZYX');
const qc = new THREE.Quaternion().setFromEuler(c);
console.log('tras reorder, angulo:', qa.angleTo(qc).toFixed(9)); // 0
console.log('angulos nuevos:', [c.x, c.y, c.z].map((r) => +THREE.MathUtils.radToDeg(r).toFixed(2)));
reorder hace lo correcto: pasa por la orientación real y vuelve a extraer los ángulos en el orden nuevo. Cambiar la propiedad order a mano, en cambio, reinterpreta los mismos números con otra convención y cambia la orientación. Los dos comportamientos son útiles y confundirlos es fácil.
La misma orientación, ternas distintas
Los ángulos de Euler no son una representación única. Para cualquier orientación hay infinitas ternas que la producen, y no solo por sumar vueltas completas.
El ejemplo canónico es una media vuelta. Girar ciento ochenta grados alrededor del eje Y produce exactamente la misma orientación que girar ciento ochenta grados en X y otros ciento ochenta en Z. Son dos ternas sin ningún parecido numérico y describen el mismo objeto en el mismo sitio.
import * as THREE from 'three';
const uno = new THREE.Euler(0, Math.PI, 0, 'XYZ');
const dos = new THREE.Euler(Math.PI, 0, Math.PI, 'XYZ');
const q1 = new THREE.Quaternion().setFromEuler(uno);
const q2 = new THREE.Quaternion().setFromEuler(dos);
console.log('misma orientacion:', q1.angleTo(q2) < 1e-9); // true
console.log('ternas identicas:', uno.equals(dos)); // false
// Y al leer de vuelta desde la orientacion, sale una tercera terna canonica.
const canonica = new THREE.Euler().setFromQuaternion(q2, 'XYZ');
console.log('lo que devuelve three:', [canonica.x, canonica.y, canonica.z]
.map((r) => +THREE.MathUtils.radToDeg(r).toFixed(2))); // (0, 180, 0)
Esa no unicidad tiene una consecuencia práctica inmediata: no puedes comparar orientaciones comparando ángulos de Euler, ni interpolar entre dos ternas esperando que el resultado sea razonable. Si dos ternas describen la misma orientación con números distintos, interpolar entre ellas producirá un giro completo donde no debería haber ningún movimiento.
Y otra consecuencia menos obvia: los ángulos que Three.js devuelve al extraer un Euler desde una orientación son la forma canónica —con el ángulo central en un rango de menos noventa a noventa grados—, que casi nunca coincide con los que escribiste. Un objeto al que le asignas una rotación de doscientos setenta grados y del que después lees la rotación te devolverá menos noventa. Es correcto, es la misma orientación, y rompe cualquier lógica que dependa de acumular el valor leído.
Para qué siguen siendo la mejor opción
Con tres defectos estructurales, cabría concluir que no hay que usarlos. Sería un error: los ángulos de Euler siguen siendo la representación correcta en varios sitios y no por inercia.
Para exponer controles a una persona. Nadie ajusta un cuaternión a mano. Un panel de depuración con tres deslizadores de grados es comprensible al instante, y es lo que hacen todas las herramientas de modelado.
Para rotaciones sobre un solo eje. Una puerta que gira, una rueda, un planeta sobre su eje, una torreta que apunta en horizontal. Con un solo ángulo activo no hay ambigüedad, no hay orden que importe y no hay singularidad posible. Este caso es muchísimo más frecuente de lo que parece.
Para animación por canales. Un sistema de curvas de animación necesita valores escalares independientes para poder editarlos por separado, y tres ángulos encajan de forma natural. Los formatos de intercambio modernos guardan las rotaciones como cuaterniones precisamente para evitar los problemas de interpolación, pero las herramientas de autoría siguen mostrando curvas de Euler.
Para almacenar orientaciones legibles. Un archivo de configuración con tres números en grados se lee y se corrige a mano; cuatro números entre menos uno y uno, no.
Cada Object3D tiene una propiedad rotation de tipo Euler y una propiedad quaternion, y mucha gente asume que son dos estados independientes que hay que mantener a mano, o que una de las dos es la buena. Ninguna de las dos cosas es cierta. En el constructor de Object3D, Three.js registra sendas funciones de notificación: cuando cambias cualquier componente de rotation, se recalcula el cuaternión inmediatamente; cuando cambias el cuaternión, se recalcula el Euler inmediatamente, conservando el order que ya tuviera. La sincronización es inmediata, no diferida, y funciona en las dos direcciones. Eso significa tres cosas prácticas. Primera: objeto.rotation.y += 0.01 es perfectamente válido y no tiene ninguna penalización oculta más allá de la conversión. Segunda: escribir en las dos representaciones en el mismo fotograma no las combina, la segunda escritura sobrescribe a la primera, y ese es un bug real cuando dos sistemas distintos —por ejemplo un control de cámara y un sistema de sacudida— tocan cada uno la suya. Tercera, y la más sutil: si escribes un Euler y luego lo lees, puedes obtener números distintos de los que pusiste, porque la ida y vuelta pasa por el cuaternión y devuelve la forma canónica. Cualquier lógica del tipo «acumular la rotación leyendo la propiedad, sumar y volver a escribir» funciona hasta que cruza los noventa grados en el eje central, y entonces da un salto. La solución no es pelearse con la conversión: es guardar tus propios ángulos acumulados en variables tuyas y escribir en el objeto, sin leer nunca de vuelta.
El problema que queda
Los tres defectos vistos hasta aquí —orden, no unicidad y la ida y vuelta— son molestos pero manejables con disciplina. El cuarto no lo es, porque no es un problema de la API sino de la representación misma: en ciertas orientaciones, los tres ángulos dejan de poder describir todas las rotaciones posibles. Un grado de libertad desaparece.
Ese fenómeno tiene nombre propio, una explicación algebraica exacta y una historia aeroespacial, y merece su propia lección.