Kann jemand die (Gründe für die) Auswirkungen von colum vs row major auf die Multiplikation / Verkettung erklären?


11

Ich versuche zu lernen, wie man Ansichts- und Projektionsmatrizen erstellt, und erreiche aufgrund meiner Verwirrung über die beiden Standards für Matrizen immer wieder Schwierigkeiten bei meiner Implementierung.
Ich weiß, wie man eine Matrix multipliziert, und ich kann sehen, dass das Transponieren vor der Multiplikation das Ergebnis vollständig verändern würde, weshalb in einer anderen Reihenfolge multipliziert werden muss.

Was ich jedoch nicht verstehe, ist, was nur unter "Notationskonvention" zu verstehen ist - aus den Artikeln hier und hier scheinen die Autoren zu behaupten, dass es keinen Unterschied macht, wie die Matrix gespeichert oder auf die GPU übertragen wird, sondern auf der zweiten Seite, die Matrix ist eindeutig nicht gleichbedeutend mit der Anordnung im Speicher für Zeilenmajor ; und wenn ich mir eine aufgefüllte Matrix in meinem Programm ansehe, sehe ich die Übersetzungskomponenten, die das 4., 8. und 12. Element belegen.

Vorausgesetzt, dass:

"Das Nachmultiplizieren mit Spalten-Hauptmatrizen führt zum gleichen Ergebnis wie das Vormultiplizieren mit Zeilen-Hauptmatrizen."

Warum im folgenden Codeausschnitt:

        Matrix4 r = t3 * t2 * t1;
        Matrix4 r2 = t1.Transpose() * t2.Transpose() * t3.Transpose();

Ist r! = R2 und warum ist pos3! = Pos für :

        Vector4 pos = wvpM * new Vector4(0f, 15f, 15f, 1);
        Vector4 pos3 = wvpM.Transpose() * new Vector4(0f, 15f, 15f, 1);

Ändert sich der Multiplikationsprozess abhängig davon, ob die Matrizen Zeilen- oder Spaltenmajor sind , oder ist es nur die Reihenfolge (für einen äquivalenten Effekt?)

Eine Sache, die nicht dazu beiträgt, dass dies klarer wird, ist, dass meine Spalten-Haupt-WVP-Matrix bei Bereitstellung für DirectX erfolgreich verwendet wird, um Scheitelpunkte mit dem HLSL-Aufruf zu transformieren: mul (Vektor, Matrix), was dazu führen sollte, dass der Vektor behandelt wird Zeilen-Haupt , wie kann die Spaltenhaupt Matrix durch meine Mathematik - Bibliothek Arbeit zur Verfügung gestellt?



Antworten:


11

Wenn ich mir eine ausgefüllte Matrix in meinem Programm ansehe, sehe ich die Übersetzungskomponenten, die das 4., 8. und 12. Element belegen.

Bevor ich beginne, ist es wichtig zu verstehen: Dies bedeutet , dass Ihre Matrizen Eklat . Deshalb beantworten Sie diese Frage:

Meine Spalten-Haupt-WVP-Matrix wird erfolgreich verwendet, um Scheitelpunkte mit dem HLSL-Aufruf zu transformieren: mul (Vektor, Matrix), was dazu führen sollte, dass der Vektor als Zeilen-Haupt behandelt wird. Wie kann also die von meiner Mathematikbibliothek bereitgestellte Spalten-Hauptmatrix funktionieren?

ist ganz einfach: Ihre Matrizen sind Zeilenmajor.

So viele Menschen verwenden Zeilenmajor- oder transponierte Matrizen, dass sie vergessen, dass Matrizen nicht auf natürliche Weise so ausgerichtet sind. Sie sehen also eine Übersetzungsmatrix wie folgt:

1 0 0 0
0 1 0 0
0 0 1 0
x y z 1

Dies ist eine transponierte Übersetzungsmatrix . So sieht eine normale Übersetzungsmatrix nicht aus. Die Übersetzung erfolgt in der 4. Spalte , nicht in der vierten Zeile. Manchmal sieht man das sogar in Lehrbüchern, was völliger Müll ist.

