web-dev-qa-db-fra.com

Collision entre balles - Détection et manipulation

Avec l'aide de la communauté Stack Overflow, j'ai écrit un simulateur de physique assez basique mais amusant.

alt text

Vous cliquez et faites glisser la souris pour lancer une balle. Il rebondira et finira par s'arrêter sur le "sol".

Ma prochaine grande fonctionnalité que je veux ajouter est la collision balle à balle. Le mouvement de la balle est divisé en un vecteur vitesse x et y. J'ai la gravité (petite réduction du vecteur y à chaque étape), j'ai des frictions (petite réduction des deux vecteurs chaque collision avec un mur). Les balles se déplacent honnêtement de manière étonnamment réaliste.

Je suppose que ma question comporte deux parties:

  1. Quelle est la meilleure méthode pour détecter une collision balle à balle?
    Ai-je juste une boucle O (n ^ 2) qui itère sur chaque balle et vérifie toutes les autres balles pour voir si son rayon de chevauchement se chevauche?
  2. Quelles équations dois-je utiliser pour gérer les collisions de boule à boule? Physique 101
    Comment cela affecte-t-il les vecteurs vitesse x/y à deux boules? Quelle est la direction résultante dans laquelle les deux balles se dirigent? Comment puis-je appliquer cela à chaque balle?

alt text

La gestion de la détection de collision des "murs" et des changements de vecteur résultants était facile, mais je vois plus de complications avec les collisions balle-balle. Avec les murs, je devais simplement prendre le négatif du vecteur x ou y approprié et le placer dans la bonne direction. Je ne pense pas que ce soit le cas avec des balles.

Quelques éclaircissements rapides: pour des raisons de simplicité, je suis d'accord avec une collision parfaitement élastique pour le moment, toutes mes balles ont la même masse pour le moment, mais je pourrais changer cela à l'avenir.


Edit: Ressources que j'ai trouvées utiles

Physique des billes 2d avec vecteurs: Collisions bidimensionnelles sans trigonométrie.pdf
2d Exemple de détection de collision de billes: Ajout de la détection de collision


Succès!

La détection et la réponse de la collision de la balle fonctionnent très bien!

Code pertinent:

Détection de collision:

for (int i = 0; i < ballCount; i++)  
{  
    for (int j = i + 1; j < ballCount; j++)  
    {  
        if (balls[i].colliding(balls[j]))  
        {
            balls[i].resolveCollision(balls[j]);
        }
    }
}

