La interpolación es un método matemático utilizado para encontrar un valor intermedio entre dos puntos puntos conocidos. Cuando esos dos puntos conocidos están unidos por una línea recta, decimos que estamos ante una interpolación lineal.
Aunque la definición matemática es un poco áspera, lo cierto es que nuestra vida diaria está poblada de interpolaciones:
- Si mi coche automático tiene una velocidad máxima de 160 Km/h ¿A qué velocidad iremos cuando pisemos el acelerador a la mitad de su profundidad?
- Si la autonomía de mi avión es de 300 Km, ¿Cuántos kilómetros más podremos recorrer cuando llevemos consumido un tercio del depósito?
- Si mi nevera llena me dura 7 días y tengo la mitad de las baldas vacías ¿Cuándo me tocará hacer la compra?
Aunque los ejemplos anteriores parecen heterogéneos, en realidad no lo son. Todos se modelan mediante una función lineal, con una gráfica como la siguiente:
- En el ejemplo del coche automático, el eje X es la profundidad de pisado del acelerador, mientras que el eje Y es la velocidad del coche.
- En el caso del avión, el eje X son los litros de queroseno consumidos y el eje Y los kilómetros recorridos.
- Para la nevera, el eje X son los días que van trascurriendo y el eje Y las baldas de la nevera que se van vaciando.
En todos los casos, la estructura de la gráfica (y de la función que representa) es la misma: una línea recta. Lo que diferenciará un caso u otro es la pendiente de la línea, es decir el ritmo de cambio del eje Y conforme avanzamos en el eje X.
Si en la vida real tenemos que hacer interpolaciones, te puede imaginar que en el desarrollo de juegos también. Al fin y al cabo, ¿Qué son los videojuegos sino una recreación de la vida real?
Pongamos un ejemplo similar a los anteriores. Supongamos que estoy desarrollando un simulador y quiero dar soporte de HOTAS.
Cuando nuestro jugador mueva la palanca de gases (la de la izquierda), el Input System de Unity nos devolverá un valor entre 0 y 1: 0 cuando la palanca esté situada en su posición mínima (la más cercana al cuerpo del jugador) y 1 cuando esté en su posición máxima (la más lejana al cuerpo del jugador). Con esa información, tendremos que hacer una interpolación para calcular la velocidad del vehículo, en todas las posiciones intermedias de la palanca, sabiendo que cuando esté en 0 el vehículo debe pararse y cuando esté en 1 debe ir a la velocidad máxima.
Si volvemos a nuestra gráfica, tendremos la misma estructura, sólo que en el eje X se representará la posición de la palanca (que sólo se moverá entre X=0 y X=1) y en el eje Y se representará la velocidad del vehículo de nuestro juego.
¿Cómo haremos ese cálculo? Hay varias posibilidades, dependiendo de los rangos que representemos en los ejes X e Y. Vamos a estudiarlas de menor a mayor complejidad.
Interpolación con rango entre 0 y 1 del eje X
Es el caso más sencillo: el eje X varía entre 0 y 1, mientras que el eje Y varía entre un valor inicial (V1) y un valor final (V2).
Matemáticamente, el valor a interpolar (Y) se obtendría de la siguiente manera:
La fórmula de una función lineal |
Podríamos escribir una función que implementase esa fórmula, pero sería reinventar la rueda, porque Unity ya ofrece un método que hace eso mismo: Mathf.Lerp().
Lerp() acepta tres parámetros:
- Un valor inicial.
- Un valor final.
- Un valor entre 0 y 1.
Si el tercer parámetro fuera 1, la función devolvería el valor final, si fuera 0 devolvería el valor inicial y si estuviera entre 0 y 1 devolvería un valor intermedio obtenido con la línea que une los dos valores anteriores.
En nuestro ejemplo de la palanca de gases, le pasaríamos a Lerp(), la velocidad mínima del vehículo, su velocidad máxima y el valor, entre 0 y 1, que nos devolvería el Input System sobre la posición de la palanca de gases. El valor retornado por el método sería la nueva velocidad del vehículo.
También puede interesarnos obtener la inversa de una interpolación.
Supón que la nave de nuestro juego cuenta con unos escudos y que estos han estado recibiendo los impactos de los proyectiles de nuestro enemigos. El escudo tiene un valor que representa su fuerza y a este valor le hemos estado restando una cantidad por cada uno de los impactos. Nos interesará coger el valor actual del escudo y representarlo en el panel de mandos de la cabina para que el jugador pueda saber a qué porcentaje tiene los escudos. Es decir, tenemos el valor máximo de los escudos (pongamos que 300), tenemos el valor mínimo de los escudos (lo normal es que sea 0), tenemos el valor actual de los escudos (supongamos que 150) y queremos saber el valor sobre 1 que representa ese valor actual respecto al total de los escudos, para poder presentar un porcentaje al jugador.
Si reorganizamos la fórmula anterior, veremos que la fórmula de función inversa es:
- V2, como valor máximo de los escudos: 300
- V1, como el valor mínimo de los escudos: 0
- Y, como el valor actual de los escudos: 150
Nos sale que los escudos están al 0,5 en tanto por uno. Para sacar el porcentaje no tienes más que multiplicar por 100 el tanto por uno, así que te sale que el escudo está al 50% de su capacidad.
Por suerte, tampoco tenemos que implementar esta fórmula en un método propio. Unity ya lo ha hecho por nosotros a través del método Mathf.InverseLerp().
InverseLerp() acepta tres parámetros:
- Un valor inicial, en nuestro ejemplo 0.
- Un valor final, en nuestro caso 300.
- El valor intermedio del que queremos obtener su interpolación inversa, en nuestro caso 150.
Con esos datos, InverseLerp() devolvería 0,5.
Hay que tener en cuenta que si el valor intermedio no lo es, es decir que está por debajo del valor inicial o por encima del final, entonces InverseLerp() devuelve 0 o 1, respectivamente.
Interpolación angular
Cuando operamos con ángulos, puede ser tentador utilizar Lerp(), pero debemos evitarlo. Los ángulos son un caso especial, ya que vuelven a empezar cuando llegan al máximo de 360º. Por ejemplo, un ángulo de 380º es lo mismo que un ángulo de 20º.
Para tener en cuenta esa peculiaridad, Unity ofrece el método Mathf.LerpAngle().
Este método acepta tres parámetros:
- El ángulo inicial, en grados.
- El ángulo final, en grados.
- El valor intermedio entre 0 y 1.
Ten cuidado, porque el método no juega con ángulos de 0 a 360º, sino con un rango de -180º a 180º. Si visualizas tus 0º como un vector vertical, los 180º se alcanzarán cuando gires el vector hacia la derecha hasta que se dé media vuelta. Los -180º se alcanzarán girando a la izquierda.
Para que te hagas una idea, una llamada a LerpAngle(0.0f, 190.0f, 1.0f) devolverá -170.0f, porque a los 180º interpretará que los 10 restantes se han obtenido girando 170º hacia la izquierda. Es decir, que LerpAngle() siempre te devolverá el giro más corto para rotar tu vector. Si lo que te interesase es el giro largo (en este caso girar los 190º hacia la derecha) tendrías que usar Lerp().
Interpolación lineal con valores del eje X más allá de 0 y 1
Por defecto, Lerp() sólo te dejará meter valores intermedios entre 0 y 1, pero puede interesarte obtner valores por encima o por debajo. En ese caso, Unity ofrece Mathf.LerpUnclamped().
Sus parámetros son los mismos que Lerp(), sólo que te deja meter valores intermedios por debajo de 0 y por encima de 1. El método se limita a prolongar la línea a ambos lados del rango [0,1] y a darte el el valor resultante.
Interpolación no lineal
Hasta ahora hemos supuesto que nuestro rango en el eje X se limitaba a 0 y 1. Todos los valores intermedios que le metíamos a Lerp() estaban limitados a ese rango.
Sin embargo, puede haber situaciones en las que nos interese que el eje X sea otro rango distinto, por ejemplo porque queremos aplicar funciones lineales diferentes según avanzamos por el eje X.
Imagina que tenemos una nave y queremos que su velocidad se vea afectada por el daño acumulado, ralentizando la nave conforme acumulemos más daños. Sin embargo, no queremos que la ralentización sea uniforme, sino que preferimos que, a partir de cierto umbral de daños, la ralentización se acelere.
Tendríamos una gráfica con los daños en el eje X, la ralentización en el eje Y. De 0 al umbral del daños, la gráfica sería una línea que iría ascendiendo lentamente en un primer tramo, pero tendría un codo al llegar al umbral y, a partir de él, iniciaría un segundo tramos en el que ascendería de manera mucho más pronunciada. Ya no tendríamos una línea recta continua, sino una que se torcería en un punto determinado. Este tipo de funciones se denominan no lineales.
Hay varias maneras de implementar algo así. Podríamos normalizar el eje X del primer tramo usando el InverseLerp() con los valores inicial, final y actual del tramo en el eje X y utilizar el valor resultante para hacer un Lerp() de los valores mínimo y máximo del eje Y para ese tramo. El problema es que habría que repetir todas esas operaciones con el segundo tramo.
Podríamos simplificar un poco los cálculos usando la función remap() del paquete Mathematics de Unity. Ese método permite pasar un rango de origen (en el eje X), un rango de destino (en el ejeY) y un valor intermedio en el rango de origen, para obtener el valor equivalente en el rango de destino. Esto nos ahorraría tener que encadenar InverseLerp() y Lerp(), pero seguiría suponiendo aplicar el método remap() a cada uno de los tramos. Aparte de que nos obligaría a instalar el paquete Mathematics, a través del Package Manager.
Este método resulta insostenible en el caso de que nuestra gráfica tenga múltiples tramos y además, la transición entre un tramo y otro podría ser demasiado llamativa.
En esos casos, lo mejor es que recurramos a las AnimationCurve. Como su nombre indica, están pensadas para ser usadas en el ventana de animación, pero las podemos usar desde nuestro código. Las AnimationCurve nos permiten definir gráficamente nuestra función, con todos los tramos, curvas y rectas que queramos.
Por ejemplo, supongamos que queremos implementar un vehículo con una aceleración y una frenada suaves. Si fueran lineales sería poco natural. En vez de eso vamos a aplicar curvas. Para incluirlas en nuestro código podríamos hacer:
Los campos anteriores se verían desde el inspector de la siguiente manera:
AnimationCurves desde el inspector |
Pinchando cualquiera de las AnimationCurve podremos editarla para darle forma. Por ejemplo, la de aceleración tiene este aspecto:
Curva de la aceleración |
Si te fijas, la curva anterior está normalizada tanto en el eje X, como en el Y, ya que ambas se sitúan entre los valores 0 y 1. Sin embargo, eso no debería ser un problema con lo que que hemos aprendido hasta ahora. Por ejemplo, para calcular la velocidad durante la aceleración podemos hacer:
Muestreo de una AnimationCurve |
Dado que la variable accelerationRadius define la distancia, desde el arranque, a la que el vehículo alcanza su velocidad máxima, sabemos que esa distancia se corresponde con el valor X=1 de la curva de aceleración. Por eso, para saber en qué punto de la curva de aceleración estamos, haremos un InverseLerp(), pasando como valor intermedio nuestra distancia al punto de arranque (línea 94).
El punto obtenido del InverseLerp(), se lo pasaremos al método Evaluate() de la AnimationCurve, para que nos devuelva el valor de la gráfica en ese punto (línea 93). Dado que el eje Y de la gráfica se corresponde con la velocidad, y que el punto Y=1 es el de velocidad máxima, nos basta con multiplicar el valor que nos devuelve la curva por la velocidad máxima.
Como verás, el uso de una AnimationCurve te libera de tener que aplicar cálculos diferentes por tramos y hace las transiciones mucho más naturales y suaves.
Conclusión
Las interpolaciones son muy útiles en el desarrollo juegos. Los casos más simples se pueden resolver con el uso de los métodos Lerp e InverseLerp, contando con LerpAngle cuando manipulemos ángulos. Para casos más sofisticados y complejos lo más cómodo será contar con AnimationCurves y exponerlas a través del inspector para poder editarlas de manera visual.