Sistemas de partículas (NC6)

Un sistema de partículas es un conjunto de pequeños objetos que juntos representan un objeto más grande. Esta entrada trata precisamente sobre sistemas de partículas, y corresponde al capítulo 4 del libro «The Nature of Code» de Daniel Shiffman.

El origen de los sistemas de partículas es interesante. El término se acuñó durante la creación de una animación para Star Trek II: The Wrath of Khan. El primer sistema se creó para animar la terraformación creada por un Genesis Device.

Diseñar una partícula

Para diseñar un sistema de partículas lo primero es modelar solo una partícula. Es fácil porque el trabajo ya está hecho en entradas anteriores. Usaremos como base la clase Pelota. Le cambiamos el nombre a Particula y tenemos el siguiente código:

class Particula
{
   constructor(posicion)
   {
      this.posicion = posicion.copy();
      this.velocidad = createVector(0, 0);
      this.aceleracion = createVector(0, 0);
   }

   aplicarFuerza(fuerza)
   {
      this.aceleracion.add(fuerza);
   }

   actualizar()
   {
      this.velocidad.add(this.aceleracion);
      this.posicion.add(this.velocidad);
      this.aceleracion.mult(0);
   }

   mostrar()
   {
      let color = [168, 0, 28];
      let alpha = map(0, 1, 0, 255, this.tiempoDeVida);
      stroke(color, alpha);
      fill(color, alpha);
      ellipse(this.posicion.x, this.posicion.y, 8, 8);
   }
}

Un sistema de partículas genera, em pues, partículas; que crean el efecto de animación deseado pero que usualmente desaparecen en algún momento. Para que las partículas desaparezcan necesitamos una variable adicional, que determine el tiempo de vida. Luego hay que decrementar esa variable en la animación, y una vez que la partícula haya vivido lo suficiente hay que desaparecerla.

Todo esto podemos hacerlo agregando la siguiente línea al constructor:

this.tiempoDeVida = 255;

Luego añadimos a actualizar la linea que reduce la vida de la partícula conforme pasa el tiempo:

this.tiempoDeVida -= 2;

Y finalmente añadimos un método a la clase para verificar que la partícula sigue viva:

estaMuerta()
{
   return (this.tiempoDeVida <= 0);
}

Para este caso en particular añadiremos transparencia para que la partícula se desvanezca conforme avanza el tiempo. El valor transparencia toma valores entre 0 y 1, así que mapearemos el tiempo de vida a su valor alfa correspondiente. Este es el resultado:

mostrar()
{
   let alpha = map(0, 1, 0, 255, this.tiempoDeVida);
   stroke(0, alpha);
   fill(175, alpha);
   ellipse(this.posicion.x, this.posicion.y, 8, 8);
}

Crear y destruir objetos

Hay un problema técnico con la implementación del sistema de partículas. Como ya lo había dicho en la sección anterior, cada partícula tiene un tiempo de vida definido. Para implementar este comportamiento podemos reciclar los elementos de un arreglo y hacerlos desaparecer y aparecer en una nueva posición.

Sin embargo, en el libro Shiffman toma una dirección distinta. Su objetivo es borrar de la memoria las partículas una vez que termina su tiempo de vida. Esto representa un problema al trabajar con arreglos, por que no es posible eliminar o añadir elementos de forma arbitraria a un arreglo sin crear una copia completa del mismo. Los elementos de un arreglo se guardan de forma contigua en memoria.

La solución en Processing es ArrayList, una estructura de datos que permite manipular de forma dinámica sus elementos aún cuando los estamos recorriendo dentro de un mismo bucle.

En Javascript dicha estructura de datos no existe, pero hay una estructura estándar muy conocida en computación que en general hace lo mismo que los ArrayList, una Lista Doblemente Enlazada.

