Kapitel 8: Rendering-Pipeline

Die vollständige Kette von 3D-Geometrie zum fertigen 2D-Bild: die Shaderstufen, die Koordinatenräume (Local -> World -> Camera -> Clip -> NDC), die Viewport-Transformation in Fensterkoordinaten, die Rasterisierung mit (bi)linearer Attribut-Interpolation und der Tiefentest (Painter's Algorithm vs. z-Buffer).

Überblick

Die Rendering-Pipeline (rendering pipeline) ist die feste Abfolge von Verarbeitungsschritten, die aus Rohdaten (Vertices und Primitive) ein 2D-Array von Farbwerten auf dem Bildschirm erzeugt. Die Hauptblöcke in Reihenfolge:

Raw Vertices & PrimitivesVertex ProcessorRasterizerFragment ProcessorOutput MergingPixelsDisplay\text{Raw Vertices \& Primitives} \rightarrow \text{Vertex Processor} \rightarrow \text{Rasterizer} \rightarrow \text{Fragment Processor} \rightarrow \text{Output Merging} \rightarrow \text{Pixels} \rightarrow \text{Display}

Roter Faden:

  • Manche Schritte laufen automatisch auf der GPU, andere programmiert man selbst (Vertex Shader, Fragment Shader) oder aktiviert sie per OpenGL-Befehl.
  • Die Geometrie durchläuft eine Kette von Koordinatenräumen, jeweils per Matrixmultiplikation.
  • Bis zum Normalized Device Space ist alles auflösungsunabhängig; erst die Viewport-Transformation macht die Ausgabe geräteabhängig.
  • Der z-Buffer löst die Sichtbarkeit korrekt und ohne globales Sortieren; er ist dem reihenfolgeabhängigen Painter's Algorithm überlegen.

Shaderpipeline (Wiederholung)

Shader (shader) sind kleine Programmfragmente, die direkt auf der GPU ausgeführt werden und die hochgeladenen Daten vor bzw. während des Renderns manipulieren. Geschrieben werden sie in GLSL (OpenGL Shading Language). Alternativen: HLSL (nur Windows/DirectX) und Cg (von NVidia, 2012 eingestellt).

Die vier Shaderstufen in Reihe:

  1. Vertex Shader (vertex shader) - Pflicht, selbst implementierbar. Berechnet die finale Position jedes Vertex im Ausgabebild (Model/View/Projection-Transformationen, optional die Homogenisierung).
  2. Tesselation Shader (tessellation shader) - optional. Zerlegt Objekte in feinere Dreiecksnetze (Verfeinerung der Geometrie).
  3. Geometry Shader (geometry shader) - optional. Verändert die Geometrie-Daten zur Laufzeit.
  4. Fragment Shader (fragment shader) - Pflicht, selbst implementierbar. Berechnet die Farbe jedes Fragments (genauer: jedes Fragments, nicht direkt jedes Pixels).

Merke: Der Einsatz der optionalen Shader ist ein Tradeoff Speicherplatz versus Performance (trade-off storage vs. performance). Pflicht sind nur Vertex und Fragment Shader.

Fragment vs. Pixel

Ein Pixel (pixel) ist ein Bildschirmelement. Ein Fragment (fragment) ist die Vorstufe eines Pixels. Diese Unterscheidung ist prüfungsrelevant:

  • Mehrere Fragmente können ein Pixel ergeben (Transparenz).
  • Nicht alle Fragmente werden zu einem Pixel: der Tiefentest (z-Buffer) verwirft verdeckte Fragmente.

Koordinatenräume

Die Vertexdaten durchlaufen eine Kette von Koordinatensystemen (KS = Koordinatensystem), jeweils durch eine Matrixmultiplikation überführt:

LocalModelMatrixWorldViewMatrixCameraProjectionMatrixClippingHomogenisierungNDC\text{Local} \xrightarrow{\text{ModelMatrix}} \text{World} \xrightarrow{\text{ViewMatrix}} \text{Camera} \xrightarrow{\text{ProjectionMatrix}} \text{Clipping} \xrightarrow{\text{Homogenisierung}} \text{NDC}
RaumBeschreibungÜbergang per
Local SpaceVertexdaten im lokalen KS des jeweiligen ObjektesModelMatrix
World SpaceAlle Objekte in einem globalen KS platziertViewMatrix
Camera Space (View Space)Kamera im Ursprung, Blick entlang der negativen z-AchseProjectionMatrix
Clipping SpacePerspektivische oder orthographische Projektion in den [w,w]3[-w, w]^3 WürfelHomogenisierung
Normalized Device Space (NDC)Normalisierte Darstellung im [1,1]3[-1, 1]^3 WürfelViewport Transformation

Die Homogenisierung (homogenization / perspective divide) ist die Division durch die homogene Koordinate ww, die den [w,w]3[-w, w]^3 Würfel auf den [1,1]3[-1, 1]^3 Würfel abbildet.

Wichtig (Grenze der Berechnung): Local -> Clipping Space werden selbst im Vertex Shader berechnet; ab der Homogenisierung/NDC laufen die Berechnungen automatisch auf der Grafikkarte. Die Homogenisierung kann aber auch manuell im Vertex Shader vorgenommen werden.

Viewport-Transformation

Die Viewport-Transformation (viewport transformation) bildet den Normalized Device Space [1,1]3[-1, 1]^3 auf den Screen Space (Fensterkoordinaten) [0..w]×[0..h]×[0..1][0..w] \times [0..h] \times [0..1] ab. Sie hängt von der Bildschirmauflösung (Breite ww, Höhe hh) ab und ist damit nicht auflösungsunabhängig (abhängig vom Ausgabemedium: Laptop, HD Monitor, Smartphone).

Für einen NDC-Punkt p=(px,py,pz)p = (p_x, p_y, p_z) in [1,1]3[-1,1]^3 ergibt sich der Bildpunkt pp':

px=12(px+1)wp'_x = \tfrac{1}{2}\,(p_x + 1)\cdot w py=12(py+1)hp'_y = \tfrac{1}{2}\,(p_y + 1)\cdot h pz=12(pz+1)p'_z = \tfrac{1}{2}\,(p_z + 1)

Die xx- und yy-Formeln skalieren und verschieben vom Bereich [1,1][-1,1] nach [0,w][0,w] bzw. [0,h][0,h]; der Tiefenwert zz wird auf [0..1][0..1] abgebildet.

Hinweis (mögliche Unstimmigkeit): Die Folie nennt zusätzlich Pixelmittelpunkte wie (0.5,0.5)(0.5, 0.5) unten links und (w0.5,h0.5)(w-0.5, h-0.5) oben rechts. Die oben angegebene Grundformel liefert an den NDC-Rändern exakt 00 bzw. ww (nicht 0.50.5 bzw. w0.5w-0.5); die Pixelmittelpunkt-Angaben beschreiben die diskreten Rasterzentren, nicht das Ergebnis der reinen Skalierungsformel. Beide Darstellungen stehen auf der Folie nebeneinander; in einer Klausur genügt in der Regel die Grundformel.

Rasterisierung

Die Rasterisierung (rasterization) wandelt die Vektorgeometrie (Dreiecke) in diskrete Fragmente auf dem Pixelraster um. Sie läuft automatisch auf der Grafikkarte in drei Schritten:

  1. Bestimmung der Pixel-Koordinaten der Vertices (V1, V2, V3).
  2. Interpolation der Vertex-Attribute entlang der Dreieckskanten. Die Strecke zwischen zwei Vertices wird zunächst normiert; ein Parameter t1t_1 läuft von 00 bis 11 entlang der normierten Strecke, danach erfolgt die lineare Interpolation abhängig von t1t_1.
  3. Interpolation der Pixelwerte entlang der einzelnen Rasterzeilen (Scanlines): horizontales Ausfüllen zwischen den interpolierten Kantenwerten.

Interpolation der Vertex-Attribute

Attribute (Farbe, Normalen, Texturkoordinaten etc.), die an den Vertices definiert sind, werden während der Rasterisierung pro Fragment interpoliert. Konkret werden die Ausgaben des Vertex Shaders zu Eingaben des Fragment Shaders, wobei die Werte automatisch interpoliert übergeben werden:

// Vertex Shader
out vec3 color;   // Ausgabe pro Vertex

// Fragment Shader
in vec3 color;    // kommt automatisch interpoliert an

(Bi)lineare Farbinterpolation

  • Lineare Interpolation (linear interpolation): Verfahren für Linien (1D entlang einer Strecke).
  • Bilineare Interpolation (bilinear interpolation): Verfahren für Flächen (2D, z.B. innerhalb eines Dreiecks).

Die lineare Farbinterpolation mit Parameter t[0,1]t \in [0,1]:

F(t)=(1t)F(0)+tF(1)F(t) = (1 - t)\cdot F(0) + t\cdot F(1)

Komponentenweise für eine RGB-Farbe:

F(t)=(1t)(R(0)G(0)B(0))+t(R(1)G(1)B(1))F(t) = (1 - t)\cdot \begin{pmatrix} R(0) \\ G(0) \\ B(0) \end{pmatrix} + t\cdot \begin{pmatrix} R(1) \\ G(1) \\ B(1) \end{pmatrix}

Merke: Der Gewichtungsfaktor von F(0)F(0) ist (1t)(1-t), nicht tt. Bei t=0t=0 ergibt sich F(0)F(0), bei t=1t=1 ergibt sich F(1)F(1).

Worked Example: Lineare Farbinterpolation bei t=1/4t = 1/4

Gegeben F(0)=(1,0,0)F(0) = (1, 0, 0) (rot) und F(1)=(0,0,1)F(1) = (0, 0, 1) (blau), Parameter t=14t = \tfrac{1}{4}:

F ⁣(14)=(114)(100)+14(001)=34(100)+14(001)=(3/401/4)F\!\left(\tfrac{1}{4}\right) = \left(1 - \tfrac{1}{4}\right)\begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix} + \tfrac{1}{4}\begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} = \tfrac{3}{4}\begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix} + \tfrac{1}{4}\begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix} = \begin{pmatrix} 3/4 \\ 0 \\ 1/4 \end{pmatrix}

