Tutorial OpenGL3 Lineare Algebra

Aus DGL Wiki
Version vom 7. August 2009, 20:16 Uhr von TAK2004 (Diskussion | Beiträge) (Konstruieren von Matrizen)

Wechseln zu: Navigation, Suche

Vorwort

Lineare Algebra ist ein Teilgebiet der Mathematik und beschäftigt sich mit Vektorräumen. Die für OpenGL wichtigen Unterbereiche sind Vektoren und Matrizen. Der größte Teil der 3D Programmierung beschäftigt sich mit Linearer Algebra, daher sollte auch diese Grundlage eine besondere Aufmerksamkeit gewidmet werden. Sollte der Inhalt vieleicht zu viel für einmal sein, dann wäre es ratsam in mehreren Etappen zu bewältigen aber es sollte auf jedenfall vollständig verstanden werden, bevor man sich ernsthaft mit OpenGL auseinander setzen will.

Trigonometrie

Da später in der Linearen Algebra auf die Trigonometrie zurück gegriffen werden wird, soll als erstes die notwendigen Grundlagen in diesem Bereich beleuchtet werden.

Bogenmaß und Gradmaß

Man unterscheidet bei der Darstellung eines Winkels zwischen Bogenmaß(rad) und Gradmaß(deg). Das Bogenmaß wird durch die Konstante Pi beschrieben, wobei der Wertebereich von 0 bis 2*Pi geht. Das Gradmaß ist eine Einteilung, welche von 0 bis 360° abgebildet wird. 0° sind 0, 90° sind 0.5*Pi, 180° sind Pi, 270° sind 2/3*Pi und 360° sind 2*Pi oder auch 0° und 0.

Um vom Bogenmaß in Gradmaß um zu rechnen, kann man folgende Formel verwenden.

Tutorial Lineare Algebra rad2deg.png

Für die Umwandlung von Bogenmaß in Gradmaß gilt diese Formel.

Tutorial Lineare Algebra deg2rad.png

Trigonometrische Funktionen

Für das sinnvolle arbeiten mit Winkelfunktionen benötigen wir einen Einheitskreis. Dieser ist ein Kreis, dessen Radius 1 ist und somit eine Reihe von Funktionen zu lässt.

einheitsvektor.png

Der Einheitskreis ist wie folgt Beschriftet. In Blau sind die Bogenmaß Werte angegeben, in dunkelgrün die äquivalenten Werte der Kosinusfunktion und Orang ist der Winkel. Wenn man den Kosinus und Sinus von dem Winkel errechnet, dann erhält man den Hellgrünen x und Roten y Wert. Egal welchen Winkel man in der Sinus- und Kosinus-Funktion einsetzt, der Wert wird nie größer 1 oder kleiner -1 werden. Aber halt, wieso

Trigonometrie im allgemeinen Dreieck

Man unterscheidet in der Trigonometrie zwischen rechtwinkligen Dreiecken und allgemeinen Dreiecken. Die allgemeinen Dreiecke sind allerdings für den weiteren Verlauf des Artikels wichtig und werden deswegen behandelt.

Sinus- und Kosinussatz

Ein wichtige Gleichung, welche später wieder aufgegriffen werden wird, ist der Kosinutzsatz. Doch zuvor sollte der Sinussatz genauer betrachtet werden.

sinussatz.png

Durch das Umstellen der Gleichungen kann man die einzelnen Winkel oder Seiten, eines Dreiecks, erhalten. Hierzu werden entweder 2 Seiten und ein Winkel oder 2 Winkel und eine Seite benötigt.

Der Kosinussatz

kosinussatz.png

ermöglicht es, entweder aus drei gegebenen Seiten die Winkel auszurechnen oder aus zwei Seiten und ihrem Zwischenwinkel die gegenüber liegende Seite.

Eigenschaften und Formeln

Es kann hilfreich sein, eine Sinus Funktion in eine Kosinus Funktion umzuwandeln oder umgekehrt. Hierzu benötigt man die Komplementärformeln, welche wie folgt aussehen.

costosin sintocos.png

Um den Rückgabewert, der Sinus- oder Kosinus-Funktion in den Bogenmaß um zu wandeln, gibt es folgende Umkehrfunktionen.

umkehrfunktion sin cos tan.png