Ceci vérifiera les collisions entre chaque balle, mais évitera les vérifications redondantes (si vous devez vérifier si la balle 1 entre en collision avec la balle 2, vous n'avez pas besoin de vérifier si la balle 2 entre en collision avec la balle 1. Elle ignore également les collisions avec elle-même. ).

Ensuite, dans ma classe de balle, j'ai mes méthodes de collision () et de resolCollision ():

public boolean colliding(Ball ball)
{
    float xd = position.getX() - ball.position.getX();
    float yd = position.getY() - ball.position.getY();

    float sumRadius = getRadius() + ball.getRadius();
    float sqrRadius = sumRadius * sumRadius;

    float distSqr = (xd * xd) + (yd * yd);

    if (distSqr <= sqrRadius)
    {
        return true;
    }

    return false;
}

public void resolveCollision(Ball ball)
{
    // get the mtd
    Vector2d delta = (position.subtract(ball.position));
    float d = delta.getLength();
    // minimum translation distance to Push balls apart after intersecting
    Vector2d mtd = delta.multiply(((getRadius() + ball.getRadius())-d)/d); 


    // resolve intersection --
    // inverse mass quantities
    float im1 = 1 / getMass(); 
    float im2 = 1 / ball.getMass();

    // Push-pull them apart based off their mass
    position = position.add(mtd.multiply(im1 / (im1 + im2)));
    ball.position = ball.position.subtract(mtd.multiply(im2 / (im1 + im2)));

    // impact speed
    Vector2d v = (this.velocity.subtract(ball.velocity));
    float vn = v.dot(mtd.normalize());

    // sphere intersecting but moving away from each other already
    if (vn > 0.0f) return;

    // collision impulse
    float i = (-(1.0f + Constants.restitution) * vn) / (im1 + im2);
    Vector2d impulse = mtd.normalize().multiply(i);

    // change in momentum
    this.velocity = this.velocity.add(impulse.multiply(im1));
    ball.velocity = ball.velocity.subtract(impulse.multiply(im2));

}

Code source: Source complète pour le collisionneur balle à balle.

Si quelqu'un a des suggestions pour améliorer ce simulateur physique de base, faites-le moi savoir! Une chose que je n'ai pas encore ajoutée, c'est angular momentum afin que les balles roulent de manière plus réaliste. D'autres suggestions? Laissez un commentaire!

261
mmcdole

Pour détecter si deux billes entrent en collision, il suffit de vérifier si la distance entre leurs centres est inférieure à deux fois le rayon. Pour faire une collision parfaitement élastique entre les balles, il suffit de se préoccuper de la composante de la vitesse qui se trouve dans la direction de la collision. L'autre composant (tangent à la collision) restera le même pour les deux balles. Vous pouvez obtenir les composants de collision en créant un vecteur unitaire pointant dans la direction d’une balle à l’autre, puis en prenant le produit scalaire avec les vecteurs vitesse des billes. Vous pouvez ensuite brancher ces composants dans une équation de collision parfaitement élastique 1D.

Wikipedia a un très bon résumé de l'ensemble du processus . Pour les billes de n'importe quelle masse, les nouvelles vitesses peuvent être calculées à l'aide des équations (où v1 et v2 sont les vitesses après la collision, et u1, u2 sont d'avant):

v_{1} = \frac{u_{1}(m_{1}-m_{2})+2m_{2}u_{2}}{m_{1}+m_{2}}

v_{2} = \frac{u_{2}(m_{2}-m_{1})+2m_{1}u_{1}}{m_{1}+m_{2}}

Si les billes ont la même masse, les vitesses sont simplement commutées. Voici un code que j'ai écrit qui fait quelque chose de similaire:

void Simulation::collide(Storage::Iterator a, Storage::Iterator b)
{
    // Check whether there actually was a collision
    if (a == b)
        return;

    Vector collision = a.position() - b.position();
    double distance = collision.length();
    if (distance == 0.0) {              // hack to avoid div by zero
        collision = Vector(1.0, 0.0);
        distance = 1.0;
    }
    if (distance > 1.0)
        return;

    // Get the components of the velocity vectors which are parallel to the collision.
    // The perpendicular component remains the same for both fish
    collision = collision / distance;
    double aci = a.velocity().dot(collision);
    double bci = b.velocity().dot(collision);

    // Solve for the new velocities using the 1-dimensional elastic collision equations.
    // Turns out it's really simple when the masses are the same.
    double acf = bci;
    double bcf = aci;

    // Replace the collision velocity components with the new ones
    a.velocity() += (acf - aci) * collision;
    b.velocity() += (bcf - bci) * collision;
}

En ce qui concerne l'efficacité, Ryan Fox a raison, vous devriez envisager de diviser la région en sections, puis de détecter les collisions dans chaque section. Gardez à l'esprit que les balles peuvent entrer en collision avec d'autres balles sur les limites d'une section, ce qui rendrait votre code bien plus compliqué. L’efficacité n’aura probablement pas d’importance tant que vous n’aurez pas plusieurs centaines de balles. Pour les points bonus, vous pouvez exécuter chaque section sur un noyau différent ou fractionner le traitement des collisions au sein de chaque section.

115
Jay Conrod

