Normalizar ángulos

En ocasiones tenemos un ángulo que nos determina la rotación de un objeto y necesitamos actualizarlo girando en un sentido u en otro. La forma más simple de actualizar este ángulo es sumando o restando otro valor que nos indique cuánto queremos girar el objeto.

Esta manera de actualizar ángulos, aunque es simple, tiene un pequeño defecto: si seguimos girando en el mismo sentido continuamente el ángulo resultante tendrá un valor muy grande.

Si bien, matemáticamente un ángulo muy grande equivalente a uno más pequeño de cero y 360°, hay varias razones por la que nos podría interesar limitar el valor del ángulo a este rango:

  • Si como parte de la interfaz de nuestro juego queremos mostrar el valor de un ángulo, sería poco conveniente mostrar un ángulo no normalizado, por ejemplo 3630°, en vez del ángulo normalizado equivalente (en el ejemplo, 30°).

  • La precisión de los ordenadores a la hora de trabajar con números decimales es limitada, y al llegar a valores muy grandes descartan las cifras menos significativas. Si incrementamos el valor de un ángulo una y otra vez sin normalizarlo llegará un momento en el que el valor sea tan grande que los incrementos que se hagan tendrán una precisión reducida o incluso dejarán de tener efecto.

    Por ejemplo, sumar 10.000.000 + 1 en una variable float de 32 bits devuelve 10.000.000 porque la variable no tiene espacio suficiente para almacenar el número entero.

    Teniendo en cuenta que en una variable float, de los 32 bits disponibles, 23 bits se utilizan para guardar la parte fraccionaria (referencia) y que para representar un ángulo con precisión capaz de distinguir 512 valores necesitamos 9 bits, empezaríamos a tener problemas tras girar nuestro objeto $2^{(23-9)}=2^{14}= 16.384$ vueltas, suponiendo que ha partido de un valor de cero. Si consideramos que esto puede suceder, nos interesará normalizar los ángulos para evitarlo.

Algoritmo de normalización

En primer lugar estudiaremos el algoritmo de normalización en genérico, de manera que nos pueda servir para cualquier variable cíclica. Después la especializaremos para grados y radianes.

Variables cíclicas

Entenderemos una variable cíclica como aquella en la que existe un rango normal de valores (definido por un mínimo y un máximo $[min,max)$) y en la un valor fuera del rango normal es funcionalmente equivalente a ese mismo valor sumado o restado la amplitud del rango ($max - min$) un número entero de veces.

Un ejemplo cotidiano de variable cíclica es el reloj de 12 horas: las 11 + 4 horas son las 15, cuyo valor normalizado es $15-12=3$ (las 3).

Un aspecto a destacar es que el máximo del rango de una variable cíclica, por definición, no es un valor normalizado. Por ejemplo, si nuestro rango es $[0^{\circ}, 360^{\circ})$, el valor normalizado de 360° es 0°.

Proceso de normalización

Seguiremos utilizando el ejemplo del reloj de 12 horas. En la figura siguiente se ven representados sobre una línea el el rango de valores normal y un valor val que intentaremos normalizar. Una hora normalizada tiene cualquier valor entre 1 y 13, con el 13 no incluido (1:00 es una hora normalizada y 12:59 también).

En primer lugar desplazaremos todos los valores de manera que el rango comience en cero.

El proceso seguir después, depende de si $val - min$ es positivo o negativo.

Si es positivo

En el caso de que $val - min$ sea positivo, estará a la derecha del cero en nuestra representación. Calculamos el resto de la división de $val - min$ entre la amplitud ($max - min$).

$a\ %\ b$ denota el resto obtenido al hacer la división de $a$ entre $b$.

Con esto hemos conseguido ajustar el valor de $val$ entre 0 y 12. pero nuestro rango inicial era entre 1 y 13. Sumando el mínimo (1) obtenemos el valor deseado.

$$normalizar(val, min, max)\ |\ val \geq min \\ = min + (val - min)\ %\ (max - min)$$