Es ist leicht zu erkennen, ob eine Matrix in einem Array Zeilen- oder Spaltenmajor ist. Wenn es sich um Zeilenmajor handelt, wird die Übersetzung in den Indizes 3, 7 und 11 gespeichert. Wenn es sich um eine Spalte handelt, wird die Übersetzung in den Indizes 12, 13 und 14 gespeichert. Null-Basis-Indizes natürlich.

Ihre Verwirrung rührt von der Annahme her, dass Sie Spalten-Hauptmatrizen verwenden, obwohl Sie tatsächlich Zeilen-Hauptmatrizen verwenden.

Die Aussage, dass Zeile gegen Spalte Major nur eine Notationskonvention ist, ist völlig richtig. Die Mechanismen der Matrixmultiplikation und der Matrix / Vektor-Multiplikation sind unabhängig von der Konvention gleich.

Was sich ändert, ist die Bedeutung der Ergebnisse.

Eine 4x4-Matrix ist schließlich nur ein 4x4-Zahlenraster. Es muss nicht haben zu einer Änderung bezieht sich das Koordinatensystem. Sobald Sie jedoch einer bestimmten Matrix eine Bedeutung zugewiesen haben , müssen Sie wissen, was darin gespeichert ist und wie Sie sie verwenden.

Nehmen Sie die Übersetzungsmatrix, die ich Ihnen oben gezeigt habe. Das ist eine gültige Matrix. Sie können diese Matrix auf float[16]zwei Arten speichern :

float row_major_t[16] =    {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1};
float column_major_t[16] = {1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1};

Ich sagte jedoch, dass diese Übersetzungsmatrix falsch ist, weil die Übersetzung am falschen Ort ist. Ich habe ausdrücklich gesagt, dass es relativ zur Standardkonvention zum Erstellen von Übersetzungsmatrizen transponiert ist, die so aussehen sollte:

1 0 0 x
0 1 0 y
0 0 1 z
0 0 0 1

Schauen wir uns an, wie diese gespeichert werden:

float row_major[16] =    {1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1};
float column_major[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1};

Beachten Sie, dass column_majorist genau das gleiche wie row_major_t. Wenn wir also eine richtige Übersetzungsmatrix nehmen und als Spaltenmajor speichern, ist dies dasselbe wie das Transponieren dieser Matrix und das Speichern als Zeilenmajor.

Das ist es, was nur als Notationskonvention gemeint ist. Es gibt wirklich zwei Arten von Konventionen: Speicher und Transposition. Der Speicher ist Spalte gegen Zeile Haupt, während die Transposition normal gegen transponiert ist.

Wenn Sie eine Matrix haben, die in Zeilenreihenfolge generiert wurde, können Sie den gleichen Effekt erzielen, indem Sie das Spalten-Hauptäquivalent dieser Matrix transponieren. Und umgekehrt.

Die Matrixmultiplikation kann nur auf eine Weise erfolgen: Bei zwei Matrizen in einer bestimmten Reihenfolge multiplizieren Sie bestimmte Werte miteinander und speichern die Ergebnisse. Nun, A*B != B*Aaber der tatsächliche Quellcode für A*Bist der gleiche wie der Code für B*A. Beide führen denselben Code aus, um die Ausgabe zu berechnen.

Dem Matrixmultiplikationscode ist es egal, ob die Matrizen zufällig in Spalten-Haupt- oder Zeilen-Hauptreihenfolge gespeichert sind.

Das Gleiche gilt nicht für die Vektor / Matrix-Multiplikation. Und hier ist warum.

Vektor / Matrix-Multiplikation ist eine Lüge; es kann nicht gemacht werden. Sie können jedoch eine Matrix mit einer anderen Matrix multiplizieren. Wenn Sie also so tun, als wäre ein Vektor eine Matrix, können Sie effektiv eine Vektor / Matrix-Multiplikation durchführen, indem Sie einfach eine Matrix / Matrix-Multiplikation durchführen.

Ein 4D-Vektor kann als Spaltenvektor oder Zeilenvektor betrachtet werden. Das heißt, ein 4D-Vektor kann als 4x1-Matrix (denken Sie daran: In der Matrixnotation steht die Zeilenanzahl an erster Stelle) oder als 1x4-Matrix betrachtet werden.

