ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4603585
Jetzt (Chat):
21 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - TutorialsDruckversion

ZFX Graphics Series - Folge 4:
Das Tutorials von Stefan Zerbst

© Copyright 2003 [whole tutorial] by Stefan Zerbst
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


Abstrakt
Diese Folge beschäftigt sich mit der Verwendung von Vertex- und Pixel-Shadern als Alternative zu der Fixed-Function Pipeline der Grafikkarte, was bedeutet dass wir quasi direkt auf der GPU arbeiten analog einem Assembler Programm für die CPU
. Als konkretes Beispiel werden wir das Programm aus der Folge 3 dieser Serie durch eine Implementierung über Shader realisieren, wobei gleichzeitig die Beleuchtung mit direktionalem Licht ergänzt wird.

Auf einen Blick

Dieses Kapitel behandelt das folgende Haupt-Thema:

  • Rotieren, Verschieben und Beleuchten geometrischer Objekte
    im 3D-Raum unter Verwendung von Direct3D und Shadern

Verwendete Methoden und Strukturen:

  • D3DXMATRIX Struktur
  • D3DVERTEXELEMENT9 Struktur
  • IDirect3DDevice9::CreateVertexDeclaration Methode
  • IDirect3DDevice9::CreateVertexShader Methode
  • IDirect3DDevice9::CreatePixelShader Methode
  • IDirect3DDevice9::SetVertexDeclaration Methode
  • IDirect3DDevice9::SetVertexShader Methode
  • IDirect3DDevice9::SetPixelShader Methode
  • IDirect3DDevice9::SetVertexShaderConstantF Methode
  • D3DXAssembleShader Funktion
  • D3DXMatrixTranspose Funktion
  • D3DXMatrixInverse Funktion
  • D3DXMatrixRotationY Funktion
  • D3DXMatrixRotationZ Funktion
  • D3DXMatrixRotationYawPitchRoll Funktion
  • D3DXMatrixTranslation Funktion

Voraussetzungen für dieses Kapitel:

  • Grundlegende Kenntnisse der WinAPI und C++
  • Direct3D Basiswissen im Umfang von Kapitel 1 und 2


Demo-Screenshot


Auf los geht's los

In der letzten Folge dieser Serie haben wir uns ja damit beschäftigt, ein Rechteck zu verschieben und zu rotieren. Dabei haben wir der Grafikkarte eigentlich nur eine entsprechende Transformationsmatrix mitgeteilt mit der diese dann das Rechteck transformiert hat. Dies nennt man die Fixed-Function Pipeline, da die Grafikkarte die Transformation auf die Vertices nach einem vorbestimmten Schema anwendet welches wir nicht anders beeinflussen können als durch Veränderung der Variablen für die Berechnung, also die Transformationsmatrix beispielsweise. Dem gegenüber steht die Möglichkeit, dass man nicht nur die Parameter selber variiert, sondern auch die Regeln und Funktionen selbst definieren kann die auf diese Parameter angewendet werden. Dazu verwendet man sogenannte Shader, und zwar Vertex-Shader und Pixel-Shader. Ein Shader ist dabei definiert als kleines Programm, dass auf der Grafikkarte arbeitet und die Input-Parameter zu dem Output auf dem Bildschirm umsetzt. Wer von den Shadern bisher noch nichts gehört hat, oder nicht genau weiß wie diese technisch funktionieren, welche Input Register und Output Register usw. es gibt, der sollte nun zuerst das entsprechende Grundlagen Tutorial auf dieser Homepage lesen: http://www.zfx.info/Tutorials.php?action=Article&ID=3. Das dauert vielleicht 5 Minuten, ist aber notwendig um diese Folge wirklich verstehen und nicht nur abschreiben zu können ;-)


Deklarationen von Vertices