Es ist zu beachten, dass diese den Bogenmaß zurück geben und diese für Gradmaß entsprechend umgerechnet werden müssen.

Vektor

Ein Vektor kann mit einem Array oder einer Liste vergleicht werden, wenn man z.B. einen 3-dimensionalen Vektor meint, dann wäre es ein Array mit 3 Elementen. Die übliche Schreibweise eines Vektors sieht wie folgt aus.

generischer vektor.png

Eine entsprechende C++ Representation wäre z.B. folgende

template<typename T,unsigned int DIMENSION>
class TVector
{
  public:
    T m_Vec[ DIMENSION];
};
//typedef Vec4 TVector<float,4>;

OpenGL verwendet auf der Grafikkarte immer 4-dimensionale Vektoren, auch wenn nur 1 oder 3 benötigt werden. Die restlichen Elemente des Vektors werden dann mit 0 aufgefüllt. Vektoren werden in OpenGL in 2 Arten verwendet, als absoluter und als relativer Wert. Absolute Werte wären z.B. Positionen und Farbwerte wärend relative Werte z.B. eine Transformation wäre. Der OpenGL Vektor sieht wie folgt aus.

opengl vektor.png

Einheitsvektor

Ein besondere Form eines Vektors ist der Einheitsvektor, welcher immer eine Länge von 1 ergibt. Der Einheitsvektor wird normalerweise als klein e gekennzeichnet.

Die Berechnung eines Einheitsvektors wird später in der der Magnitude und Normalisierung Funktion näher erläutert. Einheitsvektoren sind als Normalen/Richtungsvektoren in OpenGL im Einsatz und ist die Basis für Rotationen.

Addition

addition vektor.png

addition vektor1.png

Die Addition wird Komponentenweise ausgeführt, was bedeutet, man kann sich eine Addition von Vektoren als eine Addition von jeden einzelnen Element mit dem entsprechenden Element im anderem Vektor Vorstellen.

addition vektor visual.png

Die Addition von Vektoren kann man sich sehr einfach Vorstellen, in dem man die einzelnen Vektoren als Bewegungsbefehle sieht. Wenn man also 2 schritte vorwärts,einen schritt seitwärts laufen soll und danach ein halben Schritt vorwärts und ein Schritt seitwärts, dann kann man diese beiden Befehle auch zu einem Befehl zusammen fassen. Laufe 2 1/2 Schritte vorwärts und 2 Schritte Seitwärts und wir stehen am gleichen Punkt und dieser Befehl wäre dann unser Ergebnis Vektor c.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
TVector<T,DIMENSION> operator+(const TVector<T,DIMENSION>& b)
{
  TVector<T,DIMENSION> c;
  for (unsigned int i=0;i<DIMENSION,i++)
    c.m_Vec[i]=this->m_Vec[i]+b.m_Vec[i];
  return c;
}
//...
};
//Vec4 a,b,c; c=a+b;

Subtraktion

subtraktion vektor.png

subtraktion vektor1.png

Hier gilt gleiches, wie bei der Addition, nur das Komponentenweise Subtrahiert wird.


Subtraktion vektor visual.png

Bei der Subtraktion eines Vektors wendet man den ersten Befehl an und läuft z.B. 2 Schritte nach vorne und einen Seitwärts, dann wendet man den 2. Schritt an aber wechselt das Vorzeichen jeder einzelnen Komponente. Also wird ein positive Komponente zu einer negativen und eine negative zu einer Positiven. Wenn man also einen schritt seitwärts,nach links, laufen soll, dann läuft man ein Schritt seitwärts, nach rechts, sowie vorwärts statt rückwärts. Man kann eine Subtraktion über eine Addition realisieren, wenn man jede Subtraktion, den rechten Vektor zuvor invertiert und dann addiert.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
TVector<T,DIMENSION> operator-(const TVector<T,DIMENSION>& b)
{
  TVector<T,DIMENSION> c;
  for (unsigned int i=0;i<DIMENSION,i++)
    c.m_Vec[i]=this->m_Vec[i]-b.m_Vec[i];
  return c;
}
//...
};
//Vec4 a,b,c; c=a-b;

Betrag

magnitude vektor.png

Magnitude ist der Englische Begriff für die Berechnung des Betrags eines Vektors oder auch die Länge. Die Länge des Vektors wird benötigt, wenn man einen Vektor Normalisieren will oder fest stellen möchte ob ein Vektor ein Einheitsvektor ist.