Aber hier ist die Sache: Bei zwei Matrizen A und B A*Bwird nur definiert, wenn die Anzahl der Spalten von A der Anzahl der Zeilen von B entspricht. Wenn also A unsere 4x4-Matrix ist, muss B eine Matrix mit 4 Zeilen sein drin. Daher können Sie nicht ausführen A*x, wenn x ein Zeilenvektor ist . Ebenso können Sie nicht ausführen, x*Awenn x ein Spaltenvektor ist.

Aus diesem Grund gehen die meisten Matrix-Mathematikbibliotheken von dieser Annahme aus: Wenn Sie einen Vektor mit einer Matrix multiplizieren, möchten Sie wirklich die Multiplikation durchführen, die tatsächlich funktioniert , und nicht die, die keinen Sinn ergibt.

Definieren wir für jeden 4D-Vektor x Folgendes. Cso ist die Spaltenvektormatrixform , und wird der Zeilenvektor seines Matrix - Form . Wenn dies gegeben ist, repräsentiert für jede 4 × 4-Matrix A die Matrix, die A mit dem Spaltenvektor multipliziert . Und stellt eine Matrix dar, die den Zeilenvektor mit A multipliziert .xRxA*CxR*Ax

Wenn wir dies jedoch mit strenger Matrixmathematik betrachten, sehen wir, dass diese nicht äquivalent sind . R*A kann nicht dasselbe sein wie A*C. Dies liegt daran, dass ein Zeilenvektor nicht dasselbe ist wie ein Spaltenvektor. Sie sind nicht die gleiche Matrix, daher liefern sie nicht die gleichen Ergebnisse.

Sie sind jedoch in einer Hinsicht miteinander verbunden. Es ist wahr, dass R != C. Allerdings ist es auch wahr , dass , wo T die Transponierung ist. Die beiden Matrizen sind Transponierte voneinander.R = CT

Hier ist eine lustige Tatsache. Da Vektoren als Matrizen behandelt werden, haben auch sie eine Frage zur Speicherung von Spalten und Zeilen. Das Problem ist, dass beide gleich aussehen . Das Array der Floats ist das gleiche, sodass Sie den Unterschied zwischen R und C nicht nur anhand der Daten erkennen können. Der einzige Weg, um den Unterschied zu erkennen, besteht darin, wie sie verwendet werden.

Wenn Sie zwei Matrizen A und B haben und A als Zeilenmajor und B als Spaltenmajor gespeichert ist, ist das Multiplizieren dieser Matrizen völlig bedeutungslos . Sie bekommen dadurch Unsinn. Nicht wirklich. Mathematisch ist das, was Sie bekommen, das Äquivalent zu tun . Oder ; Sie sind mathematisch identisch.AT*BA*BT

Daher ist eine Matrixmultiplikation nur dann sinnvoll, wenn die beiden Matrizen (und denken Sie daran: Vektor / Matrix-Multiplikation ist nur eine Matrixmultiplikation) in derselben Hauptreihenfolge gespeichert sind.

Ist also ein Vektor Spaltenmajor oder Zeilenmajor? Es ist beides und keines von beiden, wie bereits erwähnt. Es ist nur dann Spaltenmajor, wenn es als Spaltenmatrix verwendet wird, und es ist Zeilenmajor, wenn es als Zeilenmatrix verwendet wird.

Wenn Sie also eine Matrix A haben, die Spalte Major ist, x*Abedeutet dies ... nichts. Nun, es bedeutet wieder , aber das ist nicht das, was Sie wirklich wollten. In ähnlicher Weise erfolgt die transponierte Multiplikation, wenn sie zeilengroß ist.x*ATA*xA

Daher ändert sich die Reihenfolge der Vektor / Matrix-Multiplikation abhängig von Ihrer Hauptreihenfolge der Daten (und davon, ob Sie transponierte Matrizen verwenden).

Warum im folgenden Codeausschnitt r! = R2

Weil dein Code kaputt und fehlerhaft ist. Mathematisch , . Wenn Sie dieses Ergebnis nicht erhalten, ist entweder Ihr Gleichheitstest falsch (Gleitkommapräzisionsprobleme) oder Ihr Matrixmultiplikationscode ist fehlerhaft.A * (B * C) == (CT * BT) * AT

warum ist pos3! = pos für

