Projektionsmatrix
Mit der Model-Matrix haben wir die Position unserer Vertices in der 3D-Welt berechnet. Die View-Matrix hat diese Positionen anschließend in das Koordinatensystem der Kamera umgerechnet.
Jetzt stellt sich eine weitere Frage: Welcher Teil unserer 3D-Welt soll überhaupt auf dem Canvas erscheinen?
Diese Aufgabe übernimmt die Projektionsmatrix.
Der sichtbare Bereich der Kamera
Abschnitt betitelt „Der sichtbare Bereich der Kamera“Unsere Kamera kann nicht die gesamte 3D-Welt gleichzeitig darstellen. Sie zeigt nur einen bestimmten Ausschnitt der Szene.
Diesen räumlichen Ausschnitt nennt man Sichtvolumen oder Frustum.
Wir können uns das Sichtvolumen als eine Pyramide vorstellen, deren Spitze an der Kamera liegt und die sich in Blickrichtung öffnet. Für die Darstellung verwenden wir davon nur einen begrenzten Abschnitt, sodass die Form einer abgeschnittenen Pyramide entsteht.
Dieser Bereich wird durch sechs Begrenzungsflächen beschrieben: links, rechts, oben, unten sowie eine nahe und eine ferne Fläche.
Die nahe Fläche legt fest, ab welcher Entfernung vor der Kamera etwas sichtbar sein kann. Die ferne Fläche bestimmt, wie weit die Kamera in die Szene hineinblickt.
Die seitlichen, oberen und unteren Begrenzungen legen fest, wie weit sich der sichtbare Ausschnitt öffnet.
Was macht die Projektionsmatrix?
Abschnitt betitelt „Was macht die Projektionsmatrix?“Die Projektionsmatrix beschreibt dieses Sichtvolumen mathematisch.
Wir wenden sie im Vertex-Shader an, nachdem wir die Position eines Vertex bereits mit der Model- und View-Matrix transformiert haben.
Sie rechnet die Kamerakoordinaten in einen neuen Koordinatenraum um. Die dabei entstehenden Werte heißen Clip-Koordinaten.
Anhand dieser Koordinaten kann WebGPU bestimmen, welche Teile einer Geometrie innerhalb des sichtbaren Bereichs liegen. Was außerhalb liegt, wird abgeschnitten. Dieser Vorgang heißt Clipping.
Ein Dreieck, das teilweise außerhalb des Sichtvolumens liegt, muss deshalb nicht vollständig verschwinden. WebGPU kann den sichtbaren Teil weiterhin darstellen.
Von Clip-Koordinaten zu Bildschirmpositionen
Abschnitt betitelt „Von Clip-Koordinaten zu Bildschirmpositionen“Bisher haben wir unseren Vertex-Positionen im Shader die vierte Koordinate w = 1 hinzugefügt:
let localPosition = vec4f(position, 1.0);Die perspektivische Projektionsmatrix verändert jedoch auch diese vierte Koordinate. Bei unserer Kamera hängt der neue Wert von w von der Entfernung des Vertex zur Kamera ab.
Ein Vertex, der fünf Einheiten vor der Kamera liegt, erhält beispielsweise w = 5. Bei zehn Einheiten Entfernung ergibt sich w = 10.
Der Vertex-Shader gibt die vier berechneten Clip-Koordinaten an @builtin(position) zurück.
Erst danach führt WebGPU automatisch die sogenannte perspektivische Division durch. Dabei werden die ersten drei Clip-Koordinaten durch den neuen Wert von w geteilt:
x_neu = x / wy_neu = y / wz_neu = z / wNehmen wir beispielsweise einen Vertex, dessen berechnete X-Koordinate 1 beträgt.
Bei w = 5 ergibt sich:
x_neu = 1 / 5 = 0.2Bei w = 10 dagegen:
x_neu = 1 / 10 = 0.1Der weiter entfernte Vertex erscheint näher an der Bildmitte. So werden auch die Abstände zwischen den Vertices eines entfernten Objekts kleiner.
Durch diese Division entsteht die perspektivische Verkleinerung.
Das Ergebnis der Division sind die normalisierten Gerätekoordinaten, kurz NDC.
Sie bilden einen einheitlichen Koordinatenraum, unabhängig davon, wie groß unser Canvas ist:
- X reicht von
-1bis1, von links nach rechts. - Y reicht von
-1bis1, von unten nach oben. - Z reicht bei WebGPU von
0bis1und enthält die Tiefeninformation.
WebGPU verwendet die X- und Y-Koordinaten anschließend, um die Positionen auf dem Canvas zu bestimmen. Die Z-Koordinate wird unter anderem für den Tiefentest benötigt, damit näher liegende Flächen weiter entfernte verdecken können.
Die Projektion im Vertex-Shader
Abschnitt betitelt „Die Projektion im Vertex-Shader“Für unser Würfelbeispiel verwenden wir zunächst eine fertige Projektionsmatrix:
let projectionMatrix = mat4x4f( vec4f(0.75, 0.0, 0.0, 0.0), vec4f(0.0, 1.0, 0.0, 0.0), vec4f(0.0, 0.0, -1.01, -1.0), vec4f(0.0, 0.0, -0.101, 0.0));Den ersten Wert können wir bereits erklären. Unser Canvas ist 800 Pixel breit und 600 Pixel hoch. Daraus ergibt sich das Seitenverhältnis:
aspect = 800 / 600 = 1.33331 / aspect = 600 / 800 = 0.75Die 0.75 ist also der Kehrwert des Seitenverhältnisses. Sie skaliert die X-Koordinaten passend zur Canvas-Breite. Deshalb erscheint die Vorderseite unseres Würfels jetzt als Quadrat statt als verzerrtes Rechteck.
Wie die übrigen Werte der Projektionsmatrix zustande kommen und wie wir die gesamte Matrix selbst berechnen, erklären wir später ausführlich beim Aufbau unserer Kamera.
Wir wenden sie im Vertex-Shader nach der Model- und View-Matrix an:
let worldPosition = modelMatrix * position;let viewPosition = viewMatrix * worldPosition;let clipPosition = projectionMatrix * viewPosition;
return VertexOutput(clipPosition, normals);Damit werden die Kamerakoordinaten in Clip-Koordinaten umgerechnet. Die anschließende perspektivische Division übernimmt WebGPU automatisch.
ToDo:
- Ein ausführliches Kapitel zu Kamera und Projektionsmatrix, near, far, fov schreiben