www.codeworx.org/directx_tuts/Teil 1: Einführung in die Vertex Shader

Teil 1: Einführung in die Vertex Shader

Einleitung

Seit dem Erscheinen der ersten 3dfx Vodoo Karten, stieg die Performance immer höher an. Mehr Polygone, höhere Aulösungen. Die Grafik, besser gesagt die grafischen Fähigkeiten, bleiben aber fast immer gleich. Der Grund war/ist, das Grafikkartenentwickler Funktionen und Algorithmen, wie zB: zum Rendern von Polygonen, direkt in den Grafikchip gebrannt haben. Um diese Performance zu nutzen, musste man eben diese hard-coded Funktionen verwenden und konnte keinen eigenen Algorithmen verwenden.

Die Verwendung von Vertex Shader ist eigentlich nichts Neues. Pixar verwendet diese schon seit Jahren in ihrem Programm RenderMan, mit dem, zum Beispiel, Filme wie "Toy Story", oder "A Bug's Life" gemacht wurden. Der Vorteil von RenderMan ist, dass die Programmierer fast keine Grenzen, für die Darstellung, haben und dadurch sehr flexiblel und kreativ waren. Leider waren das aber alles nur Software-Implementationen.

Mit eben den neuen Grafikkarten sind wir jetzt in der Lage noch schönere und schnellere Grafik zu programmieren, eben indem die Funktionen nicht mehr in den Silizium-Kern gebrannt werden, sondern durch den Programmierer selbst geschrieben werden können.

Bevor wir aber jetzt in Freuden-Schreien ausbrechen: Um einen Film wie "A Bug's Life" in Real-Time zu rendern dauert es noch Jahrzehnte. Einen einzigen Frame auf einem heutigen Computer zu rendern würde ca. drei Stunden dauern und über ein Gigabyte an Geometrie-Daten benötigen.

Aufbau der Grafikpipeline

Der Aufbau der Pipeline für DirectX 8.0 hat sich um zwei Dinge geändert. Erstens die Vertex Shader und zweitens die Pixel Shader. Uns aber interessiert nur der Abschnitt, der Transformation & Beleuchtung (TnL) ersetzt. Diese TnL-Engine war dazu da, Vertices (Vektoren) noch schneller zu transformieren. Bei noch älteren Grafikkarten als solche die eine GeForce 256 GPU hatten, wurde eben dieser Teil noch von der CPU übernommen. Mit dem Aufkommen der TnL in Hardware, hatte man den Prozessor ziemlich entlastet. Die Grafikkarte transformierte und beleuchtete die Vertices von selbst. Dadurch konnte man noch mehr Vertices an die Grafikkarte schicken, als es mit dem Prozessor möglich wäre. Noch dazu war jetzt mehr Prozessor-Zeit frei für andere Dinge, wie eben KI, Physik,...

Doch eben alle Funktionen der TnL-Engine sind in den Kern gebrannt und dadurch nicht zu verändern. Vor allem der Beleuchtungs-Abschnitt gefiel nicht vielen Entwickler und so verwendeten die meisten eben nur den T-Teil.

Und eben diese Teil, der TnL Teil, ist jetzt frei programmierbar durch die Entwickler, also durch euch alle da draussen.

Was ist ein Vertex Shader?

Ein Vertex Shader ist eigentlich ein kleines Programm, das in einer Sprache geschrieben wird die Assembler ähnlich schaut. Dieses kleine, selbstgeschriebene Programm wird für jeden Vertex, den man an die Grafikkarte übergibt, ausgeführt. Ein Vertex Shader kann daher nur an einem Vertex zu einer bestimmten Zeit arbeiten. Auch kann es keine zusätzlichen Vertices erzeugen. Ein Vertex Shader kann alle Eigenschaften eines Vertex verändern, die man auch der Grafikkarte/DirectX übergibt. Jedoch kann ein VS (Vertex Shader) folgendes nicht: Polygon-Operationen, Culling, Clipping gegen den Frustum oder anderen Clipping-Planes,, andere Vertices verändern, Vertices erzeugen.