Bisher haben wir unsere Vertex Strukturen erstellt und über das Flexible Vertex Format (FVF) dem Direct3D Device Objekt mitgeteilt was diese Struktur so alles enthält damit das Device unsere Vertices korrekt transformieren kann. Eben dieses FVF wird jetzt überflüssig, da wir unsere Vertices durch den Einsatz unserer eigenen Shader selber transformieren. Leider können wir aber nicht ganz auf die Publizierung unseres Vertex-Formats verzichten und müssen nun anstelle des FVF eine sogenannte Vertex Declaration erzeugen. Das ist quasi der selbe Rattenschwanz und dient nun dazu, die Elemente eines Vertex in die richtigen Register (siehe anderes Tutorial oben) zu verschieben. Für solche Deklarationen gibt es in Direct3D ein eigenes Interface, nämlich IDirect3DVertexDeclaration9, das man mit einem Aufruf der folgenden Methode erzeugen kann:

   HRESULT IDirect3DDevice9::CreateVertexDeclaration(
                     CONST D3DVERTEXELEMENT9* pVertexElements,
                     IDirect3DVertexDeclaration9** ppDecl);

Der erste Parameter ist dabei ein Array von Elementen der Struktur D3DVERTEXELEMENT9 in denen die eigentlichen Informationen drinstecken, der zweite Parameter ist ein Pointer auf die zu erstellende Deklaration. Aber schauen wir uns zunächst einmal diese ominöse Struktur an:

   typedef struct _D3DVERTEXELEMENT9 {
      BYTE Stream;
      BYTE Offset;
      BYTE Type;
      BYTE Method;
      BYTE Usage;
      BYTE UsageIndex;
      } D3DVERTEXELEMENT9;

Ein solches Vertex-Element hat wie man sehen kann einen Haufen von Feldern. Ich möchte hier nicht explizit auf alle möglichen Optionen eingehen, da das offensichtlich viel zu viele sind. Ich empfehle hierzu einen Blick in die Dokumentation des DirectX SDK unter dieser Struktur. Das Feld Stream gibt an, aus welchem Vertexstream die entsprechenden Daten kommen. Wir erinnern uns ja noch dunkel, dass man einen Vertexbuffer über die Methode IDirect3DDevice9::SetStreamSource einem bestimmten Stream zuordnen kann, wobei wir in der Regel den Stream 0 verwenden. Das Feld Offset sagt aus, ab welcher Stelle in Bytes das entsprechende Element eines Vertex beginnt. Für die folgenden Felder Type, Method und Usage gibt es jeweils einen eigenen Enumerationstyp, also D3DDECLTYPE, D3DDECLMETHOD und D3DDECLUSAGE, die Ihr Euch wiederum in der SDK Doku ansehen solltet. Die Bezeichner für Type definieren den Datentyp in dem das Element vorliegt, beispielsweise D3DDECLTYPE_FLOAT3 für ein Array von drei float Werten wie etwa die Position eines Vertex. Die Methode kann man dazu verwenden, eine Tesselierung zu definieren aber wir begnügen uns hier mit der Angabe eines Default Wertes D3DDECLMETHOD_DEFAULT. Das Usage Feld dient nun endgültig dazu festzulegen, wofür dieses Element des Vertex verwendet wird, beispielsweise für die Position, den Normalenvektor usw. Ein konkretes Beispiel für unsere Vertexstruktur sehen wir weiter untem im Quellcode, dann wird das alles etwas deutlicher. 

Um eine Vertex-Deklaration zu aktivieren reicht es aber natürlich nicht aus, diese zu erzeugen, denn das müssen wir ja für jede unterschiedliche Vertex-Struktur machen die wir deklariert haben. Um eine solche Deklaration für das Device zu aktivieren rufen wir die folgende Methode mit dem entsprechenden Objekt auf:

   HRESULT SetVertexDeclaration(IDirect3DVertexDeclaration9 *pDecl);

Dieser Aufruf ist jedesmal dann notwendig, wenn wir eine andere Vertex-Struktur verwenden. Bleiben wir, wie in unserem Beispiel, bei einer einzigen Struktur für Vertices, so reicht es natürlich auch aus, die zugehörige Deklaration ein einziges Mal zu aktivieren. 


Assemblierung und Erstellen eines Shaders

