Tutorial Objekt immer um eigene Achse drehen

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Vorwort

Wie bereits am Ende des letzten Tutorials erwähnt, gibt es bei der Drehung der Objekte im 3D-Raum ein kleines Problem. Gut, im letzten Tutorial haben wir gelernt, wie wir mit Hilfe eines Vektors (ich habe ihn einfach mal Look-Vector genannt) das Objekt immer nach vorne bewegen können, egal wo es hinschaut. Wer nun dachte: Super, nun schreib ich mir mein eigenen Space-Shooter, der hat gleich gemerkt, das da etwas nicht stimmt: die Drehung des Objektes erfolgte immer um das Welt-Koordinaten-System, aber nicht um die eigenen Achsen. Und nu?!? Auch dafür gibt es eine Lösung, jedoch sollte das vorherige Tutorial und das Thema Vektor-Rotation verstanden worden sein, da dieses Tutorial darauf aufbaut.

Was wir hier lernen

Um das ganze korrekt durchführen zu können, benötigen wir 3 Vektoren (für ein lokales Koordinaten-System). Also wenden wir die Rotation gleich auf 3 Vektoren an. Außerdem benötigen wir nun eine eigene Matrix für unser Modell sowie Funktionen, dieser Matrix die entsprechenden Rotationen zuzuweisen. Somit wird hier auch kurz angerissen, wie man eine eigene Matrix an OpenGL zuweist.

Ein Kurzes Wort zu den Matrizen

Wer sich noch nicht mit Matrizen auskennt, sollte sich im Netz die entsprechenden Infos holen. Nur soviel sei hier zu diesem Thema erwähnt:

Matrizen sind halb so wild, wie sie auf den ersten Blick scheinen. Eine Matrix ist einfach Array mit 4x4 Zahlenvariablen vom Typ single. Man kann sich das wie ein kariertes Blatt Papier vorstellen, welches 4 Zeilen zu je 4 Karos hat. In der Mathematik gibt es alle möglichen Arten von Matrizen, jedoch ist diese 4x4 Matrix voll und ganz ausreichend für OpenGL. Denn in der 3D-Grafik wird nur eine 4x4 Matrix verwendet. Was diese Matrix genau macht? Sie enthält die entsprechenden Werte für Translation., Rotation, Skalierung usw. Wenn wir also glRotate aufrufen, wir der angegebene Wert (also die Rotation) auf eine OpenGL interne Matrix angewendet, welche dann später beim Darstellen der Objekte mit der Projektions-Matrix entsprechend verrechnet wird.

Soviel zum Thema Matrix. Mehr brauchen wir hier nicht zu wissen. Wer mehr wissen möchte, sollte sich im Netz einmal umschauen. Es gibt verschiedene Seiten, wo das ganze gut beschreiben ist. Oder, für die Schüler unter uns: fragt euren Mathelehrer. Der freut sich und ihr hinterlasst guten Eindruck - getreu dem Motto: wer kriecht, stolpert nicht ;o).

Die notwendigen Datentypen

Bevor wir nun wirklich loslegen, müssen wir die benötigten Datentypen festlegen. Übrigens: wie bereits im vorherigen Tutorial verwende ich auch hier die Geometry.pas von GLScene. Dort sind die Typen TMatrix4f und TVector3f sowie die mathematischen Funktionen deklariert.

var
  LocalX   : TVector3f;
  LocalY   : TVector3f;
  LocalZ   : TVector3f;          // Die 3 Vektoren, welche das lokale Koordinatensystem darstellen.
  ObjMat   : TMatrix4f;

Die RotateX, RotateY und RotateZ Variablen werden nicht mehr benötigt, da die Rotation direkt auf die Vektoren bzw. auf die Matrix angewandt wird.

Dann hätten wir noch ein paar Konstanten:

const
  ROTATE_LOCAL_X = 1;
  ROTATE_LOCAL_Y = 2;
  ROTATE_LOCAL_Z = 3;   // Stellen die Flags für die Funktion UpdateRotation dar.

  ROTATION_SPEED = 0.8;  // Die Geschwindigkeit, um welche rotiert wird.
  MOVE_SPEED     = 0.02;  // Die Geschwindigkeit der Bewegung.

Diese Variablen werden dieses mal nicht in der WinProc beim Initialisiert, sondern in der Funktion glResetData, welche wiederum in der WinProc beim Initialisieren aufgerufen wird. Dabei sind die 4 folgenden Werte wichtig:

    LocalX[0] := 1;
    LocalX[1] := 0;
    LocalX[2] := 0;

    LocalY[0] := 0;
    LocalY[1] := 1;
    LocalY[2] := 0;

    LocalZ[0] := 0;
    LocalZ[1] := 0;
    LocalZ[2] := 1;

    ObjMat := IdentityHmgMatrix;