Ergebnis: eine überwiegend rötliche Mischfarbe mit leichtem Blauanteil. Auf genau diese Weise werden alle Vertex-Attribute interpoliert (Vertex-Shader out wird interpoliert zu Fragment-Shader in).

Tiefentest

Der Tiefentest (depth test) bestimmt die Sichtbarkeit der Fragmente. Ziel: eine explizite globale Sortierung der Fragmente nach Tiefenwert soll vermieden werden.

Painter's Algorithm

Der Painter's Algorithm (painter's algorithm / Maleralgorithmus) ist das naive Verfahren: später gezeichnete Objekte überdecken frühere, wie Farbe auf einer Leinwand. Damit hängt die Darstellung von der Reihenfolge der Renderbefehle ab.

  • Nachteil: falsche Verdeckung bei ungünstiger Reihenfolge.
  • Kann sich schneidende Polygone nicht korrekt darstellen.
  • Wird verwendet, wenn der Tiefentest nicht aktiviert ist.

Besser: Verdeckung abhängig vom Abstand des jeweiligen Pixels zur Kamera.

z-Buffer

Beim z-Buffer (z-buffer / Tiefenpuffer) wird pro Fragment die Entfernung zur Kamera (= z-Koordinate, der Tiefenwert) gespeichert. Ein Pixel wird nur aktualisiert, wenn der neue Tiefenwert geringer ist (näher an der Kamera). Das löst das Sichtbarkeitsproblem korrekt und ohne globales Sortieren, auch bei sich schneidenden Polygonen.