So, jetzt kommen wir erst mal zum handwerklichen Kern dieser Folge. Wir gehen hier davon aus, dass wir unsere Shader in einem char Array vorliegen haben, also quasi als ASCII String. Es gibt zusätzlich auch die Möglichkeit, bereits kompilierte Shader zu laden und zu verwenden aber darauf gehen wir hier nicht näher ein. Und eine zweite Implikation leiten wir daraus ab: Ein Shader Programm welches wir geschrieben haben muss wie ein normales C++ Programm auch kompiliert werden. Doch leider haben wir nicht den geeigneten Compiler dazu, oder doch? Es gibt hier verschiedene Varianten von Compilern, und erfreulicherweise liefert DirectX auch gleich noch einen eigenen Compiler für Vertex- und Pixel-Shader in den Direct3D Extensions mit. Diesen rufen wir über die folgende Funktion auf:

   HRESULT WINAPI D3DXAssembleShader(      
              LPCTSTR          pSrcData,
              UINT             SrcDataLen,
              CONST D3DXMACRO* pDefines,
              LPD3DXINCLUDE    pInclude,
              DWORD            Flags,
              LPD3DXBUFFER*    ppShader,
              LPD3DXBUFFER*    ppErrorMsgs);

Wie man bereits vermuten kann ist der erste Parameter natürlich der zu kompilierender Shader in Form eines Strings, dessen Länge der zweite Parameter angibt. Die folgenden drei Parameter setzen wir für unsere Zwecke einfach auf 0, da man mit ihnen fortgeschrittene Dingen tun kann die wir noch nicht beherrschen. Aber ich halte jeden der diese Folge entsprechend verinnerlicht hat an, sich in der SDK Dokumentation über die drei Lümmel zu informieren. Die beiden letzten Parameter sind Buffer-Objekte von D3DX, in denen einfach Daten zwischengeparkt werden. Im vorletzten Parameter liegen die Daten des kompilierten Shaders in Form eine DWORD Pointers für uns zur Abholung bereit und sollte man etwas schief gegangen sein, dann erzeugt der Compiler für uns auch gleich eine nette Fehlermeldung die er in Form eines char Pointers in den letzten Parameter steckt. Mit der Methode GetBufferPointer() eines solchen D3DX Buffers können wir einen Zeiger auf die dort gelagerten Daten erhalten. 

Gehen wir mal davon aus, dass alles rund lief und wir nun unseren kompilierten Shader in Form eine DWORD Pointers vorliegen haben. Nun müssen wir daraus noch ein für Direct3D verwendbares Objekt machen, und dazu haben wir die folgenden beiden Methoden:

   HRESULT IDirect3DDevice9::CreateVertexShader(
                        const DWORD *pFunction,
                        IDirect3DVertexShader9** ppShader);

   HRESULT IDirect3DDevice9::CreatePixelShader(
                        const DWORD *pFunction,
                        IDirect3DPixelShader9** ppShader);

So, was soll ich sagen? Das war auch schon fast alles was man wissen muss um in Direct3D Vertex- und Pixel-Shader zu verwenden. Abschließend müssen wir uns nur noch kurz anschauen, wie man einen Shader für ein Direct3D Device aktivieren kann:

   HRESULT IDirect3DDevice9::SetVertexShader(IDirect3DVertexShader9* pShader);
   HRESULT IDirect3DDevice9::SetPixelShader( IDirect3DPixelShader9*  pShader);

Wenn man wieder die Fixed Function Pipeline nach dem Rendern mit einem Shader verwenden möchte, so setzt man hier einfach NULL als Parameter für die Methodenaufrufe ein. Okay, jetzt haben wir unsere Shader aktiviert doch es bleibt eine brennende Frage:


Wie geben wir dem Shader Input?

Gehen wir mal davon aus, dass wir unsere Shader erstellt und aktiviert haben. Gut und schön, doch der Shader braucht Input mit dem er rechnen kann. Für den Pixel-Shader lässt sich die Frage ganz leicht beantworten, denn er erhält automatisch als Input die zu rendernden Pixel von der Grafikkarte die über die Fixed Function Pipeline oder einen Vertex-Shader berechnet wurden. Doch wenn wir nun einen Vterex-Shader verwenden, wie erhält dieser seinen Input? Die Vertex- und Indexdaten können wir wie gehabt für das Device festlegen, doch die Methoden SetTransform(), SetLight(), usw. des Direct3D Devices sind Funktionen für die Fixed Function Pipeline und haben entsprechend keinen Effekt wenn wir mit einem Vertex-Shader arbeiten. Um nun variable Daten wie beispielsweise die Transformationsmatrix zu dem Vertex-Shader zu bekommen müssen wir sie in die entsprechenden Konstanten Register der GPU schieben (vgl. den oben erwähnten Einführungsartikel in Shader), und dazu haben wir insbesondere die folgende Methode:

   HRESULT SetVertexShaderConstantF(
                 UINT  StartRegister,
                 CONST float *pConstantData,
                 UINT  Vector4fCount);

