Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung | Blog RSS Vorab: DirectX 9.0a installieren!

Allgemeines zu .fx und Cg (Fortsetzung)

Mit MrWiggle.fx: die Vertex-Shader-Funktion verdoppeln/ändern und in mehreren Passes anwenden.
Dazu im jeweiligen pass Alpha-Compositing anschalten:
AlphaBlendEnable = true;
SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha;
Das gibt die üblichen Probleme mit dem z-Puffer. Abhilfe ggf. durch Änderungen bei
ZEnable = true;
ZWriteEnable = true;
CullMode = None;

Bedingte Befehle werden auf den älteren Grafikkarten raffiniert compiliert:
if(wiggleX > 0.0) wiggleX = 10.0;
und
if(wiggleX > 0.0) OUT.diffCol.r = 0.0;
Sogar for-Schleifen funktionieren auf älteren Grafikkarten -- wenn der Compiler sie komplett auflösen kann, d.h. die Zahl der Durchläufe fest ist und die Länge des entstehenden Codes noch in den Chip passt:
float wiggleX=0.0;
float wiggleY=0.0;
for(float n = 2; n<4; ++n)
{
    wiggleX += sin(n*iny) * Horizontal;
    wiggleY += cos(n*iny) * Horizontal;
}
Was nicht mit älterer Hardware geht:


Mit DirectX-9-Hardware oder unter OpenGL einer großen GeForceFX ginge das. Dazu muss man bei compile ein anderes Profil benutzen. Leider scheint der CgFX Viewer nicht mit vollständing nVidias NV30-Emulator (NVEmulate.exe) zusammenzusarbeiten. Sonst könnte man den Emulator anschalten und die Profile vp30 (neue Vertex-Shader) und fp30 (neue Pixel-Shader) benutzen. Aber etwas geht doch, s.u.

Pixel-Shader

Ein Pixelshader liest die Datenstruktur, die ein Vertex-Shader ausgibt. Aber: Die Werte, die im Pixel-Shader ankommen, sind nicht unbedingt die, die der Vertex-Shader geschrieben hat. Der Vertex-Shader erhält ja nur die Vertizes. Dazwischen wird linear interpoliert!

Nur Pixel-Shader, nicht Vertex-Shader haben Zugriff auf Texturen. (Verztex-Shader können allerdings die Textur-Koordinaten verbiegen.) Beispiel für Textur-Zugriff: DiffTex.fx. Das Dateiformat für Texturen, die aus .fx-Dateien angesprochen werden, ist .dds (DirectDrawSurface); ein Export-Plug-in für Photoshop gibts bei nVidia.

Die Textureinheiten des Chips sind im Programmtext "Texture Sampler". Sie werden formal als Argumente an den Pixel-Shader übergeben. Dazu sind sie zu deklarieren. In der Deklaration finden sich auch Einstellungen (MIP-Map usw.). Im Pixelshader werden Texturen typischeweise mit <Farbvariable> = tex2D(<Sampler>,<Koordinaten>) ausgelesen.

Im Pixel-Shader ist anders als im Standardmodus von Grafikkarten die Lichtberechnung pro Pixel möglich. Sonst wird die Beleuchtung nur pro Vertex berechnet und linear interpoliert (Gouraud-Interpolation, zu erkennen an den hässlichen sechseckigen Glanzlichtern). Mit Pixel-Shadern kann man nun Phong-Interpolation (Normalenvektoren interpolieren, nicht die Farben) benutzen wie in der "richtigen" 3D-Software und damit runde Glanzlichter produzieren.

Typische Anwendnung von per-pixel-lighting: Bump-Map, ggf. spiegelnd mit Environment-Map (auch missverständlich Reflection-Map genannt). In älteren Grafikkarten sind die dafür nötigen Funktionen weitgehend vorverdrahtet (siehe DiffuseBumpCg.fx). Auf den neuen Grafikkarten sind auch Pixel-Shader sehr frei zu programmieren.

Um das Verhalten der aktuellen Grafikkarten auf einer GeForce4 zu emulieren: Fenster von CgFX Viewer klein ziehen (Rechenaufwand!), NV30-Emulation und Software-Rasterer anschalten (NVEmulate.exe). Dann werden die neuen DirectX-9-Shader (compile ps_2_... etc.) automatisch emuliert, wenn man auf OpenGL stellt (sehr eigenwillig).