betrag vektor.png

Der Betrag eines Vektors kann über den Satz des Pytagoras ermittelt werden, welcher die Wurzel der Summe, der Quadrate, aller Komponenten ist. Dies ist natürlich für wenige gut Vorstellbar und daher hier mal eine bessere Erklärung. Ein Vektor kann in n Komponenten zerlegt werden, der 4 Komponenten Vektor von OpenGL in 4 Komponenten. Jede Komponente stellt eine Dimension dar, welche x,y,z und w sind. Pytagoras lernt man in der Schule im 2 Dimensionalen Raum kennen, also wie es im Abbild über diesen Text dargestellt ist. Die Regel besagt, das der Flächeninhalt einer Seite der Summe der anderen entspricht, also l²=x²+y². Diese Flächen sind Quadratisch also hat jede Seite der Fläche die gleiche Kantenlänge. Wenn man die Quadratwurzel von der Fläche zieht, bekommt man also die Kantenlänge. Wenn man nun die Flächen von x,y,z und w summiert erhält man die Fläche l². Zieht man von l² die Quadratwurzel, dann hat man die Kantenlänge l von dem 4Komponenten Vektor, welche als Betrag oder Länge bezeichnet wird. Da bei einem Vektor w=0.0 gesetzt wird, hat diese keinen Einfluss auf diese Operation und wird in den Formeln auch in der Regel nicht mit hin geschrieben.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
T Magnitude(){
  T len=T(0);
  for (unsigned int i=0;i<DIMENSION,i++)
    len+=this->m_Vec[i]*this->m_Vec[i];
  return sqrt(len);
}
//...
};
//Vec4 a; float len; len=a.Magnitude();

Skalarprodukt

skalarprodukt vektor.png

Das Skalarprodukt erlaubt uns die Berechnung, des Winkels, zwischen 2 Vektoren. Hierfür müssen allerdings beide Vektoren Normalisiert sein, also jeweils einen Betrag von 1 ergeben. Sonnst muss dies noch nachträglich getan werden.

skalarprodukt vektor1.png

Wenn die 2 Vektoren a und b vorliegen, sollte man davon ausgehen, dass der Betrag beider Vektoren jeweils 1 ist. Sollte es nicht der Fall sein, so wie im Bild über diesen Text, dann muss dies durch die Normalisierung nachgeholt werden. Dies passiert, indem man den Betrag beider Vektoren errechnet und dann diesen Komponentenweise mit dem Vektor dividiert. Die oben stehende Formel wird aus dem Kosinussatz abgeleitet und Umgestellt. Darraus ergibt sich am Ende, dass die Summe, der Komponentenweise multiplizierten Vektoren a und b den Kosinus des Winkels ergibt. Es ist wichtig zu beachten, dass nicht der Winkel sondern der Kosinus des Winkels in c wieder zu finden ist. Wenn man den Winkel haben möchte, dann muss man den Arkuskosinus von c berechnen und das Ergebnis von Bogenmaß in Gradmaß umwandeln um den Winkel(als Delta makiert) zu bekommen.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
T DotProduct(const TVector<T,DIMENSION>& b)
{
  T alpha=T(0);
  for (unsigned int i=0;i<DIMENSION,i++)
    alpha+=this->m_Vec[i]*b.m_Vec[i];
  return c;
}
//...
};
//Vec4 a,b; float c; c=a.DotProduct(b);

Kreuzprodukt

kreuzprodukt vektor.png

Das Kreuzprodukt errechnet einen Vektor, der senkrecht zu den Vektoren a und b steht, wenn a und b den selben Ursprung haben. Dieses Verhalten wird genutz, um die Normale einer Fläche zu errechnen.

kreuzprodukt vektor1.png