Weil das keinen Sinn ergibt. Der einzige Weg , um wahr zu sein, wäre, wenn . Und das gilt nur für symmetrische Matrizen.A * t == AT * tA == AT


@Nicol, jetzt fängt alles an zu klicken. Es gab Verwirrung aufgrund einer Trennung zwischen dem, was ich sah und dem, was ich dachte, dass ich sein sollte, da meine Bibliothek (aus Axiom entnommen) erklärt, dass Spaltenmajor (und alle Multiplikationsreihenfolgen usw.) dem entsprechen Speicherlayout jedoch Zeile ist -major (gemessen an den Übersetzungsindizes und der Tatsache, dass HLSL unter Verwendung der nicht transponierten Matrix korrekt funktioniert); Ich sehe jetzt jedoch, wie dies nicht in Konflikt steht. Vielen Dank!
September

2
Ich hätte dir fast eine -1 gegeben, weil du Dinge wie "So sieht eine normale Übersetzungsmatrix nicht aus" und "Das ist völliger Müll" gesagt habe. Dann fahren Sie fort und erklären nett, warum sie völlig gleichwertig sind und daher keiner "natürlicher" ist als der andere. Warum entfernen Sie diesen kleinen Unsinn nicht einfach von Anfang an? Der Rest Ihrer Antwort ist in der Tat ziemlich gut. (Auch für Interessierte: steve.hollasch.net/cgindex/math/matrix/column-vec.html )
imre

2
@imre: Weil es kein Unsinn ist. Konventionen sind wichtig, da es verwirrend ist, zwei Konventionen zu haben. Mathematiker haben sich vor langer Zeit für die Matrizenkonvention entschieden . "Transponierte Matrizen" (benannt, weil sie vom Standard transponiert sind) verstoßen gegen diese Konvention. Da sie gleichwertig sind, bieten sie dem Benutzer keinen tatsächlichen Nutzen. Und da sie unterschiedlich sind und missbraucht werden können, entsteht Verwirrung. Oder anders ausgedrückt, wenn es keine transponierten Matrizen gegeben hätte, hätte das OP dies niemals gefragt. Und deshalb schafft diese alternative Konvention Verwirrung.
Nicol Bolas

1
@Nicol: Eine Matrix mit einer Übersetzung in 12-13-14 kann immer noch Zeilenmajor sein - wenn wir dann Zeilenvektoren damit verwenden (und als vM multiplizieren). Siehe DirectX. ODER es kann als Spaltenmajor angesehen werden, das mit Spaltenvektoren (Mv, OpenGL) verwendet wird. Es ist wirklich das gleiche. Wenn umgekehrt eine Matrix eine Übersetzung in 3-7-11 hat, kann sie entweder als Zeilen-Hauptmatrix mit Spaltenvektoren oder als Spalten-Hauptmatrix mit Zeilenvektoren angesehen werden. Die 12-13-14-Version ist in der Tat häufiger, aber meiner Meinung nach 1) ist es nicht wirklich ein Standard, und 2) es kann irreführend sein, sie als Spaltenmajor zu bezeichnen, da dies nicht unbedingt der Fall ist.
Imre

1
@imre: Es ist Standard. Fragen Sie einen tatsächlich ausgebildeten Mathematiker, wohin die Übersetzung geht, und er wird Ihnen sagen, dass sie in der vierten Spalte steht. Mathematiker erfanden Matrizen; Sie sind es, die die Konventionen festlegen.
Nicol Bolas

3

Hier gibt es zwei verschiedene Arten von Konventionen. Eine ist, ob Sie Zeilenvektoren oder Spaltenvektoren verwenden und die Matrizen für diese Konventionen Transponierungen voneinander sind.

Die andere ist, ob Sie die Matrizen im Speicher in Zeilenreihenfolge oder Spaltenreihenfolge speichern . Beachten Sie, dass "Zeilenmajor" und "Spaltenmajor" nicht die richtigen Begriffe für die Erörterung der Zeilenvektor / Spaltenvektor-Konvention sind ... obwohl viele Leute sie als solche missbrauchen. Die Speicherlayouts für Zeilen- und Spaltenmajors unterscheiden sich auch durch eine Transponierung.

