Tutorial Lineare Algebra
Inhaltsverzeichnis
Vorwort
Hi!
Den Meisten von euch ist sicher schon aufgefallen, dass 90% der 3D Programmierung aus Mathematik besteht. Wie willst du das nächste Doom schreiben, wenn du nicht mal feststellen kannst, ob der Spieler durch eine Wand läuft? Das problematische an der Mathematik ist, dass wenn man nicht von Natur aus eine Interesse dafür hat, erscheint sie oft etwas "trocken". Ich werde mein bestes tun, um mit Hilfe von vielen bunten Diagrammen das Ganze interessant und verständlich zu machen. Seid jedoch gewarnt, dass dies mein erstes Tutorial ist und ich mathematisch viele Sachen vielleicht etwas anders erkläre/mache als "richtig" oder optimal ist.
Ein Haufen praktischer Funktionen
Es gibt ein paar Funktionen, die man können muss, sonst kommt man nicht weit mit OpenGl. Man kann sich natürlich eine 3D Mathe Unit aus dem Internet ziehen, dies ist jedoch, meiner Meinung nach, eine schlechte Idee. Wir sind ja schließlich hier um was zu lernen. Darum lieber eine eigene kleine Unit schreiben, die man richtig versteht. Warum die Funktionen praktisch sind, erkennt man leider erst am Ende, wenn man sie in Kombination gebrauchen kann.
Vektoren
In der Linearen Algebra geht es hauptsächlich um Vektoren. Also brauchen wir ein Vektoren Typ:
type
TVector = record
X,Y,Z : Single; //Dasselbe wie TGLFloat
end;
Wir wollen die Werte nicht einzeln in unseren Vektor eintragen, sondern so:
aVector := MakeVector(1,3,8);
Also brauchen wir Folgendes:
function MakeVector(X,Y,Z : Single) : TVector;
begin
result.x := x;
result.y := y;
result.z := z;
end;
Addition und Subtraktion
Oben sehen wir zwei Vektoren, die beide im Ursprung (gelber Punkt) anfangen. Das Resultat der Addition wird der grüne Vektor sein. Die zwei Vektoren kannst du dir als zwei separate Anweisungen denken. Die rote "Anweisung" ist gehe 2 Meter Nord; die blaue "Anweisung" ist gehe 1 Meter Ost. Wenn wir die rote "Anweisung" ausführen und die Blaue dazu addieren, d.h. auch ausführen, dann stehen wir sqr(5) Meter (Pythagoras) vom Ursprung entfernt. Das Ganze kann man auch zu einer Anweisung zusammenfassen: der grüne Vektor. Die Addition funktioniert mit alle Vektoren, nicht nur mit denen, die im 90° Winkel zueinander stehen, wie in meinem Nord-Ost Beispiel. Um die Vektoren-Summe zu erhalten, addiert man die Komponente der zwei Vektoren. Genug erklärt, hier ist die Funktion:
function AddVectors(VectorA, VectorB : TVector) : TVector;
begin
{Einfache Vektoraddition}
Result := MakeVector(VectorA.X + VectorB.X,
VectorA.Y + VectorB.Y,
VectorA.Z + VectorB.Z);
end;
Und das Selbe noch für die Subtraktion:
Das Einzige, was an diesem Diagram verwirrend sein mag, ist, dass der blaue Vektor in die "falsche" Richtung zeigt. Das liegt an dem Minus Zeichen in der Subtraktion:
Ein Minus Zeichen dreht einen Vektor um. Wenn die zwei Parameter der Subtraktions-Funktion vertauscht werden, dann erhält man den negativen Vektor. Das lässt sich auch mit einer einfachen Rechnung darstellen:
9 - 4 = 5
4 - 9 = -5
Hätte ich in dem Diagram oben erst den blauen Vektor angegeben wäre Folgendes rausgekommen:
Die Subtraktions-Funktion ist vom Aufbau her, der Additions-Funktion natürlich sehr ähnlich:
function SubVectors(VectorA, VectorB : TVector) : TVector;
begin
{Einfache Vektorsubtraktion}
Result := MakeVector(VectorA.X - VectorB.X,
VectorA.Y - VectorB.Y,
VectorA.Z - VectorB.Z);
end;
Die Länge (Betrag) eines Vektors
Du kennst hoffentlich den Satz des Pythagoras:
Das geht auch in 3D, nur zusätzlich mit der Z-Komponente:
function Magnitude(Vector : TVector) : Single;
begin
{Gibt die Länge (Betrag) eines Vektors zurück}
result := sqrt(Vector.X * Vector.X+
Vector.Y * Vector.Y+
Vector.Z * Vector.Z);
end;
Es hat sich eingebürgert, dass man die Länge eines Vektoren auch so schreibt: |Vektor|, auch genannt der Betrag eines Vektors. Um es nicht mit dem Absolut Zeichen zu verwechseln, schreiben Manche auch: ||Vektor||.
Einheitsvektoren
Einheitsvektoren sind Vektoren der Länge 1. Es ist leichter, mit Einheitsvektoren zu rechnen, als mit normalen Vektoren, da Multiplikationen mit 1 wegfallen (Siehe Skalarprodukt). Um einen Vektor der Länge 1 zu erhalten, dividieren wir die Komponente des Vektors durch die Länge des Vektors. Der Vektor ist dann "normalisiert". Die Länge des Vektors zu ermitteln ist kein Problem mehr, also können wir eine Funktion aufstellen die (fast) jeden beliebigen Vektor normalisiert:
procedure Normalize(var Normal : TVector);
var
Length : Single;
begin
{Der angegebene Vektor wird auf eine Länge (Betrag) von 1 gekürzt,
um dies zu erreichen muss jede Vektorkomponente durch die Länge
(Betrag) des Vektors selbst dividiert werden }
Length := Magnitude(Normal);
if (Length = 0) then Length := 1;//Wir wollen keine Division durch 0
Normal.X := Normal.X / Length;
Normal.Y := Normal.Y / Length;
Normal.Z := Normal.Z / Length;
end;
Das Einzige, was hier unerwartet sein mag, ist die "if" Abfrage. Wenn der Vektor (0,0,0) ist, würde die Länge 0 sein und somit einen "Division by Zero" Fehler erzeugen. Falls die Länge 0 ist, dividieren wir deshalb durch 1.
Manchmal ist es nötig, einen Vektor um einen Faktor zu verlängern. Um dies zu erreichen, wird jede Komponente des Vektors mit dem Faktor multipliziert:
function ScaleVector(Vector : TVector; Multi : Single) : TVector;
begin
{ Der Vektor wird skaliert mit dem angegeben Faktor Multi }
{ Dies ändert nicht die Richtung des Vektors! }
result.x := Vector.x * Multi;
result.y := Vector.y * Multi;
result.z := Vector.z * Multi;
end;
Bogenmaß und Gradmaß
Wenn du in OpenGl (und auch sonst bei PCs) irgendwo einen Winkel siehst, kannst du fast davon ausgehen, dass er im Bogenmaß (Radians) angegeben wird. Sollten es wirklich Grad sein, steht das meistens groß daneben [Amk: Bitte Überprüfen: siehe glRotate ]. Die trigonometrischen Funktionen in Delphi sind übrigens auch alle auf Bogenmaß ausgelegt. Gradmaß(deg) und Bogenmaß(rad) kann man leicht umrechnen:
Diese Umrechnung ist hier wirklich nur als Hilfe gedacht. Rechnet bitte nicht in euren OpenGl Programmen alles nach Grad um und später wieder zurück nach Bogenmaß. Wenn du es ernst meinst, mit der 3D Programmierung, dann lern gleich mit dem Bogenmaß umzugehen.
Skalarprodukt (Punktprodukt = DotProduct)
Das Skalarprodukt ist eine der meistgebrauchten Funktionen - und das aus gutem Grund: Wenn es irgendwie um einen Winkel in einer Berechnung geht, ist wahrscheinlich das Skalarprodukt mit drin.
Diese Formel ist noch nicht brauchbar, da zwei der Variablen unbekannt sind (Das Skalarprodukt und der Winkel, im weiteren ß genannt). Bem: Wenn beide Vektoren Einheitsvektoren sind, dann ist das Skalarprodukt gleich dem Kosinus des Zwischenwinkels.
Das Problem mit den zwei unbekannten Variablen hat zum Glück jemand für uns gelöst:
Wenn wir die zwei Gleichungen gleichsetzen, erhalten wir folgendes:
Sehr schön. Damit können wir etwas anfangen: Wir können mit ein paar Rechnungen an den Winkel zwischen den zwei Vektoren kommen.
Aus Performance Gründen sollte dies möglichst vermieden werden, da die ArcCos Funktion langsam ist. Auch um dies Problem gibt es (teilweise) eine schlaue Lösung. Lassen wir das mit dem ArcCos weg und schauen uns den Kosinus Graphen an:
Falls der Winkel Pi/2 oder 3Pi/2 ist, wird der Kosinus 0 sein. Andersrum gedacht: Wenn der Kosinus 0 ist dann ist der Winkel Pi/2 oder 3Pi/2. Wenn Cos(ß) zwischen Pi/2 und 3Pi/2 liegt, ist das Vorzeichen von Cos(ß) negativ. Am Graphen solltest du auch sehen können das beim Rest das Vorzeichen positiv ist. Und das Ganze noch mal graphisch: (Der Rote Vektor ist der "Hauptvektor")
Wir werden das Skalarprodukt so ausrechnen:
function DotProduct(VectorA,VectorB : TVector) : Single;
begin
{The result of the DotProduct is the cosine of the angle
between the two vectors. If the result is exactly 0 then
the angle between the vectors is 90 degrees. If it is
positive then the angle is smaller than 90, if it is negative
the angle is greater than 90. Use Arccos to get the angle itself.
Anmerkung des Korrektors: Die Annahme, dass das Punktprodukt
zweier Vektoren dem Kosinus des Zwischenvektors entspricht, ist
nur dann korrekt, wenn die beiden Vektoren Einheitsvektoren sind.
(siehe: Definition des Punktproduktes). Um auch ohne Einheitsvektoren
den Winkel zu erhalten, muss man das Punktprodukt durch die
Längen der Vektoren teilen.}
result := VectorA.X*VectorB.X+
VectorA.Y*VectorB.Y+
VectorA.Z*VectorB.Z;
end;
Kreuzprodukt (Vektorprodukt = CrossProduct)
Das Kreuzprodukt ist noch eine Funktion, die man kennen muss. Das Kreuzprodukt nimmt, genau wie das Skalarprodukt, zwei Vektoren entgegen. Als Resultat erhält man beim Kreuzprodukt nicht eine Zahl, sondern einen Vektor. Das Schöne an diesem Vektor ist, dass er genau senkrecht auf der Fläche steht die durch die zwei ursprünglichen Vektoren beschrieben. Dazu ein Diagram:
Ich habe hier das Kreuzprodukt des blauen und pinken Vektoren gebildet. Die Länge des Vektors, den man durch das Kreuzprodukt erhält, ist nicht immer die selbe. Darum wird das Resultat des Kreuzprodukts meistens danach normalisiert.
function CrossProduct(VectorA,VectorB : TVector) : TVector;
begin
{Creates a Vector that is perpendicular to the plane
created by the two given vectors}
Result.X := VectorA.Y*VectorB.Z - VectorA.Z*VectorB.Y;
Result.Y := VectorA.Z*VectorB.X - VectorA.X*VectorB.Z;
Result.Z := VectorA.X*VectorB.Y - VectorA.Y*VectorB.X;
end;
Die Reihenfolge in der man die Vektoren der Kreuzprodukt Funktion überreicht ist wichtig. Wenn ich in dem vorigen Diagram erst den pinken Vektor überreicht hätte wäre Folgendes rausgekommen:
Auf diesem Diagram ist genau die selbe Fläche zu sehen wie auf dem vorigen, nur aus einem anderen Blickwinkel, damit man den Vektor überhaupt sehen kann. Der Vektor, den wir zurück bekommen, hat genau die selbe Länge wie der Vorige, er zeigt jedoch in die gegengesetzte Richtung. Er ist also das Negativ des vorigen Vektors. Negative Vektoren wurde schon in "Addition und Subtraktion" besprochen.
Es gibt eine Methode, sich zu merken, in welche Richtung das Resultat zeigen wird: Wenn du deine rechte Hand ausstreckst, gilt Folgendes: Der Zeigefinger ist der erste Vektor, der Mittelfinger der Zweite und der Daumen das Resultat. Graphisch:
Das nennt man die Rechte-Hand-Regel. Ich hoffe, dass du in dem Bild eine rechte Hand erkennen kannst ;-)
Alles was normal ist
Zum Abschluss will euch eine Funktion zeigen, die die Normale eines beliebigen Dreiecks finden kann. Dazu müssen wir erst Dreiecke speichern können:
type
TTriangle = array[0..2] of TVector;
Um die Normale zu finden benötigen wir zwei Vektoren. Wir haben nur die drei Punkte des Dreiecks. Durch Subtraktion kann man aus den drei Punkten zwei Vektoren bilden:
Ich hoffe das du den Zusammenhang zwischen den zwei roten bzw. blauen Vektoren sehen kannst. Wenn nicht schau dir die Vektoren Subtraktion bitte noch mal genau an. Wir gebrauchen das Kreuzprodukt um einen Vektor zu finden der senkrecht auf diesen zweien steht. Zum Schluss wird dieser normalisiert.
function GetNormal(Triangle : TTriangle) : TVector;
var
VectorA, VectorB, Normal : TVector;
begin
{To get the normal we first need to define a plane.
To do this we use the three vertices to create two
vectors, thereby defining a plane. If we give these
two vectors to the CrossProduct function we get a
vector that is perpendicular to the plane in return.
All we then need to do is cut this vector to a length
of 1 and we have our normal.}
VectorA := SubVectors(Triangle[0],Triangle[1]);
VectorB := SubVectors(Triangle[1],Triangle[2]);
Normal := CrossProduct(VectorA,VectorB);
Normalize(Normal);
result := Normal;
end;
Wenn alles glatt läuft, haben wir die Normale des Dreiecks.
Nachwort
Ich hoffe dieses Tutorial hat dem ein oder anderen was gebracht. Ich habe auf jeden Fall eine Menge gelernt durch das Schreiben. Ist irgendwie immer im Leben so. Sobald man etwas erklären möchte, muss man das erst selber konkret durchdenken...
Vielen dank an Delphic für die sprachliche Hilfe (War sehr nötig).
|
||
Vorhergehendes Tutorial: - |
Nächstes Tutorial: Tutorial Nachsitzen |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |