• Keine Ergebnisse gefunden

Hilfsfunktionen für die Vektorrechnung

Kontakt und Updates

2.2 Einführung in die 3D-Grafik

2.2.2 Theorie der 3D-Grafik

2.2.3.10 Hilfsfunktionen für die Vektorrechnung

Operationen wie das Bilden des Kreuz- oder Punktprodukts, die Berechnung der Vektorlänge und Ähnliches haben wir bisher noch nicht eingebaut. Wie bereits besprochen, sollen diese Dinge durch separate Funktionen erledigt werden. Wir geben ihnen das Präfix „tbVector3“.

Diese Funktionen werden wir nun Schritt für Schritt erarbeiten.

Vektorlänge und Quadrat der Vektorlänge

Oft wird die Länge eines Vektors benötigt, und manchmal reicht auch das Quadrat der Vektor-länge aus, welches schneller berechnet werden kann. Möchte man zum Beispiel zwei Vektoren in Hinsicht auf ihre Länge vergleichen (welcher ist länger?), dann reicht es, die Quadrate der Vektorlängen zu vergleichen.

Hierfür sollen die beiden Funktionen tbVector3Length (Länge) und tbVector3LengthSq (Quadrat der Länge, wobei Sq für Squared steht, was so viel wie zum Quadrat bedeutet) implementiert werden. Erinnern Sie sich noch an die Methode zur Berechnung der Vektorlänge mit Hilfe des Satzes des Pythagoras?

// Berechnung der Vektorlänge

inline float tbVector3Length(const tbVector3& v) {

// Länge = Wurzel(x² + y² + z²)

return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);

}

// Quadrat der Vektorlänge berechnen

inline float tbVector3LengthSq(const tbVector3& v) {

// Wenn man nur das Quadrat der Vektorlänge benötigt, dann fällt die Wurzel weg // (die Berechnung erfolgt viel schneller).

return v.x * v.x + v.y * v.y + v.z * v.z;

}

Listing 2.11 Berechnung der Vektorlänge und ihres Quadrats

Normalisieren eines Vektors

Auch zum Normalisieren eines Richtungsvektors wollen wir uns eine eigene Funktion schrei-ben, die den Namen tbVector3Normalize trägt. Man soll ihr als Parameter einen Vektor überge-ben, und die Funktion liefert eine normalisierte Version dieses Vektors als Rückgabewert. Da-zu muss der Vektor nur durch seine eigene Länge geteilt werden.

Nun könnte es sein, dass ein Vektor die Länge null hat. Das wäre äußerst ungünstig, da eine Division durch null bekanntlich nicht erlaubt ist und zu einem Fehler führen würde. Von daher habe ich mir folgenden Ansatz überlegt: Man addiert einfach einen sehr kleinen Wert zur Län-ge hinzu, zum Beispiel 0.0001. Dann lässt die Genauigkeit zwar ein wenig nach, aber eine Di-vision durch null ist nicht mehr möglich, und es dürfte schneller als eine if-Abfrage sein, die prüft, ob die Länge möglicherweise null ist.

// Normalisieren eines Vektors

inline tbVector3 tbVector3Normalize(const tbVector3& v) {

// Vektor durch seine Länge teilen

return v / sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);

}

// “Sicheres“ Normalisieren eines Vektors

inline tbVector3 tbVector3NormalizeEx(const tbVector3& v) {

// Vektor durch seine Länge + 0.0001 teilen

return v / (sqrtf(v.x * v.x + v.y * v.y + v.z * v.z) + 0.0001f);

}

Listing 2.12 Normalisierungsfunktionen für Vektoren

Wenn man sich also nicht sicher ist, ob ein zu normalisierender Vektor möglicherweise die Länge null haben könnte und man geringe Genauigkeitsverluste in Kauf nehmen kann, so soll-te man tbVector3NormalizeEx verwenden.

Das Kreuzprodukt

Kommen wir nun zur Funktion tbVector3Cross, die für uns das Kreuzprodukt zweier Vektoren berechnen soll. Zur Erinnerung: Verknüpft man zwei Richtungsvektoren mit dem Kreuzpro-dukt, so erhält man einen weiteren Richtungsvektor, der senkrecht auf den beiden Ausgangs-vektoren steht. Die Implementierung der Funktion ist recht simpel:

// Berechnung des Kreuzprodukts zweier Vektoren inline tbVector3 tbVector3Cross(const tbVector3& a, const tbVector3& b) {

return tbVector3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);

}

Listing 2.13 tbVector3Cross berechnet das Kreuzprodukt.

Punktprodukt und Winkel zwischen zwei Vektoren

Mit dem Punktprodukt können wir auf einfache Weise den Winkel bestimmen, der von zwei Richtungsvektoren eingeschlossen wird. Dabei multipliziert man die Komponenten beider Vektoren einzeln und addiert die Produkte. Genau das soll die Funktion tbVector3Dot (Dot Product: Punktprodukt) tun.

// Berechnung des Punktprodukts

inline float tbVector3Dot(const tbVector3& a, const tbVector3& b) {

return a.x * b.x + a.y * b.y + a.z * b.z;

}

Listing 2.14 Berechnung des Punktprodukts

Bei der Besprechung des Punktprodukts wurden Sie mit folgender Gleichung bezüglich des Winkels zwischen zwei Richtungsvektoren konfrontiert:

arccos x y arccos x y x y

α = ⋅ = ⋅

⋅ r r r r

r r

Und genau diese Gleichung wollen wir nun einsetzen, um in der Funktion tbVector3Angle den Winkel zwischen zwei Richtungsvektoren zu bestimmen. Der Winkel wird im Bogenmaß ge-liefert. Als Erstes wird das Punktprodukt der Vektoren berechnet. Dieses wird nun durch das

Produkt der beiden Vektorlängen dividiert, wie rechts im Bruch zu sehen ist. Dieses Ergebnis setzen wir in die Funktion acosf ein, die uns den Arcus-Kosinus liefert, also den Winkel zu einem gegebenen Kosinus:

// Berechnung des Winkels zwischen zwei Vektoren (Bogenmaß) inline float tbVector3Angle(const tbVector3& a,

const tbVector3& b) {

return acosf(tbVector3Dot(a, b) /

(tbVector3Length(a) * tbVector3Length(b)));

}

// Das lässt sich noch ein wenig optimieren ...

// Neue Version:

