Im Internet gibt es zahlreiche Infos über den GJK-Algorithmus. Diese Seite hier ist hauptsächlich für diejenigen gedacht, die eine eigene Implementation anstreben.
Was ist GJK?
Mit dem GJK(Gilbert–Johnson–Keerthi)-Algorithmus kann u.a. bestimmt werden, ob sich zwei konvexe Polynome überlappen (kollidieren). Die Anwendung kann in zwei oder drei Dimensionen erfolgen.
Eine allgemeine Einführung (Minkowski-Differenz, Simplex ...) wird hier ausgelassen, da diese sehr genau und ausführlich im Video (erster Link in der obigen Liste) zu finden ist. Stattdessen werde ich mehr Beachtung der Implementation zuwenden (im Speziellen dem Tetraeder Fall).
Hinweis: Ich bin kein professioneller Programmierer. Meine Implementation ist nicht die schnellste und vielleicht auch nicht die schönste. Der Pfad der Optimierung ist lang und weit. Die hier bereitgestellten Informationen sind als ergänzende Informationen für Personen gedacht, die selber einen GJK-Algorithmus programmieren wollen (und schon Wissen haben). Einführendes Material ist in der Liste oben angeführt.
Header - Dateien:
Als erstes definieren wir zwei Klassen (shape & simplex) für die Headerdatei gjk.h
shape negate(); // x -> -x ; y -> -y ; z -> -z float dot(shape d); // inneres Produkt
shape cross(shape cpt); // Kreuzprodukt von zwei Vektoren(shapes)
void set(shape set); // Ändern der x/y/z Werte einer shape
shape rot_x(float rot_x); // rotiert einen Vektor um die x-Achse
shape rot_y(float rot_y); // rotiert einen Vektor um die y-Achse
shape rot_z(float rot_z); // rotiert einen Vektor um die z-Achse
};
Als Klassenbezeichnung wurde hier das Englische Wort für Form (shape) gewählt. Jedes Klassenobjekt besteht aus drei Werten x/y/z, was einem Vektoräquivalent in drei Dimensionen entspricht.
Es kann aber genauso ein Array aus Klassenobjekten initialisiert werden.
Die (Header)Funktionen sind:
negate() - ändert das Vorzeichen eines Klassenelementes
dot(shape d) - Skalarprodukt von zwei Vektoren (shapes)
cross(shape cpt) - Kreuzprodukt
set(shape set) - ändert die x/y/z-Werte einer Klassenelementes
rot_x/y/z(float rot_x/y/z) - rotiert einen Vektor (shape) (wird hauptsächlich zur graphischen Darstellung genutzt)
class simplex
{
public:
void add(shape simplex_add); // einen neuen Punkt in den Simplex hinzufügen
void set_zero(void); // kompletten Simplex zurücksetzen
void del(int id); // Eintrag aus dem Simplex löschen mit gegebener id (id = 0, 1, 2)
shape getLast(void); // den letzten Punkt aus dem Simplex zurückgeben
shape get(int id); // beliebigen Eintrag aus dem Simplex holen id (= 0 ... 3)
int size(void); // Anzahl der Elemente im Simplex ermitteln (0 ... 4)
Die Klasse simplex and deren Funktionen werden genutzt um die Punkte (des simplex) zu speichern und zu manipulieren. Im simplex werden maximal vier Punkte gespeichert.
GJK Funktion(en):
001
bool gjk(shape *A, shape *B, int dim_a, int dim_b);
Die Funktion gjk(): Sie übernimmt zwei Arrays aus Klassenobjekten der Klasse shape (shape *A und shape *B) und die Anzahl der x/y/z-Einträge der jeweiligen Arrays. Als Rückgabewert erfolgt true wenn die Polynome kollidieren und false falls sie dies nicht tun. Weiter unten ist ein kurzes Beispiel, wie diese Funktion aufgerufen wird.
return_shape.x = x;
return_shape.y = y * cos(rot_x * PI / 180.0) - z * sin(rot_x * PI / 180.0);
return_shape.z = y * sin(rot_x * PI / 180.0) + z * cos(rot_x * PI / 180.0);
Einen Eintrag aus der simplex-Liste entfernen (id = 1 ... 4). Die Liste wird dann von vorne nach hinten befüllt, sodass immer am Ende der Liste ein freier Platz ist.
if (id < 0 or id > 3)
{
std::cout << "cannot fetch simplex point with given index " << id << std::endl;
}
else
{
return_shape.x = x[id];
return_shape.y = y[id];
return_shape.z = z[id];
Der Aufruf der Funktion gjk() ist hier an einem Beispiel illustriert: eine Seite eines Dreiecks (ts1[]) wird mit einer Seite eines Würfels (cs1_1[]) auf Kollision geprüft.
GJK - Hauptfunktion:
Zuerst werden zwei Variablen definiert vom typ bool (check und check_failsafe). Die Variable check wird auf true oder false gesetzt, je nachdem ob die Polynome kollidieren oder nicht. Diese Variable wird auch als Rückgabewert der Funktion gjk() genutzt. Die Variable check_failsafe ist eine Sicherheitsmaßnahme, damit der Algorithmus nicht in einer Endlosschleife hängen bleibt (wenn nacheinander die selben Punkte aus der Minkowski-Differenz hinzugefügt und gelöscht werden). Üblicherweise reichen zwei oder drei Tetraeder Fälle um eine Kollision (oder das ausbleiben einer) zu ermitteln. Wird der Tetraeder Fall mehr als 10 mal durchlaufen, so kommt die Variable check_failsafe zum Einsatz und eine neue zufällige Suchrichtung wird ermittelt und der Algorithmus startet mit dieser. Die Zahl 10 ist hier komplett willkürlich gewählt.
In der ersten while-Schleife wird der geometrische Mittelpunkt beider Polynome mit der Funktion mass_middle_point() ermittelt. Dann wird der Verbindungsvektor zwischen beiden Mittelpunkten berechnet.
Daraufhin wird der simplex initialisiert (mit simplex.set_zero()) und der erste Punkt wird dem simplex hinzugefügt (simplex.add(support_function(A, B, dim_a, dim_b, d));). Danach wird die Suchrichtung negiert und die äußere While-Schleife beginnt. In dieser wird am Anfang ein neuer Punkt dem simplex hinzugefügt. Sind im simplex vier Punkte (Tetraeder Fall), so wird die Variable für den failsafe erhöht.
Ist der zuletzt hinzugefügte Punkt (in Suchrichtung) nicht über dem Nullpunkt, so finded keine Kollision zwischen den Polynomen statt und wir können die while-Schleifen beenden. check und check_failsafe werden auf false gesetzt und beide while-Schleifen beendet.
Wenn der letzte Punkt aber den Nullpunkt überschreitet, so können wir noch nichts genaues über eine Kollision sagen: Die Funktion containsOrigin(simplex, d) wird aufgerufen.
Je nachdem, wie viele Punkte im simplex sind, macht die Funktion containsOrigin(simplex, d) folgendes:
Linien-Fall: 2 Punkte befinden sich im simplex: bestimme eine neue Suchrichtung in Richtung des Nullpunktes
Dreieck-Fall: 3 Punkte im simplex: bestimme, welche Seite des Tetraeders am nähesten zum Ursprung ist. Der Punkt, welcher nicht element dieses Dreiecks ist, wird aus der simplex-Liste entfernt und das Dreieck wird wieder der while-Schleife zugeführt (Dreieck Fall -> Tetrader Fall ...).
Immer nachdem die Funktion containsOrigin(simplex, d) aufgerufen wurde, wird ein neuer Punkt der simplex-Liste hinzugefügt (in Suchrichtung) und dann wird überprüft, ob dieser Punkt in Suchrichtung den Nullpunkt überschreitet.
Die Funktion containsOrigin(simplex, d) überprüft ebenso, ob der Ursprung im Tetraeder enthalten ist (weiter unten mehr).
Diese Funktion (support_function()) nimmt zwei Arrays des Typs shape und eine Suchrichtung, und berechnet den Punkt in der Minkowski-Differenz, welcher in Suchrichtung am weitesten entfernt ist.
Die Funktion containsOrigin(simplex& simplex, shape& d):
shape ac_c_ab = ac.cross(ab); // ac_c_ab = AC x AB float v_abc = ac_c_ab.dot(a0);
shape ab_c_ad = ab.cross(ad); // ab_c_ad = AB x AD float v_abd = ab_c_ad.dot(a0);
shape ad_c_ac = ad.cross(ac); // ad_c_ac = AD x AC float v_acd = ad_c_ac.dot(a0);
int amount_neg = 0;
int amount_pos = 0;
if (v_acd > 0)
amount_pos++;
else
amount_neg++;
if (v_abd > 0)
amount_pos++;
else
amount_neg++;
if (v_abc > 0)
amount_pos++;
else
amount_neg++;
if (amount_pos == 3) // Ursprung befindet sich im Tetraeder -> Kollision
{
return true;
}
else if (amount_neg == 3) // Ursprung befindet sich im Tetraeder -> Kollision
{
return true;
}
else // Punkt entfernen
{
if (amount_neg == 2 and amount_pos == 1)
{
if (v_acd > 0)
{
simplex.del(3); // Punkt entfernen
d.set(ad_c_ac); // neue Suchrichtung speichern
}
else if (v_abd > 0)
{
simplex.del(2); // Punkt entfernen
d.set(ab_c_ad); // neue Suchrichtung speichern
}
else
{
simplex.del(1); // Punkt entfernen
d.set(ac_c_ab); // neue Suchrichtung speichern
}
}
else if (amount_neg == 1 and amount_pos == 2)
{
if (v_acd < 0)
{
ad_c_ac.negate();
simplex.del(3); // Punkt entfernen
d.set(ad_c_ac); // neue Suchrichtung speichern
}
else if (v_abd < 0)
{
ab_c_ad.negate();
simplex.del(2); // Punkt entfernen
d.set(ab_c_ad); // neue Suchrichtung speichern
}
else
{
ac_c_ab.negate();
simplex.del(1); // Punkt entfernen
d.set(ac_c_ab); // neue Suchrichtung speichern
}
}
else
{
std::cout << "error(number pos/neg)" << std::endl;
}
}
}
else if (simplex.size() == 3) // Dreieck (drei Punkte im simplex)
{
shape return_sd;
return_sd.x = 0;
return_sd.y = 0;
return_sd.z = 0;
shape b = simplex.get(1);
shape c = simplex.get(0);
Der letzte Punkt im simplex wird immer als Referenz (Koordinatensystem) genommen. Dann wird eine neue Suchrichtung bestimmt und wenn vier Punkte im simplex sind wird einer entfernt.
Auf den Linien-Fall und Dreieck-Fall wird hier nicht eingegangen, da dieser in http://mollyrocket.com/849 ausführlich erklärt wird.
Der Tetraeder Fall
Der Tetraeder Fall wird in dem obigen Video aber nicht erklärt. Wir wissen, wie der Tetraeder programmiertechnisch ermittelt wurde: Eine Grundfläche (Dreiecks-Fall), welche durch eine neue Suchrichtung (Richtung Ursprung) ein neuer Punkt hinzugefügt wurde. Der Ursprung kann nun weder im Tetraeder (Kollision!) noch unter dem Tetraeder (Suchrichtung) liegen. Somit wissen wir, welche Seiten wir überprüfen müssen (zu welcher Seite der Nullpunkt am nähesten liegt). Um dies zu ermitteln, werden die Flächennormalvektoren zu allen drei Flächen gebildet und das Skalarprodukt dieser Vektoren mit a0 wird berechnet.
Dann wird die Anzahl von positiven und negativen Skalarprodukten gezählt. Sind alle positiv oder negativ können wir auf eine Kollision schließen (der Nullpunkt liegt im Tetraeder). Wenn dies nicht der Fall ist, so entfernen wir den jeweiligen Punkt (welcher nicht Teil des Dreiecks ist, welches am nähesten zum Ursprung ist).
Bilder von den wichtigsten Fällen:
Das Bild oben zeigt den Fall, wenn der Algorithmus nach dem Linien-Fall stoppt. Rot und Grün sind die Polygone, welche auf Kollision getestet werden und Weiß die jeweiligen geometrischen Mittelpunkte. Die gelben Punkte stellen die Minkowski-Differenz dar. Der rote Punkt in der Minkowski-Differenz wird zuerst zum simplex hinzugefügt, dann der grüne. Das große grüne Rechteck rechts unten zeigt an, dass keine Kollision stattgefunden hat (Indikator).
Oben der Tetraeder Fall: Ein dritter (Blau) und vierter Punkt (Pink) wurde dem simplex hinzugefügt. Diese spannen mit den anderen zwei Punkten den Tetraeder auf. Für jedes der Dreiecke (Tetraeder Fall) ist der geometrische Mittelpunkt (Weiß) und die Seitenflächennormale eingezeichnet. Der Ursprung liegt im Tetraeder und die beiden Polynome kollidieren.
Wie im obigen Fall: Kollision mit den Flächennormalen nach außen zeigend.
Oben dargestellt der Tetraeder Fall, wo nicht direkt klar ermittelt wurde, ob eine Kollision stattfindet oder nicht. Die neue Suchrichtung (Rot) wird ermittlet, indem die Flächennormale (türkis) negiert wird.
Oben der Tetraeder Fall mit drei Iterationen (die pinken Punkte sind jeweils die Punkte aus dem Tetraeder Fall). Schön ersichtlich ist, wie sich die pinken Punkte Richtung Ursprung annähern (Suchrichtung!).