Zum Inhalt springen

Vertex-Transformationen

Jedes 3D-Objekt besteht aus Vertices, deren Positionen durch drei Koordinaten beschrieben werden: x, y und z.

Wenn wir beispielsweise einen Würfel erstellen, legen wir die Positionen seiner acht Ecken fest. Diese Koordinaten bilden die Grundlage seiner Geometrie.

Möchten wir den Würfel nun an einer anderen Stelle der Szene darstellen, drehen oder vergrößern, müssen wir die Positionen seiner Vertices verändern.

Diesen Vorgang nennen wir Vertex-Transformation. Dabei wird aus der ursprünglichen Position eines Vertex eine neue Position berechnet.

In der 3D-Grafik unterscheiden wir drei grundlegende Transformationen:

  • Translation (Verschiebung): Wir bewegen ein Objekt entlang der X-, Y- oder Z-Achse.
  • Rotation (Drehung): Wir drehen ein Objekt um eine Achse.
  • Skalierung: Wir vergrößern oder verkleinern ein Objekt.

Betrachten wir einen Vertex mit der Position (1, 2, 3).

Wenn wir ihn um zwei Einheiten entlang der X-Achse verschieben, erhalten wir:

Ursprüngliche Position: (1, 2, 3)
Neue Position: (3, 2, 3)

Die X-Koordinate wurde um 2 erhöht. Die Y- und Z-Koordinaten bleiben unverändert.

Wenden wir dieselbe Verschiebung auf sämtliche Vertices eines Würfels an, bewegt sich der gesamte Würfel um zwei Einheiten entlang der X-Achse. Seine Form und Größe bleiben dabei unverändert.

Bei einer Rotation ändern sich die Positionen der Vertices so, dass sich der Würfel dreht. Bei einer Skalierung verändern sich die Abstände seiner Vertices zum gewählten Bezugspunkt.

Die Positionen unserer Vertices speichern wir in einem Vertex-Buffer. Wir könnten diese Daten verändern, sobald wir ein Objekt bewegen möchten.

Das wäre jedoch unnötig aufwendig. Stattdessen lassen wir die ursprünglichen Vertex-Daten unverändert und berechnen die transformierten Positionen im Vertex-Shader.

Der Vertex-Shader wird für jeden Vertex ausgeführt. Er erhält dessen ursprüngliche Position und berechnet daraus die Position, die für die weitere Verarbeitung verwendet werden soll.

Eine einfache Verschiebung können wir direkt in WGSL schreiben:

WGSL
@vertex
fn vertexMain(
@location(0) position: vec3f
) -> @builtin(position) vec4f {
let movedPosition = position + vec3f(2.0, 0.0, 0.0);
return vec4f(movedPosition, 1.0);
}

Die Zeile

WGSL
let movedPosition = position + vec3f(2.0, 0.0, 0.0);

addiert zu jeder Vertex-Position den Vektor (2, 0, 0). Dadurch verschieben wir alle Vertices um zwei Einheiten entlang der X-Achse.

Mit @builtin(position) geben wir die berechnete Position an WebGPU weiter.

Dieses vereinfachte Beispiel zeigt eine Verschiebung direkt im Vertex-Shader. Es setzt voraus, dass die ausgegebenen Positionen im sichtbaren Clip-Space liegen. Für unsere späteren 3D-Szenen benötigen wir zusätzlich die passenden Kamera- und Projektionsberechnungen.

Stellen wir uns einen Vertex an der Position (1, 2, 3) vor. Wir möchten ihn um zwei Einheiten entlang der X-Achse verschieben.

Dazu können wir eine Transformationsmatrix verwenden. Eine Matrix ist eine Anordnung von Zahlen in Zeilen und Spalten.

Für unsere 3D-Transformation verwenden wir eine Matrix mit vier Zeilen und vier Spalten:

| 1 0 0 2 |
| 0 1 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |

Sehen wir uns die letzte Spalte an. Dort steht oben eine 2. Sie gibt an, dass wir den Punkt um zwei Einheiten entlang der X-Achse verschieben möchten.

Die beiden Nullen darunter bedeuten, dass wir ihn weder entlang der Y- noch entlang der Z-Achse verschieben.

Um die Matrix anzuwenden, ergänzen wir unsere Vertex-Position zunächst um eine vierte Koordinate mit dem Wert 1:

(1, 2, 3) → (1, 2, 3, 1)

Diese vierte Koordinate benötigen wir, damit wir auch eine Verschiebung als Matrix-Vektor-Multiplikation berechnen können. Ohne sie müssten wir die Verschiebung separat addieren.

Nun multiplizieren wir die Matrix mit dem Positionsvektor:

| 1 0 0 2 | | 1 | | 3 |
| 0 1 0 0 | | 2 | | 2 |
| 0 0 1 0 | × | 3 | = | 3 |
| 0 0 0 1 | | 1 | | 1 |

Wie kommt das Ergebnis zustande?

Für jede neue Koordinate multiplizieren wir die Zahlen einer Matrixzeile mit den entsprechenden Zahlen des Vektors und addieren die Ergebnisse.

Für die X-Koordinate ergibt das:

1 × 1 + 0 × 2 + 0 × 3 + 2 × 1 = 3

Die Y- und Z-Koordinaten bleiben unverändert.

Unser Vertex liegt nach der Transformation also bei (3, 2, 3) statt bei (1, 2, 3).

Die Matrix legt fest, wie der Punkt verändert wird. Die Matrix-Vektor-Multiplikation berechnet seine neue Position.

Dieselbe Berechnung können wir direkt in WGSL durchführen:

WGSL
let position = vec4f(1.0, 2.0, 3.0, 1.0);
let translation = mat4x4f(
vec4f(1.0, 0.0, 0.0, 0.0),
vec4f(0.0, 1.0, 0.0, 0.0),
vec4f(0.0, 0.0, 1.0, 0.0),
vec4f(2.0, 0.0, 0.0, 1.0)
);
let newPosition = translation * position;

mat4x4f erzeugt eine 4×4-Matrix. Anders als in unserer Darstellung mit vier Zeilen geben wir ihre Werte in WGSL spaltenweise an. Deshalb steht die Verschiebung um zwei Einheiten im vierten vec4f.

Die Multiplikation

WGSL
let newPosition = translation * position;

liefert das Ergebnis vec4f(3.0, 2.0, 3.0, 1.0).

Wir haben damit dieselbe Verschiebung wie zuvor berechnet, diesmal durch eine Matrix-Vektor-Multiplikation.

Wenden wir dieselbe Matrix auf alle Vertices eines Würfels an, verschiebt sich der gesamte Würfel.

Andere Matrizen können einen Würfel drehen oder skalieren. Wir können solche Matrizen auch miteinander multiplizieren und dadurch mehrere Transformationen kombinieren.

Die zusätzliche Koordinate w hat neben der Verschiebung noch weitere Aufgaben. Damit beschäftigen wir uns im nächsten Abschnitt über homogene Koordinaten.