Eine Matrix ist eine Tabelle aus Zahlen, die einen Punkt dreht, streckt, verschiebt oder auf den Bildschirm bringt, so wie in 3D-Grafik mit WebGL und in Shadern, und die in Gaussian Splatting die Form eines Splats beschreibt. Hier steht, wie man mit ihr rechnet was die einzelnen Matrizen tun und wie Quaternionen eine Drehung in vier Zahlen speichern. Mit Punkten und Richtungen selbst geht es unter Vektoren.
Eine Matrix sind Zahlen in Zeilen und Spalten. Eine 2 × 2-Matrix hat zwei Zeilen und zwei Spalten:
$$M = \begin{pmatrix} a & b \\ c & d \end{pmatrix}$$
Mal einen Vektor genommen, gibt sie einen neuen Vektor: Jede Zeile wird mit dem Vektor multipliziert wie beim Skalarprodukt, Zahl für Zahl, und die Produkte zusammengezählt.
$$\begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} a x + b y \\ c x + d y \end{pmatrix}$$
Zum Beispiel macht \(\begin{pmatrix} 2 & 0 \\ 0 & 1 \end{pmatrix}\) aus \((3, 4)\) den Punkt \((6, 4)\): Alles wird doppelt so breit, die Höhe bleibt.
Die erste Spalte ist, wohin der Punkt \((1, 0)\) kommt, die zweite, wohin \((0, 1)\) kommt. Weil jeder andere Punkt aus diesen beiden zusammengesetzt ist, weiß man damit, was die Matrix mit allen Punkten tut. Wer eine Matrix bauen will, überlegt sich also, wohin die Achsen sollen, und schreibt sie als Spalten hin.
Um den Winkel \(\theta\) gegen den Uhrzeigersinn drehen, strecken und spiegeln:
| was | Matrix in 2D | was mit den Achsen passiert |
|---|---|---|
| nichts (Einheitsmatrix) | \(\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}\) | \((1, 0)\) bleibt |
| drehen um \(\theta\) | \(\begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}\) | \((1, 0)\) wird \((\cos\theta, \sin\theta)\) |
| strecken um \(s_x\) und \(s_y\) | \(\begin{pmatrix} s_x & 0 \\ 0 & s_y \end{pmatrix}\) | \((1, 0)\) wird \((s_x, 0)\) |
| an der y-Achse spiegeln | \(\begin{pmatrix} -1 & 0 \\ 0 & 1 \end{pmatrix}\) | \((1, 0)\) wird \((-1, 0)\) |
| scheren | \(\begin{pmatrix} 1 & k \\ 0 & 1 \end{pmatrix}\) | \((1, 0)\) bleibt, \((0, 1)\) wird \((k, 1)\) |
In den Bildern ist das F vorher gestrichelt und nachher blau. Die roten und grünen Pfeile sind, wohin \((1, 0)\) und \((0, 1)\) kommen: die beiden Spalten der Matrix.
Im Raum dreht man um eine Achse, und die Zahl auf dieser Achse bleibt. Um die y-Achse, wie eine Figur, die sich auf der Stelle dreht:
$$R_y(\theta) = \begin{pmatrix} \cos\theta & 0 & \sin\theta \\ 0 & 1 & 0 \\ -\sin\theta & 0 & \cos\theta \end{pmatrix}$$
Um die x-Achse (nicken) stehen Kosinus und Sinus in der zweiten und dritten Zeile und Spalte, um die z-Achse (kippen) in der ersten und zweiten.
Zwei Matrizen lassen sich zu einer zusammenfassen: In \(A \cdot B\) ist die Zahl in Zeile \(i\), Spalte \(j\) das Skalarprodukt aus Zeile \(i\) von \(A\) und Spalte \(j\) von \(B\). Die neue Matrix tut dasselbe wie erst \(B\) und dann \(A\):
$$(A \cdot B) \cdot v = A \cdot (B \cdot v)$$
Gelesen wird also von rechts nach links, und die Reihenfolge zählt: Erst drehen und dann verschieben bringt ein Modell an eine andere Stelle als erst verschieben und dann um den Ursprung drehen, das Modell kreist dann um ihn.
Darum steht in einem Grafikprogramm projection * view * model * punkt: zuerst das Modell an seinen Platz, dann in die Sicht der Kamera, zuletzt auf den Bildschirm.
Für viele Punkte rechnet man das Produkt der Matrizen einmal aus und nimmt dann jeden Punkt nur noch mit einer Matrix mal. So macht es die Grafikkarte mit Millionen Ecken.
Eine 3 × 3-Matrix kann drehen und strecken, aber nicht verschieben: Der Ursprung bleibt immer, wo er ist. Der Trick ist eine vierte Zahl, \(w\). Ein Punkt bekommt \(w = 1\), eine Richtung \(w = 0\) (homogene Koordinaten). Dann verschiebt diese Matrix um \((t_x, t_y, t_z)\):
$$\begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + t_x \\ y + t_y \\ z + t_z \\ 1 \end{pmatrix}$$
Die vierte Spalte kommt nur über das \(w = 1\) des Punktes ins Ergebnis. Eine Richtung mit \(w = 0\) bleibt, wie sie ist: Eine Richtung hat keinen Ort, den man verschieben könnte. Darum sind in Grafikprogrammen fast alle Matrizen 4 × 4 (mat4), und drehen, strecken und verschieben lassen sich zu einer einzigen zusammenfassen.
Mit vier Zahlen je Punkt sind das die Bausteine, aus denen jede Bewegung eines Modells zusammengesetzt wird:
| Matrix | tut | 4 × 4 |
|---|---|---|
| \(T(t_x, t_y, t_z)\) | verschieben | \(\begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}\) |
| \(S(s_x, s_y, s_z)\) | strecken | \(\begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}\) |
| \(R_x(\theta)\) | um die x-Achse drehen (nicken) | \(\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}\) |
| \(R_y(\theta)\) | um die y-Achse drehen (sich umdrehen) | \(\begin{pmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}\) |
| \(R_z(\theta)\) | um die z-Achse drehen (kippen) | \(\begin{pmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}\) |
Dieselben Bewegungen wie oben in der Ebene, jetzt mit dem F als Block im Raum. Grau ist der Block vorher, blau der Block nachher; verdeckte Kanten sind gestrichelt, beim grauen Block gepunktet. Die x-Achse ist rot, die y-Achse grün, die z-Achse blau und zeigt zu dir.
Ein Modell soll doppelt so groß werden, sich um 90° um die z-Achse drehen und dann um 2 nach rechts rücken. Gelesen von rechts nach links ist das
$$M = T(2, 0, 0) \cdot R_z(90°) \cdot S(2, 2, 2) = \begin{pmatrix} 0 & -2 & 0 & 2 \\ 2 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}$$
Der Punkt \((1, 0, 0)\) wird erst zu \((2, 0, 0)\) gestreckt, dann auf \((0, 2, 0)\) gedreht, dann nach \((2, 2, 0)\) verschoben; \(M\) allein, einmal mit dem Punkt multipliziert, gibt dasselbe. Die übliche Reihenfolge ist erst strecken, dann drehen, dann verschieben, \(T \cdot R \cdot S\): So streckt und dreht sich das Modell um seine eigene Mitte. Erst verschoben und dann gedreht, kreist es um den Ursprung der Welt.
Eine Tür dreht sich um ihre Angeln, ein Kreisel um seine schräge Achse: Oft ist die Achse keine der drei Koordinatenachsen. Für eine Achse \(\vec{k}\) mit der Länge 1 und einen Winkel \(\theta\) zerlegt man einen Vektor \(\vec{v}\) in zwei Teile. Der Teil entlang der Achse, \((\vec{k} \cdot \vec{v})\,\vec{k}\), bleibt, wie er ist. Der Rest steht quer zur Achse und dreht sich in der Ebene senkrecht zu ihr um \(\theta\). Zusammen ergibt das die Formel von Rodrigues (mit dem Skalarprodukt und dem Kreuzprodukt, Handbuch, Vektoren):
$$\vec{v}' = \vec{v}\cos\theta + (\vec{k} \times \vec{v})\sin\theta + \vec{k}\,(\vec{k} \cdot \vec{v})\,(1 - \cos\theta)$$
Als Matrix, mit \(c = \cos\theta\), \(s = \sin\theta\), \(t = 1 - c\) und \(\vec{k} = (x, y, z)\):
$$R = \begin{pmatrix} c + x^2 t & xy\,t - z s & xz\,t + y s \\ yx\,t + z s & c + y^2 t & yz\,t - x s \\ zx\,t - y s & zy\,t + x s & c + z^2 t \end{pmatrix}$$
Mit \(\vec{k} = (0, 1, 0)\) wird daraus genau \(R_y(\theta)\) von oben. Im Bild dreht sich das F um 60° um die Diagonale \(\vec{k} = \frac{1}{\sqrt{3}}(1, 1, 1)\), die zwischen den drei Achsen liegt:
Dieselbe Drehung ließe sich auch aus Drehungen um x, y und z zusammensetzen; die Formel sagt sie in einem Schritt. Sie heißt nach Olinde Rodrigues, der sie 1840 veröffentlicht hat; Leonhard Euler kannte sie schon 1770.
Eine Drehmatrix hat neun Zahlen, eine Drehung braucht aber nur drei Angaben: zwei für die Richtung der Achse und eine für den Winkel. Ein Quaternion kommt mit vier Zahlen aus. Für die Drehung um den Winkel \(\theta\) um die Achse \(\vec{k}\) mit der Länge 1 ist
$$q = (w, x, y, z) = \left(\cos\tfrac{\theta}{2},\ \sin\tfrac{\theta}{2}\,\vec{k}\right)$$
Ein Quaternion für eine Drehung hat immer die Länge 1, \(w^2 + x^2 + y^2 + z^2 = 1\). Die Drehung um 90° um die z-Achse ist zum Beispiel \(w = z = \cos 45° \approx 0{,}707\) und \(x = y = 0\). Der halbe Winkel kommt daher, dass man beim Drehen zweimal mit \(q\) multipliziert, einmal von links und einmal von rechts.
Schreibt man \(\vec{u} = (x, y, z)\) für den Teil mit der Achse, dann sind zwei Drehungen hintereinander das Produkt der beiden Quaternionen:
$$q_1\, q_2 = \left(w_1 w_2 - \vec{u}_1 \cdot \vec{u}_2,\ \ w_1 \vec{u}_2 + w_2 \vec{u}_1 + \vec{u}_1 \times \vec{u}_2\right)$$
Wie bei Matrizen kommt es auf die Reihenfolge an: \(q_1 q_2\) dreht erst mit \(q_2\), dann mit \(q_1\). Rückwärts dreht \(\bar{q} = (w, -x, -y, -z)\), dieselbe Achse andersherum. Einen Vektor \(\vec{v}\) dreht man, indem man ihn als \(p = (0, \vec{v})\) schreibt und \(q\, p\, \bar{q}\) ausrechnet; heraus kommt \((0, \vec{v}')\). Übrigens drehen \(q\) und \(-q\) genau gleich.
Für die Grafikkarte macht man aus dem Quaternion meist wieder eine Matrix:
$$R = \begin{pmatrix} 1 - 2(y^2 + z^2) & 2(xy - wz) & 2(xz + wy) \\ 2(xy + wz) & 1 - 2(x^2 + z^2) & 2(yz - wx) \\ 2(xz - wy) & 2(yz + wx) & 1 - 2(x^2 + y^2) \end{pmatrix}$$
Mit \(w = z \approx 0{,}707\) und \(x = y = 0\) steht da \(R_z(90°)\): in der ersten Zeile 0, −1, 0.
Drei Winkel hintereinander, etwa erst um y, dann um x, dann um z (beim Flugzeug Gieren, Nicken und Rollen, die Eulerwinkel), sind leicht zu verstehen, haben aber zwei Schwächen:
Quaternionen haben beides nicht. Zwischen zwei Quaternionen \(q_1\) und \(q_2\) mischt Slerp (spherical linear interpolation) so, dass sich das Modell mit gleichmäßiger Geschwindigkeit um eine feste Achse dreht, auf dem kürzesten Weg; Ken Shoemake hat das 1985 in die Computergrafik gebracht. Ist \(\cos\Omega = q_1 \cdot q_2\) (die vier Zahlen paarweise multipliziert und addiert), dann ist
$$\operatorname{slerp}(q_1, q_2, t) = \frac{\sin((1 - t)\,\Omega)}{\sin\Omega}\, q_1 + \frac{\sin(t\,\Omega)}{\sin\Omega}\, q_2$$
Ist \(q_1 \cdot q_2\) negativ, nimmt man vorher \(-q_2\), sonst geht es den langen Weg herum. Noch ein Vorteil: Eine Matrix, die über viele Bilder immer wieder mit kleinen Drehungen multipliziert wird, sammelt Rundungsfehler und beginnt zu verzerren. Ein Quaternion teilt man einfach durch seine Länge, und es ist wieder eine reine Drehung.
William Rowan Hamilton (1805–1865), irischer Mathematiker, Physiker und Astronom. Mit 17 kam er an das Trinity College in Dublin und wurde noch als Student Professor für Astronomie. Komplexe Zahlen kann man als Punkte in der Ebene sehen; Hamilton suchte dasselbe für Punkte im Raum.
Am 16. Oktober 1843 ging er mit seiner Frau am Royal Canal in Dublin spazieren, als ihm die Lösung einfiel: vier Zahlen statt drei, mit den Regeln \(i^2 = j^2 = k^2 = ijk = -1\). Er ritzte sie mit seinem Taschenmesser in die nahe Broom Bridge. Heute drehen Quaternionen Modelle in Spielen, in glTF-Dateien und die Splats in Gaussian Splatting.
Foto: 1885, Library of Congress, gemeinfrei, zugeschnitten (Wikimedia Commons)
rot_0 bis rot_3, in der Reihenfolge \((w, x, y, z)\) und nicht unbedingt mit der Länge 1; vor dem Umrechnen teilt man durch die Länge (Gaussian Splatting).Quaternion.Die Inverse \(M^{-1}\) macht rückgängig, was \(M\) tut. Für die Bausteine ist sie leicht hinzuschreiben, ohne zu rechnen:
| Matrix | Inverse | denn |
|---|---|---|
| \(T(t)\) | \(T(-t)\) | zurückschieben |
| \(S(s)\) | \(S(1/s)\) | wieder schrumpfen; mit \(s = 0\) geht es nicht |
| \(R(\theta)\) | \(R(-\theta) = R^\top\) | zurückdrehen; zur Transponierten unten mehr |
Ein Produkt macht man rückgängig, indem man die Schritte rückwärts und in umgekehrter Reihenfolge geht, wie beim Ausziehen erst die Schuhe, dann die Socken:
$$(A \cdot B)^{-1} = B^{-1} \cdot A^{-1}$$
Für \(M = T \cdot R \cdot S\) von oben ist \(M^{-1} = S^{-1} \cdot R^{-1} \cdot T^{-1}\): erst zurückschieben, dann zurückdrehen, dann wieder schrumpfen.
Damit ergibt sich die View-Matrix wie von selbst. Steht die Kamera mit \(C = T(\text{auge}) \cdot R\) in der Welt, an der Stelle \(\text{auge}\) und um \(R\) gedreht, muss die View-Matrix das rückgängig machen, damit die Kamera im Ursprung landet:
$$\text{view} = C^{-1} = R^{-1} \cdot T(\text{auge})^{-1} = R^\top \cdot T(-\text{auge})$$
Erst die Welt so verschieben, dass das Auge im Ursprung liegt, dann sie entgegen der Kamera drehen. So baut auch 3D-Grafik mit WebGL seine View-Matrix, aus Drehungen und einem translate mit negativen Zahlen.
Eine Kamera ist dieselbe Rechnung rückwärts: Statt die Kamera in die Welt zu stellen, rückt die View-Matrix die ganze Welt so, dass die Kamera im Ursprung steht und in Richtung −z schaut. Steht die Kamera bei \((0, 0, 5)\), verschiebt die View-Matrix alles um \((0, 0, -5)\); dreht sie sich nach links, dreht die View-Matrix die Welt nach rechts.
Die Projektion bringt den Raum vor der Kamera auf den Bildschirm, und was weiter weg ist, wird kleiner. Eine Matrix kann nicht teilen, darum schreibt sie die Entfernung in die vierte Zahl: Nach der Projektion ist \(w = -z\). Die Grafikkarte teilt danach \(x\), \(y\) und \(z\) durch \(w\):
$$x_\text{Bildschirm} = \frac{x}{w}, \qquad y_\text{Bildschirm} = \frac{y}{w}$$
Doppelt so weit weg heißt doppelt so großes \(w\), also halb so groß auf dem Bildschirm. Die Ergebnisse von −1 bis 1 heißen Clip-Koordinaten in WebGL: −1 ist der linke oder untere Rand, 1 der rechte oder obere. Die übliche Perspektivmatrix für den Öffnungswinkel \(\theta\), das Seitenverhältnis \(a\) und die Grenzen \(n\) (nah) und \(f\) (fern), mit \(s = 1 / \tan(\theta / 2)\):
$$P = \begin{pmatrix} s / a & 0 & 0 & 0 \\ 0 & s & 0 & 0 \\ 0 & 0 & \frac{f + n}{n - f} & \frac{2 f n}{n - f} \\ 0 & 0 & -1 & 0 \end{pmatrix}$$
Die Transponierte \(M^\top\) vertauscht Zeilen und Spalten: Die erste Zeile wird die erste Spalte. \(M^{-1} \cdot M\) ist die Einheitsmatrix; nicht jede Matrix hat eine Inverse, eine, die alles auf eine Linie drückt, lässt sich nicht umkehren.
Bei einer Drehung ist es einfach: Die Inverse ist die Transponierte, \(R^{-1} = R^\top\), denn zurückdrehen heißt um denselben Winkel in die andere Richtung, und \(\sin(-\theta) = -\sin\theta\) tauscht genau die beiden Zahlen außerhalb der Diagonalen.
Eine Normale dreht man mit dem Modell mit, aber nicht mit derselben Matrix, wenn das Modell ungleich gestreckt ist: Ein in die Breite gezogener Kreis hat an der Seite eine Normale, die steiler wird, nicht flacher. Die richtige Matrix für Normalen ist \((M^{-1})^\top\); für reine Drehungen ist das wieder \(M\) selbst.
Eine Matrix kann auch eine Form beschreiben. Ein Ellipsoid entsteht aus einer Kugel, die man mit \(S\) entlang der Achsen streckt und mit \(R\) dreht. Seine Kovarianzmatrix ist
$$\Sigma = R \, S \, S^\top R^\top$$
Sie ist symmetrisch, die Zahl in Zeile \(i\), Spalte \(j\) ist dieselbe wie in Zeile \(j\), Spalte \(i\): Von neun Zahlen sind nur sechs verschieden. Auf der Diagonalen steht, wie breit das Ellipsoid entlang x, y und z ist (im Quadrat), daneben, wie schräg es liegt. So speichert jeder Splat in Gaussian Splatting seine Form. Von der Kamera aus gesehen wird daraus eine 2 × 2-Matrix, eine Ellipse auf dem Bildschirm.
| JavaScript (WebGL) | GLSL | Python (numpy) | |
|---|---|---|---|
| Matrix | 16 Zahlen in einem Array, spaltenweise |
mat4, mat3
|
np.array([[…], […]]), zeilenweise |
| Matrix mal Vektor | selbst geschrieben | m * v |
m @ v |
| Matrix mal Matrix | selbst geschrieben | a * b |
a @ b |
| transponieren | selbst geschrieben | transpose(m) |
m.T |
| umkehren | selbst geschrieben | inverse(m) |
np.linalg.inv(m) |
| an den Shader | gl.uniformMatrix4fv(ort, false, m) |
uniform mat4 view; |
Spaltenweise heißt: Die ersten vier Zahlen im Array sind die erste Spalte, nicht die erste Zeile. Die Verschiebung \((t_x, t_y, t_z)\) steht deshalb an den Indizes 12, 13 und 14. Wer eine Matrix zeilenweise hinschreibt und so an WebGL gibt, bekommt die Transponierte: Statt zu verschieben, verzerrt sie den Würfel. In numpy ist * die Multiplikation Zahl für Zahl; das Matrixprodukt ist @.