www.codeworx.org/directx_tuts/Teil 2: Das erste eigene Vertex Shader Programm

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