Comment créer un moteur physique 2D personnalisé Corps rigides orientés

Jusqu'à présent, nous avons couvert la résolution des impulsions, l'architecture de base et les frictions. Dans ce dernier tutoriel de cette série, nous aborderons un sujet très intéressant: l’orientation.

Dans cet article, nous aborderons les sujets suivants:

  • Rotation math
  • Formes orientées
  • Détection de collision
  • Résolution de collision

J'ai fortement recommandé de lire les trois articles précédents de la série avant de tenter de s'attaquer à celui-ci. Une grande partie des informations clés dans les articles précédents sont des conditions préalables au reste de cet article..


Exemple de code

J'ai créé un petit exemple de moteur en C ++, et je vous recommande de parcourir le code source tout au long de la lecture de cet article, car de nombreux détails de mise en œuvre pratiques ne pourraient pas s'intégrer à l'article lui-même..


Ce référentiel GitHub contient l'exemple de moteur, ainsi qu'un projet Visual Studio 2010. GitHub vous permet de visualiser la source sans avoir à télécharger la source elle-même, pour plus de commodité..

Articles Similaires
  • Philip Diffenderfer a créé le repo pour créer une version Java du moteur.!

Math Orientation

Les maths impliquant des rotations en 2D sont assez simples, bien qu'il soit nécessaire de maîtriser le sujet pour créer quelque chose de précieux dans un moteur physique. La deuxième loi de Newton stipule:

\ [Équation \: 1: \\
F = ma \]

Une équation similaire concerne spécifiquement la force angulaire et l'accélération angulaire. Cependant, avant de pouvoir afficher ces équations, une description rapide du produit croisé en 2D est nécessaire..

Produit croisé

Le produit croisé en 3D est une opération bien connue. Cependant, le produit croisé en 2D peut être assez déroutant, car il n’ya pas vraiment d’interprétation géométrique solide.

Le produit vectoriel 2D, contrairement à la version 3D, ne renvoie pas un vecteur mais un scalaire. Cette valeur scalaire représente en réalité la magnitude du vecteur orthogonal le long de l'axe des z, si le produit croisé devait effectivement être réalisé en 3D. En un sens, le produit vectoriel 2D n’est qu’une version simplifiée du produit 3D, car il s’agit d’une extension du calcul vectoriel 3D..

Si cela prête à confusion, ne vous inquiétez pas: une compréhension approfondie du produit en croix 2D n'est pas tout à fait nécessaire. Il suffit de savoir exactement comment effectuer l'opération et de savoir que l'ordre des opérations est important: \ (a \ times b \) n'est pas identique à \ (b \ times a \). Cet article fera un usage intensif du produit croisé afin de transformer la vitesse angulaire en vitesse linéaire.

Connaissance Comment effectuer le produit croisé en 2D est cependant très important. Deux vecteurs peuvent être croisés, un scalaire avec un vecteur et un vecteur avec un scalaire. Voici les opérations:

 // Deux vecteurs croisés renvoient un CrossProduct flottant scalaire (const Vec2 & a, const Vec2 & b) return a.x * b.y - a.y * b.x;  // Formes plus exotiques (mais nécessaires) du produit croisé // avec un vecteur a et des scalaires, renvoyant tous les deux un vecteur Vec2 CrossProduct (const Vec2 & a, float s) return Vec2 (s * ay, -s * ax )  Vec2 CrossProduct (float s, const Vec2 & a) return Vec2 (-s * a.y, s * a.x); 

Couple et vitesse angulaire

Comme nous devrions tous le savoir dans les articles précédents, cette équation représente une relation entre la force agissant sur un corps avec la masse de ce corps et son accélération. Il y a un analogue pour la rotation:

\ [Équation \: 2: \\
T = r \: \ times \: \ omega \]

\ (T \) représente couple. Le couple est la force de rotation.

\ (r \) est un vecteur du centre de masse (COM) à un point particulier d’un objet. \ (r \) peut être considéré comme faisant référence à un "rayon" de COM à un point. Chaque point unique sur un objet nécessitera une valeur \ (r \) différente pour être représenté dans l'équation 2.

\ (\ oméga \) est appelé "oméga" et fait référence à la vitesse de rotation. Cette relation sera utilisée pour intégrer la vitesse angulaire d'un corps rigide.