Die Vektoren werden mit 1 in der Richtung initialisiert, für welche sie zuständig sind. Also, der Vektor für die Lokale X Koordinate hat bei X 1 und der Rest ist 0. Dadurch sind die Lokalen Koordinaten-Vektoren am Anfang noch mit dem Welt-Koordinaten-System identisch. Die Vektoren für das lokale Koordinatensystem haben demzufolge auch eine Länge von 1. Die Matrix wird mit einer Identity-Matrix initialisiert. Das entspricht dem, was ihr mit der OpenGL-Matrix macht, wenn ihr glLoadIdentity aufruft. Die Matrix wird also komplett zurückgesetzt.

Nun geht's los

Die Funktionen, welche hier nicht aufgeführt werden, sind aus dem vorherigen Tutorial unverändert übernommen worden. Wenn ihr also das ein oder andere nicht versteht, schaut im anderen Tut nach. Jetzt geht's aber wirklich los.

Schauen wir uns zuerst einmal die bisherigen UpdateRotateX, UpdateRotateY und UpdateRotateZ Prozeduren an. Hier ist nun nicht mehr die Erhöhung bzw. Verminderung der Rotate-Variablen (diese gibt's ja nicht mehr), sondern es wird die Prozedur UpdateRotation aufgerufen. Als Parameter wird die Achse, um welche rotiert wird, und der Winkel übergeben. Ich habe die Rotations-Funktionen in eine Prozedur gepackt, um das ganze übersichtlicher zu halten. Also schauen wir uns diese Funktion einmal genauer an.

Das Herz-Stück: UpdateRotation

Ich erklär das ganze einfach mal anhand der Rotation um die X-Achse. Die beiden anderen Achsen arbeiten nach dem gleichen Prinzip, deswegen brauch ich hier nicht näher darauf eingehen.

Was passiert bei einer Rotation um die X-Achse?!? Nun, wir drehen die Y und Z-Achse um die X-Achse und passen die Objekt-Matrix an. Ganz einfach, oder?!? Aber zuerst zur Rotation der Y und Z-Achse. Im vorherigen Tutorial wurde bereits erklärt, wie man einen Vektor rotiert. Allerdings wurde folgender Befehl verwendet:

  RotateVector(LookVec, RotZAxis, -DegToRad(RotateZ));
  // ...um die Y-Achse...
  RotateVector(LookVec, RotYAxis, -DegToRad(RotateY));
  // ... und um die X-Achse drehen.
  RotateVector(LookVec, RotXAxis, -DegToRad(RotateX));

Dort haben wir den Vektor also immer um die Welt-Koordinaten gedreht (diese sind durch Rot?Axis beschreiben). Nun machen wir das ganz anders. Wir drehen um den momentan aktuellen, lokalen Koordinaten Vektor:

  RotateVector(LocalY, LocalX, Ang);
  RotateVector(LocalZ, LocalX, Ang);

Hä, warum wird hier nicht DegToRad(Ang) verwendet?!? Ganz einfach: Ich brauch die Konvertierung für 3 Drehungen (2 Achsen und eine Matrix). Also wird Ang am Anfang der UpdateRotation-Prozedur in einen Rad-Wert konvertiert (Ang := DegToRad(Ang)). Ok, weiter im Text.

Der Unterschied ist also, dass wir um den aktuellen Vektor drehen, welche eine Achse des lokalen Koordinaten-Systems beschreibt. Also in diesem Fall den Y und Z Vektor um den X Vektor. Dabei spielt es keine Rolle, in welche Richtung im 3D-Raum mein X-Vektor zeigt. Es wird immer um diesen X-Vektor gedreht und somit seine aktuelle Position bei der Drehung berücksichtigt. Wenn wir um die Y-Achse drehen wollen, sieht das ganze so aus:

  RotateVector(LocalX, LocalY, Ang);
  RotateVector(LocalZ, LocalY, Ang);

Nochmal im Klartext: Es werden immer 2 Achsen um die gewünschte, dritte Achse rotiert (X & Y um Z, Y & Z um X und X & Z um Y). Dadurch dreht sich unser lokales Koordinaten-System (durch die 3 Verschiedenfarbigen Linien im Beispiel-Programm dargestellt.).

Der Clou liegt darin, dass die Rotation dann durchgeführt wird, wenn die entsprechende Taste gedrückt wurde. Stellt sich nur die Frage, warum?!?

Die Reihenfolge - ein Fall für sich...

Info DGL.png Im Tutorial_Matrix2 findet man einige hilfreiche Erklärungen samt Bildern zur Reihenfolge der Operationen.

Wenn Ihr schon mal mit glRotate rumgespielt habt, dann werdet ihr festgestellt haben, dass die Reihenfolge eine wichtige Rolle spielt. Gehen wir vom folgenden Beispiel aus:

  glLoadIdentity;
  glRotate(2, 0, 0, 1);
  glRotate(15, 0, 1, 0);
  glRotate(12, 1, 0, 0);
  DrawObjekt;

Hier wird also zuerst die OpenGL interne Matrix zurückgesetzt (glLoadIdentity) und dann um die X, die Y und zum Schluss um die Z-Achse gedreht. Somit läuft in OpenGL folgendes ab:

  • Matrix zurücksetzen.
  • 2 Grad um die Z-Achse drehen, ausgehend von den bereits gedrehten X und Y-Achsen.
  • 15 Grad um die Y-Achse, ausgehend von der bereits gedrehten X-Achse (wird auch sofort ausgeführt).
  • 12 Grad um die X-Achse drehen (wird sofort ausgeführt)

Es kommt zu dieser intern veränderten Reihenfolge, weil OpenGL intern einen Matrixstack verwendet, nach dem Prinzip "First in, last out" was soviel heißt wie was als erstes reingeht kommt als letztes raus. Wenn man also zuerst um Z drehen will muss der Aufruf als letztes kommen.

Wenn nun die Reihenfolge geändert wird, ändert sich auch das Drehverhalten. Probiert es aus, und ihr werdet feststellen, das immer 2 Achsen sich richtig verhalten, und bei der 3. Achse ein komisches etwas herauskommt nur nicht der gewünschte Effekt. Nun stellt sich die Frage: woher weiß ich, in welcher Reihenfolge ich mein Objekt jetzt im Moment drehen muss?!? Antwort: Gar nicht. Aber die Feststellung mit den 2 Achsen, welche immer Richtig sind, hilft uns weiter.

Eine Achse bleibt Fest

Und das ist die Lösung:

  1. Die Achse, um welche ich im Moment rotieren will, bleibt unberührt und Fest stehen.
  2. Die Rotation wird nicht vom Ursprung aus durchgeführt, sondern von der aktuellen Position der Achsen. (Wenn also die Aktuelle Position 12 Grad gedreht ist, und ich eine Endposition von 13 Grad Drehung will, wird nicht alles zurückgesetzt und um 13 Grad sondern von den 12 aus um 1 Grad gedreht, womit ich wieder bei den 13 wäre).

Und voilá: es klappt.

Uff, war das ‚ne Menge verwirrendes Zeugs. Lest es euch noch mal in ruhe durch. Das sollte soweit verstanden sein. Kleiner Tipp: Verwendet für die 3 Achsen einfach mal den Daumen, Zeigefinger und Mittelfinger. Richtet diese so aus, das einer nach oben, einer nach rechts und einer zu Euch hin zeigt. Nun dreht um EINEN beliebigen Finger. Nachdem ihr gedreht habt' dreht um einen anderen Beliebigen Finger. Somit könnt ihr deutlich das Verhalten der 3 Lokalen Achsen Vektoren an euren Fingern sehen. Es andern immer nur 2 Finger ihre Position und das "Koordinatensystem" bleibt in seiner eigentlichen Form erhalten! Passt aber auf, dass Ihr Euch keinen Knoten in den Arm macht ;o)

Schön und gut, aber das Objekt?!?

Ok, nun wissen wir, wie wir das lokale Koordinaten-System dreht. Aber was bringt uns das? Gleich mal vorweg: diese 3 Vektoren werden NIE auf der Ausgabe dargestellt (außer im Programm zu diesem Tutorial, um es sichtbar zu machen). Sie sind nur notwenig, um das Objekt ausrichten zu können. Das wird dann folgendermaßen gemacht:

Wir benötigen eine Rotations-Matrix. Ist halb so wild, denn wir brauchen NICHT zu wissen, wie man die genau erstellt oder wie sie aussieht. Wie auch immer, diese Matrix erhalten wir folgendermaßen:

Var
  RotMat : Tmatrix4f;  // Die Rotations-Matrix.

  RotMat := CreateRotationMatrix(LocalX, Ang);

Auch hier gilt wieder : nur die Winkel-Veränderung wird in Ang angegeben, NICHT die totale Gradzahl. Eben wie bei den Vektoren. Nun haben wir also eine Matrix, welche die Rotation beschreibt. Diese Matrix wird übrigens auch in OpenGL beim verwenden von glRotate erstellt und mit der Model-View Matrix multipliziert. Und was machen wir damit?!? Wir multiplizieren sie mit unserer Objekt Matrix:

ObjMat := MatrixMultiply(ObjMat, RotMat);

Dabei wird, ähnlich wie bei den Vektoren, der Aktuelle Zustand der Matrix berücksichtigt, also immer von der aktuellen "Verdrehung" aus gedreht. Aus diesem Grund wird ObjMat auch als erster Parameter von MatrixMultiply übergeben. Diese wird dann eben mit RotMat multipliziert und wieder an ObjMat zugewiesen. Hier gehen wir nun von einer Drehung um die Lokale X-Achse aus. Bei der Y Achse wird dann einfach als Parameter von CreateRotationMatrix LocalY und bei Z LocalZ übergeben. Einfach, oder?!?

Eine eigene Matrix an OpenGL zuweisen

Hmm, spitze. Jetzt haben wir eine eigene Matrix. Und nu?!? Nach allem was wir bisher gelernt haben, müssen wir nun diese Matrix mit der Model-View Matrix von OpenGL multiplizieren. Aber woher bekomme ich diese Matrix?

Nun, wir benötigen sie nicht. OpenGL hat eine nette Lösung für unser Problem:

glMultMatrix(@ObjMat);

Diese Prozedur benötigt den Zeiger auf unsere Matrix, liest diese aus, multipliziert sie mit der aktuellen View-Matrix und Fertig. Unsere Zeichen-Prozedur (glDraw) sieht dann also so aus:

  // Den Ausgabe-Bereich und T-Buffer leeren.
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
  //Einheitsmatrix wiederherstellen.
  glLoadIdentity;
  // Das Objekt positionieren.
  glTranslate( PosVect[0],  PosVect[1],  PosVect[2]);
  // Das Koordinatensystemzeichnen.
  DrawCoordSys;
  // Die Matrix für die Korrekte Positionierung des Objektes setzen.
  glMultMatrixf(@ObjMat);
  //Das Objekt und das lokale Koordinaten-System zeichnen.
  DrawTheObject;

Zuerst wird der Back- und Z-Buffer gelehrt. Dann wird die Modell-View mit der Einheitsmatrix initialisiert, das eine Verschiebung anhand des Positions-Vektors durchgeführt (siehe vorheriges Tut), zur Orientierung das Lokale Koordinaten-System dargestellt, die Objekt-Matrix zugewiesen und das Objekt gezeichnet. Das war's!!!

Der Positions-Vektor berechnet sich hier übrigens nicht aus dem Look-Vector (den gibt es ja nicht mehr), sondern aus einem der Lokalen Achsen Vektoren. Ich ab' hier einfach den Y-Vektor verwendet, damit das Dreieck sich immer zu seiner Spitze bewegt. Es kann aber auch z.B. der Y-Vektor für eine seitliche Bewegung verwendet werden. Denkbar wäre auch die Kombination von verschiedenen Vektoren (X & Z oder so). Spielt einfach mal damit rum und Testet aus.

Zeit für Optimierung

Ok, das ganze läuft soweit. Jetzt kann man endlich seinen eigenen Space-Shooter entwickeln. Aber Moment, im ganzen Quellcode von diesen Tutorial ist nicht einmal RotateVector zu finden. Nun, der Grund ist ganz einfach: Ich habe etwas Optimiert. Schauen wir uns einmal an, was die RotateVector Funktion der Geometry.pas eigentlich macht:

var
   rotMatrix : TMatrix4f;
begin
   rotMatrix:=CreateRotationMatrix(axis, Angle);
   vector:=VectorTransform(vector, rotMatrix);
end;

Es wird also zuerst eine Rotations-Matrix um die übergebene Achse erstellt und dann wird der zu rotierende Vektor anhand dieser Matrix transformiert. Bei einer Drehung (um die X-Achse) machen wir folgendes:

  RotateVector(LocalY, LocalX, Ang);
  RotateVector(LocalZ, LocalX, Ang);

  RotMat := CreateRotationMatrix(LocalX, Ang);
  ObjMat := MatrixMultiply(ObjMat, RotMat) ;

Somit wird hier erst eine Rotations-Matrix um die X-Achse für die erste Vektor-Rotation erstellt, dann noch mal eine für die zweite Vektor-Rotation und zum Schluss noch eine für die Anpassung der Matrix. DAS kann man definitiv optimieren, zumal die Rotations-Matrizen ja immer die Selben sind. Außerdem liefert RotateVector einen Vektor mit 4 Werten zurück. Folglich muss entsprechend zwischen 4-Wertigen und 3-Wertigen Vektoren konvertiert werden. Wenn Ihr die letzten beiden Sätze nicht verstanden habt, dann vergesst sie einfach ;o) Nun optimieren wir das ganze mal:

  RotMat := CreateRotationMatrix(LocalX, Ang);

  LocalY := VectorTransform(LocalY, RotMat) ;
  LocalZ := VectorTransform(LocalZ, RotMat) ;
  ObjMat := MatrixMultiply(ObjMat, RotMat) ;

Somit wird nur EINMAL eine Rotations-Matrix um die X-Achse erstellt und diese dann mit den beiden anderen Vektoren und der Objekt-Matrix verrechnet. Wir sparen uns also pro Drehung um eine Achse 2 Matrix-Erstellungen. Super, so leicht optimiert man. Wenn's doch nur immer so leicht wäre...

Die Vektoren ausrichten

Am Ende der UpdateRotation ist noch folgender Code zu sehen:

  NormalizeVector(LocalX);
  LocalY := VectorCrossProduct(LocalZ, LocalX);
  NormalizeVector(LocalY);
  LocalZ := VectorCrossProduct(LocalX, LocalY);
  NormalizeVector(LocalZ);

Hä?!? Was soll denn nun das?!? Ganz einfach: Mit NormalizeVector wird ein Vektor normalisiert und somit auf eine Länge von 1 gesetzt. NICHT auf eine Position von 1. Der X-Vektor hat nachher nicht 1, 0, 0 sondern nur eine Länge von 1. Seine Position im 3D-Raum wird NICHT verändert. Das Kreuzprodukt zweier Vektoren (VectorCrossProdukt) liefert einen 3. Vektor, welcher im 90 Grad Winkel zu den beiden anderen Steht, was bei einem Koordinaten-System in OpenGL ja der Fall sein sollte. Also wird folgendes gemacht:

LocalX wird eine Länge von 1 gesetzt. Danach wird das Kreuzprodukt zwischen LocalX und LocalZ erstellt, welches uns LocalY zurückliefert. LocalY steht nun 90 Grad zu LocalX und LocalZ. Nun wird LocalY auf die Länge von 1 gebracht das Kreuzprodukt zwischen LocalX und LocalY ermittelt. Somit haben wir LocalZ (wieder rechtwinklig zu den anderen beiden). Jetzt nur noch LocalZ auf eine Länge von 1 bringen und... fertig!

Na toll, und wozu das ganze?!? Stellt Euch vor, Ihr spiel einen Space-Shoter (oder sonst was) eine Stunde lang. Das Haupt-Objekt ist nonstop in Bewegung. Durch Rundungsfehler kann es dann sein, dass das Lokale Koordinatensystem nicht mehr im rechten Winkel zueinander steht bzw. irgendeine Achse länger oder Kürzer als 1 ist. Dadurch könnte es zu Fehlern in der Bewegung und Positionierung kommen. Durch den oberen Befehlsblock werden die Achsen nach jeder Drehung zueinander ausgerichtet und somit die Rechenfehler eliminiert. An dieser Stelle besten Dank an Stefan Zerbst, der mir den entsprechenden Hinweis gegeben hat! Ihm hab' ich auch zu verdanken, dass ich auf die Lösung der Objekt-Rotation um die eigenen Achsen gekommen bin und somit auch dieses Tutorial entstanden ist!

Phu, geschafft. Oder?

Ja, das Tutorial ist somit am Ende. Eines sei aber noch erwähnt: Es ist nicht immer notwendig, um alle Achsen zu drehen. Das hängt sehr stark vom verwendeten Objekt ab. Ein Mensch z.B. Dreht sich im Normalfall nicht um seine Z und X-Achse. Es sei denn, er schwimmt oder so. Klar werden die Spielfiguren anders dargestellt. Ich wollte Euch nur darauf hinweisen, dass ihr Euch überlegen müsst, um welche Achsen sich ein Objekt drehen soll und auch nur diese Achsen sind dann notwendig.

Das war's aber nun wirklich. Probiert einfach das ein oder andere aus, das hilft einem, die Materie besser zu verstehen. Und wie auch beim vorherigen Tutorial: liefert DGL ne menge Feedback!!!

Have a lot of fun

Euer

SchodMC

Anhang

Hier findet ihre den Sourcecode für dieses Tutorial: tut_objrot_src_api File.jpg



Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.