Mit dieser Methode können wir ein Array von vier float Werten in ein Register für den Vertex-Shader schieben. Der erste Parameter gibt dabei das Register an, in dem unsere Daten beginnen sollen, der zweite Parameter ist ein Pointer auf unser float Array und der dritte Parameter sagt aus, wie viele float[4] Elemente wie in Konstanten Register schieben wollen. Für einen einzigen Vektor geben wir hier eine 1 an, für eine Transformationsmatrix aber beispielsweise eine vier, weil diese 4*4 float Werte hat. Diese Methode hat noch zwei Geschwister die anstelle des F's ein B bzw. ein I haben und entsprechend keine float Werte sondern Integer und Boolean Werte in die Register schieben. 

Und nun können wir uns daran machen, unser Programm aus der Folge 3 dieser Serie auf Shader umzuschreiben. 


Ein einfacher Vertex-Shader

Und nun geht es an's Eingemachte. Wir schreiben unser erstes Shader Programm. Was wir dabei zu tun haben dürfte auch klar sein, denn wir müssen unsere Geometrie ja zum einen transformieren und zum anderen müssen wir sie ein wenig beleuchten. Bevor es aber an den Shader geht brauchen wir noch ein paar Variablen, die wir in dem Shader verwenden müssen, und die setzen wir hier zunächst:

   // Transformationsmatrix in Register C0-C3
   D3DXMatrixMultiply(&matShader, &mWorld, &m_matProj);
   D3DXMatrixTranspose(&matShader, &matShader);
   m_pDevice->SetVertexShaderConstantF(0, (float*)&matShader, 4);

   // ambientes Licht in Register C4
   float fCol[4] = { 0.3f, 0.3f, 0.3f, 0.0f };
   m_pDevice->SetVertexShaderConstantF(4, fCol, 1);

   // Richtung des gerichteten Lichts in Register C5
   D3DXVECTOR4 vcLight(0.0f, 0.0f, 1.0f, 0.0f);
   D3DXMatrixInverse(&matInv, NULL, &mWorld);
   D3DXVec4Transform(&vcLight, &vcLight, &matInv);
   m_pDevice->SetVertexShaderConstantF(5, (float*)&vcLight, 1);

Ich war so frei die Projektionsmatrix zu einem Attribut m_matProj der Klasse Application_C zu machen, da wir sie nun in jedem Frame brauchen. Wir multiplizieren nun also zuerst die Weltmatrix des zu rendernden Objektes mit der Projektionsmatrix und speichern das Ergebnis in matShader. Hätten wir auch eine Kameramatrix gesetzt, so müssten wir diese ebenfalls noch hinzumultiplizieren, und zwar zuerst die Weltmatrix mit der Kameramatrix und das Ergebnis dann mit der Projektionsmatrix. Nun transponieren wir die Matrix, weil wir auf der Grafikkarte intern mit Matrizen in transponierter Form arbeiten müssen, und schieben die Matrix dann in die Register C0 bis C3. In das Register C4 schieben wir ein wenig ambientes Licht, welches das Objekt konstant von allen Seiten gleich beleuchten soll. Doch nun hätten wir gerne auch noch gerichtetes Licht, also Directional Light. Dazu definieren wir einen Vektor für die Richtung in die sich das Licht bewegt, in diesem Fall in Richtung der Z Achse. Es steht also quasi ein Lichtstrahler direkt beim Betrachter und leuchtet in den Bildschirm hineinstrahlend die Szene aus. 