Il est important de comprendre que la vitesse linéaire est la vitesse de la COM d'un corps rigide. Dans l'article précédent, tous les objets n'avaient pas de composantes de rotation, donc la vitesse linéaire du COM était la même vitesse pour tous les points d'un corps. Lorsque l'orientation est introduite, les points les plus éloignés du COM pivotent plus rapidement que ceux situés à proximité du COM. Cela signifie que nous avons besoin d'une nouvelle équation pour trouver la vitesse d'un point sur un corps, car les corps peuvent maintenant tourner et se traduire en même temps..

Utilisez l'équation suivante pour comprendre la relation entre un point sur un corps et la vitesse de ce point:

\ [Équation \: 3: \\
\ omega = r \: \ times v \]

\ (v \) représente la vitesse linéaire. Pour transformer une vitesse linéaire en vitesse angulaire, croisez le rayon \ (r \) avec \ (v \).

De même, nous pouvons réorganiser l'équation 3 pour former une autre version:

\ [Équation \: 4: \\
v = \ omega \: \ times r \]

Les équations de la dernière section ne sont assez puissantes que si les corps rigides ont une densité uniforme. La densité non uniforme rend les calculs mathématiques impliqués dans le calcul de tout ce qui nécessite la rotation et le comportement d'un corps rigide beaucoup trop compliqué. De plus, si le point représentant un corps rigide n’est pas au niveau du COM, alors les calculs concernant \ (r \) vont être totalement aléatoires..

Inertie

En deux dimensions, un objet tourne autour de l’axe z imaginaire. Cette rotation peut être assez difficile en fonction de la masse d'un objet et de son éloignement du COM. Un cercle de masse égale à une tige longue et fine sera plus facile à faire pivoter que la tige. Ce facteur de "difficulté à faire pivoter" peut être considéré comme le moment d'inertie d'un objet.

En un sens, l'inertie est la masse en rotation d'un objet. Plus l'inertie est grande, plus il est difficile de la faire tourner..

Sachant cela, on pourrait stocker l'inertie d'un objet dans le corps sous le même format que la masse. Il serait judicieux de stocker également l'inverse de cette valeur d'inertie, en prenant soin de ne pas effectuer de division par zéro. Veuillez consulter les articles précédents de cette série pour plus d’informations sur la masse et la masse inverse..

L'intégration

Chaque corps rigide nécessitera quelques champs supplémentaires pour stocker les informations de rotation. Voici un exemple rapide de structure contenant des données supplémentaires:

 struct RigidBody Forme * Forme // Composants linéaires Position Vec2 Accélération du flottement de la vitesse Vec2 // Orientation du flottement des composants angulaires // Couple radian du flotteur angulaire du radians;

L'intégration de la vitesse angulaire et de l'orientation d'un corps est très similaire à l'intégration de la vitesse et de l'accélération. Voici un exemple de code rapide pour montrer comment faire (note: les détails sur l'intégration ont été abordés dans un article précédent):

 const Vec2 gravité (0, -10.0f) vitesse + = force * (1.0f / masse + gravité) * dt angularVelocity + = couple * (1.0f / momentOfInertia) * dt position + = vitesse * dt orient + = angularVelocity * dt

Avec la petite quantité d’informations présentée jusqu’à présent, vous devriez pouvoir commencer à faire pivoter diverses choses à l’écran sans problème. En quelques lignes de code, il est possible de créer quelque chose d'assez impressionnant, peut-être en projetant une forme dans l'air pendant qu'elle tourne autour du COM, la gravité le tirant vers le bas pour former un trajet arqué..

Mat22

L'orientation doit être stockée sous la forme d'une seule valeur en radians, comme indiqué ci-dessus, bien que souvent, l'utilisation d'une matrice de rotation réduite puisse constituer un bien meilleur choix pour certaines formes..

Un bon exemple est la boîte à bornes orientées (OBB). L'OBB se compose d'une largeur et d'une hauteur, qui peuvent toutes deux être représentées par des vecteurs. On peut ensuite faire pivoter ces deux vecteurs d’extension par une matrice de rotation deux sur deux pour représenter les axes d’un OBB..