Um all diese Dinge zu verwirklichen, verwendet der VS vier verschiedene Speicherstellen. Jedes dieser Speicherstellen hat eine Breite von 4 Floats/ 128 Bit und eine bestimmte Länge. Jedes Breite (4 Floats) ist ein Eintrag. In einem Eintrag kann man also Positionsdaten (xyzw), Texturkoordinaten (uvwq), Farben (rgba) oder einfach vier Werte speichern.

  • Input Data: Beinhaltet die Vertex-Daten. In dieses kann man nicht schreiben, nur lesen. Die Benennung ist von v0 - v15. Jedes Register ist ein 1-, 2-, 3-, oder 4-dimensionaler Vektor aus Floats. Die Anwendung gibt im vorraus bekannt in welches Register welche Daten geladen werden sollen. Sollte während der Ausführung eines VS auf ein Register zugegriffen werden, das keine Werte beinhaltet, wird ein Fehler ausgeworfen.
  • Constant Memory: Beinhaltet konstante Daten, die vorher geladen werden. Dies ist auch ein Nur-Lesen Speicher, während die Anwendung jedoch Werte in diesen Speicher laden kann. Verwendung findet man für Daten, die in jedem Frame sich verändern, wie Licht Eigenschaften, Transformations Matrizen, Material Eigenschaften. Dieser Speicher besitzt jedoch 96 Einträge mit der Benennung c0 - c95.
  • Temporary Register: Dient zum zwischenspeichern von Informationen. Diese können geschrieben und gelesen werden. Alle Temp-Register werden vor jeder Ausführung des VS mit 0 initialisiert. Benennung r0 - r11.
  • Vertex Output: Der VS schreibt den finalen Vertex in eben diesen Speicher. Dieser Speicher kann nur geschrieben werden und führt direkt weiter in der Grafik-Pipeline. Die Register des Vertex Output besitzen "richtige" Namen, nicht so, wie alle anderen Register. oPos ist dabei zum Beispiel die Position für die Clip-Space Vertexkoordinaten. Jeder VS muss in oPos schreiben. Dieser wird im nachhinein von der Pipeline gegen den View-Frustum geclippt. Andere Register, wie oD0 und oD1, werden zu den Grenzen 0 und 1 "geclampt" (kennt da einer eine Übersetzung???). Alle Output Register beginnen mit dem Buchstaben o für Output.
  • Address Register: Das Adress Reigster a0 ist ein zusätzliches Register, das nur geschrieben werden kann und dies nur durch die mov Instruktion. Es ist skalar, was bedeutet, das nur a0.x eine gültige Komponente ist. Es ist wie schon gesagt unlesbar, jedoch kann man in den Constant Memory "zeigen". Zum Beispiel c[a0.x + 3].

Ein weiteres Ding der Unmöglichkeit ist Daten zu speichern, die länger existieren sollen als der VS ausgeführt wird. Nach jedem Vertex werden alle Register wieder auf Initial-Werte zurückgesetzt und der nächste Vertex wird durchgeschickt durch das VS-Programm.

In den nächsten Tabellen sind alle Register und deren Möglichkeiten und Eigenschaften aufgelistet. Die erste Tabelle zeigt die VS Input Register, die zweite die Output Register und die letzte alle anderen. Die "Format" Spalte zeigt an, ob dieser Speicher ein 4-dimensionaler Vektor oder ein Skalar ist. Sollte es ein Skalar sein, ist nur die erste Stelle (zB. oFog.x) gültig. "Zugriff" beschreibt die Möglichkeiten des Zugriffs und "Zugriff durch", wer darauf zugreifen kann. Die interessanteste Spalte ist jedoch "Max. Refs/Instr". Eine einzige Instruktion eines VS darf nur an eine limitierte Anzahl von verschiedenen Speicherstellen zugreifen. Zum Beispiel kann eine Instruktion nicht auf zwei unterschiedliche Constant Memory Stellen zeigen. Daher ist add r0, c[0].x, c[1].y illegal. Da auf c0 und c1 zugegriffen wird. Dagegen ist add r0, c[0].x, c[0].y legal. Und eben dies gibt die Anzahl von maximalen Refs/Instr an.