Nun rotieren und verschieben wir unser Dreieck aber, und daher haben wir ein Problem. Ein Vertex-Shader wird für jeden Vertex einmal ausgeführt. Um die Beleuchtung für einen Vertex zu berechnen müssen wir den Winkel zwischen seinem Normalenvektor und dem Lichtvektor berechnen. Nun könnten wir beispielsweise den Normalenvektor des Vertex auch rotieren und verschieben, damit er in Weltkoordinaten vorliegt und damit im selben Bezugssystem wie der Lichtvektor. Für unser Rechteck mit vier Vertices müssten wir den entsprechenden Shader aber viermal ausführen und vier Normalenvektoren transformieren. Je mehr Vertices wir für dieses Objekt haben, desto öfters führen wir also die Multiplikation des Normalenvektors mit der Transformationsmatrix im Shader aus. Um diesen ganzen Rechenaufwand zu vermeiden, schliesslich können wir die GPU Leistung auch sinnvoller verwenden, gibt es aber eine Alternative. Wir verschieben einfach den Lichtvektor aus dem Weltkoordinatensystem in das lokale Koordinatensystem unseres Rechtecks. Dazu invertieren wir die Weltmatrix unseres Rechtecks und multiplizieren den Lichtvektor damit, welcher nun in genau der gleichen Relation zu den Vertexnormalen steht, als wenn wir die Vertexnormalen transformiert hätten. Wir sparen damit aber für jeden zu transformierenden Vertex die Transformation seines Normalenvektors. So, den Lichtvektor tun wir nun in das Register C5 und können endlich unseren Shader coden. 

   const char BaseVShader[] =
      "vs.1.1            \n"\
      "dcl_position0 v0  \n"\
      "dcl_normal0   v3  \n"\
      "dcl_texcoord0 v6  \n"\
      "m4x4 oPos, v0, c0 \n"\
      "mov oD0, c4       \n"\
      "dp3 oD1, v3, -c5  \n"\
      "mov oT0, v6       \n";

Niedlich, oder? So ein Shader ist recht klein und überschaubar, schaut aber doch noch ein wenig krytisch aus, daher hier nun seine Bedeutung Zeile für Zeile:

  • vs.1.1
    Hiermit geben wir an, dass es sich bei diesem Shader um einen Vertex-Shader handelt und zwar in der Version 1.1. 
  • dcl_position0 v0, dcl_normal0 v3 und dcl_texcoord0 v6
    Diese Zeilen hier regeln in welche Inputregister welcher Teil eines Vertex gemappt wird, hier stecken wir die Position in das Register v0, den Normalenvektor in das Register v3 und die Texturkoordinaten in das Register v6. Aufgrund der oben erstellten Vertex-Deklaration weiss die Grafikkarte auch, wo in einem Vertex sie die entsprechenden Informationen findet. 
  • m4x4 oPos, v0, c0
    Die Transformation! Die vermeintliche Instruktion m4x4 ist keine echte Instruktion, sondern ein Makro welches wiederum andere Instruktionen verwendet. Das kümmert uns im Moment aber recht wenig, so lange es seinen Zweck erfüllt. Und der Zweck ist hier, den Inhalt des Registers v0 mit der Matrix im Register c0 zu multiplizieren. Natürlich handelt es sich hierbei um die Transformation, da wir die Position des Vertex mit der Transformationsmatrix multiplizieren. In bester Assembler Manier steht das Zielregister als erstes, die beiden Operanten für die Operation als zweites und drittes. Das Ergebnis wird also im Output-Register
    oPos gespeichert (für eine Übersicht und die Funktion der Register siehe den ganz oben angegebenen Artikel).
  • mov oD0, c4
    Die
    mov Instruktion tut nichts weiter, als einen Wert aus einem Register in ein anderes zu verschieben. Das tun wir hier mit dem Wert für das ambiente Licht, welches wir aus dem Konstanten-Register in das Output-Register oD0 schieben. Man beachte, dass diese Register der Grafikkarte immer ein Array von vier float Werten aufnehmen. 
  • dp3 oD1, v3, -c5
    Für das gerichtete Licht ist etwas mehr Arbeit nötig, denn im Gegensatz zu dem ambieten Licht müssen wir hier noch berechnen, wie stark der Vertex durch das Licht beleuchtet wird. Wenn das Rechteck beispielsweise ganz von dem Licht weggedreht ist, dann darf es nicht von dem gerichteten Licht beeinflusst werden. Die Instruktion dp3 steht für Dot Product 3, also ein Punktprodukt zwischen zwei dreidimensionalen Vektoren. Das Punktprodukt drückt ja die Relation aus, in der zwei Winkel zueinander stehen. Für gerichtetes Licht ist nur entscheidend, wie gross der Winkel zwischen einem Objekt und der Lichtrichtung ist. Je grösser der Winkel desto weniger intensiv trifft das Licht da Objekt. Wir kehren hier den Lichtvektor im Register C5 zunächst um, da wir ihn als Richtungsvektor für das Licht definiert haben, ihn für diese Berechnung aber als Vektor zur Lichtquelle hin benötigen. Dann multiplizieren wir ihn mit dem Normalenvektor des Vertex im Register V3 und speichern das Ergebnis im Output-Register oD1.
  • mov oT0, v6
    Zu guter letzt verschieben wir die Texturkoordinaten aus dem Register v6 einfach ohne Operation weiter in das Outputregister.