Je suggère la création d'un Mat22 classe matricielle à ajouter à la bibliothèque mathématique que vous utilisez. J'utilise moi-même une petite bibliothèque mathématique personnalisée, intégrée à la démo open source. Voici un exemple de ce à quoi un tel objet peut ressembler:

 struct Mat22 union struct float m00, m01 flottant m10, m11; ; struct Vec2 xCol; Vec2 yCol; ; ; ;

Quelques opérations utiles incluent: construction à partir d’angle, construction à partir de vecteurs colonne, transposer, multiplier par Vec2, multiplier par un autre Mat22, valeur absolue.

La dernière fonction utile est de pouvoir récupérer soit le X ou y colonne d'un vecteur. La fonction de colonne ressemblerait à quelque chose comme:

 Mat22 m (PI / 2.0f); Vec2 r = m.ColX (); // récupère la colonne d'axe x

Cette technique est utile pour récupérer un vecteur unitaire le long d’un axe de rotation, soit le X ou y axe. De plus, une matrice deux sur deux peut être construite à partir de deux vecteurs unitaires orthogonaux, chaque vecteur pouvant être directement inséré dans les lignes. Bien que cette méthode de construction soit un peu inhabituelle pour les moteurs physiques 2D, il peut être très utile de comprendre le fonctionnement des rotations et des matrices en général..

Ce constructeur pourrait ressembler à quelque chose comme:

 Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) m00 = x.x; m01 = x.y; m01 = y.x; m11 = y.y;  // ou Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) xCol = x; yCol = y; 

Comme l'opération la plus importante d'une matrice de rotation consiste à effectuer des rotations basées sur un angle, il est important de pouvoir construire une matrice à partir d'un angle et multiplier un vecteur par cette matrice (pour faire pivoter le vecteur dans le sens anti-horaire de l'angle la matrice a été construite avec):

 Mat2 (radians réels) cd réels = std :: cos (radians); real s = std :: sin (radians); m00 = c; m01 = -s; m10 = s; m11 = c;  // Faire pivoter un vecteur const Opérateur Vec2 * (const Vec2 & rhs) const return Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

Par souci de brièveté, je ne vais pas expliquer pourquoi la matrice de rotation dans le sens anti-horaire est de la forme:

 a = angle cos (a), -sin (a) sin (a), cos (a)

Cependant, il est important de savoir à tout le moins qu'il s'agit de la forme de la matrice de rotation. Pour plus d'informations sur les matrices de rotation, veuillez consulter la page Wikipedia..

Articles Similaires
  • Construisons un moteur graphique 3D: Transformations linéaires

Transformer en base

Il est important de comprendre la différence entre l’espace modèle et l’espace mondial. L'espace modèle est le système de coordonnées local à une forme physique. L'origine est au niveau du COM et l'orientation du système de coordonnées est alignée sur les axes de la forme elle-même..

Pour transformer une forme en espace mondial, il faut la faire pivoter et la traduire. La rotation doit avoir lieu en premier, car la rotation est toujours effectuée à propos de l'origine. Puisque l'objet est dans l'espace modèle (origine au niveau du COM), la rotation tourne autour du COM de la forme. La rotation se produirait avec un Mat22 matrice. Dans l'exemple de code, les matrices d'orientation sont du nom vous.

Une fois la rotation effectuée, l’objet peut être converti en position dans le monde par addition vectorielle..

Une fois qu'un objet est dans l'espace mondial, il peut ensuite être traduit dans l'espace modèle d'un objet totalement différent à l'aide de transformations inverses. Une rotation inverse suivie d'une translation inverse est utilisée pour le faire. C'est la quantité de mathématiques simplifiée lors de la détection de collision!

Transformation inverse (de gauche à droite) de l'espace mondial vers l'espace modèle du polygone rouge.

Comme indiqué dans l'image ci-dessus, si la transformation inverse de l'objet rouge est appliquée aux polygones rouge et bleu, un test de détection de collision peut alors être réduit à la forme d'un test AABB vs OBB, au lieu de calculer des calculs complexes entre deux formes orientées.

Dans la plupart des exemples de code source, les sommets sont constamment transformés de modèle en monde, puis de nouveau en modèle, pour toutes sortes de raisons. Vous devez avoir une idée claire de ce que cela signifie pour comprendre l'exemple de code de détection de collision..


Détection de collisions et génération de manifold