Una lista doblemente enlazada consta de un elemento líder que encabeza la lista y que contiene un enlace (tradicionalmente un puntero) hacia el siguiente elemento. Ese elemento a su vez contiene enlaces al elemento previo y al siguiente, y así sucesivamente. En la sección final encontrarás recursos adicionales para entender más sobre el tema.

La implementación de esta estructura de datos consta de dos partes: los nodos y la lista. Así se ve un nodo:

class Node
{
   constructor(data)
   {
      this.data = data;
      this.next = null;
      this.previous = null;
   }
}

Y esta es la lista doblemente enlazada:

class LinkedList
{
   constructor(node)
   {
      this.head = node;
   }

   insertHead(node)
   {
      node.next = this.head;
      node.previous = null;
      this.head.previous = node;
      this.head = node;
   }

   insertAfter(newnode, predecessor)
   {
      let tmp = predecessor.next;
      if (tmp)
      {
         tmp.previous = newnode;
      }
      predecessor.next = newnode;
      newnode.previous = predecessor;
      newnode.next = tmp;
   }

   deleteNode(node)
   {
      if (!node.previous)
      {
         this.head = node.next;
      }
      else if (!node.next)
      {
         node.previous = null;
      }
      else
      {
         node.previous.next = node.next;
         node.next.previous = node.previous;
      }
   }
}

Además de implementar la estructura agregué algunos métodos para manipularla. En una entrada posterior hablaré con más calma sobre este tema.

Crear el sistema

Ya que sobrepasamos el interludio técnico, continuemos con el sistema de partículas. ¿Qué es lo que queremos que haga?

  • Que se comporte como una fuente, de la que surgen todas las partículas.
  • Que nos permita añadir nuevas partículas.
  • Que nos permita manipular las partículas.
  • Que encapsule la implementación de la animación del sistema completo.

Todo esto es lo que hace el siguiente código:

class SistemaParticulas
{
   constructor(pos)
   {
      this.posicion = pos.copy();
      let particula = new Particula(this.posicion);
      this.particulas = new LinkedList(particula);
   }

   agregarParticula()
   {
      let tparticula = new Particula(this.posicion);
      this.particulas.insertHead(tparticula);
   }

   aplicarFuerza(fuerza)
   {
      let particula = this.particulas.head;
      do
      {
         particula.aplicarFuerza(fuerza);
         particula = particula.next;
      } while(particula);
   }

   correr()
   {
      let particula = this.particulas.head;
      do
      {
         particula.actualizar();
         particula.mostrar();
         if (particula.estaMuerta())
         {
            this.particulas.deleteNode(particula);
         }
         particula = particula.next;
      } while(particula);
   }
}

Finalmente, la animación. Vamos a crear un sistema, luego le aplicaremos una fuerza vertical, para simular que las partículas están flotando. Finalmente le agregaremos una fuerza lateral para agregar un poco de viento.

let sistemaParticulas;
let fuerza;
let viento;

function setup()
{
   createCanvas(600, 600);
   let pos = createVector(width/2, height - 250);
   sistemaParticulas = new SistemaParticulas(pos);
   fuerza = createVector(0, -0.05);
   viento = createVector(-0.02, 0.02);
}

function draw()
{
   background(255);
   sistemaParticulas.agregarParticula();
   sistemaParticulas.aplicarFuerza(fuerza);
   sistemaParticulas.aplicarFuerza(viento);
   sistemaParticulas.correr();
}

Y este es el resultado:

Referencias adicionales

Para aprender más sobre listas doblemente enlazadas consulta el artículo correspondiente en Wikipedia. También puedes encontrar un excelente video al respecto en computerfile. Desafortunadamente está en inglés y no tiene subtítulos, pero trataré de contribuir con eso en los próximos días.

Todas las animaciones en este capítulo estás escritas en Javascript con la librería p5.js. Para aprender lo básico sobre programación en Javascript y p5.js puedes revisar una serie de videos en el canal de Daniel Shiffman. Subtítulos en español están disponibles.

El código de todas las animaciones usadas en este blog están disponibles en mi cuenta de GitLab.