Damit haben wir also unseren ersten echten Vertex-Shader programmiert. Die Assembler-Codierung eines Shaders ist recht gewöhnungsbedürftig wenn man früher noch nie Assembler programmiert hat. Aber heutzutage gibt es auch schon High Level Programmiersprachen für Shader, die eine C ähnliche Syntax verwenden. Beispielsweise nVidias CG oder Microsofts HLSL des DirectX SDK. Man sollte auch bedenken, dass es einige Unterschiede zwischen den verschiedenen Versionen der Shader gibt, die zum Teil sehr gravierend sind. Aber schauen wir uns jetzt lieber mal einen echten Pixel-Shader an.


Ein einfacher Pixel Shader

Der Pixel-Shader in diesem Beispiel tut eigentlich nicht wirklich viel und ist theoretisch auch gar nicht notwendig da bei einem Directional Light alle Pixel eines Triangles denselben Farbwert erhalten wenn die Vterex-Normalen parallel sind. Dennoch möchte ich hier die Operationen eines einfachen Pixel-Shaders zeigen und daher verwenden wir den Pixel-Shader, um das ambiente Licht mit dem Directional Light aufzuaddieren. Der Quellcode unseres Pixel-Shaders sieht wie folgt aus: 

   const char BasePShader[] =
      "ps.1.1           \n"\
      "tex t0           \n"\
      "add r0, v0, v1   \n"\
      "mul r0, r0, t0   \n";

Und hier die Bedeutung der Zeilen im einzelnen erklärt:

  • ps.1.1
    Diese Zeile definiert, dass es sich hierbei um einen Pixel-Shader handelt und dass wir die Version 1.1 verwenden. Nicht weiter spektakulär, oder?
  • tex t0
    Die Instruktion
    tex dient dazu, den Farbwert des Texels der zugehörigen Textur an der Stellen die für den Pixel aus den projizierten Vertices interpoliert wurde in ein Texturregister zu laden, hier das Register t0
  • add r0, v0, v1
    Nicht gerade überraschenderweise steht die Instruktion
    add für eine Addition zweier Werte, wobei nach alter Assembler Manier zuerst das Speicherziel für das Ergebnis angegeben wird und dann die beiden Summanden. In diesem Fall addieren wir die beiden Farbwerte aus den Registern v0 (hier: ambienter Lichtwert, entspricht oD0 Register) und v1 (hier: Punktprodukt zwischen Licht- und Normalenvektor, entspricht oD1 Register) und speichern das Ergebnis im Output-Register r0.
  • mul r0, r0, t0
    Logischerweise dient die
    mul Instruktion dazu, zwei Werte miteinander zu multiplizieren, und auch hier geben wir zuerst das Zielregister für das Ergebnis an, und dann die beiden Multiplikatoren. Hier multiplizieren wir das Register r0, also unseren Lichtwert aus ambientem und direktionalem Licht, mit dem Register t0, also dem Farbwert der Textur. Das Ergebnis speichern wir wiederum in r0, welches ja auch gleichzeitig das Outputregister ist. Man beachte, dass wir bei einem Pixel-Shader im Gegensatz zu einem Vertex-Shader aus den Output-Registern auch Daten lesen können. 