Der berechnete Vektor hat wie schon erwähnt die Eigenschaft, dass er senkrecht zu den Vektoren a und b ausgerichtet ist. Dies bedeutet, dass der Winkel zwischen dem Berechnetem Winkel und a oder b immer 90° beträgt. Wenn a und b 2 von den 3 Eckpunkten, eines Dreiecks ist, dann zeigt der Vektor c senkrecht zum Dreieck und bildet den Richtungsvektor des Dreiecks. Wenn man nun noch diesen Vektor normalisiert, dann erhält man die Flächenormale. Diese hat den Betrag 1 und wird für verschiedene Rendertechniken, sowie Physikberechnungen benötigt. Wenn z.B. ein Lichtstrahl solch ein Dreieck schneidet, dann kann man mit Hilfe des Vektors(vom Lichstrahl) und der Normale(der Fläche) den Reflektionsvektor berechnen und somit sagen, in welche Richtung sich das Licht weiter bewegen würde. Es ist zu beachten, dass die Reihenfolge, in der man beide Vektoren Multipliziert einen Einfluss auf die Richtung, in die c zeigt. Wenn man a und b tauscht, dann wechseln die Vorzeichen aller Komponenten von c.

template<typename T,unsigned int DIMENSION>
class TVector
{
...
TVector<T,DIMENSION> Crossproduct(const TVector<T,DIMENSION>& b)
{
  TVector<T,DIMENSION> c;
  unsigned int prev,next;
  for (unsigned int i=0;i<DIMENSION,i++)
  {
    prev=i==0?DIMENSION-1:i-1;
    next=i+1 % DIMENSION;
    c.m_Vec[i]=this->m_Vec[next]*b.m_Vec[prev]-this->m_Vec[prev]*b.m_Vec[next];
  }
  return c;
}
...
};
//Vec4 a,b,c; c=a/b;

Normalisieren

normalisieren vektor.png

Bei der Normalisierung wird der Betrag eines Vektors mit dem Vektor dividiert und man erhält ein Vektor, der in die Gleiche Richtung zeigt aber auf eine Länge 1 skaliert ist.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
TVector<T,DIMENSION> Normalize(){
  TVector<T,DIMENSION> c;
  c=(*this)/this->Magnitude();
  return c;
}
//...
};
//Vec4 a,c; c=a.Normalize();

Matrix

Eine Matrix kann man sich als 2-dimensionalen Array mit n und m Länge vorstellen.

generische matrix.png

OpenGL verwendet 4x4 große Matrizen und kann aus 4 Vektoren mit einer Länge von 4 konstruiert werden.

opengl matrix.png

template<typename T,int DIMENSION> 
class TMatrix 
{ 
  protected: 
    Tvector<T,DIMENSION> m_Matrix[DIMENSION]; //Erlaubt uns das nutzen von der eigenen Vektorklarsse.
    T m_Array[DIMENSION*DIMENSION]; //für die LoadMatrix Funktion von OpenGL
  public:
    Tvector<T,DIMENSION>& operator [](const int Index) 
    { 
      return m_Matrix[Index]; 
    }

    const T* GetMatrix1DArray() 
    { 
      int size=sizeof(T)*DIEMNSION; 
      for (int i=0;i<DIMENSION;i++)
        memcpy(&m_Array[i*4],&m_Matrix[i][0],size); 
      return &m_Array[0]; 
    }
};
//typedef Mat Tmatrix<float,4>

Für Matrizen brauchen wir nicht so viele Funktionen wie bei Vektoren, um genau zu sagen brauchen wir 2 Operationen. Diese Operationen lauten Transponieren und Multiplikation. Bei der Matrix benötigen wir einige Konstruktionsfunktionen und zwar Identity, Translate, Rotate und Scale Matrix. Diese Matrizen machen die ganze Arbeit für uns, wenn man sich in einem 3D Raum bewegen möchte, welcher als Modelview Matrix abgebildet wird.

Transponieren

transponieren matrix.png

Transponieren wird durch ein großes T über der Matrix dargestellt. Diese Funktion tauscht die Werte einer Matrix miteinander aus, so das aus einer Spalten konstruierten Matrix eine Zeilenweise konstruierte Matrix wird. Diese Funktion kann Hilfreich sein, wenn man zwischen Direct3D und OpenGL Daten austauschen will, denn nicht jeder nutzt spalten orientierte Matrizen.

opengl matrix.pngd3d matrix.png

void Transpose() //Transponieren für 4x4 Matrizen
{ 
  swap(m_Matrix[0][1],m_Matrix[1][0]); //swap kopiert b in tmp, kopiert a in b und tmp in a
  swap(m_Matrix[0][2],m_Matrix[2][0]); 
  swap(m_Matrix[0][3],m_Matrix[3][0]); 

  swap(m_Matrix[1][2],m_Matrix[2][1]); 
  swap(m_Matrix[1][3],m_Matrix[3][1]); 

  swap(m_Matrix[2][3],m_Matrix[3][2]); 
}

