Universitat Oberta de Catalunya

Animación en base a transformaciones homogéneas con lenguaje de programación Processing

Las transformaciones homogéneas permiten que traslademos, escalemos o rotemos nuestras primitivas (objetos a visualizar) 2D o 3D en la escena. Se vienen utilizando desde hace lustros y las utilizan prácticamente todos los desarrollos en los que hay objetos en movimiento. Por supuesto en Videojuegos. Lejos de la necesidad de disponer de unos conocimientos profundos de Álgebra Matricial, podemos implementarlas fácilmente a partir de llamadas a ciertas funciones de Processing. Además podemos aprovechar este ejemplo para entender cómo se “salva” y se “recupera” el estado en Processing, sobretodo en lo relativo a posicionar objetos en relación a otros y no tanto de forma absoluta según unas coordenadas de ventana que les referencien con el origen de coordenadas del canvas.

Vamos a implementar una animación basada en un sencillo Sistema Solar consistente en un Sol, un planeta Tierra y una Luna. El Sol estará centrado en nuestra ventana mientras que el planeta Tierra orbitará a su alrededor, a una cierta distancia. La Luna orbitará a una velocidad distinta alrededor del planeta Tierra, también a una cierta distancia de éste. El ejemplo está basado, y simplificado también, en el clásico programado en Lenguaje C hacia 2004 por McCormack para la librería OpenGL. Vemos el resultado en la figura 1.

Figura 1. Dos instantes en la ejecución de nuestro Sistema Solar para Processing.

Figura 1. Dos instantes en la ejecución de nuestro Sistema Solar para Processing.

Para implementar el Sistema Solar vamos a crear dos funciones específicas para a) Pintar un objeto celeste como un círculo (drawCircle(float myRadius, color myColor)) y b) Pintar todo el sistema (drawSolarSystem()), tal y como se observa en el código 1.

// A Processing coding for a simple Solar System
// based on solarsystem.c – Jon McCormack, April 2004
// for the OpenGL library

// Our variables to control angles and increments
// for the Earth and Moon (rotations)
float earthAngle;
float moonAngle;
float earthInc = 0.02;
float moonInc = 0.05;

// drawCircle draws a circle of given myRadius in the z = 0 plane
// with the given myColor
void drawCircle(float myRadius, color myColor){
fill(myColor);
ellipse(0,0,myRadius,myRadius);
}

// drawSolarSystem: draws our simplistic solar system
// with a Sun, a planet Earth and a Moon
void drawSolarSystem() {
// Our solar system properties
float SUN_RADIUS = 100.0;
float EARTH_TO_SUN_DISTANCE = 120.0;
float EARTH_RADIUS = 30.0;
float MOON_TO_EARTH_DISTANCE = 35.0;
float MOON_RADIUS = 15.0;
// 1st: draw Sun at the center of the window
// Color = yellow
pushMatrix();
color sunColor = color(255,255,0);
translate(width/2.0,height/2.0);
drawCircle(SUN_RADIUS,sunColor);
popMatrix();

// 2nd: draw Earth a) Related and b) Rotating around Sun
// Color = grey
pushMatrix();
color earthColor = color(127,127,127);
translate(width/2.0,height/2.0);
rotate(earthAngle);
translate(EARTH_TO_SUN_DISTANCE, 0.0);
drawCircle(EARTH_RADIUS,earthColor);
popMatrix();

// 3rd: draw Moon a) Related and b) Rotating around Earth
// Color = green
pushMatrix();
color moonColor = color(0,255,255);
translate(width/2.0,height/2.0);
rotate(earthAngle);
translate(EARTH_TO_SUN_DISTANCE, 0.0);
rotate(moonAngle);
translate(MOON_TO_EARTH_DISTANCE, 0.0);
drawCircle(MOON_RADIUS,moonColor);
popMatrix();
}

Código 1. Variables e inicialización de nuestro Caleidoscopio.