Worked Example: z-Buffer bei sich schneidenden Polygonen

Ausgangslage: der Puffer ist überall auf unendlich (\infty) gesetzt.

  1. Das erste (graue) Polygon schreibt in seine Fragmente konstant den Tiefenwert 55.
  2. Das zweite (gelbe) Polygon hat Verlaufstiefen von 22 bis 77. In jeder Zelle wird der kleinere Wert übernommen:
    • Wo Gelb 66 oder 77 hat, bleibt der Wert 55 (Grau ist näher).
    • Wo Gelb 22, 33 oder 44 hat, wird der Wert übernommen (Gelb ist näher).

So entsteht die korrekte Durchdringung der beiden Flächen ohne globale Sortierung; pro Zelle gewinnt der minimale z-Wert und bestimmt die Farbe.

Tiefentest in OpenGL

Der Tiefentest wird per glEnable(GL_DEPTH_TEST) aktiviert; die Vergleichsfunktion wählt man mit glDepthFunc(function). Getestet werden immer die z-Werte der Fragmente.

glEnable(GL_DEPTH_TEST);   // Tiefentest aktivieren
glDepthFunc(GL_LESS);      // Standard: naeherer (kleinerer) z-Wert gewinnt

Mögliche Funktionen: GL_LESS (Standard), GL_GREATER, GL_NEVER, GL_ALWAYS, GL_EQUAL.

Hinweis (Optimierung): Als Optimierung kann der Tiefentest u.U. vor dem Fragment Shader durchgeführt werden (Early-Z), um verdeckte Fragmente gar nicht erst einzufärben.