inline float tbVector3Angle(const tbVector3& a, const tbVector3& b) {

return acosf((a.x * b.x + a.y * b.y + a.z * b.z) / // Punktprodukt (sqrtf(a.x * a.x + a.y * a.y + a.z * a.z) * // Länge von Vektor a sqrtf(b.x * b.x + b.y * b.y + b.z * b.z))); // Länge von Vektor b }

Listing 2.15 Berechnung des Winkels zwischen zwei Vektoren

Aber halt, da ist noch eine weitere Optimierung möglich! In der gerade gezeigten Version be-nötigen wir zwei Aufrufe von sqrtf, also zweimal Wurzelziehen, was nicht unbedingt schnell ist. Wir berechnen da die Wurzel eines Terms A (Länge des ersten Vektors) und die Wurzel eines Terms B (Länge des zweiten Vektors). Anschließend werden beide Wurzeln multipli-ziert. Das geht auch einfacher, indem man beide Terme unter eine einzige Wurzel zieht, denn das ist erlaubt! Also:

// Berechnung des Winkels zwischen zwei Vektoren (Bogenmaß) inline float tbVector3Angle(const tbVector3& a,

const tbVector3& b) {

return acosf((a.x * b.x + a.y * b.y + a.z * b.z) / // Punktprodukt

sqrtf((a.x * a.x + a.y * a.y + a.z * a.z) * // Produkt der Vektorlängen (b.x * b.x + b.y * b.y + b.z * b.z))); // “

}

Listing 2.16 Optimierte Berechnung des Winkels

Minimum- und Maximumvektoren

Manchmal ist es hilfreich, den Minimum- oder den Maximumvektor vieler Vektoren zu ken-nen. Der Maximumvektor der drei Vektoren (5, 100, –250), (0, 10, 50) und (1000, 60, 40) ist der Vektor (1000, 100, 50). Man sucht sich also jeweils den größten (Maximum) beziehungs-weise den kleinsten (Minimum) Wert jeder Komponente heraus. Dafür implementieren wir die Funktionen tbVector3Min und tbVector3Max:

// Minimumvektor berechnen

inline tbVector3 tbVector3Min(const tbVector3& a, const tbVector3& b) {

return tbVector3(TB_MIN(a.x, b.x), TB_MIN(a.y, b.y), TB_MIN(a.z, b.z));

}

// Maximumvektor berechnen

inline tbVector3 tbVector3Max(const tbVector3& a, const tbVector3& b) {

return tbVector3(TB_MAX(a.x, b.x), TB_MAX(a.y, b.y), TB_MAX(a.z, b.z));

}

Listing 2.17 Berechnung des Minimum- und des Maximumvektors

Zufallsvektoren

In Computerspielen wird sehr häufig mit Zufallsfunktionen gearbeitet, zum Beispiel wenn es darum geht, Explosionen oder Rauchpartikel anzuzeigen. Durch den Zufall bringt man mehr Abwechslung und mehr Variation ins Spiel. Die Funktion tbVector3Random soll uns einen zu-fälligen normalisierten Vektor liefern. Die Richtung, in die er zeigt, wird durch die Funktion

tbFloatRandom bestimmt.

// Zufälligen normalisierten Vektor berechnen inline tbVector3 tbVector3Random()

{

return tbVector3NormalizeEx(tbVector3(tbFloatRandom(-1.0f, 1.0f), tbFloatRandom(-1.0f, 1.0f), tbFloatRandom(-1.0f, 1.0f)));

}

Listing 2.18 Diese Funktion versorgt uns mit einem zufälligen normalisierten Vektor.

Lineare Interpolation zwischen Positionsvektoren

Angenommen, wir möchten ein 3D-Objekt entlang eines vordefinierten Pfades durch den Raum bewegen. Wir kennen die Position, die es zur Zeit t1 haben soll, und die, die es zur Zeit t2 haben soll. Doch wie ist es nun möglich, die Position des Objekts zu bestimmen, die es ir-gendwann zwischen diesen beiden Zeitpunkten haben soll? Wir nehmen an, dass sich das Ob-jekt zwischen den beiden Punkten geradlinig (linear) bewegt.

Hier hilft eine lineare Interpolation weiter. Man gibt zwei Vektoren an und zusätzlich noch einen Interpolationsfaktor. Dieser wäre in unserem Beispiel mit der Animation gleich bedeu-tend mit der Zeit. Der Interpolationsfaktor nimmt immer nur Werte zwischen 0 und 1 an. Setzt man null ein, dann ist das Ergebnis der Interpolation der erste Vektor, und wenn man 1 ein-setzt, ist das Ergebnis der zweite Vektor. Gibt man 0.5 an, liegt das Ergebnis exakt zwischen den beiden Punkten.

Eine lineare Interpolation mit dem Ergebnisvektor pr zwischen den Vektoren xr und yr mit dem Interpolationsfaktor s kann wie folgt berechnet werden:

( )

p= + ⋅x s y x

r r r r

Der Term

(

y xr r

)

ist dabei sozusagen die „Richtung“ von xr nach yr. Wir sehen: Setzt man für s null ein, fällt die Klammer weg, und das Ergebnis ist der Vektor xr. Setzt man s = 1, dann fällt der Vektor xr weg, und es bleibt nur noch yr stehen.

Dies nun in eine Funktion umzusetzen, ist nicht sehr schwer. Die Funktion soll den Namen

tbVector3InterpolateCoords tragen. „Coords“, weil wir damit Koordinaten, also Positionsvekto-ren interpoliePositionsvekto-ren können.

// Lineare Interpolation zwischen zwei Positionsvektoren inline tbVector3 tbVector3InterpolateCoords(const tbVector3& a, const tbVector3& b, const float s) {

return a + s * (b – a);

}

Listing 2.19 Lineare Interpolation von Positionsvektoren

Die lineare Interpolation ist das einfachste Interpolationsverfahren. Es gibt noch viele weitere, die hauptsächlich dann zum Einsatz kommen, wenn es nicht um geradlinige Strecken geht, sondern um Kurven.

Interpolation von Normalenvektoren

Wenn man nun nicht mehr zwischen Positionsvektoren interpoliert, sondern zwischen Norma-lenvektoren (Richtungsvektoren), dann muss das Verfahren leicht geändert werden. Wir neh-men an, dass die Normalenvektoren die Länge 1 haben.

Beispiel

Wir interpolieren zwischen den Normalenvektoren (1, 0, 0) und (0, 1, 0). Der Interpolationsfaktor sei nun 0.5. Das Ergebnis der linearen Interpolation wäre genau die Mitte, also (0.5, 0.5, 0). Inter-poliert man zwischen zwei Normalenvektoren, so sollte doch eigentlich wieder ein Normalenvek-tor dabei herauskommen. Jedoch hat der VekNormalenvek-tor (0.5, 0.5, 0) nicht mehr die Länge 1, sondern nur noch 0 , was ungefähr 0.71 ist. .5

Was können wir tun, damit der resultierende Vektor wieder ein Normalenvektor ist? Ganz ein-fach: Wir normalisieren ihn! Genau das wird in der Funktion tbVector3InterpolateNormal ge-tan. Wir verwenden hier zur Sicherheit die Funktion tbVector3NormalizeEx, um eine mögliche Division durch Null beim Normalisieren auszuschließen.

// Lineare Interpolation zwischen zwei Normalenvektoren inline tbVector3 tbVector3InterpolateNormal(const tbVector3& a, const tbVector3& b, const float s) {

// Normalisierten Interpolationsvektor berechnen return tbVector3NormalizeEx(a + s * (b – a));

}

Listing 2.20 Interpolation von Normalenvektoren

Wohlgemerkt gibt es noch weitere Interpolationsmethoden, die für viele Zwecke viel besser geeignet sind als diese hier, die jedoch auch mathematisch komplizierter sind.

Vektor ins Logbuch schreiben

Wenn man einmal wieder auf der Suche nach einem heimtückischen Fehler ist, ist es oft hilf-reich, wenn man einen Vektor ins Logbuch schreiben kann, um später nachsehen zu können, ob er „in Ordnung“ ist. Dazu gibt es die Funktion tbWriteVector3ToLog. Man übergibt ihr einen Vektor, und sie schreibt dessen Komponenten und dessen Länge in die Logbuchdatei.

Übersicht der Hilfsfunktionen

Damit wäre die Programmierung der Hilfsfunktionen zum Thema Vektorrechnung erst einmal abgeschlossen. Die folgende Tabelle bietet Ihnen noch einmal einen Überblick.

Tabelle 2.1 Übersicht der Vektorhilfsfunktionen

Funktionsname Ausgabetyp Beschreibung

tbVector3Length float Berechnet die Länge des angegebenen Vektors.

tbVector3LengthSq float Berechnet das Quadrat der Vektorlänge – schneller als tbVector3Length, da das Wurzelziehen entfällt.

Am besten zum Vergleich zweier Vektorlängen ge-eignet!

tbVector3Normalize tbVector3 Normalisiert den angegebenen Vektor, so dass sei-ne Länge gleich 1 ist.

tbVector3NormalizeEx tbVector3 Liefert eine Annäherung an den normalisierten Vek-tor zurück und verhindert eine Division durch Null.

tbVector3Cross tbVector3 Berechnet das Kreuzprodukt zweier Vektoren.

tbVector3Dot float Berechnet das Punktprodukt zweier Vektoren.

tbVector3Angle float Benutzt das Punktprodukt und den Arcus-Kosinus, um den Winkel zwischen zwei Vektoren zu bestim-men (liefert Werte im Bogenmaß – mit dem Makro TB_RAD_TO_DEG in Grad umwandelbar).

tbVector3Min tbVector3 Berechnet das Minimum zweier Vektoren (kompo-nentenweises Minimum).

tbVector3Max tbVector3 Berechnung des Maximums zweier Vektoren

tbVector3Random tbVector3 Berechnet mit Hilfe der Zufallsfunktion tbFloatRandom einen zufälligen normalisierten Vektor, der in jede Richtung zeigen kann.

tbVector3InterpolateCoords tbVector3 Interpoliert linear zwischen zwei Positionsvektoren mit dem angegebenen Interpolationsfaktor (zwi-schen 0 und 1), der als dritter Parameter angege-ben wird. Parameter 1 und 2 sind die beiden Vekto-ren. Beim Faktor 0.5 liegt der resultierende Vektor genau in der Mitte, bei 0 liegt er direkt beim ersten Vektor und bei 1 direkt beim zweiten und so weiter.

tbVector3InterpolateNormal tbVector3 Interpoliert linear zwischen zwei normalisierten Richtungsvektoren und normalisiert das Ergebnis.

Ansonsten wie tbVector3InterpolateCoords. tbWriteVector3ToLog Schreibt den angegebenen Vektor (Komponenten

und Länge) in die Logbuchdatei.

Der folgende Beispielcode demonstriert die Verwendung dieser Funktionen.

// Länge, Quadrat der Länge und Normalisieren eines Vektors tbVector3 a(3.0f, 4.0f, 0.0f);

float f = tbVector3Length(a); // f erhält die Länge von Vektor a (5).

f = tbVector3LengthSq(a); // f erhält das Längenquadrat (25).

a = tbVector3Normalize(a); // Vektor a wird normalisiert (Länge = 1).

// Kreuzprodukt

a = tbVector3(1.0f, 0.0f, 0.0f); // x-Achse (1, 0, 0) tbVector3 b(0.0f, 1.0f, 0.0f); // y-Achse (0, 1, 0)

tbVector3 c(tbVector3Cross(a, b)); // c steht jetzt senkrecht auf a und b.

// Punktprodukt und Winkel

a = tbVector3(1.0f, 0.0f, 0.0f); // x-Achse (1, 0, 0) b = tbVector3(0.0f, 0.0f, 1.0f); // z-Achse (0, 0, 1)

f = tbVector3Dot(a, b); // Kosinus des Winkels in f speichern f = tbVector3Angle(a, b); // Den Winkel direkt berechnen (Bogenmaß) f = TB_RAD_TO_DEG(f); // Bogenformat in Grad umwandeln

// Minimalen und maximalen Vektor bestimmen a = tbVector3(1.0f, 5.0f, 17.0f);

b = tbVector3(6.0f, 18.0f, 2.0f);

c = tbVector3Min(a, b); // c ist jetzt (1, 5, 2) c = tbVector3Max(a, b); // c ist jetzt (6, 18, 17) // Einen zufälligen Bewegungsvektor erstellen

// (zufällige Richtung und zufällige Geschwindigkeit zwischen 1 und 100) a = tbVector3Random(); // Zufälligen Richtungsvektor erzeugen f = tbFloatRandom(1.0f, 100.0f); // Zufällige Geschwindigkeit

a *= f; // Geschwindigkeit einmultiplizieren // Positionsvektoren linear interpolieren

a = tbVector3(100.0f, 0.0f, 0.0f);

b = tbVector3(200.0f, 0.0f, 10.0f);

c = tbVector3InterpolateCoords(a, b, 0.0f); // c = a

c = tbVector3InterpolateCoords(a, b, 0.5f); // c = (150.0f, 0.0f, 5.0f) c = tbVector3InterpolateCoords(a, b, 1.0f); // c = b

// Vektor c ins Logbuch schreiben tbWriteVector3ToLog(c);

Listing 2.21 Anwendung der Hilfsfunktionen für die tbVector3-Klasse