Multiplikation

Multiplikation ist die wichtigste Operation bei Matrizen, wenn wir uns mit OpenGL beschäftigen. Dies liegt daran, dass OpenGL 2 verschiedene Matrizen verwendet, um Vektoren in Bildschirmkoordinaten um zu wandeln. Damit dies Funktioniert muss jeder Vektor mit diesen Matrizen jeweils multipliziert werden. Um eine Matrix zu manipulieren wird diese mit einer Konstruierten Matrix multipliziert. Diese Operation die ist meist ausgeführte Operation sowohl in der OpenGL Pipeline als auch in einem Spiel. Seit OpenGL3 gibt es kein Matrizen support mehr, was bedeutet, dass man diese selber implementieren muss und dann die fertigen Matrizen an OpenGL übergibt. Die lässt viel Raum für Optimierung und kann somit ein OpenGL3 Programm schneller machen als ein OpenGL2 Programm, wenn man zuvor die glTranslate,glRotate und weiteren Funktionen verwendet hat. Es ist also ratsam sich eine sehr Performance Bibliothek zu laden oder mit einem Performance Analyzer bewaffnet den eigenen Code zu optimieren.

Multiplikation mit einem Vektor

multiplikation matrix vektor.png

multiplikation matrix vektor1.png

Um eine Matrix mit einem Vektor zu multiplizieren, braucht man man nur den Vektor[n] von der Matrix mit der Komponente[n] von dem Vektor multiplizieren und die resultierenden Vektoren anschließend addieren. Hierbei wird also ein Vektor mit einer einzelnen Komponente multipliziert, was weiter oben, bei den Vektoren, erklärt wurde.

TVector<T,DIMENSION>& operator *(TVector<T,DIMENSION>& V) 
{ 
  TVector<T,DIMENSION> v=m_Matrix[0]*V[0]+m_Matrix[1]*V[1]+m_Matrix[2]*V[2]+m_Matrix[3]*V[3];
  return v; 
}

Multiplikation mit einer Matrix

multiplikation matrix.png

multiplikation matrix1.png

Es sieht recht aufwändig aus allerdings kann man es dank der vorigen Matrix-Vektor Multiplikation auf eine recht übersichtliches Maß runter streichen. Man braucht dann nur noch die Matrix a jeweils mit einen der Vektoren von Matrix b multiplizieren und hat die Matrix-Matrix Multiplikation erledigt.

Tmatrix<T,DIMENSION>& operator *(TMatrix<T,DIMENSION>& M) 
{ 
  TMatrix<T,DIMENSION> M1((*this)*M[0]), (*this)*M[1], (*this)*M[2], (*this)*M[3]); 
  return M1; 
}

Konstruieren von Matrizen

Identity

Die Identity Matrix ist die Initialisierungsmatrix, mit der alle Matrizen belegt werden. Diese ist recht einfach und hat denn Sinn, dass bei einer Multiplikation immer der Wert herraus kommt, mit dem diese Multipliziert wurde. Wer aufmerksam gelsen hat, der wird nun an Einheitsvektoren denken und liegt richtig. Aufgrund der beschaffenheit einer Matrix benötigen wir in jeder Reihe und Spalte jeweils ein Element, welche den Wert 1 an nimmt und die restlichen nehmen den Wert 0 an. Die sieht dann wie folgt aus.

Tutorial Nachsitzen IdentityMatrix.png

Es ist nicht zwingend notwendig die Einheitsvektoren in dieser Reihenfolge zu verwenden, man könnte auch den ersten und dritten Vektor tauschen und es würde trotzdem das gleiche Ergebnis geben.

static TKar_Vector<T> Identity[4];

void LoadIdentity()
{
  for (int i=0;i<4;i++)
    m_Matrix[i]=Identity[i];
}

template<typename T> TKar_Vector<T> TKar_Matrix<T>::Identity[4]={TKar_Vector<T>(1.0f,0.0f,0.0f,0.0f),TKar_Vector<T>(0.0f,1.0f,0.0f,0.0f),TKar_Vector<T>(0.0f,0.0f,1.0f,0.0f),TKar_Vector<T>(0.0f,0.0f,0.0f,1.0f)};