Beispiel: BumpReflectCgDX9.fx (nebenbei: andere "Technique" für ältere Karten enthalten!). BumpHeight auf null stellen: Man sieht eine Umgebungstextur (environment) reflektiert. Die ist wie allgemein üblich als Cube-Map angelegt (siehe Environment-Probekapitel aus dem nVidia-Buch), scheint also in einen unendlich ausgedehnten Würfel gemalt zu sein. Im Vergleich zur Projektion mit einer Kugel erspart das erspart unschöne Stellen wie sie durch die Pixelhäufungen an den Polen der Kugel auftreten. Im Pixel-Shader wird die Cube-Map einfach mit texCUBE und einer Richtungsangabe ausgelesen.

Environment-Maps sind für große Objekte geometrisch falsch und zeigen keine Selbstreflexionen. Aber solche Fehler fallen nur geschulten Betrachtern auf.

Praktisch der gesamte Programmcode von Vertex- und Pixel-Shader in BumpReflectCgDX9.fx dient dazu, zu berechnen, in welcher Richtung in der Cube-Map nachgeschaut wird, in welche Richtung also der Sehstrahl gespiegelt wird. Dazu muss man die Richtung des Sehstrahls und den Normalenvektor in Zahlenangaben im Welt-Koordinatensystemkennen. Den Rest erledigt reflect.

Das Problem ist der Normalenvektor: Er soll von einer Textur gesteuert vom geometrischen Normalenvektor abweichen.

Dazu liefert man zu jedem Vertex nicht nur den (unverbogenen) Normalenvektor n mit, sondern auch einen tangentialen Vektor t und eine Binormale b = n x t. Die drei Vektoren t, b, n sind Einheitsvektoren, stehen paarweise senkrecht aufeinander und bilden ein Rechtssystem.

Per Textur lässt sich der Normalenvektor nun steuern, indem man sagt, welche Anteile von t, b und n er an einen bestimmten Stelle der Textur besitzen soll. Ist er gleich n, so ist die Oberfläche unverforrmt. Je mehr Anteile von t und b hinzukommen, um so stärker wird die Neigung.

Die Anteile von t, b und n lassen sich als RGB-Textur ("Normal Map") codieren: Die Anteile von t, b und n können zwischen -1 und 1 liegen, aber die RGB-Werte umfassen nur jeweils den Bereich 0 bis 1. Also muss man stauchen: -1 wird in der Textur zu 0 und 0 wird zu 1/2. Eine Texur für einen unverformten Körper ist damit hellbau: (0, 0, 1) = 0*t + 0*b +1*n wird zu (0.5, 0.5, 1.0). Der erste Schritt nach dem Einlesen der Normal Map besteht darin, diese Umrechnung rückgängig zu machen: mal 2, minus 1.

Eine solche Normal Map ist also keine Bump-Map, wie aus der 3D-Software bekannt, sondern ungefähr deren Gradient!

In BumpReflectCgDX9.fx werden die R- und G-Anteile der Normal Map mit einem globalen Faktor multipliziert, um darüber die Stärke der Verformung zu regeln. Danach muss der Normalvektor allerdings wieder auf die Länge 1 gebracht werden.

Der aufwendigste Teil ist nun, den Normalenvektor aus dem tangentialen System in Weltkoordinaten umzurechnen, denn die übrigen Größen liegen ebenfalls in Weltkoordinaten vor und auch das Ergebnis (Reflexionsrichtung) muss in Weltkoordinaten angegeben werden. Um den Normalenvektor aus dem tangentialen System auf Objekt-Koordinaten unzurechnen (in denen sind z.B. die Vertizes und unverbogenen Normalenvektoren angegeben), kann man rechnen:
normalObj = t*normal.x + b*normal.y + n*normal.z = (t, b, n)*normal
Die aus den drei Vektoren gebildete Matrix (t, b, n) hat in BumpReflectCgDX9.fx den Namen TangentToObjSpace. Nun kann man dieses Ergebnis mit der Matrix World (eigentlich besser WorldIT) in Weltkoordinaten transformieren:
worldNorm = World * normalObj
= world * TangentToObjSpace * normal.

Das erste Produkt enthält nur Größen, die sich pro Vertex ändern (und dazwischen linear interpoliert werden). Das Produkt mit normal muss dagegen pro Pixel passieren. Also berechnet man im Vertex-Shader world * TangentToObjSpace und übertragt das als einen Satz von drei Texturkoordinatenvektoren verkappt an den Pixel-Shader:
(TexCoord1, TexCoord2, TexCoord3) = (world * TangentToObjSpace)T

Der Pixel-Shader hat damit nur noch
worldNorm = (TexCoord1, TexCoord2, TexCoord3)T * normal
zu berechnen.

Das zeigt abermals eine sinnvolle Arbeitsteilung: Alle Teilergebnisse, die sich schon pro Vertex bestimmen lassen, soll man aus dem Pixel-Shader heraushalten. Sonst entsteht der x-fache Rechenaufwand, weil es (meist) viel mehr Pixel als Vertizes gibt.