Name Format Zugriff Zugriff durch Max. Refs/Instr Verwendung
v0-v15 Vektor Nur Lesen Anwendung/VB 1 Vertex Daten
c[0]-c[95] Vektor Nur Lesen Anwendung 1 Konstante Daten geladen von der Anwendung

Name

Format Zugriff Zugriff durch Verwendung
oPos Vektor Nur Schreiben Vertex Shader Clip-Space Koord.
oD0 Vektor Nur Schreiben Vertex Shader Diffuse Farbe des Vertex
oD1 Vektor Nur Schreiben Vertex Shader Speculäre Farbe des V.
oT0-oT3 Vektor Nur Schreiben Vertex Shader Texturkoordinaten des V. für Textur Stage 0-3
oFog.x Skalar Nur Schreiben Vertex Shader Nebel Wert f.V.
oPts.x Skalar Nur Schreiben Vertex Shader Sprite-Größe f.V.

Name

Format Zugriff Zugriff durch Max. Refs/Instr Verwendung
r0-r11 Vektor Lesen/Schreiben Vertex Shader 3 Temporäre Register
a0.x Skalar Verwenden/Schreiben Vertex Shader 1 Indirektes Adressierung des Konstanten Speichers

Wie man schon in der Grafik erkennt, kann ein VS maximal 128 Instruktionen lang sein. Insgesamt gibt es 17 verschiedene Instruktionen die man verwenden kann. Diese Zahlen sehen war klein aus, jedoch wird der VS für jeden übergebenen Vertex ausgeführt.

Unmöglich sind Sprünge und Schleifen in VS. Auch kann man den VS nicht vorzeitig verlassen. Jede Instruktion wird für jeden Vertex ausgeführt. Hier nun eine Auflistung der möglichen Instruktionen und deren mathematische Bedeutung:

Instruktion
Beschreibung 
add d, s0, s1
d.x = s0.x + s1.x 
d.y = s0.y + s1.y 
d.z = s0.z + s1.z 
d.w = s0.w + s1.w 
dp3 d, s0, s1
d.x = d.y = d.z = d.w = s0.x*s1.x + s0.y*d1.y + s0.z*s1.z 
dp4 d, s0, s1 
dp4 d, s0, s1 d.x = d.y = d.z = d.w = 
s0.x*s1.x + s0.y*s1.y + s0.z*s1.z + s0.w*s1.w 
dst d, s0, s1 
d.x = 1 
d.y = s0.y * s1.y 
d.z = s0.z 
d.w = s1.w 
expp d, s0 
d.x = 2^floor(s0.w) 
d.y = s0.w - floor(s0.w) 
d.z = 2^(s0.w) 
d.w = 1 
lit d, s0 
d.x = 1 
d.y = (s0.x > 0) ? s0.x : 0 
d.z = (s0.x > 0 && s0.y > 0) ? s0.y ^s0.w : 0 
d.w = 1 
logp d, s0 
d.x = d.y = d.z = d.w = 
(s0.w != 0) ? log(abs(s0.w))/log(2) : -(unendlich) 
mad d, s0, s1, s2 
d.x = s0.x*s1.x + s2.x 
d.y = s0.y*s1.y + s2.y 
d.z = s0.z*s1.z + s2.z 
d.w = s0.w*s1.w + s2.w 
max d, s0, s1 
d.x = (s0.x >= s1.x) ? s0.x : s1.x 
d.y = (s0.y >= s1.y) ? s0.y : s1.y 
d.z = (s0.z >= s1.z) ? s0.z : s1.z 
d.w = (s0.w >= s1.w) ? s0.w : s1.w 
min d, s0, s1 
d.x = (s0.x < s1.x) ? s0.x : s1.x 
d.y = (s0.y < s1.y) ? s0.y : s1.y 
d.z = (s0.z < s1.z) ? s0.z : s1.z 
d.w = (s0.w < s1.w) ? s0.w : s1.w 
mov d, s0 
d.x = s0.x 
d.y = s0.y 
d.z = s0.z 
d.w = s0.w 
mul d, s0, s1 
d.x = s0.x * s1.x 
d.y = s0.y * s1.y 
d.z = s0.z * s1.z 
d.w = s0.w * s1.w 
rcp d, s0 
d.x = d.y = d.z = d.w = (s0.w == 0) ? unendlich : 1/s0.w 
rsq d, s0 
d.x = d.y = d.z = d.w = 
(s0.w == 0) ? unendlich : 1/sqrt(abs(s0.w)) 
sge d, s0, s1 
d.x = (s0.x >= s1.x) ? 1 : 0 
d.y = (s0.y >= s1.y) ? 1 : 0 
d.z = (s0.z >= s1.z) ? 1 : 0 
d.w = (s0.w >= s1.w) ? 1 : 0 
slt d, s0, s1 
d.x = (s0.x < s1.x) ? 1 : 0 
d.y = (s0.y < s1.y) ? 1 : 0 
d.z = (s0.z < s1.z) ? 1 : 0 
d.w = (s0.w < s1.w) ? 1 : 0 
sub d, s0, s1 
d.x = s0.x - s1.x 
d.y = s0.y - s1.y 
d.z = s0.z - s1.z 
d.w = s0.w - s1.w 

