Un sistema de partículas es una técnica útil en computación gráfica y fue acuñado en la creación del efecto génesis de la película Viaje a las estrellas II.
-Un sistema de partículas es una colección de muchas muchas partículas diminutas que juntas representan un objeto difuso.
-Durante un periodo de tiempo, las partículas se generan en un sistema, se mueven, cambian dentro del sistema y mueren desde el sistema.
Los sistemas de partículas han sido utilizados en innumerables juegos de video, animaciones, obras de arte digitales e instalaciones para modelar varios tipos irregulares de fenómenos naturales, tales como fuego, humo, cascadas, niebla, hierba, burbujas, etcétera.
¿Cuales son las estrategias de implementación para programar un sistema de partículas?
¿Cómo organizamos nuestro código?
¿Dónde almacenamos la información relacionada con las partículas individuales frente a la información relacionada con el sistema como un todo?
Los ejemplos que vamos a ver se centrarán en gestionar los datos asociados a un sistema de partículas. Utilizarán formas sencillas para las partículas y aplicarán solo los comportamientos más básicos (por ejemplo, la gravedad). Sin embargo, al usar este marco de trabajo y construir formas más interesantes para desplegar las partículas y calcular comportamientos, puedes lograr una variedad de efectos.
Un sistema de partículas es una colección de objetos sencillos.
-Ya tratamos antes con la programación de colecciones de objetos, como arreglos de "Movers" que simulan pelotas rebotando.
-Para sistemas de partículas, nuestras colecciones son más complejas.
-Las colecciones variarán en tamaño: a veces habrá cero partículas, a veces diez, a veces diez mil.
-Las colecciones tendrán comportamiento y propiedades en sí mismas, no solo las partículas de las cuales están conformadas.
Nuestro objetivo es escribir un programa que se vea así:
var ps = new ParticleSystem(); draw = function() { background(255, 255, 255); ps.run(); };
Ni una sola partícula está referenciada en ese código, pero el resultado estará lleno de partículas volando por toda la pantalla. Crearemos programas con múltiples tipos de objetos, y objetos que seguirán la pista de otras colecciones de objetos, que nos ayudará a crear sistemas de partículas poderosos, pero también nos prepararán para escribir programas más poderosos en general.
Para programar sistemas de partículas usaremos dos técnicas avanzadas de programación orientada a objetos: herencia y polimorfismo.
-Con los ejemplos que hemos visto hasta ahora, siempre hemos tenido un arreglo de un solo tipo de objeto, como "movers"" u “osciladores”.
-Con la herencia (y el polimorfismo), aprenderemos una manera conveniente para almacenar un solo arreglo que contenga objetos de diferentes tipos.
-De esta manera, un sistema de partículas no necesita ser un sistema de un solo tipo de partícula.
-En esta sección veremos los usos más típicos de los sistemas de partículas, pero el hecho de que las partículas en este capítulo se vean o comporten de cierta manera no debe limitar tu imaginación.
-Solo porque estos sistemas de partículas tiendan a parecer brillantes, volar hacia adelante, y caer con la gravedad no significa que esas sean las características que los tuyos deben tener.
-El objetivo aquí es solo cómo hacer un seguimiento de un sistema de muchos elementos.
-Qué hacen y cómo se ven esos elementos depende de tu creatividad.
Ejemplo 1 para pegar en KA:
// Un simple objeto "partícula"
var Particle = function(position) {
this.acceleration = new PVector(0, 0.05);
this.velocity = new PVector(random(-1, 1), random(-1, 0));
this.position = position.get();
this.timeToLive = 255.0;
};
Particle.prototype.run = function() {
this.update();
this.display();
};
Particle.prototype.update = function(){
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.timeToLive -= 2;
};
Particle.prototype.display = function() {
stroke(255, 255, 255, this.timeToLive);
strokeWeight(2);
fill(127, 127, 127, this.timeToLive);
ellipse(this.position.x, this.position.y, 12, 12);
};
// la partícula sigue estando viva?
Particle.prototype.isDead = function() {
if (this.timeToLive < 0) {
return true;
} else {
return false;
}
};
var particle = new Particle(new PVector(width/2, 20));
var draw = function() {
background(0);
particle.run();
if (particle.isDead()) {
particle = new Particle(new PVector(width/2, 20));
}
};
Antes de que podamos crear un ParticleSystem (sistema de partículas en inglés) completo, tenemos que crear un objeto que describirá una sola partícula. La buena noticia: ya hicimos esto. Nuestro objeto Mover de la sección de Fuerzas sirve como la plantilla perfecta. Para nosotros, una partícula es un cuerpo independiente que se mueve sobre la pantalla. Tiene location (ubicación), velocity (velocidad) y acceleration (aceleración), un constructor para inicializar esas variables y las funciones display() para ser desplegado y update() para actualizar su ubicación.
// Un objeto Particle sencillo var Particle = function(position) { this.acceleration = new PVector(); this.velocity = new PVector(); this.position = position.get(); }; Particle.prototype.update = function(){ //función para actualizar this.velocity.add(this.acceleration); this.position.add(this.velocity); }; Particle.prototype.display = function() { //función para desplegar stroke(0, 0, 0); fill(175, 175, 175); ellipse(this.position.x, this.position.y, 8, 8); };
Esto es lo más sencillo que puede ser una partícula. De aquí podríamos llevar a nuestra partícula en varias direcciones. Podríamos agregar un método applyForce() (aplicarFuerza) para afectar el comportamiento de la partícula (haremos precisamente esto en un ejemplo futuro). Podríamos agregar variables para describir el color y la forma, o usar image() para dibujar la partícula. Por ahora, sin embargo, vamos a enfocarnos en agregar solo un detalle adicional: lifespan (duración).
Los sistemas de partículas típicos involucran algo llamado un emisor (emitter en inglés). El emisor es la fuente de las partículas y controla la configuración inicial de las partículas, su ubicación, su velocidad, etc. Un emisor puede emitir una sola ráfaga de partículas, o un flujo continuo de partículas, o ambos. El punto es que para una implementación típica como esta, una partícula nace en el emisor pero no vive para siempre. Si viviera para siempre, nuestro programa eventualmente se paralizaría a medida que, con el tiempo, el número de partículas aumentara a un número difícil de manejar. Cuando nacen nuevas partículas, necesitamos dejar morir a las partículas viejas. Esto crea la ilusión de un flujo infinito de partículas, y el rendimiento de nuestro programa no sufre.
Hay muchas maneras diferentes para decidir cuando muere una partícula. Por ejemplo, podría entrar en contacto con otro objeto, o simplemente podría dejar la pantalla. Para nuestro primer objeto Particle, sin embargo, simplemente vamos a añadir una propiedad timeToLive (tiempo de vida). Actuará como un cronómetro, contando desde 255 hasta 0, momento en el que consideraremos que la partícula está "muerta". Así que extendemos el objeto Particle como sigue:
// Un objeto Particle sencillo var Particle = function(position) { this.acceleration = new PVector(); this.velocity = new PVector(); this.position = position.get(); this.timeToLive = 255; }; Particle.prototype.update = function(){ this.velocity.add(this.acceleration); this.position.add(this.velocity); this.timeToLive -= 2; }; Particle.prototype.display = function() { stroke(255, 255, 255, this.timeToLive); fill(127, 127, 127, this.timeToLive); ellipse(this.position.x, this.position.y, 8, 8); };
La razón por la cual decidimos iniciar timeToLive en 255 y contar regresivamente hasta 0 es por conveniencia. Con esos valores, también podemos utilizar timeToLive como la transparencia alfa para la elipse. Cuando la partícula esté “muerta” también se habrá desvanecido en la pantalla.
Con la incorporación de la propiedad timeToLive, también necesitaremos un método adicional: una función a la que se le pueda consultar (para una respuesta true o false) si la partícula está viva o muerta. Esto va a ser útil cuando estemos escribiendo el objeto ParticleSystem, cuya tarea será administrar la lista de las propias partículas. Escribir esta función es bastante fácil; solo necesita regresar true si el valor de timeToLive es menor que 0.
Particle.prototype.isDead = function() { return this.timeToLive < 0; };
Antes de llegar al siguiente paso de hacer muchas partículas, vale la pena tomarse un momento para asegurarnos de que nuestra partícula funcione correctamente y crear un boceto con un solo objeto Particle. Aquí está el código completo, con dos pequeñas adiciones. Agregamos un método de conveniencia llamado run() que simplemente llama tanto a update() como a display(). Además, a la partícula le damos una velocidad inicial aleatoria, así como una aceleración hacia abajo (para simular la gravedad).
Ahora tenemos un objeto para describir una sola partícula, pero ¿Cómo podemos llevar un seguimiento de muchas partículas, cuando no podemos asegurar exactamente cuántas partículas podemos tener en un momento dado?
Hasta ahora logramos crear una sola partícula que hacemos renacer cada vez que muere. Ahora queremos crear un flujo continuo de partículas, agregando una nueva con cada ciclo por medio de draw(). Podríamos simplemente crear un arreglo y agregarle (push) una nueva partícula cada vez:
var particles = []; draw = function() { background(133, 173, 242); particles.push(new Particle(new PVector(width/2, 50))); for (var i = 0; i < particles.length; i++) { var p = particles[i]; p.run(); } };
Si pruebas y ejecutas ese código por unos minutos, probablemente comenzarás a ver la velocidad de los cuadros de animación más y más lentos hasta que el programa se paralice. Eso es porque creamos más y más partículas que tenemos que procesar y deplegar, sin nunca quitar ninguna. Una vez que las partículas están muertas, son inútiles, así que mejor podemos ahorrarle trabajo innecesario a nuestro programa y eliminar esas partículas.
Para eliminar elementos de un arreglo en JavaScript, podemos usar el método splice(), al especificar el índice que deseamos eliminar y el número a borrar (solo uno). Hacemos eso después de consultar si la partícula en efecto está muerta:
var particles = []; draw = function() { background(133, 173, 242); particles.push(new Particle(new PVector(width/2, 50))); for (var i = 0; i < particles.length; i++) { var p = particles[i]; p.run(); if (p.isDead()) { particles.splice(i, 1); } } };
Aunque el código anterior funcionará bien (y el programa nunca se paralizará), hemos abierto una caja de Pandora mediana. Cada vez que manipulamos el contenido de un arreglo mientras iteramos sobre ese mismo arreglo, podemos meternos en problemas. Considera, por ejemplo, el siguiente código:
for (var i = 0; i < particles.length; i++) { var p = particles[i]; p.run(); particles.push(new Particle(new PVector(width/2, 50))); }
Este es un ejemplo algo extremo (con lógica errónea), pero prueba el punto. En el caso anterior, para cada partícula en el arreglo, agregamos una nueva partícula al arreglo (lo que cambia la propiedad length del arreglo). Esto resultará en un bucle infinito, ya que i nunca puede aumentar más allá de particles.length.
Mientras que quitar elementos del arreglo de partículas durante un bucle no causa que el programa se bloquee (como lo hace cuando le agregamos), el problema es casi más malicioso porque no deja ninguna evidencia. Para descubrir el problema, primero debemos establecer un hecho importante. Cuando se elimina un elemento de un arreglo, todos los elementos son desplazados un lugar a la izquierda. Observa el siguiente diagrama, donde se elimina la partícula C (índice 2). Las partículas A y B mantienen el mismo índice, mientras que las partículas D y E cambian de 3 y 4 a 2 y 3, respectivamente.
Supongamos que somos i recorriendo el arreglo.
cuando i = 0 → revisa partícula A → no borrar
cuando i = 1 → revisa partícula B → no borrar
cuando i = 2 → revisa partícula C → ¡borrar!
(desplazar a las partículas D y E de las posiciones 3 y 4 a las 2 y 3)
cuando i = 3 → revisa partícula E → no borrar
¿Te das cuenta del problema? ¡Nunca revisamos a la partícula D! Cuando C fue eliminada de la posición #2, D se movió a la posición #2, pero i ya se había movido a la posición #3. Esto no es un desastre, ya que la partícula D será revisada la próxima vez. Aún así, la expectativa es que estamos escribiendo código para iterar sobre cada elemento individual del arreglo. Saltarse un elemento es inaceptable.
Hay una solución sencilla para este problema: simplemente iterar hacia atrás sobre el arreglo. Si estás desplazando elementos de derecha a izquierda a medida que los elementos se quitan, es imposible omitir un elemento por accidente. Todo lo que tenemos que hacer es modificar las tres partes en el bucle for:
for (var i = particles.length-1; i >= 0; i--) { var p = particles[i]; p.run(); if (p.isDead()) { particles.splice(i, 1); } }
Juntándolo todo, tenemos esto:
Bien. Ya hicimos dos cosas. Escribimos un objeto para describir un Particle individual (partícula en inglés). Averiguamos cómo utilizar arreglos para manejar muchos objetos Particle (con la posibilidad de agregarlos y borrarlos a voluntad).
Podríamos parar aquí. Sin embargo, un paso adicional que podemos y debemos tomar, es crear un objeto para describir la colección de objetos Particle: el objeto ParticleSystem (sistema de partículas). Esto nos permitirá quitar la lógica engorrosa de iterar sobre todas las partículas de la pestaña principal, así como abrir la posibilidad de tener más de un sistema de partículas.
Si recuerdas el objetivo que definimos al principio de este capítulo, queríamos que nuestro programa se pareciera a esto:
var ps = new ParticleSystem(new PVector(width/2, 50)); draw = function() { background(0, 0, 0); ps.run(); };
Tomemos el programa que escribimos arriba y veamos cómo encajaría en el objeto ParticleSystem.
Aquí está lo que teníamos antes. Observa las líneas en negritas:
var particles = []; draw = function() { background(133, 173, 242); particles.push(new Particle(new PVector(width/2, 50))); for (var i = particles.length-1; i >= 0; i--) { var p = particles[i]; p.run(); if (p.isDead()) { particles.splice(i, 1); } } };
Aquí está cómo podemos reescribir eso en un objeto. Vamos a hacer que el arreglo particles sea una propiedad del objeto, hacer un método que lo envuelvaaddParticle para agregar nuevas partículas. y poner toda la lógica de ejecutar la partícula en run:
var ParticleSystem = function() { this.particles = []; }; ParticleSystem.prototype.addParticle = function() { this.particles.push(new Particle()); }; ParticleSystem.prototype.run = function() { for (var i = this.particles.length-1; i >= 0; i--) { var p = this.particles[i]; p.run(); if (p.isDead()) { this.particles.splice(i, 1); } } };
También podríamos agregar algunas nuevas características al propio sistema de partículas. Por ejemplo, sería útil para el objeto ParticleSystem llevar un seguimiento de un punto de origen de dónde se crean las partículas. Esto empata con la idea de que el sistema de partículas sea un “emisor”, un lugar donde nacen las partículas y de donde son enviadas al mundo. El punto de origen debería inicializarse en el constructor.
var ParticleSystem = function(position) { this.origin = position.get(); this.particles = []; }; ParticleSystem.prototype.addParticle = function() { this.particles.push(new Particle(this.origin)); };
Aquí está todo junto:
Revisemos por un momento en dónde estamos. Sabemos cómo hablar acerca de un objeto individual Particle. También sabemos cómo hablar de un sistema de objetos Particle, y a esto lo llamamos un “sistema de partículas”. Y definimos un sistema de partículas como una colección de objetos independientes. ¿Pero un sistema de partículas no es en sí mismo un objeto? Si ese es el caso (que lo es), no hay ninguna razón por la cual no podríamos tener también una colección de sistemas de muchas partículas, es decir, un sistema de sistemas.
Por supuesto que esta línea de pensamiento nos podría llevar aún más lejos, y tal vez te encierres en un sótano durante días haciendo un esbozo de un diagrama de un sistema de sistemas de sistemas de sistemas de sistemas de sistemas. De sistemas. Después de todo, así es como funciona el mundo. Un órgano es un sistema de células, un cuerpo humano es un sistema de órganos, un vecindario es un sistema de cuerpos humanos, una ciudad es un sistema de vecindarios, y así sucesivamente. Mientras que este es un camino interesante para recorrer, está más allá de donde tenemos que estar en este momento. No obstante, es muy útil saber cómo escribir un programa que lleve un registro de muchos sistemas de partículas, cada uno de los cuales lleva un registro de muchas partículas. Tomemos el siguiente escenario:
Empiezas con una pantalla en blanco.
Haces clic con el ratón y generas un sistema de partículas en la posición del ratón.
Cada vez que haces clic en el ratón, se crea un nuevo sistema de partículas en la posición del ratón.
Pruébalo tú mismo:
¿Cómo podríamos lograr esto en código? En el artículo anterior, almacenamos una sola referencia a un objeto ParticleSystem en la variable ps.
var ps = new ParticleSystem(new PVector(width/2, 50)); draw = function() { background(50, 50, 50); ps.addParticle(); ps.run(); };
Ahora que tenemos múltiples sistemas, un número potencialmente siempre creciente, no queremos almacenarlos en variables individuales. En su lugar, vamos a utilizar un arreglo para llevar el registro de todos los sistemas. Vamos a iniciarlo vacío cuando se inicie el programa:
var systems = [];
Cada vez que se presione el ratón, se crea un nuevo objeto ParticleSystem y se agrega al arreglo:
mousePressed = function() { systems.push(new ParticleSystem(new PVector(mouseX, mouseY))); };
Y en draw(), en lugar de hacer referencia a un solo objeto ParticleSystem, ahora vemos a todos los sistemas del arreglo y llamamos run() sobre cada uno de ellos.
draw = function() { background(50, 50, 50); for(var i = 0; i < systems.length; i++){ systems[i].addParticle(); systems[i].run(); } };
Ahora, pruébalo otra vez, con el código que escribimos:
Vamos a utilizar técnicas de programación orientada a objetos más avanzadas, como herencia, así que puede ser que quieras revisar "Herencia" en el curso de Introducción a JS y regresar.
Vamos a utilizar la herencia para hacer diferentes tipos de subobjetos de Particle, los cuales comparten gran parte de la misma funcionalidad pero también difieren en aspectos clave.
Vamos a revisar una implementación simplificada de Particle:
var Particle = function(position) { this.acceleration = new PVector(0, 0{,}05); this.velocity = new PVector(random(-1, 1), random(-1, 0)); this.position = position.get(); }; Particle.prototype.run = function() { this.update(); this.display(); }; Particle.prototype.update = function(){ this.velocity.add(this.acceleration); this.position.add(this.velocity); }; Particle.prototype.display = function() { fill(127, 127, 127); ellipse(this.position.x, this.position.y, 12, 12); };
A continuación, creamos un nuevo tipo de objeto basado en Particle, al que llamaremos Confetti. Vamos a empezar con una función constructora que acepte el mismo número de argumentos y simplemente llame al constructor de Particle, pasándole esos argumentos:
var Confetti = function(position) { Particle.call(this, position); };
Ahora, para asegurarnos de que nuestros objetos Confetti compartan los mismos métodos que los objetos Particle, tenemos que especificar que su prototipo debe basarse en el prototipo de Particle:
Confetti.prototype = Object.create(Particle.prototype); Confetti.prototype.constructor = Confetti;
En este punto, tenemos objetos Confetti que actúan exactamente del mismo modo que los objetos Particle. El punto de la herencia no es hacer duplicados, es hacer nuevos objetos que compartan mucha de la funcionalidad pero también difieran de algún modo. Entonces, ¿cómo es diferente un objeto Confetti? Bueno, solo con base en el nombre, parece que debería verse diferente. Nuestros objetos Particle son elipses, pero el confeti generalmente son pequeños pedacitos cuadrados de papel, así que por lo menos, deberíamos cambiar el método display para mostrarlos como rectángulos:
Confetti.prototype.display = function(){ rectMode(CENTER); fill(0, 0, 255, this.timeToLive); stroke(0, 0, 0, this.timeToLive); strokeWeight(2); rect(0, 0, 12, 12); };
Aquí está un programa con una instancia del objeto Particle y una del objeto Confetti. Observa que se comportan de un modo parecido pero difieren en apariencia:
Hagamos esto un poco más sofisticado. Digamos que queremos hacer que la partícula Confetti rote mientras vuela por el aire. Podríamos, por supuesto, modelar la velocidad angular y la aceleración como lo hicimos en la sección de Oscilaciones. En vez de eso, intentaremos una solución rápida y sucia.
Sabemos que una partícula tiene una posición x entre 0 y el ancho de la ventana. ¿Qué pasa si dijéramos: cuando la posición x de la partícula sea 0, su rotación debe ser 0; cuando su posición x sea igual al ancho, la rotación debe ser igual a TWO_PI? ¿Te suena conocido? Cada vez que tengamos un valor con un rango que queramos mapear a otro rango, podemos usar la función map() de ProcessingJS para calcular fácilmente el nuevo valor.
var theta = map(this.position.x, 0, width, 0, TWO_PI);
Y para darle un poco más de giro, en realidad podemos mapear el rango del ángulo de 0 a TWO_PI * 2. Veamos cómo encaja este código en el método display().
Confetti.prototype.display = function(){ rectMode(CENTER); fill(0, 0, 255); stroke(0, 0, 0); strokeWeight(2); pushMatrix(); translate(this.position.x, this.position.y); var theta = map(this.position.x, 0, width, 0, TWO_PI * 2); rotate(theta); rect(0, 0, 12, 12); popMatrix(); };
Aquí está cómo se ve eso.
Reinícialo unas cuantas veces para ver el efecto de la rotación:
También podríamos basar a theta en la posición y, lo cual tiene un efecto un poco diferente. ¿Por qué sucede esto? Bueno, la partícula tiene una aceleración constante distinta de cero en la dirección y, lo que significa que la velocidad en y es una función lineal del tiempo, y que la posición en y es en realidad una función parabólica del tiempo. Puedes ver lo que eso significa en las siguientes gráficas (que fueron generadas con base en el programa anterior):
Eso significa que si basamos la rotación del confeti en la posición en y, la rotación también será parabólica. Esto no será físicamente muy exacto ya que la rotación real del confeti cayendo a través del aire es bastante complicada, pero ¡pruébalo y ve qué tan realista se ve! ¿Puedes pensar en otras funciones que puedan parecer incluso más realistas?
Ahora, lo que realmente queremos es ser capaces de crear muchos objetos Particle y muchos objetos Confetti. Para eso es que hicimos el objeto ParticleSystem, así que ¿tal vez podamos solamente extenderlo para también llevar un registro de los objetos Confetti? Aquí hay una manera como podríamos hacer eso, copiando lo que hicimos para los objetos Particle:
var ParticleSystem = function(position) { this.origin = position; this.particles = []; this.confettis = []; }; ParticleSystem.prototype.addParticle = function() { this.particles.push(new Particle(this.origin)); this.confettis.push(new Confetti(this.origin)); }; ParticleSystem.prototype.run = function(){ for (var i = this.particles.length-1; i >= 0; i--) { var p = this.particles[i]; p.run(); } for (var i = this.confettis.length-1; i >= 0; i--) { var p = this.confettis[i]; p.run(); } };
Observa que tenemos dos arreglos separados, uno para las partículas y otro para el confeti. Cada vez que le hagamos algo al arreglo de las partículas, ¡tenemos que hacérselo al arreglo de confeti! Eso es molesto, porque significa que tenemos que escribir el doble de código y, si cambiamos algo, tenemos que cambiarlo en dos lugares.
En realidad podríamos evitar esta duplicación, porque tenemos permitido almacenar objetos de diferentes tipos en arreglos en JavaScript, y como nuestros objetos tienen la misma interfaz: estamos llamando el método run() y ambos tipos de objetos definen esa interfaz. Así que, regresaremos a solo almacenar un arreglo, vamos a decidir aleatoriamente qué tipo de partícula agregar y regresaremos a iterar a través del único arreglo.
Este es un cambio mucho más simple. Lo que terminaremos modificando es el método addParticle:
var ParticleSystem = function(position) { this.origin = position; this.particles = []; }; ParticleSystem.prototype.addParticle = function() { var r = random(1); if (r < 0{,}5) { this.particles.push(new Particle(this.origin)); } else { this.particles.push(new Confetti(this.origin)); } }; ParticleSystem.prototype.run = function(){ for (var i = this.particles.length-1; i >= 0; i--) { var p = this.particles[i]; p.run(); if (p.isDead()) { this.particles.splice(i, 1); } } };
¡Todo junto ahora!
O
Hasta ahora en esta sección, nos hemos centrado en estructurar nuestro código de una manera orientada a objetos para gestionar una colección de partículas. Tal vez te hayas dado cuenta, o tal vez no, pero durante este proceso involuntariamente tomamos un par de pasos hacia atrás de donde estábamos en las secciones anteriores. Examinemos el constructor de nuestro objeto simple de partículas Particle:
var Particle = function(position) { this.acceleration = new PVector(0, 0{,}05); this.velocity = new PVector(random(-1, 1), random(-1, 0)); this.position = new PVector(position.x, position.y); this.timeToLive = 255{,}0; };
Y ahora veamos el método update():
Particle.prototype.update = function(){ this.velocity.add(this.acceleration); this.position.add(this.velocity); this.timeToLive -= 2; };
Observa que nuestra aceleración es constante, nunca se le da un valor fuera del constructor. Un marco de trabajo mucho mejor sería seguir la segunda ley de Newton (\vec{F} = M\vec{A}
F
=MA
F, with, vector, on top, equals, M, A, with, vector, on top) e incorporar el algoritmo de acumulación de fuerzas en el cual trabajamos tan duro en la sección de Fuerzas.
El primer paso es agregar un método applyForce(). (Recuerda, tenemos que hacer una copia de PVector antes dividirlo entre la masa).
Particle.prototype.applyForce = function(force) { var f = force.get(); f.div(this.mass); this.acceleration.add(f); };
Una vez que tengamos esto, podemos agregar una línea más de código para limpiar la aceleración al final de update().
Particle.prototype.update = function() { this.velocity.add(this.acceleration); this.position.add(this.velocity); this.acceleration.mult(0); this.timeToLive -= 2{,}0; };
Y por lo tanto, tenemos un objeto Particle al que se le puede aplicar fuerza. Ahora, ¿dónde deberíamos llamar a la función applyForce()? ¿En qué lugar del código es apropiado aplicarle una fuerza a una partícula? La verdad del asunto es que no hay una respuesta correcta o incorrecta; realmente depende de la funcionalidad exacta y de los objetivos de un programa en particular. Aún así, podemos crear una situación genérica que es probable que se aplique a la mayoría de los casos y hacer un modelo para aplicarle fuerzas a partículas individuales en un sistema.
Consideremos el siguiente objetivo: aplicar una fuerza globalmente cada vez a todas las partículas por medio de draw(). Comenzaremos por aplicar una fuerza sencilla como de viento que empuja a las partículas a la derecha:
var wind = new PVector(0,4, 0);
Dijimos que siempre debe aplicarse, es decir, en draw(), así que veamos como está nuestra función draw().
draw = function() { background(168, 255, 156); particleSystem.addParticle(); particleSystem.run(); };
Bueno, parece que tenemos un pequeño problema. applyForce() es un método escrito dentro del objeto Particle, pero no tenemos ninguna referencia a las partículas individuales, solo al objeto ParticleSystem: la variable particleSystem.
Sin embargo, como queremos que todas las partículas reciban la fuerza, podemos decidir aplicarle la fuerza al sistema de partículas y dejarlo que gestione la aplicación de la fuerza sobre todas las partículas individuales.
draw = function() { background(168, 255, 156); particleSystem.applyForce(wind); particleSystem.addParticle(); particleSystem.run(); };
Por supuesto, si llamamos una nueva función en el objeto ParticleSystem en draw(), bueno, tendremos que escribir esa función en el objeto ParticleSystem. Vamos a describir el trabajo que esa función debe realizar: recibir una fuerza como un PVector y aplicarle esa fuerza a todas las partículas.
Ahora en código:
ParticleSystem.prototype.applyForce = function(f){ for(var i = 0; i < this.particles.length; i++){ this.particles[i].applyForce(f); } };
Casi parece una tontería escribir esta función. Lo que estamos diciendo es “aplícale una fuerza a un sistema de partículas para que el sistema pueda aplicarle esa fuerza a todas las partículas individuales”. Sin embargo, es bastante razonable. Después de todo, el objeto ParticleSystem está encargado de gestionar a las partículas, así que si queremos hablarle a las partículas, tenemos que hablarles a través de su gestor.
Aquí está, todo junto. Juega con la fuerza del viento y ve cómo afecta el movimiento de las partículas, y observa cómo las partículas de masa diferente responden de manera diferente. Piensa en por qué es eso.
Ahora apliquemos una fuerza más compleja, la gravedad, que es distinta del viento porque varía con base en la masa de los objetos a los que se les aplica.
Recordemos la ecuación para calcular la fuerza de gravedad entre dos masas: \vec{F_g} = \frac{Gm_1m_2}{||r||^2} \hat{r}
F
=∣∣r∣∣
r^
F, start subscript, g, end subscript, with, vector, on top, equals, start fraction, G, m, start subscript, 1, end subscript, m, start subscript, 2, end subscript, divided by, vertical bar, vertical bar, r, vertical bar, vertical bar, squared, end fraction, r, with, hat, on top
Recuerda que cuando modelamos la fuerza de gravedad en la Tierra, la fuerza ejercida por la Tierra sobrepasa a todas las otras fuerzas gravitacionales, así que la única ecuación con la que estamos tratando es la de calcular la fuerza de gravedad entre la Tierra y el objeto. GGG y m_1m
g
Gm1
2
m2
1
m, start subscript, 1, end subscript son las mismas para cada partícula y rrr (el radio de la Tierra) es básicamente el mismo (ya que el radio de la Tierra es tan grande en comparación con lo poco que las partículas se alejan), así que típicamente las simplificamos solo como g, la constante de gravedad en la Tierra:
g = \frac{Gm_1}{||r||^2}g=
∣∣r∣∣
g, equals, start fraction, G, m, start subscript, 1, end subscript, divided by, vertical bar, vertical bar, r, vertical bar, vertical bar, squared, end fraction
Ahora, la fuerza de gravedad es solo una constante g por la masa de las partículas, multiplicada por un vector unitario en la dirección de la fuerza (que siempre será hacia abajo):
\vec{F_g} = g m_2 \hat{r}
Gm1
2
F
=gm2
r^
F, start subscript, g, end subscript, with, vector, on top, equals, g, m, start subscript, 2, end subscript, r, with, hat, on top
En código, eso significa que tendremos que aplicarle una fuerza de gravedad diferente a cada partícula con base en su masa. ¿Cómo podemos hacer eso? No podemos reutilizar la función applyForce existente, porque espera la misma fuerza para cada partícula. Podríamos considerar pasarle un parámetro que le dé instrucciones a applyForce de multiplicarla por la masa, pero vamos a dejar sin cambio a esa función y crear una nueva, applyGravity, que calcule la fuerza con base en un vector constante global:
// Un vector constante hacia abajo, declarado hasta arriba var gravity = new PVector(0, 0{,}2);
ParticleSystem.prototype.applyGravity = function() { for(var i = 0; i < this.particles.length; i++) { var particleG = gravity.get(); particleG.mult(this.particles[i].mass); this.particles[i].applyForce(particleG); } };
Ahora, si hicimos esto correctamente, todas nuestras partículas deberían caer a la misma tasa en la siguiente simulación. Eso es porque la fuerza de gravedad está basada en multiplicar la masa, pero la aceleración está basada en dividir entre la masa, así que al final, la masa no tiene un efecto. Puede parecer tonto pasar por todo ese esfuerzo para no tener efecto, pero es importante una vez que empecemos a combinar múltiples fuerzas diferentes.
¿Qué pasa si quisiéramos llevar este ejemplo un paso más lejos y agregar un objeto repelente que empuje a las partículas a medida que se acercan? Sería similar al objeto atractor que creamos anteriormente, solo que empuja en la dirección opuesta. Una vez más, como la gravedad, debemos calcular una fuerza diferente para cada partícula, pero en el caso del repelente, la diferencia es que el cálculo no está basado en la masa, sino en la distancia. Para la gravedad, todas nuestros vectores de fuerza tenían la misma dirección, pero para el repelente, todos los vectores de fuerza tendrán diferentes direcciones:
g
Fuerza de gravedad: todos los vectores tienen la misma dirección
Como el cálculo de una fuerza repelente es un poco más complejo que el cálculo de la gravedad (¡y en última instancia podríamos querer muchos repelentes!), resolveremos este problema incorporando un nuevo objeto Repeller (repelente en inglés) en nuestro ejemplo sencillo de un sistema de partículas más gravedad. Vamos a necesitar dos adiciones importantes a nuestro código:
Un objeto Repeller (declarado, inicializado y desplegado).
Una función que pase el objeto Repeller al ParticleSystem para que pueda aplicarle una fuerza a cada partícula.
var particleSystem = new ParticleSystem(new PVector(width/2, 50)); var repeller = new Repeller(width/2-20, height/2); var gravity = new PVector(0, 0{,}1); draw = function() { background(214, 255, 171); // Apply gravity force to all Particles particleSystem.applyForce(gravity); particleSystem.applyRepeller(repeller); repeller.display(); particleSystem.addParticle(); particleSystem.run(); };
Hacer que un objeto Repeller sea visible es fácil; es un duplicado del objeto Attractor que creamos anteriormente:
var Repeller = function(x, y) { this.position = new PVector(x, y); }; Repeller.prototype.display = function() { stroke(255); strokeWeight(2); fill(127); ellipse(this.position.x, this.position.y, 32, 32); };
La pregunta más difícil es, ¿cómo escribimos el método applyRepeller()? En lugar de pasarle un PVector a una función como lo hacemos con applyForce(), vamos a pasarle un objeto Repeller a applyRepeller() y pedirle a esa función que haga el trabajo de calcular la fuerza entre el repelente y todas las partículas:
ParticleSystem.prototype.applyRepeller = function(r) { for(var i = 0; i < this.particles.length; i++){ var p = this.particles[i]; var force = r.calculateRepelForce(p); p.applyForce(force); } };
La gran diferencia aquí es que se calcula una nueva fuerza para cada partícula, porque, como vimos anteriormente, la fuerza es diferente dependiendo de las propiedades de cada partícula en relación con el repelente. Calculamos esa fuerza utilizando la función calculateRepelForce, que es el inverso de la función calculateAttractionForce de nuestros Attractores.
Repeller.prototype.calculateRepelForce = function(p) { // Calcular el vector para la fuerza entre los objetos var dir = PVector.sub(this.position, p.position); // Calcular la distancia entre los objetos var dist = dir.mag(); // Mantener la distancia dentro de un rango razonable dist = constrain(dist, 1, 100); // Calcular la fuerza de repulsión, // inversamente proporcional a la distancia al cuadrado var force = -1 * this.power/ (dist * dist); // Normalizar el vector de dirección // (descartar la información de la distancia) dir.normalize(); // Calcular el vector de fuerza: dirección * magnitud dir.mult(force); return dir; };
Observa cómo a lo largo de todo este proceso de agregar un repelente al medio ambiente nunca consideramos editar el propio objeto Particle. Una partícula en realidad no tiene por qué saber nada acerca de los detalles de su entorno; simplemente tiene que gestionar su ubicación, velocidad y aceleración, así como tener la capacidad para recibir una fuerza externa y actuar en consecuencia.
Así que ahora podemos ver este ejemplo en su totalidad. Intenta cambiar la intensidad de las fuerzas que actúan sobre las partículas (la gravedad y la repelente) y ver cómo eso las cambia:
Fuerza repelente: todos los vectores de dirección son diferentes
O