Si es negativo

En el caso de que $val - min$ sea positivo, estará a la izquierda del cero en nuestra representación, como podemos ver en la siguiente figura.

Podemos alternar el signo dando la vuelta a los operandos de la resta y operar como en el caso de que fuera positivo.

$$-(val - min)\
= -val -(-min) \
=min - val$$

El resultado es correcto, pero está invertido debido al cambio de signo. Podemos corregir esto restando a la amplitud $(max - min)$ el valor calculado.

Finalmente, igual que en el caso de los valores positivos, sumamos $min$ para obtener una hora entre 1 y 13.

$$normalizar(val, min, max)\ |\ val \lt min \\ = min + (max - min) \\ - ((min - val) %\ (max - min))$$

$min-min$ se anula y queda…

$$normalizar(val, min, max)\ |\ val \lt min \\ = max - ((min - val)\ %\ (max - min))$$

Fórmula y código

Juntando los dos casos anteriores, esta es la definición matemática de nuestra función de normalización:

$$normalizar(val, min, max)\ |\ val \lt min \\ = min + (max - min) \\ - ((min - val)\ %\ (max - min))$$

En código:

function normCyclic(val, min, max) {
   if (val >= min) {
     return min + (val - min) % (max - min);
   } else {
     return max - (min - val) % (max - min);
   }
}

Con esta función podemos normalizar cualquier varible cíclica. Para terminar el ejemplo, la siguiente función normalizaría tiempo en formato de 12 horas.

function norm12Hours(val) {
  return normCyclic(val, 1, 13);
}

Normalizar ángulos en distintos formatos

Teniendo nuestra función de normalización, es trivial construir funciones que normalizen variables cíclicas de distinto tipo.

Normalizar radianes

Esta función normaliza grados medidos en radianes dentro del rango $[-\pi,\pi)$. Podemos utilizarla para actualizar variables de giro sin miedo a que se produzca pérdida de precisión.

function normRad(val) {
  return normCyclic(val, -Math.PI, Math.PI);
}

Para actualizar una variable de rotación realizando normalización utilizaríamos un código como este:

function update() {
  var rotSpeed = 3; // rad/s
  // Actualiza la rotación del objeto teniendo
  // sumándole la velocidad angular.
  this.rotation = normRad(this.rotation + rotSpeed);
}

Normalizar grados

De forma similar podemos normalizar grados. Podemos utilizar el rango $[0^{\circ}, 360^{\circ})$, con lo que obtendríamos sólo números positivos, o $[-180^{\circ}, 180^{\circ})$ si queremos tener ángulos tanto positivos como negativos.

function normDeg360(val) {
  return normCyclic(val, 0, 360);
}

function normDeg180(val) {
  return normCyclic(val, -180, 180);
}

Cuando no normalizar variables angulares

Cuando definimos al principio de la definición del algoritmo las variables cíclicas especificamos una restricción:

Entenderemos una variable cíclica como aquella en la que existe un rango normal de valores (definido por un mínimo y un máximo $[min,max)$) y en la un valor fuera del rango normal es funcionalmente equivalente a ese mismo valor sumado o restado la amplitud del rango ($max - min$) un número entero de veces.

En una variable de rotación la condición de equivalencia es cierta. Por ejemplo, un elemento rotado 540° produce el mismo resultado que rotando 180°: el elemento aparece girado media vuelta.

Sin embargo, hay otras variables en la que hay una diferencia muy importante. Es el caso de las variables de velocidad angular y aceleración angular: que un objeto gire 540°/s no produce el mismo efecto que si gira a 180°/s. En el primer caso da una vuelta completa y media en un segundo, mientras en el segundo caso, en el mismo tiempo, sólo recorre media vuelta.

La normalización de ángulos es útil para evitar condiciones inesperadas en nuestro código, así como mostrar valores al usuario en un rango razonable, pero no debe usarse de forma compulsiva en toda variable angular.