Fricción y Arrastre (NC4)

Esta es la cuarta entrada sobre el libreo The Nature of Code de Daniel Shiffman. Es una continuación sobre el capítulo de fuerzas.

En la entrada anterior hablamos sobre como modelar fuerzas en general, teniendo en cuenta la masa de un objeto para hacer que las cosas más ligeras se muevan más rápido. Luego nos concentramos en la gravedad, que a diferencia de otras fuerzas, hace que todos los objetos caigan a la misma velocidad.

En esta entrada nos enfocaremos en un par de fuerzas adicionales que al agregarlas a nuestra animación la harán verse mucho más natural: la fricción y la inercia.

Repasemos rápidamente la última animación de la entrada anterior, agregándole un poco de viento, representado por una fuerza horizontal, para hacerla más interesante.

let pelotas = [];
let color = [85, 142, 11];
let fondo = 220;

function setup()
{
    createCanvas(400, 400);

    for (let i = 0; i < 3; i++)
    {
        let masa = 20*i + 20;
        const pelota = new Pelota(
            createVector(85*i + 85, 100 - masa/2),
            color,
            masa
        );

        pelotas.push(pelota);
    }
}

function draw()
{
    background(fondo);
    let gravedad = createVector(0, 0.2);
    let viento = createVector(1.2, 0);
    for (const pelota of pelotas)
    {
        let fuerzag = gravedad.copy();
        fuerzag.mult(pelota.masa);
        pelota.aplicarFuerza(fuerzag);
        pelota.aplicarFuerza(viento);
        pelota.mover();
        pelota.verificarBorde();
        pelota.mostrar();
    }
}

Que genera los siguiente:

Nota

Para entender la implementación completa de la clase Pelota revisa la entrada anterior.

Fricción

A pesar de mi insistencia en tener animaciones que se vean naturales, lo cierto es que la animación anterior tiene un grave problema. Las pelotas son perfectamente elásticas, y seguirán botando contra la pared por toda la eternidad.

No hay nada en el mundo real que haga eso. Bueno, en mis clases de física me dijeron que las moléculas de gas hacen eso, pero no es lo que estamos tratando de crear aquí. ¿Cómo le hacemos para resolver este problema?.

La respuesta es agregar fricción a la animación. La fricción es una fuerza destructiva, que reduce la energía total del sistema, y que empuja en dirección opuesta al movimiento. La siguiente imagen, extraída directamente del libro, ilustra esta idea.

fruction image

Una simulación completa requeriría calcular un vector normal a la dirección de movimiento, esa N que aparece en la fórmula. Pero como dijimos en la entrada anterior, vasta con que la animación se vea natural, no es necesario simular el sistema exactamente. Así que solo necesitamos generar una fuerza en dirección opuesta a la velocidad, y multiplicarla por una constante, conocida como coeficiente de fricción. Eso eso reduciría nuestra fórmula a \(-1\mu\hat{v}\).

El vector con el gorrito corresponde al vector de velocidad normalizado. Normalizar un vector es dividirlo entre su magnitud. La letra griega \(\mu\) es una constante, el coeficiente de fricción, y depende del material del que esté echo el objeto.

Tomemos un coeficiente de fricción de 0.15, traducir el esta fórmula en código es fácil.

const coefFriccion = 0.15;
friccion = pelota.velocidad.copy();
friccion.normalize();
friccion.mult(-1*coefFriccion);

// Y aplicamos la fuerza a la pelota como siempre
pelota.aplicarFuerza(friccion);

Agregamos este código a la animación del inicio de esta entrada y obtenemos lo siguiente:

let pelotas = [];
let color = [85, 142, 11];
let fondo = 220;

function setup()
{
    console.log("Test");
    createCanvas(400, 400);
    for (let i = 0; i < 3; i++)
    {
        let masa = 20*i + 20;
        const pelota = new Pelota(
            createVector(85*i + 85, 100 - masa/2),
            color,
            masa
        );

        pelotas.push(pelota);
    }
}

function draw()
{
    background(fondo);
    let gravedad = createVector(0, 0.2);
    let viento = createVector(1.2, 0);

    const coefFriccion = 0.15;

    for (const pelota of pelotas)
    {
        let fuerzag = gravedad.copy();
        fuerzag.mult(pelota.masa);

        let friccion = pelota.velocidad.copy();
        friccion.normalize();
        friccion.mult(-1*coefFriccion);
        pelota.aplicarFuerza(friccion);

        pelota.aplicarFuerza(viento);
        pelota.aplicarFuerza(fuerzag);
        pelota.mover();
        pelota.verificarBorde();
        pelota.mostrar();
    }
}

Y con esto tenemos la siguiente animación.

Nota

Da click en el lienzo para reiniciar la animación.

Arrastre