OpenGL verwendet eine Spaltenvektorkonvention und eine Spalten-Hauptspeicherreihenfolge, und D3D verwendet eine Zeilenvektorkonvention und eine Zeilen-Hauptspeicherreihenfolge (also zumindest D3DX, die Mathematikbibliothek), sodass sich die beiden Transponierungen aufheben und es sich herausstellt Das gleiche Speicherlayout funktioniert sowohl für OpenGL als auch für D3D. Das heißt, dieselbe Liste von 16 Floats, die nacheinander im Speicher gespeichert sind, funktioniert in beiden APIs auf dieselbe Weise.

Dies kann damit gemeint sein, dass Leute sagen, dass "es keinen Unterschied macht, wie die Matrix gespeichert oder auf die GPU übertragen wird".

Für Ihre Codefragmente gilt r! = R2, da die Regel für die Transponierung eines Produkts (ABC) ^ T = C ^ TB ^ TA ^ T lautet. Die Transposition verteilt sich über die Multiplikation mit einer Umkehrung der Ordnung. In Ihrem Fall sollten Sie also r.Transpose () == r2 erhalten, nicht r == r2.

Ebenso pos! = Pos3, weil Sie die Multiplikationsreihenfolge transponiert, aber nicht umgekehrt haben. Sie sollten wpvM * localPos == localPos * wvpM.Tranpose () erhalten. Der Vektor wird automatisch als Zeilenvektor interpretiert, wenn er auf der linken Seite einer Matrix multipliziert wird, und als Spaltenvektor, wenn er auf der rechten Seite einer Matrix multipliziert wird. Ansonsten ändert sich nichts an der Art und Weise, wie die Multiplikation durchgeführt wird.

Schließlich zu: "Meine Spalten-Haupt-WVP-Matrix wird erfolgreich verwendet, um Scheitelpunkte mit dem HLSL-Aufruf zu transformieren: mul (Vektor, Matrix)." Ich bin mir nicht sicher, aber möglicherweise hat Verwirrung / ein Fehler dazu geführt, dass die Matrix herauskommt Die Mathematikbibliothek wurde bereits umgesetzt.


1

In 3D-Grafiken verwenden Sie eine Matrix, um sowohl Vektoren als auch Punkte zu transformieren. In Anbetracht der Tatsache, dass Sie über eine Übersetzungsmatrix sprechen, spreche ich nur über Punkte (Sie können einen Vektor nicht mit einer Matrix übersetzen, oder besser gesagt, Sie können, aber Sie erhalten denselben Vektor).

In der Matrixmultiplikation die Anzahl der Spalten der ersten Matrix gleich der Anzahl der Zeilen der zweiten Matrix sein (Sie können die anxm-Matrix für einen mxk multiplizieren).

Ein Punkt (oder ein Vektor) wird durch 3 Komponenten (x, y, z) dargestellt und kann sowohl als Zeile als auch als Spalte betrachtet werden:

colum (Dimension 3 x 1):

| x |

| y |

| z |

oder

Reihe (Abmessung 1 x 3):

| x, y, z |

Sie können die bevorzugte Konvention auswählen, es ist nur eine Konvention. Nennen wir es T die Übersetzungsmatrix. Wenn Sie die erste Konvention wählen, müssen Sie eine Nachmultiplikation verwenden, um einen Punkt p für eine Matrix zu multiplizieren:

T * v (Dimension 3x3 * 3x1)

Andernfalls:

v * T (Dimension 1x3 * 3x3)

Die Autoren scheinen zu behaupten, dass es keinen Unterschied macht, wie die Matrix gespeichert oder auf die GPU übertragen wird

Wenn Sie immer dieselbe Konvention verwenden, macht dies keinen Unterschied. Dies bedeutet nicht, dass eine Matrix unterschiedlicher Konventionen dieselbe Speicherdarstellung hat, sondern dass Sie durch Transformieren eines Punkts mit den zwei verschiedenen Konventionen denselben transformierten Punkt erhalten:

p2 = B * A * p1; // erste Konvention

p3 = p1 * A * B; // zweite Konvention

p2 == p3;


1

Ich sehe, dass die Übersetzungskomponenten, die das 4., 8. und 12. Element belegen, bedeuten, dass Ihre Matrizen "falsch" sind.

Die Übersetzungskomponenten werden immer als Einträge Nr. 13, Nr. 14 und Nr. 15 der Transformationsmatrix angegeben ( wobei das allererste Element des Arrays als Element Nr. 1 gezählt wird ).

Eine Zeilenhaupttransformationsmatrix sieht folgendermaßen aus:

[ 2 2 2 1 ]   R00 R01 R02 0  
              R10 R11 R12 0 
              R20 R21 R22 0 
              t.x t.y t.z 1 

Eine Spaltenhaupttransformationsmatrix sieht folgendermaßen aus:

 R00 R01 R02 t.x   2  
 R10 R11 R12 t.y   2 
 R20 R21 R22 t.z   2 
  0   0   0   1    1 

Zeilenhauptmatrizen werden in den Zeilen angegeben .

Wenn ich die obige Zeilenhauptmatrix als lineares Array deklariere, würde ich schreiben:

ROW_MAJOR = { R00, R01, R02, 0,  // row 1 // very intuitive
              R10, R11, R12, 0,  // row 2
              R20, R21, R22, 0,  // row 3
              t.x, t.y, t.z, 1 } ; // row 4

Das scheint sehr natürlich. Weil Hinweis, Englisch wird "Zeilen-Major" geschrieben - die Matrix erscheint im obigen Text genau so, wie es in Mathe sein wird.

Und hier ist der Punkt der Verwirrung.

Spaltenhauptmatrizen werden in den Spalten angegeben

Das heißt, um die Haupttransformationsmatrix der Spalte als lineares Array im Code anzugeben, müssten Sie schreiben:

    COLUMN_MAJOR = {R00, R10, R20, 0, // COLUMN # 1 // sehr kontraintuitiv
                     R01, R11, R21, 0,
                     R02, R12, R22, 0,
                     tx, ty, tz, 1};

Beachten Sie, dass dies völlig kontraintuitiv ist !! Bei einer Spaltenhauptmatrix werden die Einträge beim Initialisieren eines linearen Arrays in den Spalten angegeben , also in der ersten Zeile

COLUMN_MAJOR = { R00, R10, R20, 0,

Gibt die erste Spalte der Matrix an:

 R00
 R10
 R20
  0 

und nicht die erste Zeile , wie das einfache Layout des Textes Sie glauben machen würde. Sie müssen eine Spaltenhauptmatrix mental transponieren, wenn Sie sie im Code sehen, da die ersten 4 angegebenen Elemente tatsächlich die erste Spalte beschreiben. Ich nehme an, dies ist der Grund, warum viele Leute Zeilen-Hauptmatrizen im Code bevorzugen (GO DIRECT3D !! Husten.)

Die Übersetzungskomponenten befinden sich also immer auf den linearen Array-Indizes Nr. 13, Nr. 14 und Nr. 15 (wobei das erste Element Nr. 1 ist), unabhängig davon, ob Sie Zeilen- oder Spaltenhauptmatrizen verwenden.

Was ist mit Ihrem Code passiert und warum funktioniert er?

Was in Ihrem Code passiert, ist, dass Sie eine Spalten-Hauptmatrix haben, ja, aber Sie setzen die Übersetzungskomponenten an der falschen Stelle. Wenn Sie die Matrix transponieren, geht Eintrag Nr. 4 zu Eintrag Nr. 13, Eintrag Nr. 8 zu Nr. 13 und Eintrag Nr. 12 zu Nr. 15. Und da hast du es.


0

Einfach ausgedrückt ist der Grund für den Unterschied, dass die Matrixmultiplikation nicht kommutativ ist . Wenn bei regelmäßiger Multiplikation von Zahlen A * B = C ist, folgt daraus, dass B * A auch = C ist. Dies ist bei Matrizen nicht der Fall. Aus diesem Grund wählen Sie entweder Zeilen- oder Spaltenmajor.

Warum es keine Rolle spielt, ist, dass Sie in einer modernen API (und ich spreche hier speziell von Shadern) Ihre eigene Konvention auswählen und Ihre Matrizen in der richtigen Reihenfolge für diese Konvention in Ihrem eigenen Shader-Code multiplizieren können. Die API erzwingt Ihnen nicht mehr das eine oder andere.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.