Probablemente la fuerza más famosa de todas es la gravedad. Los humanos en la Tierra pensamos en la gravedad como una manzana cayéndole en la cabeza a Isaac Newton. La gravedad significa que las cosas caen. Pero esta es solo nuestra experiencia de la gravedad. En realidad, así como la Tierra jala a la manzana hacia ella debido a la fuerza gravitacional, la manzana también jala a la Tierra. La cosa es que la Tierra es tan masiva que sobrepasa todas las interacciones gravitacionales de todos los demás objetos en el planeta. Cualquier objeto con masa ejerce una fuerza gravitacional sobre todos los demás objetos. Y hay una fórmula para calcular la intensidad de estas fuerzas, como se representa en el siguiente diagrama:
Diagrama de fuerzas gravitacionales entre dos esferas
Examinemos esta fórmula un poco más de cerca.
  • F se refiere a la fuerza gravitacional, el vector que en el fondo queremos calcular y pasarle a nuestra función applyForce().
  • G es la constante de gravitación universal, que en nuestro mundo equivale a 6.67428 x 10^-11 metros cúbicos por kilogramo por segundo cuadrado. Este es un número muy importante si tu nombre es Isaac Newton o Albert Einstein. No es un número importante si eres un programador de ProcessingJS. De nuevo, es una constante que podemos utilizar para hacer las fuerzas en nuestro mundo más fuertes o más débiles. Hacerla igual a uno e ignorarla no es una opción terrible.
  • m, start subscript, 1, end subscript y m, start subscript, 2, end subscript son las masas de los objetos 1 y 2. Como vimos con la segunda ley de Newton (F, with, vector, on top, equals, M, A, with, vector, on top), la masa también es algo que podríamos escoger ignorar. Después de todo, las figuras dibujadas en la pantalla no tienen masa física en realidad. Sin embargo, si mantenemos estos valores podemos crear simulaciones más interesantes en las que objetos “más grandes” ejerzan una fuerza gravitacional más fuerte que los más pequeños.
  • r^ \hat{r} se refiere al vector unitario que apunta del objeto 1 al objeto 2. Como veremos en un momento, podemos calcular este vector de dirección al restar la ubicación de un objeto de la ubicación del otro.
  • r, start superscript, 2, end superscript se refiere al cuadrado de la distancia entre los dos objetos. Tomemos un momento para pensar sobre esto un poco más. Con todo lo que está en la parte de arriba de la fórmula (G, m_1, m, start subscript, 2, end subscript), mientras más grande es su valor, más intensa es la fuerza. Masa grande, fuerza grande. G grande, fuerza grande. Ahora, cuando dividimos entre algo, tenemos lo contrario. La intensidad de la fuerza es inversamente proporcional al cuadrado de la distancia. Mientras más lejos está un objecto, más débil es la fuerza; mientras más cerca, mayor.
Espero que ahora la fórmula tenga sentido para nosotros. Vimos un diagrama y analizamos los componentes individuales de la fórmula. Ahora es tiempo de averiguar cómo traducir las matemáticas en código de ProcessingJS. Vamos a hacer las siguientes suposiciones.
Tenemos dos objetos, y:
  1. Cada objeto tiene una ubicación de PVector: location1 y location2.
  2. Cada objeto tiene una masa numérica: mass1 y mass2.
  3. Hay una variable numérica G para la constante de gravitación universal.
Dadas estas suposiciones, queremos calcular una fuerza PVector, la fuerza de gravedad. Lo haremos en dos partes. Primero, vamos a calcular la dirección de la fuerza r^ \hat{r}  en la fórmula de arriba. Segundo, vamos a calcular la intensidad de la fuerza de acuerdo con las masas y la distancia.
¿Recuerdas cuando averiguamos cómo tener un objeto que se acelera hacia el ratón? Vamos a usar la misma lógica aquí.
Un vector es la diferencia entre dos puntos. Para hacer un vector que apuntara del círculo al ratón, simplemente restamos un punto del otro:
var dir = PVector.sub(mouse, location);
En nuestro caso, la dirección de la fuerza de atracción que el objeto 1 ejerce sobre el objeto 2 es igual a:
var dir = PVector.sub(location1, location2);
No olvides que como queremos un vector unitario, un vector que nos diga solo la dirección, necesitaremos normalizar el vector después de restar las ubicaciones:
dir.normalize();
Bien, tenemos la dirección de la fuerza. Ahora solo necesitamos calcular la magnitud y escalar el vector de manera correspondiente.
var m = (G * mass1 * mass2) / (distance * distance);
dir.mult(m);
El único problema es que no conocemos la distancia. G, mass1 y mass2 estaban dados, pero vamos a tener que realmente calcular la distancia antes de que funcione el código anterior. ¿No acabamos de hacer un vector que apunta desde una dirección a la otra? ¿La longitud de ese vector no sería la distancia entre los dos objetos?
Bueno, si agregamos una sola línea de código y tomamos la magnitud de ese vector antes de normalizarlo, entonces tendremos la distancia.
// El vector que apunta de un objeto a otro
var force = PVector.sub(location1, location2);