Dans cette section, je présenterai les grandes lignes des collisions de polygones et de cercles. Veuillez consulter l'exemple de code source pour plus de détails sur l'implémentation..

Polygone à polygone

Commençons par la routine de détection de collision la plus complexe de toute cette série d'articles. L'idée de vérifier la collision entre deux polygones est préférable (à mon avis) avec le théorème d'axe de séparation (SAT).

Cependant, au lieu de projeter les étendues de chaque polygone les unes sur les autres, il existe une méthode légèrement plus récente et plus efficace, décrite par Dirk Gregorius dans sa conférence GDC 2013 (diapositives disponibles ici gratuitement)..

La première chose à apprendre est le concept de points d'appui.

Points de support

Le point de support d'un polygone est le sommet le plus éloigné dans une direction donnée. Si deux sommets ont des distances égales dans la direction donnée, l'un ou l'autre est acceptable.

Pour calculer un point d'appui, le produit scalaire doit être utilisé pour trouver une distance signée dans une direction donnée. Comme cela est très simple, je vais vous montrer un exemple rapide dans cet article:

 // Le point extrême le long d'une direction dans un polygone Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; pour (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > meilleure projection) bestVertex = v; bestProjection = projection;  return bestVertex; 

Le produit scalaire est utilisé sur chaque sommet. Le produit scalaire représente une distance signée dans une direction donnée, ainsi le sommet avec la distance projetée la plus grande serait le sommet à renvoyer. Cette opération est effectuée dans l'espace modèle du polygone donné dans le moteur de l'exemple..

Trouver l'axe de séparation

En utilisant le concept de points d'appui, une recherche de l'axe de séparation peut être effectuée entre deux polygones (polygone A et polygone B). L'idée de cette recherche est de parcourir en boucle toutes les faces du polygone A et de trouver le point de support dans la normale négative à cette face..

Dans l'image ci-dessus, deux points d'appui sont indiqués: un sur chaque objet. La normale bleue correspondrait au point de support sur l'autre polygone au sommet le plus éloigné dans la direction opposée à la normale bleue. De même, la normale rouge serait utilisée pour trouver le point de support situé au bout de la flèche rouge.

La distance entre chaque point d'appui et le visage actuel serait la pénétration signée. En enregistrant la plus grande distance, un axe de pénétration minimal possible peut être enregistré.

