Teil 2: Das erste eigene Vertex Shader Programm
Das erste Beispielprogramm wurde in zwei Teile aufgeteilt. In main.cpp wird
das Fenster erzeugt und Direct3D initialisiert und in der Datei vs.cpp findet
man alles VS-spezifische. Der VS befindet sich in einer eigenen Textdatei, die
durch den nVidia Vertex and Pixel Shader Macro Assember (kurz NVASM) kompiliert
wird. Wir verwenden nicht den Assembler von DirectX und deren D3DX-Funktionen
um den Shader zu kompilieren, da NVASM mehr und bessere Funktionen liefert als
der Assembler von DirectX.
Wir fügen einfach den VS in unser Projekt ein und gehen in die Projekteinstellungen
für nur diese Datei. Hier findet man zwei Reiter "Allgemein"
und "Benutzerdefiniertes Erstellen". Im Reiter "Benutzerdefiniertes
Erstellen" können wir nämlich angeben, mit welchem Programm eben
diese Datei kompiliert werden soll. Hier geben wir den Pfad zu NVASM an und
deren Kommandozeilenparameter, damit er den VS kompiliert. Er erstellt dabei
nur eine weitere Datei, die ein DWORD-Array besitzt. Dieses Array ist der ganze
VS.
Beginnen wir aber jetzt von dem Programmstartpunkt.
Auf Unterstützung prüfen
Während der Initialisierungsphase muss man den Device schon auf die Unterstützung
von VS überprüfen. Um dies zu machen, überprüft man jeden
möglichen Device in seine Caps auf die Unterstützung.
if (pCaps->VertexShaderVersion < D3DVS_VERSION(1,1))
return false;
Es gibt dabei unterschiedliche Versionen:
Version |
Erklärung |
0.0 |
DirectX 7.x |
1.0 |
DirectX 8.0 ohne dem Adress Register a0 |
1.1 |
DirectX 8.0 und DX 8.1 mit einem Adress Register a0 |
2.0 |
DirectX 9 |
Deklaration
Es gibt ja insgesamt 16 Input Register. Wo man aber welchen Wert eines Vertices
findet, das muss man durch eine Deklaration erst bekannt geben.
float c[4] = {0.0f,0.5f,1.0f,2.0f};
DWORD dwVSDecl[] = {
D3DVSD_STREAM( 0 ),
D3DVSD_REG( D3DVSDE_POSITION, D3DVSDT_FLOAT3 ), // D3DVSDE_POSITION = 0
D3DVSD_REG( D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ), // D3DVSDE_DIFFUSE = 5
D3DVSD_REG( D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ), // D3DVSDE_TEXCOORD0 = 7
D3DVSD_CONST( 0, 1 ), *(DWORD*)&c[0], *(DWORD*)&c[1], *(DWORD*)&c[2], *(DWORD*)&c[3],
D3DVSD_END()
};
Als erstes wird der Daten-Strom 0 an den VS gebunden. Dieser Daten-Strom ist
der Vertex Buffer, der mit der Methode pDev->SetStreamSource() an einen Daten-Strom,
später während des Programmlaufs, gebunden wird. Dadurch können
wir verschiedene VB mit dem gleichen VS benutzen, da immer der VB verwendet
wird, der zur Zeit an den ersten Daten-Strom gebunden ist.
Dann müssen die Werte aus dem VB in die verschiedensten Input Register
geladen werden. Im obrigen Beispiel, wird in das Input Register 0 (also v0)
die ersten drei Floats des VB geladen ( D3DVSD_REG( 0, D3DVSDT_FLOAT3) ). Dann
wird ein Wert vom Typ D3DCOLOR in das Input Register 5 und in das Register 7
zwei Floats geladen. Wohin welcher Wert geladen wird, bleibt ganz und gar dem
Programmierer überlassen. Die Konstanten mit dem Beginn D3DVSDE_* sind
nur eine Hilfe, die inder Datei d3d8types.h deklariert sind. Man kann auch ganz
einfach Zahlen einsetzen.
Also nochmal: Der erste Parameter von D3DVSD_REG() gibt das Input Register
an. Der zweite Parameter die Größe des Datentyps. Es gibt folgenden
mögliche Werte (gefunden in d3d8types.h):
Konstante |
Bedeutung |
D3DVSDT_FLOAT1 |
1 Float laden; Register Aufbau: (Wert, 0.0, 0.0, 1.0) |
D3DVSDT_FLOAT2 |
2 Float laden; Register Aufbau: (Wert, Wert, 0.0, 1.0) |
D3DVSDT_FLOAT3 |
3 Float laden; RA: (Wert, Wert, Wert, 1.0) |
D3DVSDT_FLOAT4 |
4 Float laden; |
D3DVSDT_D3DCOLOR |
1 DWORD laden; Format ARGB wird in RGBA umgewandelt. Werte werden in die
Größen von 0.0 bis 1.0 umgewandelt. |
D3DVSDT_UBYTE4 |
4 unsigned bytes |
D3DVSDT_SHORT2 |
2 signed shorts; (value, value, 0.0, 1.0) |
D3DVSDT_SHORT4 |
4 signed shorts |
Nach dem jetzt der VB zu den Registern "gemappt" wurde. Gibt es da
noch eine weitere Deklaration. Mit D3DVSD_CONST() kann man Werte in den Constant
Memory laden. Der erste Parameter ist das Constant Register. Der zweite Parameter
ist die Anzahl von Vektoren (4-Floats) die zu laden sind. In unserem Beispiel
laden wir in das Constant Register 0 (also c0) 1 Vektor. Dann werden die vier
Werte angegeben, die zu laden sind.
Abgeschlossen wird diese Deklaration mit D3DVSD_END().
Werte in den Constant Memory laden
Es gibt aber noch einen weiteren Weg Werte in den Constant Memory zu laden.
Die erste Möglichkeit ist nur dazu da um Daten, die man bei jedem VS Lauf
benötigt, einfacher zu laden. Möglicherweise erspart man sich auch
einiges an an Zeit durch diese Art der Einbindung von konstanten Werten, da
man sie nicht immer wieder von neuem laden muss.
Mit dieser jetzt vorgestellten Methode kann man Werte vor jedem DrawPrimitive()
Aufruf in die Register laden. Dies macht man dieser Methode:
HRESULT SetVertexShaderConstant (
DWORD Register,
CONST void* pConstantData,
DWORD ConstantCount );
Jetzt kann man eben vor jedem Rendergang Werte neue laden, wie zum Beispiel
die Welt-Sicht-Projektions Matrize, verschiedene Farben, Nebel, oder irgendwelche
Werte:
pDev->SetVertexShaderConstant( 4, &matWorldViewProjTrans, 4);
pDev->SetVertexShaderConstant( 8, &color, 1);
pDev->SetVertexShaderConstant( 11, &fambient, 1);
Der erste Parameter gibt das Register an, ab welches Werte geladen werden sollen.
Wie schon bekannt gibt es 96 Constant Register. Der letzte Parameter beinhaltet
die Anzahl von Numern ( 4x 32-bit Werte) gealden werden sollen. Das heißt,
nach dem obigen Beispiel, in die Register 4, 5, 6, 7 werden vier Vektoren (
4 mal 4 32-bit Werte) geladen. In Register 8 ein Array von vier Floats und genauso
in Register 11.
Vertex Shader schreiben und kompilieren
Die verfügbaren Befehle kennen wir ja schon aus dem ersten Kapitel. Die
Grafikkarte besitzt dabei zwei Verarbeitungsunits. Die erste Unit verarbeitet
Befehle wie mov, mul, add, mad, dp3, dp4, dst, min, max , slt und sge. Die zweite
ist verantworlich für die Befehle rcp, rsq, log, expp und lit. Die meisten
Befehle werden in einem Tick verarbeitet, rcp und rsq benötigen aber manchmal
mehr als einen Tick um ausgeführt zu werden.
Und hier jetzt der VS unseres ersten Beispielprogramms:
;--------------------------------------------------------
; Konstanten
; c0 = (0,0.5,1.0,2.0) *nicht verwendet*
; c4-7 = Trans der WorldViewProj Matrix
; c8 = Konstante Farbe (0,1,0,0) *nicht verwendet*
; Stream 0
; v0 = Position ( 4x1 vector )
; v5 = Diffuse Farbe (rgba)
; v7 = Texturkoordinaten ( 2x1 vector )
;--------------------------------------------------------
vs.1.1 ;Shader version 1.1
dp4 oPos.x , v0 , c4 ; X Position berechnen
dp4 oPos.y , v0 , c5 ; Y Position berechnen
dp4 oPos.z , v0 , c6 ; Z Position berechnen
dp4 oPos.w , v0 , c7 ; W Position berechnen
mov oT0.xy , v7 ; Texturkoordinaten weiterleiten
;mov oD0 , v5 ; Diffuse Farbe weiterleiten
Kommentare beginnen anstatt // (C++) mit ; wie in den Assemblersprachen. Es
ist immer hilfreich sich in dem Shader zu notieren, welche Werte in welchen
Registern zu finden sind. Das erste in einem Shader muss immer die Versions-Definition
sein (ausser Kommentare). Diese ist im Format vs.HauptVersion.SubVersion. Die
Syntax für jede Instruktion ist OpName dest, (-)s0, (-)s1, (-)s2 ; Kommentar.
Wieviele Source Register man aber benötigt hängt von der Instruktion
ab.
Gehen wir jetzt Schritt für Schritt weiter: Als erstes bilden wir das
Punktprodukt des Vertex und dem ersten Vektor der Transpose der WorldViewProj
Matrix und speichern diesen Wert nur in der x Komponente in dem Output Register.
Dies machen wir für jeden Vektor in der Matrix mit der Position. Die Transpose
einer Matrix wird wie folgt berechnet: Ein Eintrag c[i,j] in der normalen Matrix,
ist in der Transpose-Matrix c[j,i]. Dadurch ist in unserem Beispiel in c4 der
Right-Vektor, in c5 der Up-Vektor, in c6 der Richtungs-Vektor und in c7 die
Translations-Werte. Durch das Punkt Produkt werden die Vertices in den Clip-Space
transformiert.
Mit der mov Instruktion verschieben wir einfach die Texturkoordinaten in das
dafür existierende Output Register. Jetzt bestehe noch die Möglichkeit
auch die diffuse Farbe einfach zu verschieben, aber dies wurde mit einem Kommentar
weggeklammert.
Mit diesem einfache kleinen VS Programm haben wir schon die fixe TnL Pipeline
ersetzt. Kompiliert wird das ganze nicht mit dem C++ Compiler, sondern mit NVASM
der in den Build-Prozess des Programms einfach mit einbezogen wird. Dieser kompiliert
den VS und erstellt ein DWORD-Array das den ganzen VS beinhaltet. Dieses Array
übergeben wir Direct3D.
Erzeugen
Um den VS zu erzeugen gibt es folgenden Methode:
HRESULT CreateVertexShader() (
CONST DWORD* pDeclaration,
CONST DWORD* pFunction,
DWORD* pHandle,
DWORD Usage );
Zuerst übergeben wir die Deklaration mit der wir Daten-Ströme (VB)
zu den Input Registern verbinden können. Der zweite Parameter ist der kompilierte
VS, also unser erzeugtes Array von NVASM. In pHandle speichert D3D dann das
Handle des VS, das wir benötigen um diesen zu aktivieren. Im letzten Parameter
können wir Software-Processing verlangen mit D3DUSAGE_SOFTWAREPROCESSING.
Dies muss gesetzt werden, wenn der Render-State D3DRS_SOFTWAREVERTEXPROCESSING
== true ist.
Rendern
Um den VS zu aktiveren und damit ein 3D Objekt damit zu verändern, verwendet
man die gleicher Funktion wie schon mit den FVF.
pDev->SetVertexShader( hVertexShader );
Der Vertex Shader wird jetzt so oft aufgerufen, wie wir Vertices an die Grafikkarte
schicken, mit den DrawPrimitive*() Funktionen.
Freigeben
Wenn das Programm sich beenden möchte, muss man auch wieder den VS freigeben.
Wir rufen einfach DeleteVertexShader() mit dem VS Handle auf:
if (hVertexShader != 0xffffffff)
{
pDev->DeleteVertexShader(hVertexShader);
hVertexShader = 0xffffffff;
}
Und das war alles um diesen kleinen VS zum laufen zu bringen.
Vorteile von NVASM gegenüber MVSAM
Warum verwenden wir eigentlich nicht den VS Assembler von Microsoft. Meiner
Meinung nach besitzt der NVASM einfach einige Vorteile gegenüber sein Gegenprodukt.
Erstens besitzt er genaueres Fehler-Handling, #define Möglichkeiten, die
Möglichkeit Makros zu definieren,...
Noch dazu kann man ihn einfach in die MSC++ IDE integrieren um den VS kompilieren
zu lassen.
(c) by Kongo
Bei Fragen, Beschwerden oder Wünschen E-Mail an: kongo@codeworx.org
Letztes Update: 04.02.2002