// La longitud (magnitud) de ese vector es la distancia entre los dos objetos.
var distance = force.mag();

// Usa la fórmula para la gravedad para calcular la intensidad de la fuerza.
var strength = (G * mass1 * mass2) / (distance * distance);

// Normaliza y escala el vector de fuerza a la magnitud apropiada.
force.normalize();
force.mult(strength);
Observa que también renombré la “dir” de PVector como “fuerza” (force en inglés). Después de todo, cuando hayamos terminado con los cálculos, el PVector con el que comenzamos terminará siendo el vector de fuerza real que queríamos desde el principio.
Ahora que hemos trabajado las matemáticas y el código para calcular una fuerza atractiva (emulando a la gravedad), tenemos que volver nuestra atención a la aplicación de esta técnica en el contexto de un programa real de ProcessingJS. Anteriormente en esta sección, creamos un objeto Mover sencillo: un objeto con la ubicación, velocidad y aceleración de PVector, así como una applyForce(). Tomemos esta misma clase y pongámosla en un programa con:
  • Un solo objeto Mover.
  • Un solo objeto Attractor (un nuevo tipo de objeto que tendrá una ubicación fija).
El objeto Mover experimentará un jalón gravitacional hacia el objeto Attractor, como se ilustra en el diagrama.
Podemos empezar por hacer el nuevo objeto Attractor muy sencillo: dándole una ubicación y una masa, junto con un método para desplegarlo (atando la masa al tamaño).
var Attractor = function() {
    this.position = new PVector(width/2, height/2);
    this.mass = 20;
    this.G = 1;
    this.dragOffset = new PVector(0, 0);
    this.dragging = false;
    this.rollover = false;
};

// Método para desplegar
Attractor.prototype.display = function() {
    ellipseMode(CENTER);
    strokeWeight(4);
    stroke(0);
    fill(175, 175, 175, 200);
    ellipse(this.position.x, this.position.y, this.mass*2, this.mass*2);
};
Después de definir eso, podemos crear una instancia del tipo de objeto Attractor.
var mover = new Mover();
var attractor = new Attractor();