Und das war schon der ganze Pixel-Shader. Wir laden den interpolierten Farbwert für den Pixel aus der Textur, addieren sämtliches auf den Pixel einwirkendes Licht und modifizieren den Farbwert der Textur mit der entsprechenden Lichtintensität. Das Ergebnis ist die Farbe mit der der Pixel am Bildschirm auftaucht. 


Das Demoprogramm

So, wir haben nun bereits alles besprochen was wir für unser neues Programm brauchen. Wir beginnen zunächst mit den neuen Methoden unserer Klasse, die das Device auf eine Unterstützung von Shadern hin prüfen und die notwendigen Objekte dann erstellen:

// check if our device supports shaders at all?
bool Application_C::ShadersSupported(void) {
   D3DCAPS9 d3dCaps;

   // query shader support
   m_pDevice->GetDeviceCaps(&d3dCaps);

   if (d3dCaps.VertexShaderVersion < D3DVS_VERSION(1,1))
      return false;

   if (d3dCaps.PixelShaderVersion < D3DPS_VERSION(1,1))
      return false;

   return true;
   } // ShadersSupported
/*------------------------------------------------------*/

// creates the shaders and vertex declarations
HRESULT Application_C::CreateShaderStuff(void) {
   LPD3DXBUFFER pCode=NULL;
   LPD3DXBUFFER pDebug=NULL;
   DWORD *pdwShader=NULL;

   const char BaseVShader[] =
      "vs.1.1            \n"\
      "dcl_position0 v0  \n"\
      "dcl_normal0 v3    \n"\
      "dcl_texcoord0 v6  \n"\
      "m4x4 oPos, v0, c0 \n"\
      "mov oD0, c4       \n"\
      "dp3 oD1, v3, -c5  \n"\
      "mov oT0, v6       \n";

   const char BasePShader[] =
      "ps.1.1         \n"\
      "tex t0         \n"\
      "add r0, v0, v1 \n"\
      "mul r0, r0, t0 \n";


   // vertex declarations needed for shaders
   D3DVERTEXELEMENT9 declVertex[] = {
      { 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, 
      { 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, 
      { 0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0}, 
      D3DDECL_END()
      };

   m_pDevice->CreateVertexDeclaration(declVertex, &m_pDeclVertex);

   // create the vertex shader
   if ( FAILED( D3DXAssembleShader(BaseVShader, 
                        strlen(BaseVShader),
                        NULL, NULL, 0,
                        &pCode, &pDebug) )) {
      fprintf(m_pLog, "error: %s\n", (char*)pDebug->GetBufferPointer());
      return E_FAIL;
      }
   else {
      pdwShader = (DWORD*)pCode->GetBufferPointer();

      if ( FAILED( m_pDevice->CreateVertexShader(
                                    pdwShader,
                                    &m_pVShader) )) {
         fprintf(m_pLog, "error: CreateVertexShader() failed\n");
         return E_FAIL;
         }
      }

   // create the pixel shader
   if ( FAILED( D3DXAssembleShader(BasePShader, 
                        strlen(BasePShader),
                        NULL, NULL, 0,
                        &pCode, &pDebug) )) {
      fprintf(m_pLog, "error: %s\n", (char*)pDebug->GetBufferPointer());
      return E_FAIL;
      }
   else {
      pdwShader = (DWORD*)pCode->GetBufferPointer();

      if ( FAILED( m_pDevice->CreatePixelShader(
                                 pdwShader,
                                 &m_pPShader) )) {
         fprintf(m_pLog, "error: CreatePixelShader() failed\n");
         return E_FAIL;
         }
      }
   return ActivateShaders();
   } // CreateShaderStuff
/*------------------------------------------------------*/

Mit diesen beiden Methoden haben wir die gesamte Erstellungs-Arbeit für die Shader Objekte erledigt, und jetzt müssen wir sie nur noch aktivieren. Doch auch das ist ja wohl ein Klacks für uns, oder?

// activate the shaders to use 'em
HRESULT Application_C::ActivateShaders(void) {
   // activate shader
   if ( FAILED( m_pDevice->SetVertexDeclaration(m_pDeclVertex) )) {
      fprintf(m_pLog, "error: SetVertexDecalration() failed\n");
      return E_FAIL;
      }

   // activate vertex shader
   if ( FAILED( m_pDevice->SetVertexShader(m_pVShader) )) {
      fprintf(m_pLog, "error: SetVertexShader() failed\n");
      return E_FAIL;
      }

   // activate pixel shader
   if ( FAILED( m_pDevice->SetPixelShader(m_pPShader) )) {
       fprintf(m_pLog, "error: SetPixelShader() failed\n");
      return E_FAIL;
      }

   m_pDevice->SetFVF(NULL);

   return S_OK;
   } // ActivateShaders
/*------------------------------------------------------*/

Gut, die entsprechenden Funktionen rufen wir natürlich bei der Initialisierung unserer Klasse auf. Das sollte klar sein aber Ihr könnt Euch gerne den Code aus dem Download anschauen wenn Ihr Euch da ein wenig unsicher seid. Unsere Tick() Methode müssen wir aber noch ein wenig schleifen, damit sie nun wie folgt aussieht, wobei die Hauptänderungen natürlich im Betanken der Konstanten-Register für den Vertex-Shader zu sehen sind.

// do this once each frame
HRESULT Application_C::Tick(float dt) {
   D3DXMATRIX mY, mZ, m, matShader, matInv;
   static float fRot=0.0f;
   static bool i=true;
   static float t=0.1f;

   // calculate rotation
   fRot += dt*2.0f;
   if (fRot > (2*3.141593f)) fRot -= (2*3.141593f);
   D3DXMatrixRotationY(&mY, fRot);
   D3DXMatrixRotationZ(&mZ, fRot);
   D3DXMatrixMultiply(&m, &mY, &mZ);

   // calculate translation
   if (i) t += (2.0f*dt);
   else t -= (2.0f*dt);
   if ( (t>5.0f) || (t<0.1f)) i=!i;
   m._43 = 4.0f+t;

   // activate translation
   D3DXMatrixMultiply(&matShader, &m, &m_matProj);
   D3DXMatrixTranspose(&matShader, &matShader);
    m_pDevice->SetVertexShaderConstantF(0, (float*)&matShader, 4);

   // transform light direction to object space
   D3DXVECTOR4 vcLight(0.0f, 0.0f, 1.0f, 0.0f);
   D3DXMatrixInverse(&matInv, NULL, &m);
   D3DXVec4Transform(&vcLight, &vcLight, &matInv);
   m_pDevice->SetVertexShaderConstantF(5, (float*)&vcLight, 1);

   m_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

   // clear back-buffer
   m_pDevice->Clear(0, NULL, 
            D3DCLEAR_TARGET |
            D3DCLEAR_ZBUFFER, 
            D3DCOLOR_XRGB(0,255,0), 
            1.0f, 0);

   // adjust device settings
   m_pDevice->SetTexture(0, m_pTexture);

   // actual rendering calls
   m_pDevice->BeginScene();

      RenderRect();

   m_pDevice->EndScene();

   if ( FAILED(m_pDevice->Present(NULL, NULL, NULL, NULL) ))
      return E_FAIL;

   return S_OK;
   } // Tick
/*------------------------------------------------------*/

Jo, langer Code, kurzer Sinn. Das war's auch schon. Wenn Ihr das Programm nun anstartet dann seht ihr wiederum das Rechteck mit der Textur wie es sich dreht und vor und zurück bewegt. Allerdings habt Ihr es nun über Shader gerendert und nicht mehr über die Fixed Function Pipeline. Ausserdem seht ihr, dass das Rechteck dunkler wird, wenn es sich aus dem Lichtvektor wegdreht. Die Rückseite des Rechtecks ist nun jedoch schwarz wenn sie nach vorne zeigt, was zwar physikalisch nicht korrekt ist da sie ja auch die volle Dröhnung Licht in dieser Ausrichtung abbekommen müsste. Das liegt aber daran, dass die Normalenvektoren in dieser Situation um 180 Grad von der Lichtquelle wegzeigen. Wenn wir die Rückseite des Rechtecks auch richtig beleuchtet haben wollen, dann müssten wir dort auch eine echte Rückseite, sprich ein eigenes Polygon mit eigenen, korrekten Normalenvektoren erstellen, und nicht einfach das Backface Culling ausstellen => Hausaufgabe sach ich nur :-)



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick