Simulación espacial simple usando Python y Box2D

Hola Habr

Este artículo se inspiró en la reciente publicación de Modeling the Universe , donde el autor mostró una simulación muy interesante de varios fenómenos cósmicos. Sin embargo, el código presentado allí no es fácil para los principiantes. Le mostraré cómo hacer simulaciones físicas con el motor Box2D escribiendo solo unas pocas líneas de código.

Voy a tener la oportunidad de cometer un error, pero esta es la primera descripción de Box2D para Python en Habré, llenamos este vacío.



Para aquellos que estén interesados ​​en cómo funciona esto, los detalles están debajo del corte.

Box2D es una biblioteca gratuita multiplataforma creada por Blizzard Erin Catto. La biblioteca se introdujo en 2007, y hoy se ha portado a casi todas las plataformas. También hay un puerto para Python, su descripción es bastante confusa, pero espero que con la ayuda de este artículo todo se aclare.

Introducción


La biblioteca pybox2d consta de dos componentes: la propia Box2D, una biblioteca multiplataforma para el modelado físico y un módulo de representación separado llamado Framework. La representación es necesaria si queremos ver los objetos creados en la pantalla, lo cual es lo suficientemente conveniente para nuestra simulación. La clase Framework puede usar varios métodos de salida (más aquí ), usaremos pygame. Si la biblioteca de pygame está instalada, se "recoge" automáticamente y no se necesita hacer nada más. Para instalar, simplemente ingrese el comando pip install Box2D pygame .

A continuación se muestra el programa en ejecución más pequeño que utiliza Box2D. El código es multiplataforma y funcionará en todas partes, tanto en Linux como en Windows.

from Box2D.examples.framework import Framework
from Box2D import *

class Simulation(Framework):
    def __init__(self):
        super(Simulation, self).__init__()

        # Ground body
        self.world.CreateBody(shapes=b2LoopShape(vertices=[(20, 0), (20, 40), (-20, 40), (-20, 0)]))
        # Dynamic body
        circle = b2FixtureDef(shape=b2CircleShape(radius=2), density=1, friction=1.0, restitution=0.5)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0,30), fixtures=circle, linearVelocity=(5, 0))

    def Step(self, settings):
        super(Simulation, self).Step(settings)

if __name__ == "__main__":
    Simulation().run()

Como puede ver, estamos creando una clase de simulación que hereda del marco ya mencionado. A continuación, creamos dos objetos llamando al método CreateBody . El primero es un objeto estático que define los límites de nuestro mundo. El segundo objeto es de tipo b2_dynamicBody, los parámetros restantes (forma, tamaño, densidad, coeficiente de fricción, velocidad inicial) son obvios del código. La función Paso se llama cada vez durante la simulación, la usaremos en el futuro. Si no se necesita la interfaz de usuario, por ejemplo, hacemos un back-end para el servidor, entonces, por supuesto, se puede omitir la clase Framework, pero para nosotros es bastante conveniente.

Eso es todo, ejecute el programa y vea la simulación terminada:



Como puede ver, acabamos de crear dos objetos y especificamos sus parámetros. Todo funciona "fuera de la caja": gravedad, fricción, elasticidad, interacción de los cuerpos, etc. Con base en esto, podemos proceder con nuestra simulación de "espacio".

Lanzamos el "satélite"


Desafortunadamente, no hay soporte incorporado para la gravedad newtoniana en Box2D, tendrá que agregarlo usted mismo agregando la función Paso. Para la primera prueba, crearemos dos cuerpos: un planeta y un satélite girando a su alrededor.

Código fuente en su conjunto:

from Box2D import *
from Box2D.examples.framework import Framework


class Simulation(Framework):
    def __init__(self):
        super(Simulation, self).__init__()

        # Default gravity disable
        self.world.gravity = (0.0, 0.0)
        # Gravity constant
        self.G = 100

        # Planet
        circle = b2FixtureDef(shape=b2CircleShape(radius=5), density=1, friction=0.5, restitution=0.5)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0,0), fixtures=circle)

        # Satellite
        circle_small = b2FixtureDef(shape=b2CircleShape(radius=0.2), density=1, friction=0.5, restitution=0.2)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 10), fixtures=circle_small, linearVelocity=(20, 0))

    def Step(self, settings):
        super(Simulation, self).Step(settings)

        # Simulate the Newton's gravity
        for bi in self.world.bodies:
            for bk in self.world.bodies:
                if bi == bk:
                    continue

                pi, pk = bi.worldCenter, bk.worldCenter
                mi, mk = bi.mass, bk.mass
                delta = pk - pi
                r = delta.length
                if abs(r) < 1.0:
                    r = 1.0

                force = self.G * mi * mk / (r * r)
                delta.Normalize()
                bi.ApplyForce(force * delta, pi, True)

if __name__ == "__main__":
    Simulation().run()

Como puede ver, "desactivamos" la gravedad estándar al establecer el parámetro self.world.gravity en 0. También agregamos el parámetro G, esta es la "constante gravitacional" de nuestro mundo virtual, que se utiliza en el cálculo del método Step. También creamos dos objetos: un satélite y un planeta. Es importante tener en cuenta los parámetros de densidad y radio. Según estos parámetros, la propia biblioteca Box2D calcula la masa que se utiliza en el cálculo. Para calcular la fuerza de interacción, se usa la fórmula habitual de "escuela" de la ley de gravedad de Newton :



ahora comenzamos la simulación. No alcanzamos la primera velocidad cósmica, y aunque el satélite todavía rodeaba todo el planeta, es difícil llamarlo "vuelo":



aumentamos la velocidad cambiando la línea de código a linearVelocity = (28, 0):



¡Nuestro "satélite" ha entrado con éxito en órbita alrededor del "planeta"! Si la velocidad aumenta aún más, la órbita se volverá elíptica:



finalmente, representaremos algo más similar a nuestro "sistema solar", agregando tres planetas de diferentes tamaños en diferentes órbitas:

circle_small = b2FixtureDef(shape=b2CircleShape(radius=0.2), density=1, friction=0.5, restitution=0.2)
circle_medium = b2FixtureDef(shape=b2CircleShape(radius=0.3), density=1, friction=1.0, restitution=0.5)
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 6), fixtures=circle_small, linearVelocity=(37, 0))
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 10), fixtures=circle_small, linearVelocity=(28, 0))
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 15), fixtures=circle_medium, linearVelocity=(22, 0))

Resultado:



Vemos que cuanto más lejos esté el planeta del "sol", mayor será su período de revolución (tercera ley de Kepler). Desafortunadamente, el motor Box2D no permite dibujar pistas de movimiento en la pantalla, por lo que es difícil "ver" las leyes primera y segunda de Keppler, pero puede estar seguro de que también se implementan.

Conclusión


Como puede ver, con Box2D, se pueden hacer simulaciones simples con un mínimo esfuerzo. Por supuesto, este motor sigue siendo un motor de juego, no científico, por lo que no debe esperar una simulación correcta de una colisión de galaxias o la expansión de la materia durante el Big Bang. Pero algunos patrones son bastante interesantes de ver.

Todo concebido en una sola pieza no encajaba. Si las estimaciones son positivas, en la segunda parte será posible considerar más ejemplos no triviales.

All Articles