Voici un exemple de fonction de l’exemple de code source qui recherche l’axe de pénétration minimal possible à l’aide du Obtenir de l'aide une fonction:

 real FindAxisLeastPenetration (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; pour (uint32 i = 0; i < A->m_vertexCount; ++ i) // Récupère une face normale de A Vec2 n = A-> m_normals [i]; // Récupère le point d'appui de B le long de -n Vec2 s = B-> GetSupport (-n); // Récupère le sommet sur la face de A, transforme en l'espace de modélisation de // B V2 v = A-> m_vertices [i]; // Calculer la distance de pénétration (dans l'espace modèle de B) real d = Point (n, s - v); // Stocke la plus grande distance si (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; return bestDistance; 

Puisque cette fonction renvoie la pénétration la plus grande, si cette pénétration est positive, cela signifie que les deux formes ne se chevauchent pas (une pénétration négative signifierait qu'il n'y a pas d'axe de séparation)..

Cette fonction devra être appelée deux fois, en retournant les objets A et B à chaque appel..

Incident de découpage et face de référence

À partir de là, l'incident et la face de référence doivent être identifiés et le visage de l'incident doit être clipsé contre les plans latéraux de la face de référence. Il s'agit d'une opération peu banale, bien qu'Erin Catto (créateur de Box2D et de toute la physique actuellement utilisée par Blizzard) ait créé de superbes diapositives couvrant ce sujet en détail..

Cette coupure générera deux points de contact potentiels. Tous les points de contact derrière la face de référence peuvent être considérés comme des points de contact.

Au-delà des diapositives d'Erin Catto, l'exemple de moteur comprend également les routines de découpage..

Cercle à polygone

La routine de collision cercle / polygone est un peu plus simple que la détection de collision polygone contre polygone. Tout d'abord, la face la plus proche du polygone par rapport au centre du cercle est calculée de la même manière que pour l'utilisation des points d'appui de la section précédente: en passant en boucle sur chaque face normale du polygone et en recherchant la distance entre le centre du cercle et la face.

Si le centre du cercle est derrière la face la plus proche, des informations de contact spécifiques peuvent être générées et la routine peut se terminer immédiatement..

Une fois la face la plus proche identifiée, le test se transforme en un test de segment de droite par rapport à un cercle. Un segment de ligne a trois régions intéressantes appelées Régions de Voronoï. Examinez le diagramme suivant:

Régions de Voronoï d'un segment de droite.

Intuitivement, en fonction de l'emplacement du centre du cercle, différentes informations de contact peuvent être dérivées. Imaginez que le centre du cercle soit situé sur l'une ou l'autre des régions du sommet. Cela signifie que le point le plus proche du centre du cercle sera un sommet d'arête et que la normale de collision appropriée sera un vecteur de ce sommet au centre du cercle..

Si le cercle est dans la région de la face, le point le plus proche du segment par rapport au centre du cercle sera le projet du centre du cercle sur le segment. La collision normale ne sera que le visage normal.

Pour calculer la région de Voronoï dans laquelle se trouve le cercle, nous utilisons le produit scalaire entre deux sommets. L'idée est de créer un triangle imaginaire et de vérifier si l'angle du coin construit avec le sommet du segment est supérieur ou inférieur à 90 degrés. Un triangle est créé pour chaque sommet du segment..

Projection du vecteur d'un sommet au centre du cercle sur le bord.

Une valeur supérieure à 90 degrés signifie qu'une région de bord a été identifiée. Si aucun des angles de sommet de bord du triangle ne dépasse 90 degrés, le centre du cercle doit alors être projeté sur le segment lui-même pour générer de nombreuses informations. Comme le montre l'image ci-dessus, si le vecteur du sommet du bord au centre du cercle parsemé du vecteur du bord lui-même est négatif, la région de Voronoï dans laquelle se trouve le cercle est connue..

Heureusement, le produit scalaire peut être utilisé pour calculer une projection signée. Ce signe sera négatif si supérieur à 90 degrés et positif si inférieur.


Résolution de collision

C'est à nouveau ce moment-là: nous allons revenir à notre code de résolution d'impulsion pour une troisième et dernière fois. A présent, vous devriez être parfaitement à l'aise pour écrire leur propre code de résolution qui calcule les impulsions de résolution, ainsi que les impulsions de friction, et peut également effectuer une projection linéaire pour résoudre le problème de la pénétration restante..

Des composants de rotation doivent être ajoutés à la résolution de friction et de pénétration. Une certaine énergie sera placée dans la vitesse angulaire.

Voici notre résolution d'impulsion, comme nous l'avons laissé dans le précédent article sur le frottement:

\ [Équation 5: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 masse ^ A + \ frac 1 masse ^ B
\]

Si nous ajoutons des composants de rotation, l'équation finale ressemble à ceci:

\ [Équation 6: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 masse ^ A + \ frac 1 masse ^ B + \ frac (r ^ A \ fois t) ^ 2 I ^ A + \ frac (r ^ B \ fois t) ^ 2 I ^ B
\]

Dans l'équation ci-dessus, \ (r \) est encore un "rayon", comme dans un vecteur du COM d'un objet au point de contact. Une dérivation plus détaillée de cette équation est disponible sur le site de Chris Hecker..

Il est important de réaliser que la vitesse d'un point donné sur un objet est:

\ [Équation 7: \\
V '= V + \ omega \ times r
\]

L'application d'impulsions change légèrement afin de tenir compte des termes de rotation:

 void Body :: ApplyImpulse (const Vec2 & impulse, const Vec2 & contactVector) vélocité + = 1.0f / masse * impulse; angularVelocity + = 1.0f / inertia * Cross (contactVector, impulsion); 

Conclusion

Ceci conclut le dernier article de cette série. A ce jour, de nombreux sujets ont été couverts, y compris la résolution basée sur les impulsions, la génération multiple, la friction et l'orientation, le tout dans deux dimensions..

Si vous avez atteint ce stade, je dois vous féliciter! La programmation par moteur physique pour les jeux est un domaine d'étude extrêmement difficile. Je souhaite bonne chance à tous les lecteurs et n'hésitez pas à commenter ou à poser des questions ci-dessous..