Prüfungsrelevanz

  • Reihenfolge und Rolle der Pipeline-Stufen benennen: Vertex Processor -> Rasterizer -> Fragment Processor -> Output Merging.
  • Vier Shaderstufen: welche Pflicht (Vertex, Fragment), welche optional (Tesselation, Geometry), und deren Aufgaben.
  • Koordinatenräume + zugehörige Matrizen in richtiger Reihenfolge (ModelMatrix, ViewMatrix, ProjectionMatrix, Homogenisierung) und die Grenze zwischen "im Vertex Shader" und "automatisch auf der GPU".
  • Viewport-Transformationsformeln anwenden (px,py,pzp'_x, p'_y, p'_z); Bewusstsein für Auflösungsabhängigkeit.
  • Lineare Farbinterpolation berechnen (Formel + RGB-Vektor, wie im Beispiel t=1/4t = 1/4).
  • Painter's Algorithm vs. z-Buffer: Nachteil des Painter's Algorithm (reihenfolgeabhängig, versagt bei sich schneidenden Polygonen); z-Buffer-Regel (Update, wenn neuer Tiefenwert geringer).
  • OpenGL-Befehle: glEnable(GL_DEPTH_TEST) und glDepthFunc(...) mit den möglichen Funktionen; Standard GL_LESS.

Typische Fehler

  • Fragment und Pixel verwechseln. Ein Fragment ist die Vorstufe eines Pixels; nicht jedes Fragment wird zum Pixel (z-Buffer verwirft), und mehrere Fragmente können zu einem Pixel beitragen (Transparenz).
  • Kamerablickrichtung falsch angeben. Die Kamera schaut im Camera Space entlang der negativen z-Achse.
  • z-Buffer-Vergleich falsch herum. Standardmäßig gewinnt der kleinere (nähere) z-Wert (GL_LESS); das Pixel wird aktualisiert, wenn der neue Tiefenwert geringer ist.
  • glEnable(GL_DEPTH_TEST) vergessen. Ohne diese Zeile wird der Painter's Algorithm verwendet (falsche Verdeckung).
  • Auflösungsunabhängigkeit falsch zuordnen. Die Szene ist bis zum NDC/Einheitswürfel auflösungsunabhängig; erst die Viewport-Transformation macht sie geräteabhängig.
  • Lineare vs. bilineare Interpolation vertauschen. Linear = Linien (1D), bilinear = Flächen (2D).
  • Homogenisierung als reinen GPU-Schritt ansehen. Sie kann auch manuell im Vertex Shader durchgeführt werden.
  • Interpolationsvorzeichen. In F(t)=(1t)F(0)+tF(1)F(t) = (1-t)\,F(0) + t\,F(1) ist der Gewichtungsfaktor von F(0)F(0) gleich (1t)(1-t), nicht tt.

Glossar

DeutschEnglish
Rendering-Pipelinerendering pipeline
Shadershader (GLSL; Alternativen HLSL, Cg)
Vertex Shadervertex shader (berechnet finale Vertex-Position)
Tesselation Shadertessellation shader (optional)
Geometry Shadergeometry shader (optional)
Fragment Shaderfragment shader (berechnet Fragmentfarbe)
Fragmentfragment (Vorstufe eines Pixels)
Pixelpixel (Bildschirmelement)
Vertex Processor / Rasterizer / Fragment Processor / Output MergingHauptblöcke der Pipeline
Koordinatensystem (KS)coordinate system
Local Spacelocal space (lokales Objekt-KS)
World Spaceworld space (globales KS)
Camera Space / View Spacecamera space (Kamera im Ursprung, Blick entlang -z)
Clipping Spaceclip space ([w,w]3[-w, w]^3 nach Projektion)
Normalized Device Space (NDC)normalized device space ([1,1]3[-1, 1]^3)
Screen Spacescreen space ([0..w]×[0..h]×[0..1][0..w] \times [0..h] \times [0..1])
ModelMatrixmodel matrix (Local -> World)
ViewMatrixview matrix (World -> Camera)
ProjectionMatrixprojection matrix (Camera -> Clipping)
Homogenisierunghomogenization / perspective divide
Viewport-Transformationviewport transformation (NDC -> Screen Space)
Rasterisierungrasterization (Dreiecke -> Fragmente)
Interpolation der Vertex-Attributevertex attribute interpolation
Lineare Interpolationlinear interpolation (Linien)
Bilineare Interpolationbilinear interpolation (Flächen)
Rasterzeilescanline
Tiefentestdepth test
Painter's Algorithmpainter's algorithm / Maleralgorithmus
Z-Buffer / Tiefenpufferz-buffer / depth buffer
Tiefenwertdepth value (z-Koordinate, Abstand zur Kamera)
Bildebeneimage plane
Early-ZTiefentest u.U. vor dem Fragment Shader
Auflösungsunabhängigresolution-independent (gilt bis NDC/Einheitswürfel)