Jede Instruktion benötigt einen Tick zum Ausführen. Das heißt, dass die VS Performance direkt proportional zu der Anzahl von verwendeten Instruktionen ist. Ein VS der nur die Hälfte eines anderen ist, wird in der Hälfte der Zeit ausgeführt. Daher sollte man unnötige Berechnungen nicht wirklich ausführen und so den VS verkürzen.

Mit Destinatin-Modifier und Source-Modifier kann man den Code noch weiter flexibel machen. Mit dem Destination-Modifier ist eigentlich eine Schreib-Maske. Dies dient dazu um nur bestimmte Komponenten eines Registers zu schreiben. Zum Beispiel mit der Instruktion mul r0.xy, v0, s0. Werden nur die xy Komponeten des Dest-Registers geschrieben. Die beiden anderen (zw) bleiben unverändert. Bei keiner Angabe eines Dest-Modifier ist gleichzusetzen mit der Verwendung von allen vier Komponeten .xyzw. Man sollte diese Möglichkeit schon verwenden, da erstens der Code lesbarer wird und zweitens mehr Optimierungen durch den Treiber durchgeführt werden könnten. Der Source-Modifier ist ein Komponent-Swizzel und optional eine Negierung. Wie auch beim Dest-Register, ist bei keiner Angabe eines Source-Modifiers dies gleichzusetzen mit .xyzw. Gibt man nur .x an, so wird dies eigentlich in ein .xxxx umgesetzt. Das heißt, beim Lesen des Source-Register der y Komponente wird dann eigentlich der x Wert genommen und nicht eben y. Ein anderes Beispiel wäre .yzwx. Hier wird als x Wert in Wirklichkeit dann y genommen. Weiters können alle vier Komponenten negiert werden, indem man vor den Registernamen ein Minus setzt. mul r0, v0.yzwx, -s0.ywxz.

Was man noch benötigt um einen VS zu programmieren ist eine VS Deklaration. Sie wird verwendet um jeden Vertex Input Register die richtigen Werte zuzuweisen. Diese wird aber erst im nächsten Kapitel genauer erklärt.

Warum sollte man Vertex Shader verwenden?