Eh bien, il y a des années, j'ai conçu le programme comme vous l'avez présenté ici.
Il y a un problème caché (ou plusieurs, selon le point de vue):

  • Si la vitesse de la balle est trop élevée, vous pouvez rater la collision.

Et aussi, presque dans 100% des cas, vos nouvelles vitesses seront fausses. Eh bien, pas vitesses, mais positions. Vous devez calculer les nouvelles vitesses avec précision au bon endroit. Sinon, il vous suffit de décaler les balles sur une petite quantité d'erreur, disponible à partir de l'étape discrète précédente.

La solution est évidente: vous devez diviser le pas de temps, vous devez donc commencer par modifier le lieu, puis entrer en collision, puis passer au reste du temps.

48
avp

Vous devez utiliser le partitionnement d’espace pour résoudre ce problème.

Lire sur partitionnement d’espace binaire et Quadtrees

20
grepsedawk

Pour clarifier la suggestion de Ryan Fox de scinder l'écran en régions et de ne rechercher que les collisions dans les régions ...

par exemple. divisez l'aire de jeu en une grille de carrés (ce qui veut dire arbitrairement une longueur d'unité par côté), et vérifiez les collisions dans chaque carré de la grille.

C'est absolument la bonne solution. Comme le soulignait une autre affiche, le seul problème est que les collisions entre frontières sont un problème.

La solution consiste à superposer une deuxième grille avec un décalage vertical et horizontal de 0,5 unité par rapport à la première.

Ensuite, toutes les collisions qui traverseraient les limites de la première grille (et ne seraient donc pas détectées) se situeraient dans les carrés de la grille de la deuxième grille. Tant que vous gardez une trace des collisions que vous avez déjà traitées (car il peut y avoir des chevauchements), vous n'avez pas à vous soucier de la gestion des cas Edge. Toutes les collisions se dérouleront dans un carré de la grille.

13
Andrew Rollings

Un bon moyen de réduire le nombre de contrôles de collision consiste à diviser l'écran en différentes sections. Vous ne comparez alors que chaque balle avec les balles de la même section.

10
Ryan Fox

Une chose que je vois ici pour optimiser.

Bien que je convienne que les balles frappées lorsque la distance est la somme de leurs rayons, il ne faut jamais calculer cette distance! Calculez plutôt son carré et travaillez de cette façon. Il n'y a aucune raison pour cette opération coûteuse de racine carrée.

De plus, une fois que vous avez trouvé une collision, vous devez continuer à évaluer les collisions jusqu'à ce qu'il n'en reste plus. Le problème est que le premier risque de causer d’autres problèmes qui doivent être résolus avant d’obtenir une image précise. Considérez ce qui se passe si la balle frappe une balle au bord? La deuxième balle frappe le bord et rebondit immédiatement dans la première balle. Si vous frappez une pile de balles dans le coin, vous risquez de rencontrer quelques collisions qui doivent être résolues avant de pouvoir itérer le cycle suivant.

En ce qui concerne le O (n ^ 2), tout ce que vous pouvez faire est de minimiser le coût de rejeter ceux qui manquent:

1) Une balle qui ne bouge pas ne peut rien toucher. S'il y a un nombre raisonnable de balles qui traînent sur le sol, cela pourrait vous épargner de nombreux tests. (Notez que vous devez toujours vérifier si quelque chose touche la balle immobile.)

2) Quelque chose d’intérêt: divisez l’écran en un certain nombre de zones mais les lignes doivent être floues - les boules situées au bord d’une zone sont répertoriées comme se trouvant dans toutes les zones pertinentes (pouvant être 4). Je voudrais utiliser une grille 4x4, stocker les zones sous forme de bits. Si un ET des zones de deux zones de boules renvoie zéro, fin du test.

3) Comme je l'ai mentionné, ne faites pas la racine carrée.

7
Loren Pechtel

J'ai trouvé une excellente page avec des informations sur la détection de collision et la réponse en 2D.

http://www.metanetsoftware.com/technique.html

Ils essaient d'expliquer comment cela se passe d'un point de vue académique. Ils commencent par la simple détection de collision d'objet à objet, puis passent à la réaction de collision et à leur mise à l'échelle.

Modifier: Lien mis à jour

6
Markus Jarderot

Vous avez deux moyens faciles de le faire. Jay a couvert la manière précise de vérifier depuis le centre de la balle.

La méthode la plus simple consiste à utiliser un rectangle de sélection, à définir une taille de 80% de la taille de la balle et vous simulerez très bien une collision.

Ajoutez une méthode à votre classe de balle:

public Rectangle getBoundingRect()
{
   int ballHeight = (int)Ball.Height * 0.80f;
   int ballWidth = (int)Ball.Width * 0.80f;
   int x = Ball.X - ballWidth / 2;
   int y = Ball.Y - ballHeight / 2;

   return new Rectangle(x,y,ballHeight,ballWidth);
}

Ensuite, dans votre boucle:

// Checks every ball against every other ball. 
// For best results, split it into quadrants like Ryan suggested. 
// I didn't do that for simplicity here.
for (int i = 0; i < balls.count; i++)
{
    Rectangle r1 = balls[i].getBoundingRect();

    for (int k = 0; k < balls.count; k++)
    {

        if (balls[i] != balls[k])
        {
            Rectangle r2 = balls[k].getBoundingRect();

            if (r1.Intersects(r2))
            {
                 // balls[i] collided with balls[k]
            }
        }
    }
}
3
FlySwat

Cette KineticModel est une implémentation de l'approche citée en Java.

3
trashgod

Je le vois insinué ici et là, mais vous pourriez aussi faire un calcul plus rapide en premier, par exemple, comparer les cadres de sélection pour le chevauchement, puis ENFUIR un chevauchement basé sur le rayon si le premier test réussit.

Le calcul mathématique addition/différence est beaucoup plus rapide pour un cadre de sélection que pour tous les trig pour le rayon, et la plupart du temps, le test du cadre de sélection exclut la possibilité d'une collision. Mais si vous testez à nouveau avec trig, vous obtenez les résultats précis que vous recherchez.

Oui, ce sont deux tests, mais ce sera plus rapide dans l'ensemble.

3
Jason Kleban

J'ai implémenté ce code en JavaScript à l'aide de l'élément HTML Canvas et il a produit de superbes simulations à 60 images par seconde. J'ai commencé la simulation avec une collection d'une dizaine de balles à des positions et vitesses aléatoires. J’ai trouvé qu’à des vitesses plus élevées, une collision entre une petite balle et une balle beaucoup plus grosse faisait apparaître la petite balle STICK au bord de la plus grosse degrés autour de la plus grosse boule avant de se séparer. (Je me demande si quelqu'un d'autre a observé ce comportement.)

Certains enregistrements des calculs ont montré que la distance de traduction minimale dans ces cas n'était pas assez grande pour empêcher les mêmes balles d'entrer en collision dans le prochain pas de temps. J'ai expérimenté et découvert que je pouvais résoudre ce problème en augmentant la valeur de la DMT en fonction des vitesses relatives:

dot_velocity = ball_1.velocity.dot(ball_2.velocity);
mtd_factor = 1. + 0.5 * Math.abs(dot_velocity * Math.sin(collision_angle));
mtd.multplyScalar(mtd_factor);

J'ai vérifié qu'avant et après ce correctif, l'énergie cinétique totale était conservée pour chaque collision. La valeur 0,5 dans le paramètre mtd_factor était approximativement la valeur minimale qui entraînait toujours la séparation des billes après une collision.

Bien que ce correctif introduise une petite quantité d’erreur dans la physique exacte du système, le compromis est qu’il est désormais possible de simuler des balles très rapides dans un navigateur sans réduire la taille du pas de temps.

2
Stefan Musarra