Iniciamos definiendo cuatro variables interesantes: earthAngle (que almacena el ángulo acumulado de rotación de la Tierra respecto del Sol), moonAngle (con el ángulo relativo de la Luna a la Tierra), earthInc (o incremento de ángulo de rotación para la Tierra a cada iteración) y moonInc (incremento de ángulo para la Luna a cada iteración). Estos valores son lógicamente revisables y ajustables a gusto y para nada son reales y referidos a las condiciones de rotación de sendos objetos celestes en nuestros cielos!.

La función drawCircle(float myRadius, color myColor) pinta un objeto celeste. Y lo hace como un círculo de radio myRadius y de color myColor, que son los parámetros de entrada que recibe. Internamente es bien simple. Definir el color con el que se desea pintar mediante la función fill() y generar un círculo a partir de la función de creación de elipses de Processing, centrada en coordenadas (0,0) y con el radio que se haya indicado.

La función drawSolarSystem() es lógicamente más compleja dado que es la encargada de pintarlo todo. El Sol, la Tierra relativa a éste y la Luna, según esté posicionada la Tierra. Tal y como se observaba en la figura 1 anterior. Definimos una série de propiedades para nuestro Sistema Solar (de nuevo inventadas y sin pretender ningún tipo de certeza física):

  • float SUN_RADIUS = 100.0; Es decir que nuestro Sol tiene un radio de 100 píxeles.
  • float EARTH_TO_SUN_DISTANCE = 120.0; Es decir que nuestra Tierra se encuentra a una distancia de 120 píxeles del Sol.
  • float EARTH_RADIUS = 30.0; Es decir que nuestra Tierra tiene un radio de 30 píxeles.
  • float MOON_TO_EARTH_DISTANCE = 35.0; Es decir que nuestra Luna se encuentra a una distancia de 35 píxeles de la Tierra.
  • float MOON_RADIUS = 15.0; Es decir que nuestra Luna tiene un radio de 15 píxeles.

Y entonces lo situamos todo en escena y pintamos después en estricto orden de jerarquía (Sol –> Tierra –> Luna). Veámoslo en detalle en la figura 2.

Figura 2. La Tierra está distanciada del Sol y dispone de un cierto ángulo al respecto de éste. La Luna de la misma forma y al respecto de la Tierra. Y ambos rotan contínuamente a velocidades distintas.

Figura 2. La Tierra está distanciada del Sol y dispone de un cierto ángulo al respecto de éste. La Luna de la misma forma y al respecto de la Tierra. Y ambos rotan contínuamente a velocidades distintas.

A partir de aquí analicemos el código 2 para cada uno de los objetos de nuestro pequeño firmamento virtual:

  • El Sol:
    • Primero salvamos el estado para poder volver a él después (pushMatrix()).
    • A continuación definimos un nuevo color, amarillo, para el Sol (sunColor) a partir de la función color() de Processing.
    • Nos trasladamos al centro de la ventana (translate(width/2.0,height/2.0)).
    • Pintamos el Sol donde nos acabamos de trasladar, es decir como un círculo con las dimensiones antes señaladas y centrado en la ventana, llamando a la función drawCircle creada para ello.
    • Finalmente recuperamos el estado, es decir que “deshacemos” las transformaciones efectuadas entre el pushMatrix()y el popMatrix(). En este caso se trata tan sólo de una traslación, y por lo tanto tras ejecutarse la función popMatrix()nos encontramos de nuevo en el orígen de la ventana y no en el centro.
  • La Tierra:
    • Primero salvamos el estado para poder volver a él después (pushMatrix()).
    • A continuación definimos un nuevo color, gris medio, para la Tierra (earthColor) a partir de la función color() de Processing.
    • Efectuamos tres transformaciones seguidas: a) Nos trasladamos al centro de la ventana (translate(width/2.0,height/2.0)) para situarnos en el mismo lugar que el Sol; b) Rotamos tantos grados como indique el ángulo entre Tierra y Sol, en la variable earthAngle; y c) Una vez rotados en la dirección adecuada, nos trasladamos por ella a la distancia del Sol que habíamos decidido (EARTH_TO_SUN_DISTANCE). Observemos que en esta última traslación nos desplazamos sólo en horizontal (coordenada X) y no en vertical (coordenada Y).
    • Pintamos la Tierra donde nos acabamos de situar, es decir como un círculo con las dimensiones antes señaladas y rotado y trasladado al respecto del Sol, llamando a la función drawCircle creada para ello.
    • Finalmente recuperamos el estado, es decir que “deshacemos” las transformaciones efectuadas entre el pushMatrix()y el popMatrix(). Nos encontramos de nuevo en el orígen de la ventana para “empezar de nuevo” con la Luna.
  • La Luna:
    • Primero salvamos el estado para poder volver a él después (pushMatrix()).
    • A continuación definimos un nuevo color, un “cyan”, para la Luna (moonColor) a partir de la función color() de Processing.
    • Efectuamos cinco transformaciones seguidas: a) Nos trasladamos al centro de la ventana (translate(width/2.0,height/2.0)) para situarnos en el mismo lugar que el Sol; b) Rotamos tantos grados como indique el ángulo entre Tierra y Sol, en la variable earthAngle; c) Una vez rotados en la dirección adecuada, nos trasladamos por ella a la distancia entre el Sol y la Tierra (EARTH_TO_SUN_DISTANCE); d) Situados en el mismo lugar que la Tierra, aplicamos la rotación entre ésta y la Luna (moonAngle); y e) Por último, nos distanciamos de la Tierra con una traslación de valor MOON_TO_EARTH_DISTANCE.
    • Pintamos la Luna donde nos acabamos de situar, es decir como un círculo con las dimensiones antes señaladas y rotado y trasladado al respecto de la Tierra, llamando a la función drawCircle creada para ello.
    • Finalmente recuperamos el estado, es decir que “deshacemos” las transformaciones efectuadas entre el pushMatrix()y el popMatrix(). Nos encontramos de nuevo en el orígen de la ventana para “empezar de nuevo” con “todo” en la siguiente iteración.

En el código 3 se muestran las funciones clásicas de cualquier programa dinámico con lenguaje Processing: setup() de inicialización y draw() como bucle infinito de iteración.

// Our setup() function will be called once at the beginning
void setup() {
size(500,500);
strokeWeight(5);
stroke(255);
smooth();
frameRate(30);
}

// the infinite loop
void draw(){
background(0);
// Draw the entire Solar System
pushMatrix();
drawSolarSystem();
popMatrix();
// Add value to the angles for next iteration
earthAngle+=earthInc;
moonAngle+=moonInc;
// Render some white text for the user to know the keys
String s = "Press + or - to change speed";
fill(255);
text(s, 10, 480, 250, 80);
}

Código 2. Funciones setup() y draw() para nuestro Sistema Solar.

En la inicialización setup() creamos una ventana de 500 x 500 píxeles para después definir un grueso de trazo para los contornos de 5 píxeles y su color como blanco. Activamos la función smooth() para garantizar mayor calidad visual en las primitivas que colocaremos en pantalla, a costa de un menor rendimiento claro está, y terminamos forzando 30 cuadros por segundo.

En el bucle infinito de interacción draw(), procedemos a definir un color de fondo negro para cada “refresco” de pantalla y a continuación llamamos a la función que hemos creado para generar todo el Sistema Solar (drawSolarSystem()). Salvamos el estado antes y después de ésta para ser académicos con el ejemplo. Así se garantiza, imaginemos el caso de no conocer exactamente lo que ocurre dentro de la función, que al salir de ella volvemos a situarnos exactamente en el mismo lugar “en coordenadas de ventana” que antes de llamarla. De todas formas realmente no sería necesario dado que la conocemos internamente y sabemos que salva y recupera el estado también, garantizando que a su salida no habrá sorpresas.

Posteriormente actualizamos las variables que almacenan los dos ángulos de que disponemos (earthAngle y moonAngle) con sus incrementos respectivos (earthInc y moonInc) de manera que así generemos una animación, tal y como queríamos.

