Universitat Oberta de Catalunya

Sistema de Partículas con lenguaje de programación Processing

Los sistemas de partículas vienen empleándose en disciplinas como la Animación, los Gráficos 3D y la Realidad Virtual desde tiempos inmemoriales. Permiten simular modelos deformables como por ejemplo el agua, fenómenos atmosféricos como un tornado o las nubes e incluso comportamientos sociales distribuidos entre “mini seres” con algo de IA, como los peces de una pecera virtual o los pájaros de una bandada “sintética” que vuela por los cielos virtuales de un Videojuego. Sólo tenéis que mirar lo que Ron Fedkiw hace con ellas o pasaros por los tutoriales de David Baraff, por cierto éste último trabajando en Pixar.

Vamos a hacer nuestra pequeña contribución al mundo de las partículas con un primer y sencillo ejemplo: un efecto de fuegos artificiales donde cada partícula actuará como si fuera uno de nuestros cohetes, ascendiendo al cielo para volver a caer después, fruto de la fuerza de gravedad. Lo podemos observar en la figura 1.

figura1

Figura 1. Nuestra contribución al entretenimiento gracias a un sistema de partículas.

En la figura 1 observamos como nuestras partículas (esferas de colores que bien pudieran ser imágenes si lo preferimos) emergen del centro de la ventana gracias a una velocidad inicial. Suben gracias a su velocidad pero la fuerza de la gravedad terminará frenándolas lo suficiente como para que terminen cayendo.

Por lo tanto necesitamos que nuestras partículas dispongan de masa (gramos o kilogramos, o ejemplo), posición XY y también velocidad XY. Además tienen que verse afectadas por fuerzas. En nuestro caso, la de la gravedad, que según los preceptos de la Física:

Fuerza de Gravedad = Masa x g

Dónde g es la constante de gravitación universal para nuestro planeta Tierra, de valor 9.81 metros por segundo al cuadrado. Es curioso como en algunos Videojuegos en los que se desea tener en cuenta este efecto, por ejemplo en uno de futbol en el que la pelota bota por el campo, a veces se varía la constante (haciéndola valer 10 por ejemplo) para conseguir exagerar el efecto un poco y, aunque no siendo entonces físicamente correcto, aparezca como visualmente más plausible.

Bueno, lo último que nos queda por recordar en esta mini lección de Física es la segunda ley de Newton, por la que:

Fuerza = Masa x Aceleración

Que de hecho nos recuerda notablemente a la fórmula anterior. Y es que si un objeto con masa recibe una fuerza…se acelera! Y si eso ocurre a lo largo del tiempo, que transcurre, varía su velocidad…y por ende su posición! Ya tenemos el mecanismo por el cual se desplazarán nuestras partículas pues!.

En un ordenador hace falta un procedimiento por el cual, vía el lenguaje de programación, implementemos ese movimiento. Necesitamos que el tiempo transcurra y que a la par, calculemos las aceleraciones que afectan a las partículas y sus variaciones en velocidad y posición. A estos procesos se les llama motores de inferencia numérica o “solvers”. Y existen distintos tipos (Euler, Runge-Kutta, Midpoint, Verlet, LepFrog, etc.) según su velocidad de ejecución y eficiencia.

Como en nuestro caso vamos a realizar una simulación bien simple emplearemos el más sencillo de todos. El “solver Euler”. Tenemos “las fórmulas” en la figura 2.

Figura 2. Fórmulas de cálculo de nuevas velocidades y posiciones gracias al solver Euler.

Figura 2. Fórmulas de cálculo de nuevas velocidades y posiciones gracias al solver Euler.

Tengamos en cuenta que con X nos referimos a la posición y que con V a la velocidad. Supongamos que nuestra partícula (círculo rojo) se encuentra en una posición actual Xn con una velocidad Vn (en el dibujo vector azul D). Lo que deseamos es desplazarla desde el momento de tiempo actual (t) al siguiente (t + h dónde h es el incremento de tiempo que transcurre). Lo que haremos será calcular las nuevas posición y velocidad Xn+1 y Vn+1 (en “el ramo” se denomina a este “par” como el “espacio de fase”) a partir de las anteriores Xn y Vn. Y eso lo hacemos gracias a las fórmulas indicadas en la figura 2:

  • La nueva velocidad Vn+1 se obtiene como la anterior Vn más un factor que no es más que un incremento o decremento de velocidad. Este incremento o decremento se obtiene multiplicando el tiempo que transcurre (Dt o h) por la aceleración…y ésta se obtiene según la segunda Ley de Newton, como la fuerza f que afecta a la partícula dividida entre su masa m.
  • La nueva posición Xn+1 se obtiene como la anterior Xn más un factor que no es más que un incremento de posición. Este incremento o decremento se obtiene multiplicando el tiempo que transcurre (Dt o h) por la velocidad.