Wie schon gesagt ersetzen die Vertex Shader die fixe TnL-Pipeline. Aber warum macht man dies? Der Grund ist, dass die TnL-Pipe nicht alle mögliche Vertex-Attribute unterstützt und daher manche Berechnungen noch mit dem Prozessor erledigt werden müssen. Die Vertex Shader können die Tnl-Pipe locker ersetzten und dabei noch viel großartigere Effekte erzielen. Und das alles mit Hardware-Unterstützung und nicht mehr unter der Verwendung des Prozessors. Da die Grafikhardware dies viel schneller berechnen kann, als ein Prozessor dies könnte, ist dies erstens ein riesen Zeitgewinn, zweitens wird der Prozessor nicht mehr mit Grafik-Schnick-Schnack belastet und hat noch mehr Zeit andere Dinge zu erledigen (KI, Physik,...). Noch dazu kann man unnötige Teile einfach nicht in den VS einbauen und sich so noch einen zusätzlichen Zeitvorsprung erarbeiten.

Beispiele für erzielbare Effekte

Hier nun einige Beispiele von Effekten die mit den VS möglich sind.

FisheyeLens Effect

Im unteren Bild sieht man, wie die Landschaft mit einer normalen TnL Pipeline darstellbar wäre. Im oberen Bild wurden alle Vertices normal transformiert und dann mit einer Deformation versehen. Dieser Effekt wäre mit einer Hardware TnL Pipeline eben nicht darstellbar.

Sine Wave Pertubation

Von diesem Bild sollte man die Animation sehen, da diese Fläche zeitabhängig deformiert wird. Dieser Effekt ist daher so interessant, da alles was man dem VS über einen Stream übergibt, eine x,y Position ist. Durch diese Position berrechnet der VS eine Vertex Model-Space Position, die Normale, den Eye-Direction Vektor und den Reflection Vektor

Tools

Was wäre die Welt ohne zusätzliche Programme.

nVidia Effects Browser

Mit Hilfe des Effects Browsers kann man einfach und schnell eigene Vertex/Pixel Shader Beispiele programmieren und testen. nVidia bietet für diesen schon genug Beispiele, bei denen man sich satt sehen kann. Jedoch unterstützen viele Grafikkarten nicht die benötigten Ressourcen. VS können ja noch, dank ihrer guten Software-Implementierung, emuliert werden. Bei Pixel Shader ist aber Schluß. Diese können einfach nicht durch eine Software-Implementierung ersetzt werden und daher auf Grafikkarten ohne Pixel Shader Unterstützung nicht betrachtet werden.

nVidia Shader Debugger

Mit Hilfe des Shader Debugger bekommt man einen Überblick über alle Register in einem Debugging-Fenster. Während man sich durch einen Shader durcharbeitet, veränderen sich auch die Daten des Debuggers interaktiv. Es gibt auch die Möglichkeit Haltepunkte zu setzen. Leider kann der Shader Debugger nur ab Windows 2000 mit Service Pack 1 gestartet werden.

Vertex Shader Assembler

Um einen Vertex Shader in eine Binary zu kompilieren, benötigt man einen Assembler. Insgesamt gibt es derzeit zwei Assembler. Einen von Microsoft, der dem DirectX 8.0 SDK (Microsoft Vertex Shader Assembler) beiliegt und einen von nVidia (nVidia Vertex and Pixel Shader Macro Assembler). In den nächsten Tutorials verwenden wir den Assembler von nVidia, da er mehr Funktionalität besitzt als sein Gegenstück von Microsoft. Möglicherweise widme ich beiden ein eigenes Tutorial (E-Mails *g*).

3D Studio MAX 4.x

Ab der Version 4.x können Vertex/Pixel Shader direkt während der Entwicklung von Modellen und Animationen erarbeitetet werden. 3D Studio stellt die Shader in WYSIWYG dar (What You See Is What You Get).

Dies war das erste Tutorial in hoffentlich ein langen Serie von Tutorials. *g* Ich freue mich auf jede E-Mail sei es ein Lob oder auch eine Beschwerde: SCHREIBT.

(c) by Kongo

Bei Fragen, Beschwerden oder Wünschen E-Mail an: kongo@codeworx.org

Letztes Update: 03.02.2002