Translate

Wenn man ein Vektor oder Matrix in x,y,z bewegen möchte, dann kann man dies mit einer Transformationsmatrix erreichen. Dazu erstellt man eine Matrix, füllt sie mit der Identity Matrix und setzt dann x,y,z in der Matrix mit den zu x,y,z Werten, um die man etwas verschieben möchte.

Tutorial Nachsitzen MoveMatrix.png

    void Translate(T x,T y,T z)
    {
      TKar_Matrix<KAR_MATH_TYPE> m;
      m[3][0]=x;
      m[3][1]=y;
      m[3][2]=z;
      (*this)*=m;
    }

Rotate

Das Bewegen auf den 3 Achsen ist allerdings oft nicht ausreichend und deswegen gibt es auch eine Rortations-Matrix. Die Rotations-Matrix wird durch 3 Vektoren beschrieben, welche jeweils für die x,y und z Achse zuständig sind. Man kann die Rotation, der einzelnen Achsen in einem Schritt oder in 3 einzelne Aufteilen. Um die Rotationsmatrix besser zu verstehen werden erst einmal alle Achsen einzeln betrachtet.

Drehen um die Z-Achse

Tutorial Nachsitzen rotz.gifTutorial Nachsitzen RotZMatrix.png

Der Z-Achsen Einheitsvektor(3. Spalte=0.0, 0.0, 1.0, 0.0) bleibt bei der Rotation unverändert - man nehme einen Finger, deute damit nach vorne. Nun drehe man diesen Finger um seine eigene Achse, wohin zeigt er? In die selbe Richtung wie vor der Drehung? Damit entspricht Z-Achsen Einheitsvektor auch der vorletzen Spalte der Matrix: (0.0, 0.0, 1.0, 0.0)

Der X-Achsen Einheitsvektor(1. Spalte=1.0, 0.0, 0.0 0.0) dreht sich hingegen mit. Trigonometrie findet der Lösung Spur. Ein Blick auf das Bild zum Einheitskreis zeigt, dass wir gerade das gleiche Problem für X und Y zu bewältigen haben: Die Rotationsachse ist in beiden Fällen die Z-Achse. Der Einheitsvektor, der gedreht wird, ist der X-Achsen Einheitsvektor also:

x = cos(ß)
y = sin(ß)

womit der Inhalt der ersten Spalte der Matrix kennen: (cos(ß), sin(ß), 0.0, 0.0)

Dies kann man auch auf den Y-Achsen Einheitsvektor(2. Spalte=0.0, 1.0, 0.0, 0.0) übertragen, man muss nur bedenken in welcher weise sich x und y vertauschen:

x = -sin(ß)
y = cos(ß)

So ergibt sich für die zweite Spalte: (-sin(ß), cos(ß), 0.0, 0.0)

Nun kann man die Rotationsmatrix für die Rotation auf der Z-Achse beschreiben, die Matrix findet man am Anfang des Abschnittes.

Drehen um die Y-Achse

Tutorial Nachsitzen roty.gifTutorial Nachsitzen RotYMatrix.png

Die Berechnung der Matrix wird auf die gleiche weise ermittelt, wie bei der Berechnung der Z-Achsen Rotationsmatrix.

Drehen um die X-Achse

Tutorial Nachsitzen rotx.gifTutorial Nachsitzen RotXMatrix.png

Rotation um alle Achsen mit einer Matrix

Es ist recht aufwändig für die CPU und für den Programmierer immer 3 Rotationen aus zu führen, deswegen hat man diese auch zu einer einzigen Operation zusammen gefasst. Die Matrizen werden einfach miteinander multipliziert und es kommt eine Rotationsmatrix herraus, welche 3 Eingabeparameter benötigt, alpha, beta und gamma sind dabei die Winkel für X-,Y- und Z-Achse.

rotationsmatrix 3angle.png