Imaginad que dentro de nuestro bucle de iteración Processing, es decir dentro de la función draw(), calculamos continuamente las posiciones de nuestras partículas y a continuación las pintamos. El resultado será una animación en la que “se mueven solas” acelerándose y frenándose por acción de la fuerza de la gravedad. Vamos a ello pues y para realizarlo utilizaremos objetos de Processing. Veamos el código 1.

// A particle system with Processing
// Several circular particles will act as fireworks
// Particles have an initial velocity that makes them
// fly although the gravity force will prevent them
// to do it for long!

// We begin by creating 5 particle objects
particle part1;
particle part2;
particle part3;
particle part4;
particle part5;

// We create several accumulators
// as variables to store forces,
// positions and velocities
float[] force = new float[2];
float[] oldPosition = new float[2];
float[] oldVelocity = new float[2];
// We also create a time variable for the system to evolve
float time;

// Our initialization setup function
void setup() {
// a 500 x 500 window
size(500,500);
// We initialize our 5 particle objects
// by calling the constructor of their class
// and passing several parameters to each
// which are color, radius and the initial
// 2D position and 2D velocity
part1 = new particle(color(255,0,0),10,250,250,5,50,1);
part2 = new particle(color(0,0,255),5,250,250,-5,70,1);
part3 = new particle(color(0,255,0),20,250,250,-3,20,1);
part4 = new particle(color(255,0,255),8,250,250,-3,80,1);
part5 = new particle(color(0,255,255),3,250,250,-8,40,1);
// The force accumulator in X equals 0 (there's no gravitational component)
force[0]=0.0;
// The force accumulator in Y equals -9.8 (this is Gravity)
force[1]=-9.8;
// Time will pass as 0.1 seconds per iteration
time=0.1;
}

Código 1. Objetos, variables e inicialización de nuestras partículas, 5 en total.

Observamos que lo primero es crear nuestras 5 partículas como objetos de un tipo que crearemos para tal ocasión (particle). Además preparamos 4 variables, 3 de ellas arrays, que necesitaremos (force, oldPosition, oldVelocity y time) para almacenar la fuerza, posiciones y velocidades antiguas de una partícula así como el tiempo, que transcurre, en nuestra simulación.

En la función de inicialización setup() creamos una ventana de 500 por 500 píxeles e inicializamos nuestras 5 partículas llamando a sus constructores respectivos. En el código 3 observaremos como son estos métodos. Básicamente crean a cada una de las partículas a partir de los parámetros de entrada que reciben, que son color, radio del círculo, posición y velocidad iniciales, en 2D por nuestra ventana y por tanto en coordenadas XY, y para terminar la masa. También definimos como deseamos que sea nuestra fuerza. Como sólo tenemos la gravedad y ésta actúa tan sólo verticalmente, ponemos a 0 la componente horizontal de la fuerza e igualamos a 9.8 m/s2 la vertical, tal y como ya hemos comentado anteriormente. Terminamos la función definiendo un incremento de tiempo (time) de 0.1 segundos para nuestra simulación. Esto quiere decir que para cada vez que iteremos y pintemos de nuevo, habrán transcurrido 0.1 segundos “para las partículas”. Se moverán por tanto, entre cuadro y cuadro generado, el espacio correspondiente a este tiempo.

En el código 2 podemos ver como nuestro bucle infinito, es decir la función draw(), refresca el color de fondo en negro y llama continuamente a los métodos asociados a nuestros objetos particle para a) Calcular la nueva posición de cada una mediante el método particleMove() y b) La muestra en pantalla gracias al método particleRender(). En el código 3 veremos en qué consisten ambos métodos.

// The infinite loop
void draw() {
// Black background
background(0);
// We call our 5 particles methods
// in order to 1) Move them and 2) Render them
part1.particleMove();
part1.particleRender();
part2.particleMove();
part2.particleRender();
part3.particleMove();
part3.particleRender();
part4.particleMove();
part4.particleRender();
part5.particleMove();
part5.particleRender();
}

Código 2. El bucle de simulación de nuestras partículas.

Vamos por tanto al código 3 de cara a tener claro como se definen nuestros objetos “personalizados” de tipo particle así como sus variables y métodos internos.

// This is the particle class
// that defines our particular objects
class particle {
// Each particle has several attributes as:
// color, radius, 2D position, 2D velocity
// 2D acceleration and mass
color color_p;
float particleRadius;
float[] position = new float[2];
float[] velocity = new float[2];
float[] acceleration = new float[2];
float mass;
// The constructor for a particle
particle(color c,float r,float posX,float posY,float velX,float velY,float m) {
color_p=c;
particleRadius=r;
position[0]=posX;
position[1]=posY;
velocity[0]=velX;
velocity[1]=velY;
mass=m;
}
// The method that renders a particle
// as a circle with white stroke and its filling color
void particleRender() {
stroke(255);
fill(color_p);
// Remember that Y coords go top to down in Processing
// and therefore we need to invert the particle's position
// if we want our fireworks to go bottom-up
ellipse(position[0],height-position[1],particleRadius,particleRadius);
}
// The method that moves a particle
void particleMove() {
// First: save the old state
oldPosition[0]=position[0];
oldPosition[1]=position[1];
oldVelocity[0]=velocity[0];
oldVelocity[1]=velocity[1];
// Second: evaluate particle's acceleration as:
// acceleration = force/mass (this is one of Newton's laws)
acceleration[0]=force[0]/mass;
acceleration[1]=force[1]/mass;
// Third: evaluate new velocity from old velocity
// by using the Euler method that stands for:
// new velocity = old velocity + time * acceleration
velocity[0]=oldVelocity[0] + time*acceleration[0];
velocity[1]=oldVelocity[1] + time*acceleration[1];
// Fourth: evaluate new position from old position
// by using the Euler method that stands for:
// new position = old position + time * velocity
position[0]=oldPosition[0] + time*velocity[0];
position[1]=oldPosition[1] + time*velocity[1];
}
}

Código 3. Definición de nuestros objetos Processing, de tipo particle.

Para crear un objeto en Processing utilizamos una estructura de tipo clase. Dentro de ella codificaremos el constructor que inicializa a cada uno de los objetos, en nuestro caso partículas, colocando los valores “que toque” a cada una de sus variables internas. Además se definen los métodos internos de los objetos, es decir aquellas funciones que se requieren para manejarlos. En definitiva:

  • Un objeto particle se define por su color, particleRadius, position, velocity, acceleration y mass.
  • El constructor (particle(color c,float r,float posX,float posY,float velX,float velY,float m)) recibe como parámetros estos valores y los introduce en cada una de las variables. Tanto la posición como la velocidad se refieren a las iniciales para cada partícula. Esencialmente centradas en la ventana y con velocidad vertical ascendente.
  • El método que “pinta” la partícula es void particleRender(). Básicamente define color de borde blanco, de relleno el de la misma partícula y la pinta como un círculo de radio igual a particleRadius, centrado en la posición X = position[0] e Y = height-position[1]. Ésta última invertida dado que en Processing las Y’s crecen hacia abajo y en cambio queremos que nuestras partículas se muevan ascendentemente.
  • El método que “mueve” la partícula y por tanto implementa al mencionado “Solver Euler” es void particleMove(). Tendrá que implementar las fórmulas que vimos en su momento en la figura 2. Así pues, empieza por almacenar tanto las posiciones XY como las velocidades XY actuales (oldPosition y oldVelocity), calcula la aceleración para la partícula actual según la segunda Ley de Newton (acceleration = force/mass) y por último calcula la posición y velocidad actuales, digamos “nuevas”, a partir de las “antiguas (velocity = oldVelocity + time * acceleration y position = oldPosition + time * velocity).

Por último, el callback de teclado keyPressed() se encarga de que el sistema vuelva a reiniciarse (llamamos de nuevo a los constructores de las 5 partículas) cada vez que se pulse una tecla cualquiera.

// The key press event will call this callback
void keyPressed(){
// We reset the system everytime that the user presses a key
// by re-calling the 5 particle's constructors
part1 = new particle(color(255,0,0),10,250,250,5,50,1);
part2 = new particle(color(0,0,255),5,250,250,-5,70,1);
part3 = new particle(color(0,255,0),20,250,250,-3,20,1);
part4 = new particle(color(255,0,255),8,250,250,-3,80,1);
part5 = new particle(color(0,255,255),3,250,250,-8,40,1);
}

Código 4. Callback de teclado. Cada vez que pulsemos una tecla la simulación se reiniciará.

Bienvenidos al potentísimo mundo de la simulación gracias a sistemas de partículas para generar todo tipo de complejas animaciones automáticas. En el código 5 tenéis el programa al completo.

// A particle system with Processing
// Several circular particles will act as fireworks
// Particles have an initial velocity that makes them
// fly although the gravity force will prevent them
// to do it for long!

// We begin by creating 5 particle objects
particle part1;
particle part2;
particle part3;
particle part4;
particle part5;

// We create several accumulators
// as variables to store forces,
// positions and velocities
float[] force = new float[2];
float[] oldPosition = new float[2];
float[] oldVelocity = new float[2];
// We also create a time variable for the system to evolve
float time;

// Our initialization setup function
void setup() {
// a 500 x 500 window
size(500,500);
// We initialize our 5 particle objects
// by calling the constructor of their class
// and passing several parameters to each
// which are color, radius and the initial
// 2D position and 2D velocity
part1 = new particle(color(255,0,0),10,250,250,5,50,1);
part2 = new particle(color(0,0,255),5,250,250,-5,70,1);
part3 = new particle(color(0,255,0),20,250,250,-3,20,1);
part4 = new particle(color(255,0,255),8,250,250,-3,80,1);
part5 = new particle(color(0,255,255),3,250,250,-8,40,1);
// The force accumulator in X equals 0 (there's no gravitational component)
force[0]=0.0;
// The force accumulator in Y equals -9.8 (this is Gravity)
force[1]=-9.8;
// Time will pass as 0.1 seconds per iteration
time=0.1;
}

// The infinite loop
void draw() {
// Black background
background(0);
// We call our 5 particles methods
// in order to 1) Move them and 2) Render them
part1.particleMove();
part1.particleRender();
part2.particleMove();
part2.particleRender();
part3.particleMove();
part3.particleRender();
part4.particleMove();
part4.particleRender();
part5.particleMove();
part5.particleRender();
}

// This is the particle class
// that defines our particular objects
class particle {
// Each particle has several attributes as:
// color, radius, 2D position, 2D velocity
// 2D acceleration and mass
color color_p;
float particleRadius;
float[] position = new float[2];
float[] velocity = new float[2];
float[] acceleration = new float[2];
float mass;
// The constructor for a particle
particle(color c,float r,float posX,float posY,float velX,float velY,float m) {
color_p=c;
particleRadius=r;
position[0]=posX;
position[1]=posY;
velocity[0]=velX;
velocity[1]=velY;
mass=m;
}
// The method that renders a particle
// as a circle with white stroke and its filling color
void particleRender() {
stroke(255);
fill(color_p);
// Remember that Y coords go top to down in Processing
// and therefore we need to invert the particle's position
// if we want our fireworks to go bottom-up
ellipse(position[0],height-position[1],particleRadius,particleRadius);
}
// The method that moves a particle
void particleMove() {
// First: save the old state
oldPosition[0]=position[0];
oldPosition[1]=position[1];
oldVelocity[0]=velocity[0];
oldVelocity[1]=velocity[1];
// Second: evaluate particle's acceleration as:
// acceleration = force/mass (this is one of Newton's laws)
acceleration[0]=force[0]/mass;
acceleration[1]=force[1]/mass;
// Third: evaluate new velocity from old velocity
// by using the Euler method that stands for:
// new velocity = old velocity + time * acceleration
velocity[0]=oldVelocity[0] + time*acceleration[0];
velocity[1]=oldVelocity[1] + time*acceleration[1];
// Fourth: evaluate new position from old position
// by using the Euler method that stands for:
// new position = old position + time * velocity
position[0]=oldPosition[0] + time*velocity[0];
position[1]=oldPosition[1] + time*velocity[1];
}
}

// The key press event will call this callback
void keyPressed(){
// We reset the system everytime that the user presses a key
// by re-calling the 5 particle's constructors
part1 = new particle(color(255,0,0),10,250,250,5,50,1);
part2 = new particle(color(0,0,255),5,250,250,-5,70,1);
part3 = new particle(color(0,255,0),20,250,250,-3,20,1);
part4 = new particle(color(255,0,255),8,250,250,-3,80,1);
part5 = new particle(color(0,255,255),3,250,250,-8,40,1);
}

Código 5. Las 5 Partículas. Programa completo.

Un comentario

Deja un comentario

  1. Que increible la informacion que dan aca y GRATIS. Me encanta cuando los articulos comparten el codigo. Muchas gracias de un principante pero fanatico de animacion.

    Respon

Deja un comentario