Zuvor haben wir in dieser Lektion erklärt, dass Vektoren (oder Punkte) als Matrizen (eine Zeile, drei Spalten) aufgeschrieben werden können. Wir hätten sie aber auch als Matrizen (drei Zeilen, eine Spalte) darstellen können. Technisch gesehen sind diese beiden Arten, Punkte und Vektoren als Matrizen auszudrücken, vollkommen gültig, und die Wahl der einen oder anderen Art ist nur eine Frage der Konvention.
Vektor geschrieben als Matrix: \( V=\begin{bmatrix}x & y & z\end{bmatrix}\)
Vektor geschrieben als Matrix:
Im ersten Beispiel (Matrix) haben wir unseren Vektor oder Punkt in der so genannten Zeilen-Hauptreihenfolge ausgedrückt: Der Vektor (oder Punkt) wird als eine Reihe von drei Zahlen geschrieben. Im zweiten Beispiel sagen wir, dass Punkte oder Vektoren in spaltenmäßiger Ordnung geschrieben werden: wir schreiben die drei Koordinaten des Vektors oder Punktes vertikal, als eine Spalte.
Erinnern Sie sich daran, dass wir Punkte und Vektoren als Matrizen ausdrücken, um sie mit Transformationsmatrizen zu multiplizieren (der Einfachheit halber werden wir mit und nicht mit Matrizen arbeiten). Wir haben auch gelernt, dass wir Matrizen nur multiplizieren können, wenn die Anzahl der Spalten der linken Matrix und die Anzahl der Zeilen der rechten Matrix gleich sind. Mit anderen Worten: Die Matrizen und können miteinander multipliziert werden, die Matrizen und jedoch nicht. Beachten Sie, dass wir einen Vektor mit einer Matrix multiplizieren können, wenn wir ihn als Matrix schreiben (vorausgesetzt, dass diese Matrix innerhalb der Multiplikation rechts steht), aber wenn wir diesen Vektor als Matrix schreiben, können wir ihn nicht mit einer Matrix multiplizieren. Dies wird in den folgenden Beispielen veranschaulicht. Die inneren Dimensionen (3 und 3) der an der Multiplikation beteiligten Matrizen sind die gleichen (in grün), so dass diese Multiplikation gültig ist (und das Ergebnis ein transformierter Punkt ist, der in Form einer Matrix geschrieben wird):
$$* = \begin{bmatrix}x & y & z\end{bmatrix} * \begin{bmatrix}c_{00}&c_{01}&{c_{02}}\\c_{10}&c_{11}&{c_{12}}\\c_{20}&c_{21}&{c_{22}}\\\end{bmatrix} =\begin{bmatrix}x’&y’&z’\end{bmatrix}$$
The inneren Dimensionen (1 und 3) der an der Multiplikation beteiligten Matrizen sind nicht gleich (in rot), daher ist diese Multiplikation nicht möglich:
$$* \rightarrow \begin{bmatrix}x\\ y\\z\end{bmatrix} * \begin{bmatrix} c_{00}&c_{01}&{c_{02}}\\ c_{10}&c_{11}&{c_{12}}\\ c_{20}&c_{21}&{c_{22}}\\ \end{bmatrix}$$
Was tun wir also? Die Lösung des Problems besteht nicht darin, den Vektor oder den Punkt mit der Matrix zu multiplizieren, sondern die Matrix M mit dem Vektor V. Mit anderen Worten, wir verschieben den Punkt oder den Vektor innerhalb der Multiplikation nach rechts:
$$* \rightarrow \begin{bmatrix} c_{00}&c_{01}&{c_{02}}\\ c_{10}&c_{11}&{c_{12}}\ c_{20}&c_{21}&{c_{22}}\ \end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} = \begin{bmatrix}x’\\y’\\z’\end{bmatrix}$$
Beachte, dass das Ergebnis dieser Operation ein transformierter Punkt in Form einer Matrix ist. Wir beginnen also mit einem Punkt und enden mit einem transformierten Punkt, was genau das ist, was wir wollen. Das Problem ist gelöst. Zusammenfassend lässt sich sagen, dass wir, wenn wir aus Konvention Vektoren oder Punkte in Zeilen-Dur-Reihenfolge () ausdrücken wollen, den Punkt auf die linke Seite der Multiplikation und den Punkt auf die rechte Seite des Multiplikationszeichens setzen müssen. Dies wird in der Mathematik als Links- oder Vormultiplikation bezeichnet. Wenn Sie die Vektoren stattdessen in Spalten-Dur-Reihenfolge schreiben wollen (), muss die Matrix auf der linken Seite der Multiplikation stehen und der Vektor oder Punkt auf der rechten Seite. Dies nennt man eine Rechts- oder Postmultiplikation.
Wir müssen vorsichtig sein, wie diese Begriffe tatsächlich verwendet werden. In der Maya-Dokumentation heißt es zum Beispiel: „Die Matrizen werden in Maya nachmultipliziert. Um zum Beispiel einen Punkt P vom Objektraum in den Weltraum (P‘) zu transformieren, müsste man mit der worldMatrix nachmultiplizieren. (P‘ = P x WM)“, was verwirrend ist, weil es sich eigentlich um eine Vormultiplikation handelt, aber in diesem speziellen Fall von der Position der Matrix in Bezug auf den Punkt die Rede ist. Das ist eigentlich eine falsche Verwendung der Terminologie. Es hätte heißen müssen, dass in Maya Punkte und Vektoren als Zeilen-Hauptvektoren ausgedrückt werden und dass sie daher vormultipliziert werden (was bedeutet, dass der Punkt oder Vektor bei der Multiplikation vor der Matrix erscheint).
Die folgende Tabelle fasst die Unterschiede zwischen den beiden Konventionen zusammen (wobei P, V und M jeweils für Punkt, Vektor und Matrix stehen).
Reihen-Hauptreihenfolge |
\(P/V=\begin{bmatrix}x & y & z\end{bmatrix}\) |
Links- oder VorMultiplikation |
P/V * M |
Spalten-Hauptordnung |
\(P/V=\begin{bmatrix}x \\\ y \\ z\end{bmatrix}\) |
Rechts- oder Post-Multiplikation |
M * P/V |
Nachdem wir nun diese beiden Konventionen kennengelernt haben, könnte man sich fragen: „Geht es dabei nicht nur darum, Dinge auf Papier zu schreiben?“. Wir wissen, wie man das Produkt von zwei Matrizen A und B berechnet: Man multipliziert jeden Koeffizienten in der aktuellen Zeile von A mit den zugehörigen Elementen in der aktuellen Spalte von B und addiert das Ergebnis. Wenden wir diese Formel unter Verwendung der beiden Konventionen an und vergleichen wir die Ergebnisse:
Zeilen-Hauptordnung | |
---|---|
$${ \begin{bmatrix}x & y & z\end{bmatrix} * \begin{bmatrix}a & b & c \\\d & e & f \\\g & h & i\end{bmatrix} }$$ |
$${ \begin{array}{l}x‘ = x * a + y * d + z * g\\y‘ = x * b + y * e + z * h\\z‘ = x * c + y * f + z * i\end{array} }$$ |
Säulen-Hauptreihenfolge | |
$${ \begin{bmatrix} a & b & c \\d & e & f \\g & h & i \end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} }$$ |
$${\begin{array}{l}x‘ = a * x + b * y + c * z\\\y‘ = d * x + e * y + f * z\\z‘ = g * x + h * y + i * z\end{array} }$$ |
Die Multiplikation eines Punktes oder eines Vektors mit einer Matrix sollte das gleiche Ergebnis liefern, unabhängig davon, ob wir eine zeilen- oder eine spaltenweise Hauptreihenfolge verwenden. Wenn Sie eine 3D-Anwendung verwenden, um einen Punkt um einen bestimmten Winkel um die z-Achse zu drehen, erwarten Sie, dass sich der Punkt nach der Drehung in einer bestimmten Position befindet, unabhängig davon, welche interne Konvention der Entwickler zur Darstellung von Punkten und Vektoren verwendet hat. Wie Sie jedoch aus der obigen Tabelle ersehen können, würde die Multiplikation eines Punktes (oder Vektors) mit Zeilen- und Spalten-Dur mit derselben Matrix eindeutig nicht dasselbe Ergebnis liefern. Um wieder auf die Beine zu kommen, müssten wir die Matrix, die bei der Multiplikation mit dem Spaltenmajor verwendet wird, transponieren, um sicher zu sein, dass x‘, y‘ und z‘ gleich sind (wenn Sie sich erinnern müssen, was die Transponierung einer Matrix ist, lesen Sie das Kapitel über Matrixoperationen). Hier ist, was wir erhalten:
Row-major order | |
---|---|
$${ \begin{bmatrix}x & y & z\end{bmatrix} * \begin{bmatrix}a & b & c \\\d & e & f \\\g & h & i\end{bmatrix} }$$ |
$${\begin{array}{l}x‘ = x * a + y * d + z * g\\y‘ = x * b + y * e + z * h\\z‘ = x * c + y * f + z * i\end{array} }$$ |
Säulen-Hauptordnung | |
$${ \begin{bmatrix} a & d & g \\b & e & h \\\c & f & i\end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} }$$ |
$${\begin{array}{l}x‘ = a * x + d * y + g * z\\\y‘ = b * x + e * y + h * z\\z‘ = c * x + f * y + i * z\end{array} }$$ |
Zusammenfassend lässt sich sagen, dass beim Übergang von der Zeilen-Dur-Ordnung zur Spalten-Dur-Ordnung nicht nur der Punkt oder Vektor und die Matrix bei der Multiplikation vertauscht werden müssen, sondern auch die Matrix transponiert werden muss, um sicherzustellen, dass beide Konventionen das gleiche Ergebnis liefern (und umgekehrt).
Aus diesen Beobachtungen können wir ersehen, dass jede Reihe von Transformationen, die auf einen Punkt oder einen Vektor angewendet werden, wenn eine Zeilen-Dur-Konvention verwendet wird, in sequentieller Reihenfolge (oder Lesereihenfolge) geschrieben werden kann. Stellen Sie sich zum Beispiel vor, Sie wollen den Punkt P mit der Matrix T verschieben, ihn dann mit Rz um die z-Achse und anschließend mit Ry um die y-Achse drehen. Sie können schreiben:
$$P’=P * T * R_z * R_y$$
Wenn Sie eine Spalten-Dur-Notation verwenden würden, müssten Sie die Transformation in umgekehrter Reihenfolge aufrufen (was man vielleicht als kontraintuitiv empfinden könnte):
$$P’=R_y * R_z * T * P$$
Sie könnten also denken: „Es muss einen Grund geben, ein System dem anderen vorzuziehen“. Tatsächlich sind beide Konventionen korrekt und führen zum gleichen Ergebnis, aber aus technischen Gründen werden in Mathematik- und Physiktexten Vektoren im Allgemeinen als Spaltenvektoren behandelt.
Die Reihenfolge der Transformation, wenn wir spaltengroße Matrizen verwenden, ähnelt in der Mathematik eher der Art und Weise, wie wir Funktionsauswertung und -komposition schreiben.
Die zeilengroße Matrixkonvention macht Matrizen jedoch einfacher zu lehren, was der Grund dafür ist, dass wir sie für Scratchapixel verwenden (ebenso wie Maya, DirectX. Sie sind auch als Standard in den RenderMan-Spezifikationen definiert). Einige 3D-APIs, wie OpenGL, verwenden jedoch eine Spalten-Hauptkonvention.
Implikation in der Codierung: Beeinflusst sie die Leistung?
Es gibt noch einen weiteren, potenziell sehr wichtigen Aspekt, der zu berücksichtigen ist, wenn Sie sich zwischen Zeilen- und Spaltenmajor entscheiden müssen, aber das hat nichts mit den Konventionen selbst zu tun und damit, wie praktisch die eine gegenüber der anderen ist. Es hat mehr mit dem Computer und seiner Arbeitsweise zu tun. Denken Sie daran, dass wir es mit Matrizen zu tun haben werden. Normalerweise sieht die Implementierung einer Matrix in C++ so aus:
Wie Sie sehen können, werden die 16 Koeffizienten der Matrix in einem zweidimensionalen Array von Floats (oder Doubles, je nach der benötigten Genauigkeit) gespeichert. Unsere C++-Matrixklasse ist eine Vorlage). Das bedeutet, dass die 16 Koeffizienten im Speicher in folgender Weise angeordnet sind: c00, c01, c02, c03, c10, c11, c12, c13, c20, c21, c22, c23, c30, c31, c32, c33. Mit anderen Worten: Sie sind im Speicher zusammenhängend angeordnet. Sehen wir uns nun an, wie auf diese Koeffizienten bei einer Vektor-Matrix-Multiplikation zugegriffen wird, bei der die Vektoren in Zeilen-Dur-Reihenfolge geschrieben werden:
Wie Sie sehen können, wird auf die Elemente der Matrix für x‘ nicht sequentiell zugegriffen. Mit anderen Worten, um x‘ zu berechnen, benötigen wir den 1., 5. und 9. Float der Matrix 16 Floats Array. Zur Berechnung von y‘ müssen wir auf die 2., 6. und 10. Gleitkommazahl dieses Arrays zugreifen. Und schließlich benötigen wir für z‘ die 3., 7. und 11. In der Welt der Datenverarbeitung ist der Zugriff auf Elemente aus einem Array in einer nicht sequentiellen Reihenfolge nicht unbedingt eine gute Sache. Es kann sogar die Cache-Leistung der CPU verschlechtern. Wir wollen hier nicht zu sehr ins Detail gehen, aber sagen wir einfach, dass der nächstgelegene Speicher, auf den die CPU zugreifen kann, Cache genannt wird. Auf diesen Cache kann sehr schnell zugegriffen werden, er kann aber nur eine sehr begrenzte Anzahl von Daten speichern. Wenn die CPU auf bestimmte Daten zugreifen muss, prüft sie zunächst, ob diese im Cache vorhanden sind. Ist dies der Fall, kann die CPU sofort auf diese Daten zugreifen (Cache-Treffer), ist dies nicht der Fall (Cache-Fehlschlag), muss sie zunächst einen Eintrag im Cache erstellen und dann die Daten aus dem Hauptspeicher dorthin kopieren. Dieser Vorgang ist natürlich zeitaufwändiger, als wenn die Daten bereits im Cache vorhanden sind, so dass wir im Idealfall Cache-Misses so weit wie möglich vermeiden wollen. Zusätzlich zum Kopieren der einzelnen Daten aus dem Hauptspeicher kopiert die CPU auch einen Teil der Daten, die sich direkt daneben befinden (z. B. die nächsten 24 Bytes), da die Hardware-Ingenieure davon ausgingen, dass Ihr Code, wenn er beispielsweise auf ein Element eines Arrays zugreifen muss, wahrscheinlich kurz darauf auf die folgenden Elemente zugreifen wird. In der Tat durchlaufen wir in Programmen oft Schleifen über die Elemente eines Arrays in sequentieller Reihenfolge, so dass diese Annahme wahrscheinlich zutrifft. Auf unser Matrixproblem angewandt, kann der Zugriff auf die Koeffizienten der Matrix in nicht sequentieller Reihenfolge daher ein Problem darstellen. Wenn man davon ausgeht, dass die CPU die angeforderte Gleitkommazahl plus die 3 Gleitkommazahlen daneben in den Cache lädt, könnte unsere derzeitige Implementierung zu vielen Cache-Misses führen, da die zur Berechnung von x‘, y‘ und z‘ verwendeten Koeffizienten im Array 5 Gleitkommazahlen voneinander entfernt sind. Verwendet man hingegen eine Notation in Spalten-Hauptreihenfolge, so ist für die Berechnung von x‘ beispielsweise ein Zugriff auf das erste, zweite und dritte Element der Matrix erforderlich.
Der Zugriff auf die Koeffizienten erfolgt in sequentieller Reihenfolge, was auch bedeutet, dass wir den Cache-Mechanismus der CPU gut nutzen (nur 3 Cache-Misses anstelle von 9 in unserem Beispiel). Zusammenfassend lässt sich sagen, dass aus programmiertechnischer Sicht die Implementierung unserer Punkt- oder Vektor-Matrix-Multiplikation unter Verwendung einer Spalten-Hauptreihenfolge-Konvention leistungsfähiger sein könnte als die Version unter Verwendung der Zeilen-Hauptreihenfolge-Konvention. In der Praxis konnten wir jedoch nicht nachweisen, dass dies tatsächlich der Fall war (wenn Sie Ihr Programm mit den Optimierungsflags -O, -O2 oder -O3 kompilieren, kann der Compiler die Arbeit für Sie erledigen, indem er Schleifen über mehrdimensionale Arrays optimiert), und wir haben die Version mit der Zeilen-Hauptreihenfolge erfolgreich verwendet, ohne dass es zu Leistungseinbußen im Vergleich zu einer Version desselben Codes mit einer Implementierung mit Spalten-Hauptreihenfolge kam.
Zeilenmajor- und Spaltenmajor-Reihenfolge im Rechnen
Der Vollständigkeit halber sei noch erwähnt, dass die Begriffe Zeilenmajor- und Spaltenmajor-Reihenfolge auch im Rechnen verwendet werden können, um die Art und Weise zu beschreiben, wie Elemente mehrdimensionaler Arrays im Speicher angeordnet werden. Bei der zeilenweisen Anordnung werden die Elemente eines mehrdimensionalen Arrays nacheinander von links nach rechts und von oben nach unten angeordnet. Dies ist die von C/C++ verwendete Methode. Zum Beispiel könnte die Matrix:
$$M = \begin{bmatrix}1&2&3\\\4&5&6\end{bmatrix}$$
in C/C++ geschrieben werden als:
und die Elemente dieses Arrays würden zusammenhängend im linearen Speicher angeordnet werden als:
In der Spalten-Hauptreihenfolge, die von Sprachen wie FORTRAN und MATLAB verwendet wird, werden die Elemente der Matrix von oben nach unten, von links nach rechts, im Speicher abgelegt. Bei Verwendung desselben Matrix-Beispiels würden die Elemente der Matrix auf folgende Weise im Speicher gespeichert (und aufgerufen) werden:
Die Kenntnis der Anordnung der Elemente einer Matrix im Speicher ist besonders wichtig, wenn Sie versuchen, mit Hilfe von Zeiger-Offset und for-Schleifen-Optimierung auf sie zuzugreifen (wir haben bereits in diesem Kapitel erklärt, dass dies die CPU-Cache-Leistung beeinträchtigen kann). Da wir uns jedoch nur mit C/C++ als Programmiersprache befassen werden, ist die Spalten-Hauptreihenfolge (angewandt auf das Rechnen) für uns nicht von großem Interesse. Wir erwähnen nur, was die Begriffe in der Informatik bedeuten, damit Sie wissen, dass sie je nach dem Kontext, in dem sie verwendet werden, zwei verschiedene Dinge beschreiben können. Sie sollten darauf achten, sie nicht zu verwechseln. Im Kontext der Mathematik beschreiben sie, ob man Vektoren (oder Punkte) als Koordinatenzeilen oder als Spalten behandelt, und im Kontext der Informatik beschreiben sie die Art und Weise, wie eine bestimmte Programmiersprache Elemente eines mehrdimensionalen Arrays (was Matrizen sind) im Speicher speichert und darauf zugreift.
OpenGL ist in dieser Hinsicht ein interessanter Fall. Als GL ursprünglich geschaffen wurde, entschieden sich die Entwickler für die Konvention des Zeilen-Dur-Vektors. Die Entwickler, die OpenGL erweitert haben, waren jedoch der Meinung, dass sie zum spaltenproportionalen Vektor zurückkehren sollten, was sie auch taten. Aus Kompatibilitätsgründen wollten sie jedoch den Code für die Punkt-Matrix-Multiplikation nicht ändern und beschlossen stattdessen, die Reihenfolge zu ändern, in der die Koeffizienten der Matrix im Speicher abgelegt wurden. Mit anderen Worten: OpenGL speichert die Koeffizienten in spaltenmajorierter Reihenfolge, was bedeutet, dass die Übersetzungskoeffizienten m03, m13 und m23 aus einer Matrix mit spaltenmajoriertem Vektor die Indizes 13, 14 und 15 im Float-Array haben, ebenso wie die Übersetzungskoeffizienten m30, m31 und m32 aus einer Matrix mit zeilenmajoriertem Vektor.
Zusammenfassung
Die Unterschiede zwischen den beiden Konventionen sind in der folgenden Tabelle zusammengefasst:
Zeilen-Hauptvektor (Mathematik) | Spalten-Hauptvektor (Mathematik) |
---|---|
\(P/V=\begin{bmatrix}x & y & z\end{bmatrix}\) |
\(P/V=\begin{bmatrix}x \\\ y \\\ z\end{bmatrix}\) |
Vor-.Multiplikation \(vM\) |
Post-Multiplikation \(Mv\) |
Aufrufreihenfolge und die Reihenfolge der Anwendung der Transformationen sind gleich: „Nimm P, transformiere nach T, transformiere nach Rz, transformiere nach Ry“ wird geschrieben als \(P’=P*T*R_z*R_y\) |
Die Aufrufreihenfolge ist die Umkehrung der Reihenfolge, in der die Transformationen angewendet werden: „Nimm P, transformiere nach T, transformiere nach Rz, transformiere nach Ry“ wird geschrieben als \(P’=R_y*R_z*T*P\) |
API: Direct X, Maya |
API: OpenGL, PBRT, Blender |
Die Zeilen der Matrix stellen die Basen (oder Achsen) eines Koordinatensystems dar (rot: x-Achse, grün: y-Achse, blau:z-Achse) $${\begin{bmatrix} \color{red}{c_{00}}& \color{red}{c_{01}}&\color{red}{c_{02}}&0\\ \color{green}{c_{10}}& \color{green}{c_{11}}&\color{green}{c_{12}}&0\\ \color{blue}{c_{20}}& \color{blue}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1 \end{bmatrix} } $$ |
Die Spalten der Matrix stellen die Basen (oder Achsen) eines Koordinatensystems dar (rot: x-Achse, grün: y-Achse, blau:z-Achse) $${ \begin{bmatrix} \color{red}{c_{00}}& \color{green}{c_{01}}&\color{blue}{c_{02}}&0\\ \color{red}{c_{10}}& \color{green}{c_{11}}&\color{blue}{c_{12}}&0\\ \color{red}{c_{20}}& \color{green}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1\end{bmatrix} }$$ |
Die Übersetzungswerte werden in den Elementen c30, c31 und c32 gespeichert. $${\begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\Tx&Ty&Tz&1\end{bmatrix} }$$ |
Die Übersetzungswerte werden in den Elementen c03, c13 und c23 gespeichert. $${\begin{bmatrix}1&0&0&Tx\\0&1&0&Ty\\0&0&1&Tz\0&0&0&1\end{bmatrix} }$$ |
Transponieren Sie die Matrix, um sie als spaltengeordnete Matrix zu verwenden |
Transponieren Sie die Matrix, um sie als zeilengeordnete Matrix zu verwendenHauptmatrix zu verwenden |
Zeilen-Hauptmatrix (Computing) | Spalten-Hauptmatrix (Computing) |
API: Direct X, Maya, PBRT |
API: OpenGL |
Ein Leser hat eine Frage auf Stackoverflow gepostet, die die obige Tabelle als verwirrend bezeichnet. Das Thema ist verwirrend, und trotz unserer Bemühungen, etwas Licht in die Sache zu bringen, sind viele Leute immer noch verwirrt. Wir dachten, unsere Antwort auf Stackoverflow könnte hoffentlich einen weiteren Einblick in die Frage bringen.
Sie haben die Theorie (was Sie in der Mathematik mit Stift und Papier machen) und was Sie mit Ihrer Implementierung (C++) machen. Das sind zwei verschiedene Probleme.
Mathematik: Man kann zwei Notationen verwenden, entweder Spalten- oder Zeilen-Dur. Bei Zeilen-Dur-Vektoren müssen Sie auf dem Papier die Vektor-Matrix-Multiplikation vM schreiben, wobei v der Zeilenvektor (1×4) und M Ihre 4×4-Matrix ist. Warum? Weil man mathematisch gesehen nur * schreiben kann, und nicht andersherum. Ähnlich verhält es sich, wenn Sie eine Spalte verwenden, dann muss der Vektor vertikal geschrieben werden, oder in der Notation (4 Zeilen, 1 Spalte). Die Multiplikation mit einer Matrix kann also nur wie folgt geschrieben werden: . Beachten Sie, dass die Matrix dem Vektor vorangestellt wird: Mv. Die erste Schreibweise wird als Links- oder Vormultiplikation bezeichnet (weil der Vektor auf der linken Seite des Produkts steht) und die zweite (Mv) als Rechts- oder Nachmultiplikation (weil der Vektor auf der rechten Seite des Produkts steht). Wie du siehst, leiten sich die Begriffe davon ab, ob der Vektor auf der linken Seite (vor oder „pre“) oder auf der rechten Seite (nach oder „post“) der Matrix steht.
Wenn du nun einen Vektor (oder einen Punkt) transformieren willst, musst du auf die Reihenfolge der Multiplikation achten, wenn du sie auf dem Papier aufschreibst. Wenn man etwas mit der Matrix T übersetzen, dann mit R drehen und dann mit S skalieren will, dann muss man in einer Spalten-Dur-Welt v‘ = S * R * T * v schreiben. In einer Zeilen-Dur-Welt muss man v‘ = v * T * R * S schreiben.
Das ist für die Theorie. Nennen wir das die Zeilen-/Spalten-Vektor-Konvention.
Computer: Dann kommt der Punkt, an dem Sie sich entscheiden, dies in C++ zu implementieren. Das Gute daran ist, dass C++ Ihnen nichts vorschreibt. Sie können die Werte der Koeffizienten Ihrer Matrix im Speicher so abbilden, wie Sie wollen, und Sie können den Code zur Durchführung einer Matrixmultiplikation mit einer anderen Matrix so schreiben, wie Sie wollen. Sie müssen klar unterscheiden zwischen der Art und Weise, wie Sie die Koeffizienten im Speicher abbilden, und den Konventionen, die Sie aus mathematischer Sicht für die Darstellung Ihrer Vektoren verwenden müssen. Dies sind zwei unabhängige Probleme. Nennen wir diesen Teil das Zeilen-/Spalten-Hauptlayout.
Sie können z.B. eine Matrixklasse als ein Array von z.B. 16 zusammenhängenden Floats deklarieren. Das ist in Ordnung. Wobei die Koeffizienten m14, m24, m34 den Übersetzungsteil der Matrix (Tx, Ty, Tz) darstellen, so dass Sie davon ausgehen, dass Ihre „Konvention“ zeilenmajor ist, obwohl Sie angewiesen werden, die OpenGL-Matrixkonvention zu verwenden, die spaltenmajor sein soll. Die mögliche Verwirrung rührt daher, dass die Zuordnung der Koeffizienten im Speicher sich von der mentalen Darstellung unterscheidet, die Sie sich von einer „spaltengroßen“ Matrix machen. Sie kodieren „Zeile“, sollten aber (aus mathematischer Sicht) „Spalte“ verwenden, daher die Schwierigkeit, zu erkennen, ob Sie etwas richtig oder falsch machen.
Wichtig ist, dass Sie eine Matrix als Darstellung eines Koordinatensystems betrachten, das durch drei Achsen und eine Translation definiert ist. Wo und wie Sie diese Daten im Speicher ablegen, ist Ihnen völlig freigestellt. Angenommen, die drei Vektoren, die die drei Achsen des Koordinatensystems darstellen, heißen AX(x,y,z), AY(x,y,z), AZ(x,y,z), und der Translationsvektor wird mit (Tx, Ty, Tz) bezeichnet, dann ergibt sich bei Verwendung eines Spaltenvektors mathematisch gesehen:
$$M = \begin{bmatrix} AXx & AYx & AZx & Tx\\\ AXy & AYy & AZy & Ty \\\ AXz & AYz & AZz & Tz \\ 0 & 0 & 1 & 1\end{bmatrix}$$
Die Achsen des Koordinatensystems werden senkrecht geschrieben. Wenn man nun, wenn man Zeilenmajor verwendet:
$$M = \begin{bmatrix} AXx & AXy & AXz & 0\\\ AYx & AYy & AYz & 0 \\\ AZx & AZy & AZz & 0 \\\ Tx & Ty & Tz & 1\end{bmatrix}$$
Die Achsen des Koordinatensystems werden horizontal geschrieben. Das Problem in der Computerwelt ist nun, wie man diese Koeffizienten im Speicher ablegen kann. Das kann man auch so machen:
Sagt Ihnen das aber, welche Konvention Sie verwenden? Nein. Du kannst auch schreiben:
oder:
Auch das gibt keinen besonderen Hinweis darauf, welche „mathematische“ Konvention man verwendet. Sie speichern einfach 16 Koeffizienten auf unterschiedliche Weise im Speicher, und das ist völlig in Ordnung, solange Sie wissen, auf welche Weise das geschieht, damit Sie später entsprechend darauf zugreifen können. Denken Sie daran, dass ein Vektor, der mit einer Matrix multipliziert wird, denselben Vektor ergibt, egal ob Sie eine zeilen- oder spaltenweise mathematische Notation verwenden. Es kommt also darauf an, dass du die (x,y,z)-Koordinaten deines Vektors mit den richtigen Koeffizienten aus der Matrix multiplizierst, was die Kenntnis darüber voraussetzt, wie „du“ dich entschieden hast, den Matrixkoeffizienten im Speicher zu speichern:
Wir haben diese Funktion geschrieben, um die Tatsache zu unterstreichen, dass das Ergebnis der Vektor * Matrix-Multiplikation unabhängig von der verwendeten Konvention nur eine Multiplikation und eine Addition zwischen den Eingangskoordinaten des Vektors und den Achsenkoordinaten AX, AY und AZ des Koordinatensystems ist (unabhängig von der verwendeten Notation und der Art und Weise, wie man sie im Speicher ablegt). Wenn Sie:
Sie müssen aufrufen:
Wenn Sie:
Sie müssen aufrufen:
Sagt Ihnen das, welche Konvention Sie verwenden? Nein. Man muss nur die richtigen Koeffizienten an den richtigen Stellen aufrufen, wenn man eine Multiplikation von vec * mat durchführt. Und das ist auch schon alles, so verwirrend das auch sein mag. Bei der Multiplikation mat * mat liegen die Dinge etwas anders. Sie können davon ausgehen, dass die Reihenfolge, in der Sie die Matrizen multiplizieren, nicht dieselbe ist. R * S * T ist also nicht dasselbe wie T * S * R. Die Reihenfolge spielt tatsächlich eine Rolle. Wenn Sie nun wieder „row major“ verwenden, müssen Sie mathematisch schreiben:
wobei ml die linke Matrix und mr die rechte ist: mt = ml * mr. Beachten Sie jedoch, dass wir keine Klammern für die Zugriffsindizes verwendet haben, weil wir nicht den Eindruck erwecken wollen, dass wir auf Elemente zugreifen, die in einem 1D-Array gespeichert sind. Wir sprechen hier nur über die Koeffizienten von Matrizen, wie sie auf dem Papier stehen. Wenn Sie dies in C++ schreiben wollen, dann hängt alles davon ab, wie Sie Ihre Koeffizienten im Speicher abgelegt haben, wie oben vorgeschlagen.