Eine weitere Möglichkeit, um eine Rotationsmatrix zu erstellen, wäre das nutzen eines Richtungsvektors und einem Winkel. Hierbei wird der Winkel auf den Richtungsvektor angewendet, wenn also man als Vektor (1,0,0) verwendet dann hat man eine Rotation auf der X-Achse, (0,1,0) Y-Achse und (0,0,1) für die Z-Achse. Man kann nun auch Vektoren wie (1,1,1) angeben, wobei die Funktion diesen Vektor dann normalisieren wird, damit er noch auf das Einheitskreis System funktioniert. Dieser Vektor ist nicht equivalent mit dem Rotieren von X-,Y- und Z-Achse nacheinander. Der Vorteil hierbei liegt in der Kompaktheitund des Codes und man kann auf Algorithmen zurück greifen, die vor OpenGL3 entwickelt wurden, da der OpenGL Treiber früher die Rotation über diese Variante Implementiert hatte.

rotationsmatrix.png

    void Rotate(T w,T x,T y,T z)
    {
      TKar_Matrix<KAR_MATH_TYPE> m;
      T rad=TKar_Math<T>::DegToRad(w);
      T c=TKar_Math<T>::Cos(rad);
      T ic=1.0-c;
      T s=TKar_Math<T>::Sin(rad);
      TKar_Vector<T> v(x,y,z,0.0);
      T mag=TKar_Math<T>::Sqrt((v*v).Sum());

      if (mag<=1.0e-4)
        return;

      v[0]=x/mag;
      v[1]=y/mag;
      v[2]=z/mag;

      m[0][0]=(v[0]*v[0]*ic)+c;
      m[0][1]=(v[0]*v[1]*ic)+(v[2]*s);
      m[0][2]=(v[0]*v[2]*ic)-(v[1]*s);

      m[1][0]=(v[0]*v[1]*ic)-(v[2]*s);
      m[1][1]=(v[1]*v[1]*ic)+c;
      m[1][2]=(v[1]*v[2]*ic)+(v[0]*s);

      m[2][0]=(v[0]*v[2]*ic)+(v[1]*s);
      m[2][1]=(v[1]*v[2]*ic)-(v[0]*s);
      m[2][2]=(v[2]*v[2]*ic)+c;

      *this*=m;
    }

Scale

skalierungsmatrix.png

Modelview

weltkoordinatensystem-vs-modellkoordinatensystem.png translation-vs-rotation.png

Projectionview

Perspektive

GluPerspective.png

GluPerspective Quads X.jpg

Orthogonal

GlOrtho Matrix.png

Mit r, l, b, t, f und n sind der rechte, linke, untere und obere Rand, sowie Distanz der far und near clipping plane, des Bildschirmausschnittes, gemeint. Die far und near clipping plane sind parallel, zum Betrachter, ausgerichtete Ebenen. Alles was zwischen diesen beiden Ebenen liegt wird gezeichnet und alles außerhalb wird in der Pipeline verworfen. Für die Orthogonal Matrix ist dieses weniger von Bedeutung, da nur mit aktiven Tiefentest und unterschiedlichen z-Werten, eine Auswirkung vorliegt.

Orthomode Quads.jpg

Exkurs in die Optimierung

Vektoren

Man hat oft den Fall, dass man auf der CPU mehrere Werte multiplizieren, dividieren, addieren, subtrahieren muss z.B. bei Bildbearbeitung.

Dieses kann man Optimieren, indem man Vektoren verwendet, dabei wird ein Pixel als Vektor interpretiert und nun kann man alle Farbkanäle mit einen einzigen Aufruf verarbeiten lassen, indem man die Vektoroperationen verwendet. Wenn die Vektoren mit einer CPU Extension wie SSE oder MMX implementiert wurden, dann werden die 4 Komponenten eines Vektors sogar gleichzeitig verarbeitet. Sollte man nur 2 oder 3 Komponenten verwenden, dann füllt man die restlichen Komponenten mit 0 auf bzw. ignoriert sie einfach beim auslesen der Komponenten.

Multiplikation

multiplikation vektor.png

Die Multiplikation von 2 Vektoren miteinander ist eigentlich nicht definiert, da es wenig Sinn macht aber wir haben nun einen Verwendungszweck gefunden und definieren ihn. Bei dieser Operation gilt das gleiches, wie bei der Addition und Subtraktion, nur mit einem Mulltiplikations Operator. Die Bildverarbeitung wird sich mit mehr Geschwindigkeit bedanken.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
TVector<T,DIMENSION> operator*(const TVector<T,DIMENSION>& b)
{
  TVector<T,DIMENSION> c;
  for (unsigned int i=0;i<DIMENSION,i++)
    c.m_Vec[i]=this->m_Vec[i]*b.m_Vec[i];
  return c;
}
//...
};
//Vec4 a,b,c; c=a*b;

Division

division vektor.png

Bei der Division muss man aufpassen, dass keiner der Element im Divisor 0 ist, da sonnst eine Division mit 0 entsteht. Eine Division mit 0 hat ein Interrupt zufolge, welcher das Programm zum beenden bittet. Also sollte man dies Vorher überprüfen oder bei einer Division den Aufruf mit einer entsprechenden Routine den Fehler abfangen. Sonnst verhält es wie bei der Multiplikation.

template<typename T,unsigned int DIMENSION>
class TVector
{
//...
TVector<T,DIMENSION> operator/(const TVector<T,DIMENSION>& b)
{
  TVector<T,DIMENSION> c;
  for (unsigned int i=0;i<DIMENSION,i++)
    c.m_Vec[i]=this->m_Vec[i]/b.m_Vec[i];
  return c;
}
//...
};
//Vec4 a,b,c; c=a/b;

Programmiersprache,Compiler und CPU Extension

Die Optimierung ist von Programmiersprache und Compiler stark abhängig. Wärend Delphi, Freepascal und MS VSC++ Compiler man nur aufwändig SSE,MMX und weitere CPU Extension implementieren kann, hat der GCC eine Vektorextension und seit Version 4.x auch eine Funktionsoptimierung. Die Vektorextension von GCC ist ein Parser Erweiterung, welche einen Typ als ein bestimmten Vektortyp(z.B. Float Länge 4) bestimmt und dann entsprechend alle Operationen in SSE1-4 oder MMX Code umwandelt. Seit Version 4.x gibt es eine Funktionsoptimierung, welche erlaubt im Quellcode mehrere Optimierungen gleichzeitig zu nutzen. Die bedeutet, dass man eine Funktion, wie Vektor.Magnitude() mehrfach implementiert und dann jeweils eine andere Optimierungs Aktiviert. Also Vektor.Magnitude_sse(), Vektor.Magnitude_mmx(),... und dann die jeweilige Funktion mit SSE,MMX oder ohne Optimierung markiert. Der Compiler optimiert dann diese Funktion für die jeweilige CPU Extension und man kann dann im Programmcode entsprechend der gegebenen Hardware auf die beste Mögliche Funktion umlenken. Dies hat den Vorteil, dass man nur noch eine Binary hat und alle CPUs(mehr Kern, ein Kern, Intel Petium1-4, AMD Athlon und so weiter) der gleichen Architektur Optimiert Unterstützen kann. Dies ist ist nützlich, wenn man alte CPUs unterstützen will. Es würde sich also lohnen, wenn man sich viel Ärger und Code sparen will, den Code in GCC als Bibliothek zu implementieren und statisch oder dynamisch zu linken.

Templates und Generics

Eine Optimierung die sich bei C++ stark auswirkt sind Templates, da diese die Eigenschaft aufweisen vom Compiler generiert zu werden. Dies bedeutet je nach Qualität des Compilers kann dieser Code starke Performanceschübe bekommen, wenn man die Operationen in Templates verpackt, da der Compiler oft Optimierungen machen kann, die man selber nicht kennt oder gar nicht machen will(Kompatibilität und Übersichtlichkeit). Templates entrollen z.B. konstante For Schleifen und wie schon vorher in den Codeschnipseln zu sehen war, haben wir davon einige in den Vektor Operationen. Das Entrollen von Schleifen entfernt eine menge Balast(Counter,Prüfung,..) welche sich auf die Performance auswirkt. Templates reduziert den Codeaufwand, da man viele weitere Implementierungen einspart(Vektor von Typ float, int, unsigned short, unsigned int, double, ...).

Testen testen testen

Wenn man Performance Test macht, dann sollte man verschiedene Test machen und einer der teilweise oft unterschätzt wird ist der Create und Free Test. Sobald man eine Klasse benutzt wird beim erstellen des Objektes immer ein Speicher alloziert und dann der Konstruktor aufgerufen, was Zeit kostet. Man sollte also einmal ein Test durchführen, der Vektoren und Matrizen erstellt und zerstört. Die Operation Test sollten die Vektoren und Matrizen vorher erstellen und in der Schleife wiederverwendet werden und nach beenden des Performance Trackings erst zerstört werden.