Fri, 04 Oct 2024
Wenn Ihr Level in der A7 Engine des 3D GameStudios so altbacken aussieht und das, obwohl Sie doch perfekte Texturen und saubere Modelle oder gar hin und wieder auf solche Shadereffekte zugreifen wie Bumpmapping, Normalmapping oder Parallaxmapping. Dann fehlen Ihnen Postprocessingeffekte. In der folgenden Tutorialserie werde ich Ihnen zeigen, wie Sie z.B. einen einfachen Blurshader programmieren und implementieren können.
Erst einmal geht es um die Grundstruktur eines solchen Shadereffektes in HLSL und um die einfache Implementierung über lite-c.
Das gesamte Tutorial zusammen mit Codebeispielen steht hier zum Download bereit.
Vorwort |
Arbeitsumgebung |
Grundstruktur |
Implementierung in die A7 Engine |
Effekte und Spielereien |
Zusammenfassung |
Additum
Postprocessing bedeutet soviel wie Nachbearbeitung. Es sind also Effekte, die erst nachträglich hinzugefügt werden. Dabei wird der Bildschirminhalt als Textur zwischengespeichert. Der Shadereffekt wird auf jeden Pixel dieser Textur angewendet und das Ergebnis anschließend auf den Bildschirm gerendert.
Dieses Tutorial richtet sich an Anfänger, die gerade erst in die Möglichkeiten der Shaderprogrammierung einsteigen. Sie sollten aber grundlegendes Wissen über das 3DGS und Lite-C mitbringen bzw. falls Sie Postprocessing Shader in einem anderen Kontext verwenden wollen, grundlegendes Wissen über C.
Was Sie im Rahmen dieses Tutorials lernen werden:
Nehmen Sie ein beliebiges buntes Level Ihrer Wahl oder das Testlevel im Downloadbereich. Es sollte mit mindestens einer Lichtquelle ausgestattet sein und bereits kompiliert worden sein. Wenn Sie möchten, können Sie auch gerne ein ganz neues Level in WED basteln, es mit Texturen belegen, Lichter einfügen und schnell kompilieren. Sollten Sie an dieser Stelle nicht weiter wissen, ziehen Sie das Benutzerhandbuch für das 3D GameStudio zu Hilfe.
Legen Sie ein neues lite-c Dokument an.
Da Sie die Acknex 7 Engine benutzen werden, müssen Sie diese per #include anbinden. Wichtig bei lite-c im Gegensatz zum C-Script in A6 ist, dass dieser Befehl nicht mit einem Semikolon abgeschlossen werden darf:
#include <acknex.h>
Zunächst erstellen Sie ein neues Material im neuen Lite-C Dokument. Ein Material enthält Parameter und Effekte, die die Darstellung von Objekten beeinflussen. Seit A7 kann man auch View-Objekten, also Ansichten wie der Standard Camera, ein eigenes Material zuweisen, das sich dann auf die Darstellung des gesamten Bildschirmes auswirkt. Genau auf diese Weise werden Sie Ihren ersten Postprocessingshader erstellen. In lite-c wird ein Material folgendermaßen definiert:
MATERIAL* mtl_MyMaterial={
//Code
}
Im Zusammenhang mit unserem Vorhaben ist für uns ein Parameter besonders interessant:
MATERIAL* mtl_MyMaterial={
effect="[Shader-Code]";
}
In den Effekt Parameter kommt entweder ein String mit Shadercode oder man erstellt eine externe .fx Datei z.B. myShader.fx und verweist auf sie in dem Parameter. Der Einfachheit halber werden wir unseren Shadercode direkt reinschreiben. Die zweite Methode wird im Additum angesprochen.
Wie sieht nun die Grundstruktur eines Postprocessing Shaders aus? Schreiben Sie erst einmal den unten stehenden Code ab oder laden Sie lite-c Datei posteff.c aus dem Dowloadbereich in den Scripteditor.
MATERIAL* mtl_MyMaterial={
effect="
//Definitionsbereich
texture TargetMap;
//Sampler
sampler mySampler = sampler_state{texture=<TargetMap>;};
//Shader
float4 PP_Shader (float2 Tex: TEXCOORD0):Color0
{
float4 Color;
Color=tex2D(mySampler,Tex.xy);
return Color;
}
}
";
}
Was haben Sie nun gemacht?
Zuerst haben Sie mit "texture TargetMap;" eine zu verwendende Textur definiert. Später wird sie von der Engine mit dem Bildschirminhalt, wie einem Schreenshot, belegt werden. (Dieser "Screenshot" ist ein Buffer, der letzte render Target, auf den die sichtbare Szene gerendert wurde.) Dieser soll eben in der Nachbearbeitung (Postprocessing) modifiziert werden.
Momentan ist dies die einzige Definition im sog. Definitionsbereich, den man nach Belieben erweitern kann, soweit der eigene Shadercode verlangt. Wohlgemerkt sollte ein guter Shader möglichst wenig Speicherplatz verbrauchen!
Da Sie in diesem Shadereffekt mit Texturen arbeiten, müssen Sie einen texture Sampler definieren, über den Sie den Zugriff und das Lesen der Texturinformationen steuern können. Z.B. kann man festlegen ob eine Textur gekachelt erscheinen soll oder nicht. Dies brauchen wir hier aber nicht zu tun. Insofern beschränkt sich der Sampler nur auf die Zuweisung der zu lesenden Textur (TargetMap), die zuvor von Ihnen definiert wurde.
"sampler mySampler = sampler_state{texture=<TargetMap>;};"
Allgemein gilt: Jede benutzte Variable und Textur muss vor der Verwendung im Shader definiert werden, selbst wenn sie in lite-c vordefiniert ist.
Die Sprache, die wir jetzt für unseren Shader benutzen, heißt HLSL und wurde dafür entwickelt den Grafikprozessor (GPU) der Grafikkarte, genauer gesagt die Direct3D Pipeline, zu programmieren. Es handelt sich dabei um eine
Hochsprache, die seit DirectX 8 existiert. Anstelle von komplizierten und fehleranfälligen maschinennahen Assemblercodes, treten bei HLSL gewohnte Elemente von C in den Vordergrund. Zudem fallen einige sinnvolle Vereinfachungen in der Syntax auf, zu denen wir aber später kommen werden.
Jetzt wird es interessant! Die Shaderfunktion wird nach normaler C-Syntax definiert. Sie bestimmt wie Ihr Effekt schlussendlich aussehen wird.
Die Syntax für eine Pixelshadersignatur sieht folgendermaßen aus:
float4 PP_Shader (float2 Tex: TEXCOORD0):Color0
Float4 ist der Datentyp des geforderten Rückgabewertes. Bei einem Pixelshader ist dies logischerweise immer eine Farbe, die Farbe des Pixels auf dem Bildschirm. Und Pixelfarben werden mithilfe von vier Fließkommawerten (floating point values) beschrieben, nämlich Rot, Grün, Blau und Alpha (Transparenz), RGBA. Daher ein solcher Array der Größe 4.
Wie Sie sehen, werden in HLSL Arrays, oder hier auch Vektoren genannt, sehr einfach definiert, einfach die Größenzahl direkt hinter den Typ schreiben.
PP_Shader ist der Name der Shaderfunktion und kann beliebig umgeschrieben werden.
In den Parametern wird "float2 Tex: TEXCOORD0" an die Funtion von jedem Pixel übergeben. Float2 ist wieder ein Fließkommaarray und der Datentyp der Parameter-Variable Tex. Diesmal beinhaltet er aber nur zwei Werte, nämlich die X- und Y-Koordinate des jeweiligen Pixels auf dem Bild(schirm). Damit die Acknex-Engine weiß, dass sie diese Variable mit den Koordinaten zu fühlen hat, weisen Sie als Typ TEXCOORD0 zu. Die 0 am Ende ist sehr wichtig und darf nicht ausgelassen werden. Die Zuweisung erfolgt über den ":" nach dem Namen.
Und schließlich wird am Ende der Definition des Pixelshaders mit ":Color0" angegeben, was er zurückliefert, nämlich eine Farbe, wie Sie sich erinnern im float4 Format. Damit weiß die GPU, was sie mit dem Rückgabewert anfangen soll, nämlich den Pixel auf dem Bildschirm in der neuen Farbe zu zeichnen.
Jetzt geht es ans Eingemachte! Zwischen den beiden geschweiften Klammern steht der entscheidende Shadercode, der mit einer return Anweisung enden muss, weil Sie doch eine Farbe zurückliefern wollen. Sonst käme es zu einer Fehlermeldung, weil die Funktion eine return-Anweisung erwartet!
Zuallererst definieren Sie eine lokale Variable des Types float und nennen Sie z.B. Color. Sie soll im nächsten Schritt die Farbe des zu zeichnenden Pixel erhalten.
In der simplen Zeile "Color=tex2D(mySampler,Tex.xy);" wird nichts anderes gemacht, als der Variable Color die Farbe des Pixels der Bildschirmtextur an der Stelle xy zuzuweisen. Hier sehen Sie eine Besonderheit von HLSL. Denn über ".Komponente1Komponente2" können in einem Befehl beide Koordinaten verwendet werden. Der Befehl Color=tex2D(Sampler,Koordinaten) wird Ihnen noch häufiger begegnen. Tex2D() liest mithilfe des Samplers die Farbe eines Pixels der angegebenen Textur an den Koordinaten ab und gibt sie zurück. Der Shader wird auf jeden Pixel angewendet, sodass die Koordinaten jedes Mal automatisch aktualisiert werden. Später wird dieser Befehl viele interessante Effekte ermöglichen.
Schließlich folgt die abschließende return Anweisung.
Nun ist alles nötige gemacht und Sie können den Shader ausprobieren. Doch halt! Wie soll die Grafikkarte wissen, was sie mit der Funktion anfangen soll? Was jetzt noch fehlt, ist das Herzstück des gesamten Shaders, die Technique:
Zuerst definieren Sie eine neue technique namens PP.
Sie kann mehrere Pässe enthalten. In jedem Pass wird der Bildschirminhalt mithilfe des benutzten Pixelshaders gerendert. Für Ihren einfachen Effekt benötigen Sie nur einen: pass p1
technique PP{
pass p1{
PixelShader= compile ps_1_0 PP_Shader();
VertexShader=null;
}
}
Im Pass wird angegeben welcher Pixelshader ausgeführt werden soll. "compile ps_1_0" vor dem Funktionsaufruf gibt an, dass der Shader für das Shadermodell 1.0 geschrieben ist. Spätere Shadermodelle wie 2.0, 3.0 oder 4.0 ermöglichen mehr Effekte, laufen aber nicht auf älteren Rechnern. Der Shader sollte deswegen stets mit der niedrigst möglichen Version kompiliert werden. Hier reicht Shadermodell 1.0 völlig aus. Weil ein Postprocessingshader nur aus einem Pixelshader besteht, schreiben Sie für VertexShader=null;
übrigens: Wenn Sie einen Shader programmieren, der ein höheres Shadermodell zwingend erfordert, ist es gut wenn möglich eine zweite technique namens fallback zu definieren. Sie sollte eine vereinfachte Version des Shaders enthalten, damit Besitzer älterer Hardware nicht ganz außer Acht gelassen werden. Sie wird ausgeführt, wenn die erste nicht kompiliert werden kann.
Nun ist Ihr erster Postprocessing Shader fertig und muss nur noch zugewiesen werden.
alles wissenswerte dieses Abschnitts:
News
28.05.2012 |
Neuestes Video Quick terrain/ Schnelles Terrain Tutorial (no sound) from Grygoriy Kulesko on Vimeo. |
Zufallsbild
|
Vote Derzeitig finden keine Votings statt. |
Partner
Werden Sie Link-Partner von kstudios.de und an
dieser Stelle erscheint ein Link zu Ihrer Website!
Kontakt