Un factor adicional a considerar en nuestra animación es el arrastre. El arrastre se refiere a la resistencia que opone el medio en que te estás moviendo. Por ejemplo, es mucho más difícil moverse en el aire que en el agua, el agua tiene mucho más arrastre.

De nuevo, existe una fórmula para calcular el arrastre que un cuerpo experimenta dentro de un medio en particular.

\[F_d = - \frac{1}{2} \rho v^2 AC_d \hat{v}\]

Veamos que significa cada parte:

  • \(\rho\) es la densidad del medio, que para nuestra animación es irrelevante.
  • \(v\) es la magnitud del vector velocidad. Esta se calcula con el método .magnitude() en p5. Fácil.
  • \(A\) es el área frontal del objeto. Por eso objetos con poca área frontal son más aerodinámicos. Para ahorrarnos problemas, no la tomaremos en cuenta.
  • \(C_d\) es el coeficiente de arrastre. Una constante que modificaremos como mejor nos parezca.
  • \(\hat{v}\) es de nuevo el vector velocidad normalizado.

Así que igual que con la fricción, tenemos una fuerza destructiva que actúa en dirección opuesta al movimiento.

La implementación es un poco más compleja, porque aplicar arrastre solo tiene sentido si nuestro «mundo» consta de más de un medio en el que se puede mover un objeto. Por supuesto la implementación depende de lo que tenga sentido para cada quien. La forma en la que yo (o, en realidad, Shiffman) abordaré el problema no es de ninguna manera la única ni la mejor.

Crearemos una nueva clase para representar medios distintos, con distintos colores, y diferentes coeficientes de arrastre.

class Liquido
{
    constructor(esquinas, coef_arrastre, color)
    {
        this.esquinas = esquinas;
        this.coef_arrastre = coef_arrastre;
        this.color = color;
        console.log(this.esquinas);
    }

    mostrar()
    {
        noStroke();
        fill(this.color);
        rect(this.esquinas.nw.x,
             this.esquinas.nw.y,
             this.esquinas.se.x,
             this.esquinas.se.y);
    }
}

Luego agregamos un par de métodos a la pelota, uno para verificar si está dentro de un líquido, y otra para aplicar el arrastre correspondiente. Para hacerlo más fácil e incluir el código completo vamos a crear un nuevo tipo de pelota que extiende la clase Pelota que creamos en en la entrada anterior.

class PelotaArrastre extends Pelota
{
    constructor(posicion, color, masa)
    {
        super(posicion, color, masa);
    }

    enLiquido(liquido)
    {
        let radio = this.diametro / 2;
        if (
            this.posicion.y + radio > liquido.esquinas.nw.y &&
            this.posicion.y - radio < liquido.esquinas.se.y
        )
            return true;

        return false;
    }

    aplicarArrastre(liquido)
    {
        const rapidez = this.velocidad.mag();
        const magnitudArrastre = liquido.coef_arrastre * rapidez * rapidez;
        let arrastre = this.velocidad.copy();
        arrastre.normalize();
        arrastre.mult(-1 * magnitudArrastre);

        this.aplicarFuerza(arrastre);
    }
}

Y el script principal para crear la animación es el siguiente:

let pelotas = [];
let color = [85, 142, 11];
let fondo = 220;
let liquido;

function setup()
{
    createCanvas(400, 400);

    const colorLiquido = [55, 100, 132, 200];
    const coefArrastre = 0.2;
    const esquinas =
        {
            nw: createVector(0, 2*height/3),
            se: createVector(width, height)
        }

    liquido = new Liquido(esquinas, coefArrastre, colorLiquido)



    for (let i = 0; i < 3; i++)
    {
        let masa = 20*i + 20;
        const pelota = new PelotaArrastre(
            createVector(85*i + 85, 100 - masa/2),
            color,
            masa
        );

        pelotas.push(pelota);
    }
}

function draw()
{
    background(fondo);
    let gravedad = createVector(0, 0.2);
    let viento = createVector(1.2, 0);

    const coef_friccion = 0.15;

    for (const pelota of pelotas)
    {
        let fuerzag = gravedad.copy();
        fuerzag.mult(pelota.masa);

        let friccion = pelota.velocidad.copy();
        friccion.normalize();
        friccion.mult(-1*coef_friccion);
        if (pelota.enLiquido(liquido))
        {
            pelota.aplicarArrastre(liquido);
        }
        pelota.aplicarFuerza(friccion);

        pelota.aplicarFuerza(viento);
        pelota.aplicarFuerza(fuerzag);
        pelota.mover();
        pelota.verificarBorde();
        pelota.mostrar();
    }
    liquido.mostrar();
}

El color para el líquido en este ejemplo es un arreglo con cuatro valores, porque además de los tres usuales para el modelo de color RGB, incluye uno adicional para transparencia. Y éste es el resultado:

Nota

Da click en el lienzo para reiniciar la animación.

Referencias Adicionales

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.