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