Finalmente mostramos un texto blanco en pantalla que informa sobre una pequeña prestación que hemos añadido a nuestro mini simulador. Se trata de que la animación se produzca a mayor o menor velocidad, es decir que variemos la magnitud de los incrementos de los ángulos, en base a si se presiona la tecla “+” o “-“. En el código 3 controlamos estas teclas para conseguirlo. Observemos que gracias al callback keyPressed() y a la variable de sistema key , podemos controlar si se pulsa “+” o “-“ y actuar en consecuencia incrementando o decrementando los incrementos de ángulo respectivamente. En nuestro caso hemos aplicado una variación de 0.01. Esto puede fijarse a gusto claro está.

// Key Control
void keyPressed(){
// More rotational speed if pressing +
if (key == '+'){
earthInc+=0.01;
moonInc+=0.01;
}
// Less rotational speed if pressing -
if (key == '-'){
earthInc-=0.01;
moonInc-=0.01;
}
}

Código 3. Control de eventos de teclado.

Para finalizar, puede ser interesante ampliar el ejemplo añadiendo nuevos planetas en distintas órbitas con todo tipo de lunas. Es una buena manera de practicar transformaciones así como control de estados. El programa completo se encuentra a continuación, en el código 4.

// A Processing coding for a simple Solar System
// based on solarsystem.c - Jon McCormack, April 2004
// for the OpenGL library

// Our variables to control angles and increments
// for the Earth and Moon (rotations)
float earthAngle;
float moonAngle;
float earthInc = 0.02;
float moonInc = 0.05;

// drawCircle draws a circle of given myRadius in the z = 0 plane
// with the given myColor
void drawCircle(float myRadius, color myColor){
fill(myColor);
ellipse(0,0,myRadius,myRadius);
}

// drawSolarSystem: draws our simplistic solar system
// with a Sun, a planet Earth and a Moon
void drawSolarSystem() {
// Our solar system properties
float SUN_RADIUS = 100.0;
float EARTH_TO_SUN_DISTANCE = 120.0;
float EARTH_RADIUS = 30.0;
float MOON_TO_EARTH_DISTANCE = 35.0;
float MOON_RADIUS = 15.0;
// 1st: draw Sun at the center of the window
// Color = yellow
pushMatrix();
color sunColor = color(255,255,0);
translate(width/2.0,height/2.0);
drawCircle(SUN_RADIUS,sunColor);
popMatrix();

// 2nd: draw Earth a) Related and b) Rotating around Sun
// Color = grey
pushMatrix();
color earthColor = color(127,127,127);
translate(width/2.0,height/2.0);
rotate(earthAngle);
translate(EARTH_TO_SUN_DISTANCE, 0.0);
drawCircle(EARTH_RADIUS,earthColor);
popMatrix();

// 3rd: draw Moon a) Related and b) Rotating around Earth
// Color = green
pushMatrix();
color moonColor = color(0,255,255);
translate(width/2.0,height/2.0);
rotate(earthAngle);
translate(EARTH_TO_SUN_DISTANCE, 0.0);
rotate(moonAngle);
translate(MOON_TO_EARTH_DISTANCE, 0.0);
drawCircle(MOON_RADIUS,moonColor);
popMatrix();
}

// Our setup() function will be called once at the beginning
void setup() {
size(500,500);
strokeWeight(5);
stroke(255);
smooth();
frameRate(30);
}

// the infinite loop
void draw(){
background(0);
// Draw the entire Solar System
pushMatrix();
drawSolarSystem();
popMatrix();
// Add value to the angles for next iteration
earthAngle+=earthInc;
moonAngle+=moonInc;
// Render some white text for the user to know the keys
String s = "Press + or - to change speed";
fill(255);
text(s, 10, 480, 250, 80);
}

// Key Control
void keyPressed(){
// More rotational speed if pressing +
if (key == '+'){
earthInc+=0.01;
moonInc+=0.01;
}
// Less rotational speed if pressing -
if (key == '-'){
earthInc-=0.01;
moonInc-=0.01;
}
}

Código 4. Programa completo de Sistema Solar.

Deja un comentario