draw = function() {
    background(50, 50, 50);

    attractor.display();
    mover.update();
    mover.display();
};
Esta es una buena estructura: un programa principal con un Mover y un objeto Attractor. La última pieza del rompecabezas es cómo conseguir que un objeto atraiga al otro. ¿Cómo hacemos que estos dos objetos se hablen entre sí?
Hay varias maneras en que podríamos hacer esto, de manera arquitectónica. Aquí hay solo algunas posibilidades.
TareaLa función
1. Una función que recibe tanto un Attractor como un Mover:attraction(a, m);
2. Un método en el objeto Attractor que recibe un Mover:a.attract(m);
3. Un método en el objeto Mover que recibe un Attractor:mover.attractedTo(a);
4. Un método en el objeto Attractor que recibe un Mover y regresa un PVector, que es la fuerza de atracción. Esa fuerza de atracción se pasa entonces al método applyForce() del Mover.`var f = a.calculateAttraction(m);
mover.applyForce(f);` |
Es bueno mirar una gama de opciones para hacer que los objetos se hablen entre sí, y probablemente podrías hacer argumentos para cada una de las posibilidades anteriores. Empecemos por descartar la primera, ya que un enfoque orientado a objetos es una mucho mejor opción que una función arbitraria no atada a los objetos Mover o Attractor. Elegir entre la opción 2 o la opción 3 es la diferencia entre decir “el atractor atrae el mover” o “el mover es atraído por el atractor”. La número 4 parece la más apropiada, al menos en términos de dónde estamos en este curso. Después de todo, pasamos mucho tiempo trabajando el método applyForce(), y creo que nuestros ejemplos serán más claros si seguimos con la misma metodología.
En otras palabras, donde una vez tuvimos:
var f = new PVector(0.1, 0); // Fuerza inventada
mover.applyForce(f);
Ahora tendremos:
var f = a.calculateAttraction(m); // Fuerza de atracción entre dos objetos
mover.applyForce(f);
Y entonces nuestra función draw() ahora puede escribirse como:
draw = function() {
    background(50, 50, 50);

    // Calcula la fuerza de atracción y aplícala
    var f = a.calculateAttraction(m);
    mover.applyForce(f);

    attractor.display();
    mover.update();
    mover.display();
};
Ya casi llegamos. Como hemos decidido poner el método calculateAttraction() dentro de tipo de objeto Attractor, en realidad tendremos que escribir esa función. La función debe recibir un objeto Mover y regresar un PVector. ¿Y qué pasa dentro de esa función? ¡Todas esas matemáticas bonitas que trabajamos para la atracción gravitacional!
Attractor.prototype.calculateAttraction = function(mover) {

    // ¿Cuál es la dirección de la fuerza?
    var force = PVector.sub(this.position, mover.position);    
    var distance = force.mag();
    force.normalize();

    // ¿Cuál es la magnitud de la fuerza?
    var strength = (this.G * this.mass * mover.mass) / (distance * distance);
    force.mult(strength);

    // ¡Regresa la fuerza para que pueda ser aplicada!
    return force;
};
Y terminamos. Más o menos. Casi. Hay un pequeño detalle que tenemos que resolver. Echemos un vistazo al código anterior otra vez. ¿Ves ese símbolo de división, la diagonal? Siempre que tengamos uno de esos, tenemos que preguntarnos lo siguiente: ¡¿¿qué pasaría si la distancia fuera en realidad una cantidad muy, muy pequeña o (¡peor aún!) cero??! Bueno, sabemos que no podemos dividir un número entre 0 y si tuviéramos que dividir un número entre algo como 0.0001, ¡eso es el equivalente de multiplicar a ese número por 10,000! Sí, esta es la fórmula del mundo real para la fuerza de la gravedad, pero no vivimos en el mundo real. Vivimos en el mundo de ProcessingJS. Y en el mundo de ProcessingJS, el mover puede terminar estando muy, muy cercano al atractor y la fuerza podría llegar a ser tan fuerte que el mover saldría volando fuera de la pantalla. Así que con esta fórmula, es bueno que seamos prácticos y limitemos el rango de valores que puede tomar la distancia. Tal vez, sin importar en dónde esté el Mover en realidad, nunca deberíamos considerarlo a menos de 5 pixeles o a más de 25 pixeles del atractor.
distance = constrain(distance, 5, 25);
Por la misma razón que necesitamos limitar la distancia mínima, es útil para nosotros hacer lo mismo con la máxima. Después de todo, si el mover estuviera, digamos, a 500 pixeles del atractor (no poco razonable), estaríamos dividiendo la fuerza entre 250,000. Esa fuerza podría terminar siendo tan débil que sería casi como si no la estuviéramos aplicando en absoluto.
Ahora, en realidad depende de ti decidir qué comportamientos quieres. Pero en el caso de que “quiero una atracción de aspecto razonable que no sea absurdamente fuerte o débil”, entonces restringir la distancia es una buena técnica.
Pongamos todo junto en un programa. El tipo de objeto Mover no ha cambiado para nada, pero nuestro programa ahora incluye un objeto Attractor y código que los une. También le agregamos código al programa para controlar el atractor con un ratón, para que sea más fácil observar los efectos.
Y podríamos, por supuesto, expandir este ejemplo usando un arreglo para incluir muchos objetos Mover, igual que como lo hicimos con la fricción y el arrastre. El cambio principal que le hicimos al programa es para ajustar a nuestro objeto Mover para aceptar masa, x y y (como lo hicimos en el pasado), inicializar un arreglo de Movers colocados aleatoriamente e iterar sobre ese arreglo para calcular la fuerza de atracción sobre cada uno de ellos, cada vez:
var movers = [];
var attractor = new Attractor();

for (var i = 0; i < 10; i++) {
    movers[i] = new Mover(random(0.1, 2), random(width), random(height));
}

draw = function() {
    background(50, 50, 50);

    attractor.display();
    for (var i = 0; i < movers.length; i++) {
        var force = attractor.calculateAttraction(movers[i]);
        movers[i].applyForce(force);

        movers[i].update();
        movers[i].display();
    }
};

Este curso de "Simulaciones Naturales" es un derivado de "La Naturaleza del Código" por Daniel Shiffman, usado bajo una Licencia Creative Commons Reconocimiento-NoComercial 3.0 Unported.