GLSL Licht und Schatten: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Grundlegendes zu den Koordinatensystemen in den Shadern)
K (Paraboloide Maps: WäHrend)
 
(27 dazwischenliegende Versionen von 6 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
{{Offline}}
 
 
 
==Vorwort==
 
==Vorwort==
  
Willkommen zu meinem erstem Tutorial. Schatten können sowohl Traum als auch Albtraum eines jeden OpenGL-Progrmamieres werden. Im Gegensatz zur einfachen Beleuchtung mit Lichtquellen, steigt der Rechenaufwand hier extrem an und ohne Optimierung zwingen die Schatten selbst die modernste Hardware in die Knie.
+
Willkommen zu meinem erstem Tutorial. Schatten können sowohl Traum als auch Albtraum eines jeden OpenGL-Programmierers werden. Im Gegensatz zur einfachen Beleuchtung mit Lichtquellen, steigt der Rechenaufwand hier extrem an und ohne Optimierung zwingen die Schatten selbst die modernste Hardware in die Knie.
Für Schatten gibt es zwei praktikable Algoritmen: Den Stencilschatten und den projezierten Schatten. Hier möchte ich mich auf den projezierten Schatten beschränken, er bietet gegenüber dem Stencilschatten einige Vorteile:
+
Für Schatten gibt es zwei praktikable Algorithmen: Den Stencilschatten und den projizierten Schatten. Hier möchte ich mich auf den projizierten Schatten beschränken, er bietet gegenüber dem Stencilschatten einige Vorteile:
  
*Die komplette Berechnung kann von der Grakfikkarte übernommen werden
+
*Die komplette Berechnung kann von der Grafikkarte übernommen werden
 
*Es ist möglich Softshadows zu realisieren
 
*Es ist möglich Softshadows zu realisieren
 
*Die Shadowmaps können unter Umständen für mehr als ein Frame verwendet werden
 
*Die Shadowmaps können unter Umständen für mehr als ein Frame verwendet werden
  
Um den Rechenaufwand zu veringern werde ich hier zusätzliche Projektionstechniken zeigen, die über den Vertexshader realisiert werden: Die perspektivische Map und das parabolide Mapping.  
+
Um den Rechenaufwand zu verringern werde ich hier zusätzliche Projektionstechniken zeigen, die über den Vertexshader realisiert werden: Die perspektivische Map und das paraboloide Mapping.  
  
Für sehr weit entfernte Lichtquellen und offene Scenen. Ist der einsatz von Perspektisch angepassten Maps sinvoll. Da das Licht quasi Parallel ausgestrahlt wird. Ist eine Winkelabhängige Cube oder dualparabolische Map kaum möglich. Auch eine einfache quadratische Shadowmap zeigt deutliche Schwächen bei der Auflösung im Nahbereich und dem zu hohem Oversampling in der Entfernung.
+
Für sehr weit entfernte Lichtquellen und offene Szenen ist der Einsatz von perspektivisch angepassten Maps sinnvoll. Da das Licht quasi parallel ausgestrahlt wird. Ist ein winkelabhängiger Cube oder dual parabolische Map kaum möglich. Auch eine einfache quadratische Shadowmap zeigt deutliche Schwächen bei der Auflösung im Nahbereich und dem zu hohem Oversampling in der Entfernung.
  
Das parabolide Mapping ermöglicht nicht nur die Simulation eine Fischaugenoptik, sondern kann auch die Cubemap vollständig ersetzen. Durch einen geringfügig höheren Rechen- und Programieraufwand genügen zwei Renderpasses um eine vollständige Tiefen- oder Reflektionsmap zu erstellen. Was gegenüber der klassischen Cubemap den Prozessor und GPU um den Faktor 3 entlastet.
+
Das paraboloide Mapping ermöglicht nicht nur die Simulation eine Fischaugenoptik, sondern kann auch die Cubemap vollständig ersetzen. Durch einen geringfügig höheren Rechen- und Programieraufwand genügen zwei Renderpasses um eine vollständige Tiefen- oder Reflektionsmap zu erstellen. Was gegenüber der klassischen Cubemap den Prozessor und GPU um den Faktor 3 entlastet.
  
 
==Vorkenntnisse==
 
==Vorkenntnisse==
Dieses Tutorial basiert auf den grundlegenden Techniken, die erst in den letzten Jahren entwickelt wurden. Jeder der hier mit anfängt, sollte die zwei anderenen GLSL Tutorials gelesen haben und einfache Fragmentshader schreiben können. Auch das Laden und Einbinden von Texturen in die Shader sollte kein Problem mehr darstellen.
+
Dieses Tutorial basiert auf den grundlegenden Techniken, die erst in den letzten Jahren entwickelt wurden. Jeder der hiermit anfängt, sollte die zwei anderen GLSL-Tutorials gelesen haben und einfache Fragmentshader schreiben können. Auch das Laden und Einbinden von Texturen in die Shader sollte kein Problem mehr darstellen.
  
 
===Framebufferobjekte===
 
===Framebufferobjekte===
Für das Erstellen von dual paraboliden Tiefentexturen ist noch das Rendern in Framebufferobjekten Vorraussetzung (zu denen es leider noch kein Tutorial gibt). Damit der Einstieg nicht zu schwer wird sollte dieser Code helfen das FBO zu initialsisieren:
+
Für das Erstellen von dual paraboloiden Tiefentexturen ist noch das Rendern in Framebufferobjekten Vorraussetzung ([[Tutorial Framebufferobject]]). Damit der Einstieg nicht zu schwer wird sollte dieser Code helfen das FBO zu initialsisieren:
  
 
Als Erstes müssen die nötigen Extensions initialisiert werden
 
Als Erstes müssen die nötigen Extensions initialisiert werden
 
Dann sollten die benötigten Modelle als VBO in die Grafikkarte hochgeladen werden.
 
Dann sollten die benötigten Modelle als VBO in die Grafikkarte hochgeladen werden.
Nun muss das Framebufferobjekt (FBO) für die Schattenmaps erzeugt werdern.
+
Nun muss das Framebufferobjekt (FBO) für die Schattenmaps erzeugt werden.
 
So könnte die Initialsierung des FBOs aussehen. C code:
 
So könnte die Initialsierung des FBOs aussehen. C code:
  
<cpp>GLuint shadow_map = 0; //  
+
<source lang="cpp">GLuint shadow_map = 0; //  
 
GLuint shadow_fbo = 0; // the shadow texture
 
GLuint shadow_fbo = 0; // the shadow texture
GLuint shadow_size = 4096; //muss kleiner sein als die maximale Texturgröße sein
+
GLuint shadow_size = 4096; //muss kleiner/gleich maximaler 2D-Texturgröße sein
  
 
glGenTextures (1, &shadow_map); //In Pascal das & durch ein @ ersetzten
 
glGenTextures (1, &shadow_map); //In Pascal das & durch ein @ ersetzten
Zeile 41: Zeile 39:
 
glDrawBuffer (GL_FALSE);
 
glDrawBuffer (GL_FALSE);
 
glReadBuffer (GL_FALSE);
 
glReadBuffer (GL_FALSE);
GLenum status = glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.</cpp>
+
GLenum status = glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.</source>
ungetesteter Pascalcode:
+
Pascal :
<pascal>var shadow_map,
+
<source lang="pascal">var  
    shadow_fbo,
+
  shadow_map,
    shadow_size : GLuint;
+
  shadow_fbo,
    status      : GLenum;  
+
  shadow_size : TGLuint;
 +
  status      : TGLenum;  
 
begin
 
begin
  shadow_map  := 0; 
+
   shadow_size := 4096; // muss kleiner/gleich maximaler 2D-Texturgröße sein
  shadow_fbo  := 0;    // the shadow texture
 
   shadow_size := 4096; // muss kleiner sein als die maximale Texturgröße sein
 
  
 
   glGenTextures(1, @shadow_map);
 
   glGenTextures(1, @shadow_map);
 
   glBindTexture(GL_TEXTURE_2D, shadow_map);
 
   glBindTexture(GL_TEXTURE_2D, shadow_map);
   glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, shadow_size, shadow_size, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL);
+
   glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, shadow_size, shadow_size, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NIL);
 
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  
Zeile 60: Zeile 57:
 
   glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, shadow_fbo);
 
   glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, shadow_fbo);
 
   glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, shadow_map, 0);
 
   glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, shadow_map, 0);
   glDrawBuffe (GL_FALSE);
+
   glDrawBuffer (GL_FALSE);
 
   glReadBuffer(GL_FALSE);
 
   glReadBuffer(GL_FALSE);
   status := glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.</pascal>
+
   Status := glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.
 
+
end;</source>
 
Jetzt sollten die Shader und weiter Texturen geladen werden.
 
Jetzt sollten die Shader und weiter Texturen geladen werden.
  
 
===Beschleunigtes Rendern===
 
===Beschleunigtes Rendern===
  
Ich empfehle auch die zu rendernden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird. Da es hierzu bereits ein Tutorial gibt werde ich hier nichts mehr darüber schrieben.
+
Ich empfehle auch die zu rendernden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird. Da es hierzu bereits [[Tutorial Vertexbufferobject|ein Tutorial]] gibt werde ich hier nichts mehr darüber schrieben.
Alternativ sollte alles was zwischen glBegin und glEnd in Displaylisten gespeichter werden. Das Speichern von Texturwechseln usw ist nicht sinvoll, da diese nicht zum Rendern der Shadowmaps benötig werden.
+
Alternativ sollte alles was zwischen [[glBegin]] und [[glEnd]] in [[Displaylisten]] gespeichter werden. Das Speichern von Texturwechseln usw. ist nicht sinnvoll, da diese nicht zum Rendern der Shadowmaps benötig werden.
  
Bei mehrfachen Lichquellen kann es wiederum Sinn machen den ersten Rendervorgang inclusive Shaderwechsel in einer Displayliste zwischenzuspeichen und die zusätzlichen Schadowmaps mit dieser zu rendern.
+
Bei mehrfachen Lichtquellen kann es wiederrum Sinn machen den ersten Rendervorgang inklusive Shaderwechsel in einer Displayliste zwischenzuspeichen und die zusätzlichen Schadowmaps mit dieser zu rendern.
  
 
===Algemeines zu GLSL===
 
===Algemeines zu GLSL===
Ich möchte nocheinmal darauf hinweisen, das sich uniformvariablen nur setzten Lassen, wenn der entsprechnde shader gebunden ist. (Es funktionier nicht wenn der shader nicht gebunden ist. Warum man den shader dabei allerdings angeben muss ist mir nicht so ganz klar)
+
Ich möchte nocheinmal darauf hinweisen, das sich Uniformvariablen nur setzten lassen, wenn der entsprechnde Shader gebunden ist. (Es funktionier nicht wenn der shader nicht gebunden ist. Warum man den shader dabei allerdings angeben muss ist mir nicht so ganz klar)
In den Beispielen werden nur die Uniformvariablen wärend des Rendervorgangs neu gesetzt die sich ändern. Die zuweisung der TMU zu einem Sampler gehöhrt in der Regel nicht dazu.  
+
In den Beispielen werden nur die Uniformvariablen während des Rendervorgangs neu gesetzt die sich ändern. Die Zuweisung der TMU zu einem Sampler gehöhrt in der Regel nicht dazu.  
  
 
===Grundlegendes zu den Koordinatensystemen in den Shadern===
 
===Grundlegendes zu den Koordinatensystemen in den Shadern===
  
Im Vertexshader müssen alle komponenten von gl_Position in einem Bereich von -1.0 bis 1.0 gebracht werden. Besonders beim Z-Wert könnte es verwirrend, dass der Bereich von gl_FragDepth im Fragmentshader von 0.0 bis 1.0 geht. Auch muss beachtet werden, dass Texturkordinaten den bereich von 0.0 bis 1.0 nutzen, wärend die Rendertargets im Bereich on -1.0 bis 1.0 arbeiten.
+
Im Vertexshader müssen alle Komponenten von gl_Position in einem Bereich von -1.0 bis 1.0 gebracht werden. Besonders beim Z-Wert könnte es verwirrend, dass der Bereich von gl_FragDepth im Fragmentshader von 0.0 bis 1.0 geht. Auch muss beachtet werden, dass Texturkoordinaten den Bereich von 0.0 bis 1.0 nutzen, während die Rendertargets im Bereich von -1.0 bis 1.0 arbeiten.
In den Shadern werden daher öfters Multimplikationen mit 0.5 und einer anschließenden Addition von 0.5 auftreten.
+
In den Shadern werden daher öfters Multiplikationen mit 0.5 und einer anschließenden Addition von 0.5 auftreten.
  
 
==Perspektivische Maps==
 
==Perspektivische Maps==
  
In dem erstem Teil dieses Artikels geht es um perspektivisch angepasste Schattenmaps. Normale projezierte Schatten haben das Problem, das sie im Nahenbereich besonders stark verpixeln und in der Entfernung durch ein viel zu hohes Oversampling Bandbreite verschwenden.
+
In dem erstem Teil dieses Artikels geht es um perspektivisch angepasste Schattenmaps. Normale projizierte Schatten haben das Problem, das sie im Nahenbereich besonders stark verpixeln und in der Entfernung durch ein viel zu hohes Oversampling Bandbreite verschwenden.
  
Für eine globale Lichquelle wird nur ein Vektor gegeben, der die Richtung des Lichtes beschreibt. Die gedachte Lichtquelle ist quasi unendlich weit weg. Da eine Entfernungsberechnung zur Lichquelle unmöglich ist, muss die Schattenberechnung relativ zu einer Referenzebene durchgeführt werden. Diese Ebene kann sowohl Senkrecht zum Lichvektor, als auch parallel zum gedachtem Boden ausgerichtet werden. Die Ausrichtung der Referenzebene beeinflusst die später sichtbare Auflösung der Schatten.
+
Für eine globale Lichtquelle wird nur ein Vektor gegeben, der die Richtung des Lichtes beschreibt. Die gedachte Lichtquelle ist quasi unendlich weit weg. Da eine Entfernungsberechnung zur Lichtquelle unmöglich ist, muss die Schattenberechnung relativ zu einer Referenzebene durchgeführt werden. Diese Ebene kann sowohl Senkrecht zum Lichtvektor, als auch parallel zum gedachtem Boden ausgerichtet werden. Die Ausrichtung der Referenzebene beeinflusst die später sichtbare Auflösung der Schatten.
  
Nach dem ein Vertex auf die Ebene projeziert wurde und der Abstand ein einen berech von 0.0...1.0 gebracht wurde, muss diese unendlich große Ebene noch auf eine endliche Größe projeziert werden, die auf eine Textur passt und die entfernungsabhängige Detailierung beachtet.
+
Nach dem ein Vertex auf die Ebene projiziert wurde und der Abstand ein einen Bereich von 0.0...1.0 gebracht wurde, muss diese unendlich große Ebene noch auf eine endliche Größe projiziert werden, die auf eine Textur passt und die entfernungabhängige Detaillierung beachtet.
  
Wenn wir unsere Referenzebene einfachhalber den Horizont schneidet (und auch den Bildschirm in obere und untere Hälfte teilt) Könnte ein Algorimus in etwa so aussehen (Dieser lässt sich später in ein Vertexshaderprogramm umsetzten):
+
Wenn wir unsere Referenzebene einfachhalber den Horizont schneidet (und auch den Bildschirm in obere und untere Hälfte teilt) Könnte ein Algorithmus in etwa so aussehen (Dieser lässt sich später in ein Vertexshaderprogramm umsetzten):
  
Projeziere den Vertex in die Modelview (Diese Schritt wurde auch bei den dualparaboliden Maps durchgeführt)
+
Projiziere den Vertex in die Modelview (Diese Schritt wurde auch bei den dual paraboloiden Maps durchgeführt)
Ermittel den Schnittpunt des vom Vertex ausgehendem gedachtem Lichtstrahl und der Referenzebene.
+
Ermittle den Schnittpunkt des vom Vertex ausgehendem gedachtem Lichtstrahl und der Referenzebene.
Benutze den Abstand zwischen Schnittpunkt und Vertex um den Z-Wert zu ermitteln. (Es sind zwei zusätzlich referenzwerte nötig die die funktion Farclip und Nearclip übernehmen)
+
Benutze den Abstand zwischen Schnittpunkt und Vertex um den Z-Wert zu ermitteln. (Es sind zwei zusätzlich Referenzwerte nötig die die Funktion Farclip und Nearclip übernehmen)
Projeziere die Ebene auf eine Ebene die in eine Textur passt pos/=abs(pos)+1.0 passt recht gut.
+
Projiziere die Ebene auf eine Ebene die in eine Textur passt pos/=abs(pos)+1.0 passt recht gut.
Anschließend wird noch die Schadowmap noch gestreckt und der Bereich hinter der Kammera entfernt.
+
Anschließend wird noch die Schadowmap noch gestreckt und der Bereich hinter der Kamera entfernt.
  
Das wichtigste ist, dass beim auswerten der shadowmaps der gleiche Projektionsalgoritmus verwendet wird wie beim generieren.
+
Das wichtigste ist, dass beim auswerten der shadowmaps der gleiche Projektionsalgorithmus verwendet wird wie beim generieren.
  
Prinzipell sollte sich der Algoritmus schon auf einer Geforce 3 oder Radeon 8500 implementieren lassen. Da da der Code hier in GLSL geschrieben ist, sollte in etwa Shadermodel 2.0 unterstützt werden.
+
Prinzipiell sollte sich der Algorithmus schon auf einer Geforce 3 oder Radeon 8500 implementieren lassen. Da da der Code hier in GLSL geschrieben ist, sollte in etwa Shadermodel 2.0 unterstützt werden.
  
Geschwindigkeitsmäßig ist dieser Algoritmus den Stencilshadows deutlich überlegen. Auch bei Stencilshadows sind mehrere Renderpasses nötig. Vorallem das extruieren der Siluetten kostet einiges an Bandbreite. Der geschätzte Aufwand ist etwa 20 (Aufwendige Shader im Finalem Renderdurchgang)bis 80% (CPU limitiert Geometrieübergabe) Mehraufwand gegenüber einer Schattenlos gerenderten Scene.
+
Geschwindigkeitsmäßig ist dieser Algorithmus den Stencilshadows deutlich überlegen. Auch bei Stencilshadows sind mehrere Renderpasses nötig. Vorallem das extrudieren der Silhouetten kostet einiges an Bandbreite. Der geschätzte Aufwand ist etwa 20 (Aufwendige Shader im Finalem Renderdurchgang)bis 80% (CPU limitiert Geometrieübergabe) Mehraufwand gegenüber einer Schattenlos gerenderten Scene.
 
Auch die Qualität ist nicht schlechter. Bei der Verwendung von weichen Schatten wirken Shadowmaps natürlicher als die extrem scharfen Stencilshadows.  
 
Auch die Qualität ist nicht schlechter. Bei der Verwendung von weichen Schatten wirken Shadowmaps natürlicher als die extrem scharfen Stencilshadows.  
  
Zeile 108: Zeile 105:
  
 
In der Hauptschleife könnte folgender Code verwendet werden:
 
In der Hauptschleife könnte folgender Code verwendet werden:
<cpp>
+
<source lang="cpp">
 
glBindTexture(GL_TEXTURE_2D, 0 ); //Entfernt alle Texturen aus der ersten TMU
 
glBindTexture(GL_TEXTURE_2D, 0 ); //Entfernt alle Texturen aus der ersten TMU
 
glUseProgramObjectARB(shadow); //lädt den Schattenshader
 
glUseProgramObjectARB(shadow); //lädt den Schattenshader
Zeile 120: Zeile 117:
 
glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0); //Normalen Framebuffer binden
 
glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0); //Normalen Framebuffer binden
 
glBindTexture(GL_TEXTURE_2D, shadow_tx ); //Tiefenmap an die erste TMU binden
 
glBindTexture(GL_TEXTURE_2D, shadow_tx ); //Tiefenmap an die erste TMU binden
glUseProgramObjectARB(final); //finalen shader binden
+
glUseProgramObjectARB(final); //finalen Shader binden
 
 
 
glViewport(0, 0, screen_size_x, screen_size_y);
 
glViewport(0, 0, screen_size_x, screen_size_y);
Zeile 126: Zeile 123:
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //Color und Z-Buffer löschen
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //Color und Z-Buffer löschen
 
render(); //rendern
 
render(); //rendern
</cpp>
+
</source>
  
 
===Shader===
 
===Shader===
  
 
Als erstes der Vertexshader zum generieren der Schadowmap. shadow.vert:
 
Als erstes der Vertexshader zum generieren der Schadowmap. shadow.vert:
<cpp>
+
<source lang="glsl">
 
void main(void){
 
void main(void){
  
 
vec3 lightdir = gl_NormalMatrix * normalize(vec3(0.1,-1.0,0.0)); //Lichtvektor in den Modelsprace rotieren
 
vec3 lightdir = gl_NormalMatrix * normalize(vec3(0.1,-1.0,0.0)); //Lichtvektor in den Modelsprace rotieren
vec3 mvertex =  vec3 (gl_ModelViewMatrix * gl_Vertex); //Vertex in den Modelspace projezieren
+
vec3 mvertex =  vec3 (gl_ModelViewMatrix * gl_Vertex); //Vertex in den Modelspace projizieren
vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y; //Schnittpunkt mit der XZ Referenzebene
+
vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y; //Schnittpunkt mit der XZ-Referenzebene
 
pos = pos / (abs(pos)+1.0); //Projektion der Ebene auf ein Quadrat
 
pos = pos / (abs(pos)+1.0); //Projektion der Ebene auf ein Quadrat
pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kammera
+
pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kamera
 
gl_Position.xy = pos;
 
gl_Position.xy = pos;
 
gl_Position.z = -mvertex.y ; // Der Bereich von +-1 über der Referenzebene wird erfasst
 
gl_Position.z = -mvertex.y ; // Der Bereich von +-1 über der Referenzebene wird erfasst
 
gl_TexCoord[0] = gl_MultiTexCoord0;
 
gl_TexCoord[0] = gl_MultiTexCoord0;
 
  }
 
  }
</cpp>
+
</source>
  
Die Lichtrichtung wird vorläufig noch fest übergeben. Eine Auswertung einer Opengl Lichtquelle oder einer Uniformvariable wäre ebenfalls möglich. Anschließend wird der Vertex in die Modelview projeziert und der Schnittpunkt des vom Vertexausgehendem Lichstrahls mit der Referenszebene berechnet. Als letztes wird noch der Abstand zur Referenzebenein einem Bereich 0..1  umgerechnet und die Texturkoordinaten für eventuelles Alphamasking oder Heightmapping durchgeschleift. Soll Alphamasking verwendet werden, muss ein entsprechender Shader geschrieben werden der bei den freizulassenden pixeln discard(); aufruft. Dies sollte jedoch nur für die betroffenen Poligone stat finden, da die Grafikkarte dann keine Early-Z Optimierungen verwenden kann. Ansonsten ist der Fragmentshader ist sehr einfach: shadow.frag:
+
Die Lichtrichtung wird vorläufig noch fest übergeben. Eine Auswertung einer OpenGL Lichtquelle oder einer Uniformvariable wäre ebenfalls möglich. Anschließend wird der Vertex in die Modelview projiziert und der Schnittpunkt des vom Vertexausgehenden Lichstrahls mit der Referenzebene berechnet. Als letztes wird noch der Abstand zur Referenzebene in einem Bereich 0..1  umgerechnet und die Texturkoordinaten für eventuelles Alphamasking oder Heightmapping durchgeschleift. Soll Alphamasking verwendet werden, muss ein entsprechender Shader geschrieben werden der bei den freizulassenden pixeln discard(); aufruft. Dies sollte jedoch nur für die betroffenen Polygone statt finden, da die Grafikkarte dann keine Early-Z Optimierungen verwenden kann. Ansonsten ist der Fragmentshader ist sehr einfach: shadow.frag:
<cpp>
+
<source lang="glsl">
 
void main(void){}
 
void main(void){}
</cpp>
+
</source>
  
Die für die Auswertung müssen wir im finalem Renderdurchgang exakt die gleichen Texturkoordinaten für die Shadowmap generieren. Allerdings gibt es eine Besonderheit: Die koordinaten einer Textur reichen von 0;0 bis 1;1, Die Koordinaten eines Rendertargets reichen jedoch von -1;-1 bis 1;1, so das eine zusätzliche korrektor nötig ist.
+
Die für die Auswertung müssen wir im finalem Renderdurchgang exakt die gleichen Texturkoordinaten für die Shadowmap generieren. Allerdings gibt es eine Besonderheit: Die Koordinaten einer Textur reichen von 0;0 bis 1;1. Die Koordinaten eines Rendertargets reichen jedoch von -1;-1 bis 1;1, so das eine zusätzliche Korrektor nötig ist.
<cpp>
+
<source lang="glsl">
 
varying vec3 spos;
 
varying vec3 spos;
  
Zeile 159: Zeile 156:
 
         vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y;
 
         vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y;
 
         pos = pos / (abs(pos)+1.0);
 
         pos = pos / (abs(pos)+1.0);
         pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kammera
+
         pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kamera
 
         pos = pos * 0.5 + 0.5; //auf Texturkoordinaten umrechnen
 
         pos = pos * 0.5 + 0.5; //auf Texturkoordinaten umrechnen
 
   
 
   
 
         spos.xy = pos; //Daten in einer Varying verpacken
 
         spos.xy = pos; //Daten in einer Varying verpacken
         spos.z = -mvertex.y * 0.5 + 0.5 - 0.005; // far and near clamping and Anti-Z-fighting-offset
+
         spos.z = -mvertex.y * 0.5 + 0.5 - 0.005; // far/near-clamping und Anti-Z-Fightingoffset
 
 
 
 
 
         gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //Die echte Transformation
 
         gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //Die echte Transformation
Zeile 170: Zeile 167:
 
         gl_TexCoord[1] = gl_MultiTexCoord1;
 
         gl_TexCoord[1] = gl_MultiTexCoord1;
 
         }
 
         }
</cpp>
+
</source>
  
Die ersten 5 Zeilen der main() sollten exakt das gleiche tun wie der Vertexshader zum gnerieren dem Map.
+
Die ersten 5 Zeilen der main() sollten exakt das gleiche tun wie der Vertexshader zum generieren der Map.
  
 
Nun fehlt noch ein Fragmentshader für den letzten Durchgang:
 
Nun fehlt noch ein Fragmentshader für den letzten Durchgang:
  
Es muss noch der Noralvektor berücksichtigt werden!!!
+
Es muss noch der Normalvektor berücksichtigt werden!!!
<cpp>
+
<source lang="glsl">
 
uniform sampler2D Shadowmap;
 
uniform sampler2D Shadowmap;
 
varying vec3 spos;
 
varying vec3 spos;
Zeile 191: Zeile 188:
 
               }
 
               }
 
         }
 
         }
</cpp>
+
</source>
  
==Parabolide Maps==
+
==Paraboloide Maps==
  
Einfache parabolide Maps können für alle Punktlichquellen verwendet werden, deren Licht nicht in alle Richtungen abgestrahlt wird. Darunter fallen vorallem Spots und an Wänden oder Decken befestigte Lampen. Im gegensatz zur Paralelem Licht oder einer Punktlichquelle bringt eine solche Lichtquelle häufig eine Helligkeitsmap mit. Für die Helligkeitsmap und die Schattenmap können die gleichen Texturkoordinaten verwendet werden.
+
Einfache paraboloide Maps können für alle Punktlichtquellen verwendet werden, deren Licht nicht in alle Richtungen abgestrahlt werden soll. Darunter fallen vorallem Spots und an Wänden oder Decken befestigte Lampen. Im Gegensatz zumr Parallelen Licht oder einer Punktlichquelle bringt eine solche Lichtquelle häufig eine Helligkeitsmap mit. Für die Helligkeitsmap und die Schattenmap können die gleichen Texturkoordinaten verwendet werden.
  
Bei kleinen Öffnungswinkeln hat die parabolide Map keinen Vorteil gegenüber einer normalen Projektion. Der Vortiel ist jedoch das der Code sich für Öffnungswinkel von bis zu ~240 Grad eignet.  
+
Bei kleinen Öffnungswinkeln hat die paraboloide Map keinen Vorteil gegenüber einer normalen Projektion. Der Vorteil ist jedoch das der Code sich für Öffnungswinkel von bis zu ~240 Grad eignet.  
  
 
Um eine solche Lichquelle zu beschreiben weden nicht weniger als drei Vektoren benötigt:
 
Um eine solche Lichquelle zu beschreiben weden nicht weniger als drei Vektoren benötigt:
Zeile 203: Zeile 200:
 
* Position
 
* Position
 
* Richtung  
 
* Richtung  
* Tangent
+
* Tangente
  
Wärend die Funktion der ersten beiden Vektoren klar ist, wird der Tangent wird dazu benötigt die Schattenmap und Helligkeitsmap auszurichten. Er ist Senkrecht zur Richtung tangential zur Textur ausgerichtet  
+
Während die Funktion der ersten beiden Vektoren klar ist, wird die Tangente dazu benötigt die Schattenmap und Helligkeitsmap auszurichten. Er ist Senkrecht zur Richtung tangential zur Textur ausgerichtet  
Beschreiben lässt sich diese Funktion lässte sich am ehesten durch das Verdrehen einer Taschenlampe. Wenn die Schattenmap gegenüber den schattenwerfenden Objekten verdreht wird, sieht das Bild auf keinen Fall mehr natürlich aus.
+
Beschreiben lässt sich diese Funktion am ehesten durch das Verdrehen einer Taschenlampe. Wenn die Schattenmap gegenüber den schattenwerfenden Objekten verdreht wird, sieht das Bild auf keinen Fall mehr natürlich aus.
 
   
 
   
Es ist sowohl möglich die Berechnungen im Vertexshader durchzuführen, als auch eine entsprechnede Matrix in den Uniformvariablen abzulegen. Wenn die Berechnungen per Matrix durchgefühert werden lässt sich das Prinzip der paraboliden Maps zuindest als Reflektionsmap nutzen. Ein weiterer Vorteil an einer vorberechneten Matrix ist, dass diese auch für shadowmap verwendet werdne kann die nicht jedes Frame neu berechnet werden.
+
Es ist sowohl möglich die Berechnungen im Vertexshader durchzuführen, als auch eine entsprechnede Matrix in den Uniformvariablen abzulegen. Wenn die Berechnungen per Matrix durchgefühert werden lässt sich das Prinzip der paraboloiden Maps zuindest als Reflektionsmap nutzen. Ein weiterer Vorteil an einer vorberechneten Matrix ist, dass diese auch für Shadowmap verwendet werden kann die nicht jeden Frame neu berechnet werden muss.
  
==Dual Parabolide Maps==
+
==Dual Paraboloide Maps==
Von OpenGL kennen wir zwei Projektionsmöglichkeiten: Orthografische und perspektivische Projektion. Beide Projektionen arbeiten ohne Verzerrung. Jede Gerade bleibt beim Transformieren eine Gerade. Wenn man dagegen das Bild eines Fischeyeobjektives betrachtet, allen einem sofort die zu Kurven verzerrten Geraden auf. Der entscheidene Vorteil an einer Fisheyeaufnahme ist, dass ein Öffnungswinkel von 180 Grad erfasst werden kann. Zwei entgegensetzte Aufnahmen können so problemlos den kompletten Raum um die Kamera erfassen.
+
Von OpenGL kennen wir zwei Projektionsmöglichkeiten: Orthografische und perspektivische Projektion. Beide Projektionen arbeiten ohne Verzerrung. Jede Gerade bleibt beim Transformieren eine Gerade. Wenn man dagegen das Bild eines Fischeyeobjektives betrachtet, allen einem sofort die zu Kurven verzerrten Geraden auf. Der entscheidende Vorteil an einer Fisheyeaufnahme ist, dass ein Öffnungswinkel von 180 Grad erfasst werden kann. Zwei entgegensetzte Aufnahmen können so problemlos den kompletten Raum um die Kamera erfassen.
  
Für das Erstellen der Tiefenmap ist es notwendig den Raum um das reflektierende Objekt in eine Textur zu rendern. Meistens wird hier eine Cubemap verwendet. Soll diese dynamisch generiert werden, ist es auffällig, dass die Scene ganze 6 mal gerendert werden muss. Mit der dual paraboliden Map sind nur noch zwei Rendervorgänge nötig, es wird zwar keine Füllrate eingespart, dafür müssen nun nur noch 1/3 der Daten transformiert werden, so das die Vertexshader und die CPU entlastet werden.
+
Für das Erstellen der Tiefenmap ist es notwendig den Raum um das reflektierende Objekt in eine Textur zu rendern. Meistens wird hier eine Cubemap verwendet. Soll diese dynamisch generiert werden, ist es auffällig, dass die Scene ganze 6 mal gerendert werden muss. Mit der dual paraboloiden Map sind nur noch zwei Rendervorgänge nötig, es wird zwar keine Füllrate eingespart, dafür müssen nun nur noch 1/3 der Daten transformiert werden, so das die Vertexshader und die CPU entlastet werden.
  
Eine weitere Optimierung ist unter Umständen möglich: Wenn bei einer Cubemap auf eine der Flächen verzichte werden kann. Z.B. wird die Reflektion im Lack eines Autos so gut wie nie verdeckte Straße zeigen. Auch ist für eine an einer  Wand befestigten Lampe nur eine halbkugelförmige Shadowmap nötig. Wenn sie einen Abstand zur Wand hat, genügt es auch hier den Blickwinkel von 180 Grad etwas zu erweitern.
+
Eine weitere Optimierung ist unter Umständen möglich: Wenn bei einer Cubemap auf eine der Flächen verzichte werden kann. Z.B. wird die Reflexion im Lack eines Autos so gut wie nie verdeckte Straße zeigen. Auch ist für eine an einer  Wand befestigten Lampe nur eine halbkugelförmige Shadowmap nötig. Wenn sie einen Abstand zur Wand hat, genügt es auch hier den Blickwinkel von 180 Grad etwas zu erweitern.
  
Die Projektion der dualparaboliden Map kann man sich am besten Bild eines Fisheyobjektives vorstellen. Mathematisch etwas genauer ist das Reflektionsbild der umgebenden Welt in einem Rotationsparaboliden.
+
Die Projektion der dualparaboloiden Map kann man sich am besten Bild eines Fisheyobjektives vorstellen. Mathematisch etwas genauer ist das Reflexionsbild der umgebenden Welt in einem Rotationsparaboloiden.
  
Die Hardwareanforderungen an den Pixelshader sind nicht gerade gering. Der Pixelshader für die 8fachen Lichquellen kommt nach dem Compelieren auf über 170 Instruktions. Eine Shader 2.0 Karte wie z.B. die Radeon 9x00 sind hier hoffnungslos überfordert da sie nur an 3 Stellen im Programm auf die Textureinheiten zugreifen können und der Assembler nicht mehr in der Lage ist die Instruktions passend umzusortieren. Noch fataler wird es wenn die Texturelookups durch dynamisches Branching übersprungen weder sollen: Die Karte hat keine Chance mehr diese umzusortieren.
+
Die Hardwareanforderungen an den Pixelshader sind nicht gerade gering. Der Pixelshader für die 8fachen Lichtquellen kommt nach dem Compilieren auf über 170 Instruktionen. Eine Shader 2.0 Karte wie z.B. die Radeon 9x00 sind hier hoffnungslos überfordert da sie nur an 3 Stellen im Programm auf die Textureinheiten zugreifen können und der Assembler nicht mehr in der Lage ist die Instruktionen passend umzusortieren. Noch fataler wird es wenn die Texturelookups durch dynamisches Branching übersprungen werder sollen: Die Karte hat keine Chance mehr diese umzusortieren.
  
Wer dualparbolide Maps auf SM2.0 Karten einsetzten will sollte damit rechnen, dass das Limit bei 2 bis 4 Maps liegen sollte.
+
Wer dualparboloide Maps auf SM2.0 Karten einsetzten will sollte damit rechnen, dass das Limit bei 2 bis 4 Maps liegen sollte.
  
 
===Hauptprogramm===
 
===Hauptprogramm===
  
So kann in der Hauptschleife das rendern der 16 paraboliden Maps und der anschließende Finale Renderdurchgang durchgeführt werden:
+
So kann in der Hauptschleife das Rendern der 16 paraboloiden Maps und der anschließende Finale Renderdurchgang durchgeführt werden:
<cpp>//Shader für Dualparabolische Mpas aktivieren
+
<source lang="cpp">//Shader für Dualparabolische Maps aktivieren
 
glUseProgramObjectARB(parabol);
 
glUseProgramObjectARB(parabol);
  
Zeile 233: Zeile 230:
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
//Die 8 Lichtquellen Rendern
+
//Die 8 Lichtquellen rendern
 
for (int light;light<8;light++){
 
for (int light;light<8;light++){
 
         glViewport (shadow_size/2*(lights/4),shadow_size/4 *(light%4),shadow_size/4,shadow_size/4);
 
         glViewport (shadow_size/2*(lights/4),shadow_size/4 *(light%4),shadow_size/4,shadow_size/4);
Zeile 254: Zeile 251:
 
//Frame rendern
 
//Frame rendern
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
render();</cpp>
+
render();</source>
  
 
ungetesteter Pascalcode:
 
ungetesteter Pascalcode:
<pascal>
+
<source lang="pascal">
//Shader für Dualparabolische Mpas aktivieren
+
//Shader für Dualparabolische Maps aktivieren
 
glUseProgramObjectARB(parabol);
 
glUseProgramObjectARB(parabol);
  
Zeile 265: Zeile 262:
 
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
 
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
  
//Die 8 Lichtquellen Rendern
+
//Die 8 Lichtquellen rendern
 
for light := 0 to 7 do
 
for light := 0 to 7 do
 
begin
 
begin
Zeile 287: Zeile 284:
 
//Frame rendern
 
//Frame rendern
 
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
 
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
render();</pascal>
+
render();</source>
  
 
===Die Shader===
 
===Die Shader===
Zeile 293: Zeile 290:
 
====Shader zum Rendern der Schattenmaps====
 
====Shader zum Rendern der Schattenmaps====
 
parabol.vert
 
parabol.vert
<cpp>  uniform int renderpass;
+
<source lang="glsl">  uniform int renderpass;
 
   uniform int light;
 
   uniform int light;
 
   varying vec3 normal;
 
   varying vec3 normal;
Zeile 318: Zeile 315:
 
         gl_Position.z = 2.0 * gl_Position.z -1.0; //Todo: optimieren
 
         gl_Position.z = 2.0 * gl_Position.z -1.0; //Todo: optimieren
 
pos=gl_Position.xyz;
 
pos=gl_Position.xyz;
}</cpp>
+
}</source>
  
 
parabol.frag
 
parabol.frag
<cpp>varying vec3 pos;
+
<source lang="glsl">varying vec3 pos;
 
void main(void){
 
void main(void){
 
if (length(pos.xy)>1.005)discard; //Diese Zeile kann durchaus entfallen. Kosten/Nutzen unbekannt.
 
if (length(pos.xy)>1.005)discard; //Diese Zeile kann durchaus entfallen. Kosten/Nutzen unbekannt.
}</cpp>
+
}</source>
  
Es gibt zwei Uniformaviablen, die gesetzt werden müssen: Eine ist die Nummer der aktuellen Lichtquelle und die zweite der Renderpass, der angibt ob die Vorder- oder Rückseite der parabolischen Map gerendert werden soll. Eine automatische Berechnung der Texturkoordinaten wäre zwar möglich, jedoch ist die Änderung des Viewports deutlich weniger Rechenaufwendig.
+
Es gibt zwei Uniformvariablen, die gesetzt werden müssen: Eine ist die Nummer der aktuellen Lichtquelle und die zweite der Renderpass, der angibt ob die Vor- oder Rückseite der parabolischen Map gerendert werden soll. Eine automatische Berechnung der Texturkoordinaten wäre zwar möglich, jedoch ist die Änderung des Viewports deutlich weniger Rechenaufwendig.
  
Statt einer Multiplikation des gl_Vertex mit der gl_ModelViewProjektionMatrix wird hier nur mit der gl_ModelViewMatrix multipliziert. Dadurch werden nur die Transformationen durchgeführt und die Projektion übersprungen.
+
Statt einer Multplikation des gl_Vertex mit der gl_ModelViewProjektionMatrix wird hier nur mit der gl_ModelViewMatrix multipliziert. Dadurch werden nur die Transformationen durchgeführt und die Projektion übersprungen.
Die Division durch -L entspricht weitgehend einer Normalsierung, spiegelt die Welt jedoch in die richtige Lage. L enthält jetzt die Tiefeninformation, gl_Position einen Vektor, der von der Kammera auf den Vertex zeigt. Mit den zwei folgenden Zeilen wird der Vektor parabolisch auf die Bildschirmkoordinaten projeziert (Intervall von -1.0 bis 1.0). Mit Hilfe der folgenden if-Abfrage werden alle Vertices hinter der Kammera geclipt. Nur sichtbare Vertices bekommen gültige z Werte für den Zbuffer im Intervall von -1.0 bis 1.0.  
+
Die Division durch -L entspricht weitgehend einer Normalisierung, spiegelt die Welt jedoch in die richtige Lage. L enthält jetzt die Tiefeninformation, gl_Position einen Vektor, der von der Kamera auf den Vertex zeigt. Mit den zwei folgenden Zeilen wird der Vektor parabolisch auf die Bildschirmkoordinaten projeziert (Intervall von -1.0 bis 1.0). Mit Hilfe der folgenden if-Abfrage werden alle Vertices hinter der Kamera geclipt. Nur sichtbare Vertices bekommen gültige z Werte für den Zbuffer im Intervall von -1.0 bis 1.0.  
  
Der Fragmentshader ist extrem einfach, da wir aufgrund des nicht vorhandem Colorbuffers keinen Farbwert benötigen lassen wir diesen wie bei den perspektivischen Maps einfach undefiniert.
+
Der Fragmentshader ist extrem einfach, da wir aufgrund des nicht vorhandem Farbbuffers keinen Farbwert benötigen lassen wir diesen wie bei den perspektivischen Maps einfach undefiniert.
Zudem verlassen wir den Shader mit discard, wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die Voteil ist, das nur der kreisförmige Bereich der Map verwendet wird, der Nachteil, das durchaus eine Early-Z optimierung nicht mehr möglich ist. Sehr Sinvoll sollte dies sein, wenn die Shadowmaps dynamisch nach benötigter Größe angeordnet werden. Da sich Kreise besser als Quadrate packen lassen.
+
Zudem verlassen wir den Shader mit discard, wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die Voteil ist, das nur der kreisförmige Bereich der Map verwendet wird, der Nachteil, das durchaus eine Early-Z optimierung nicht mehr möglich ist. Sehr Sinnvoll sollte dies sein, wenn die Shadowmaps dynamisch nach benötigter Größe angeordnet werden. Da sich Kreise besser als Quadrate packen lassen.
  
 
====Shader für den finalen Renderdurchgang====
 
====Shader für den finalen Renderdurchgang====
Zeile 338: Zeile 335:
  
 
final.vert
 
final.vert
<cpp>varying vec3 Normal;
+
<source lang="glsl">varying vec3 Normal;
 
varying vec3 ModelVertex;
 
varying vec3 ModelVertex;
  
Zeile 346: Zeile 343:
 
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 
gl_TexCoord[0] = gl_MultiTexCoord0;
 
gl_TexCoord[0] = gl_MultiTexCoord0;
  }</cpp>
+
  }</source>
  
 
final.frag
 
final.frag
<cpp>varying vec3 Normal;
+
<source lang="glsl">varying vec3 Normal;
 
varying vec3 ModelVertex;
 
varying vec3 ModelVertex;
  
Zeile 356: Zeile 353:
 
uniform int MaxLights;
 
uniform int MaxLights;
  
//Achtung folgende Zeile ist nicht GLSL konform.  
+
//Achtung folgende Zeile ist nicht GLSL-Konform.  
//Workaround: Array als Uniform übergeben oder durch sehr aufwendige berechnung erstzten
+
//Workaround: Array als Uniform übergeben oder durch sehr aufwendige Berechnung erstzten
//Sollte es auch auf ATI Karten funktinieren, frage ich mich warum es nicht erlaubt ist...
+
//Sollte es auch auf ATI-Karten funktionieren, frage ich mich warum es nicht erlaubt ist...
 
const vec2 texofset[8] = {vec2 (0.125,0.125), vec2 (0.125,0.375), vec2 (0.125,0.625), vec2 (0.125,0.875),
 
const vec2 texofset[8] = {vec2 (0.125,0.125), vec2 (0.125,0.375), vec2 (0.125,0.625), vec2 (0.125,0.875),
 
  vec2 (0.625,0.125), vec2 (0.625,0.375), vec2 (0.625,0.625), vec2 (0.625,0.875)};
 
  vec2 (0.625,0.125), vec2 (0.625,0.375), vec2 (0.625,0.625), vec2 (0.625,0.875)};
Zeile 378: Zeile 375:
 
vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0])); //Textur auslesen;  
 
vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0])); //Textur auslesen;  
 
gl_FragColor = light * color ;
 
gl_FragColor = light * color ;
}</cpp>
+
}</source>
  
Möglicher weise für ATI taugliche Variante:
+
Möglicherweise für ATI taugliche Variante:
<cpp>
+
<source lang="glsl">
 
varying vec3 Normal;
 
varying vec3 Normal;
 
varying vec3 ModelVertex;
 
varying vec3 ModelVertex;
Zeile 416: Zeile 413:
 
         vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0])); //Textur auslesen;  
 
         vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0])); //Textur auslesen;  
 
         gl_FragColor = light * color ;
 
         gl_FragColor = light * color ;
         }</cpp>
+
         }</source>
  
  
  
Diese Shader besitzen 3 Uniformvariablen, die unbedingt mit den richtigen Werten gefüllt werden müssen. Zwei davon sind Texturen, die dritte die Anzahl der aktiven Lichtquellen. Es ist wichtig zu wissen, dass der Shader nicht den OpenGL-Status der Lichquellen berücksichtigt und nur die Daten der Ersten bis MaxLights holt.
+
Diese Shader besitzen 3 Uniformvariablen, die unbedingt mit den richtigen Werten gefüllt werden müssen. Zwei davon sind Texturen, die dritte die Anzahl der aktiven Lichtquellen. Es ist wichtig zu wissen, dass der Shader nicht den OpenGL-Status der Lichtquellen berücksichtigt und nur die Daten der Ersten bis MaxLights holt.
 
Während bei den Schattenmaps die meiste Arbeit im Vertexshader erledigt werden könnte, ist dieser sehr Fragmentshader lastig. Die Texturkoordinatenberechnung ist sehr ähnlich zu der im shadow.vert. Die größte Änderung ist, dass hier abhängig von der Lichtquellennummer ein Offset aufaddiert wird um die Untertextur auszuwählen. Ein zusätzlicher Offset wird dazuaddiert, wenn auf die zweite parabolide Map einer Lichtquelle zugegriffen wird.
 
Während bei den Schattenmaps die meiste Arbeit im Vertexshader erledigt werden könnte, ist dieser sehr Fragmentshader lastig. Die Texturkoordinatenberechnung ist sehr ähnlich zu der im shadow.vert. Die größte Änderung ist, dass hier abhängig von der Lichtquellennummer ein Offset aufaddiert wird um die Untertextur auszuwählen. Ein zusätzlicher Offset wird dazuaddiert, wenn auf die zweite parabolide Map einer Lichtquelle zugegriffen wird.
 
Um ein dynamisches Branching zu vermeiden, wird mit der Stepfunktion ermittelt ob sich das Fragment im Schatten befindet.
 
Um ein dynamisches Branching zu vermeiden, wird mit der Stepfunktion ermittelt ob sich das Fragment im Schatten befindet.
  
 
==Optimierungen und Verbesserungen==
 
==Optimierungen und Verbesserungen==
Hier sind noch einige Vorschläge, die helfen können um besser Qualität oder Leistungen im eigenem Programm zu bekommen. Eine uniververselle Lösung lässt sich nur auf kosten von Performance schreiben. Viel besser ist es, wenn man die Shader an die jeweilige Situation anpasst.  
+
Hier sind noch einige Vorschläge, die helfen können um besser Qualität oder Leistungen im eigenem Programm zu bekommen. Eine universelle Lösung lässt sich nur auf kosten von Performance schreiben. Viel besser ist es, wenn man die Shader an die jeweilige Situation anpasst.  
  
  
  
 
===Schatten durch Alphatest===
 
===Schatten durch Alphatest===
Der bisherige Schattenshader kann nur ganze Polygone einen Schatten werfen lassen. Wenn shadow.vert so ergänzt wird, das die Texturkoordinaten in den Fragmentshader  weitergereicht werden, dann ist es mit folgendem Fragmentshader möglich Shatten in Abhängikeit einer Alphatextur zu rendern:
+
Der bisherige Schattenshader kann nur ganze Polygone einen Schatten werfen lassen. Wenn shadow.vert so ergänzt wird, das die Texturkoordinaten in den Fragmentshader  weitergereicht werden, dann ist es mit folgendem Fragmentshader möglich Schatten in Abhängigkeit einer Alphatextur zu rendern:
  
 
shadow.frag
 
shadow.frag
<cpp>
+
<source lang="glsl">
 
uniform sampler2D Texture0;
 
uniform sampler2D Texture0;
 
void main(void)
 
void main(void)
 
{
 
{
 
         if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
 
         if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
}</cpp>
+
}</source>
Der Shader wird verlassen wenn das Texel einen Alphawert von kleiner als 50% hat. Damit lassen sich Schatten von Pflanzen wesentlich realistischer darstellen. Dieser Shader sollte natürlich nur dann verwendet werden, wenn eine Alphakanal in der Textur vorhanden ist. Auch Shader mit Paralax- oder Displacementmapping arbeiten kann man so um einen Schatten ergänzen, wenn die Polygone transparente Teile enthalten.
+
Der Shader wird verlassen wenn das Texel einen Alphawert von kleiner als 50% hat. Damit lassen sich Schatten von Pflanzen wesentlich realistischer darstellen. Dieser Shader sollte natürlich nur dann verwendet werden, wenn eine Alphakanal in der Textur vorhanden ist. Auch Shader mit Parallax- oder Displacementmapping arbeiten kann man so um einen Schatten ergänzen, wenn die Polygone transparente Teile enthalten.
Es ist auch möglich den shader zu verlassen, wenn eine bestimmte Farbe in der Textur gefunden wird, dann nimmt die Alphamaske keinen Speicherplatz mehr weg.
+
Es ist auch möglich den Shader zu verlassen, wenn eine bestimmte Farbe in der Textur gefunden wird, dann nimmt die Alphamaske keinen Speicherplatz mehr weg.
  
 
===Farbiges Glas===
 
===Farbiges Glas===
  
Schatten hat jetzt ja sachon fast jeder. Aber wie wäre es mal mit einfärben von dynamischen Lichtquellen?
+
Schatten hat jetzt ja schon fast jeder. Aber wie wäre es mal mit einfärben von dynamischen Lichtquellen?
Der Algoritmus ist nicht schwer:
+
Der Algorithmus ist nicht schwer:
  
* Alle Undurchichtigen Sceneteile in die Tiefenmaps gerendert.  
+
* Alle undurchsichtigen Sceneteile in die Tiefenmaps gerendern.  
 
* Zusätzlich wird eine Colormap gebunden und das Schreiben in den Zbuffer unterbunden.
 
* Zusätzlich wird eine Colormap gebunden und das Schreiben in den Zbuffer unterbunden.
 
* Die Colormap wird mit Weiß oder einer Helligkeitsmap initialisiert.
 
* Die Colormap wird mit Weiß oder einer Helligkeitsmap initialisiert.
 
* Rendern aller transparenten Objekten
 
* Rendern aller transparenten Objekten
 
* Auf finales Rendern umschalten
 
* Auf finales Rendern umschalten
* Alle undurchsichtigen Objekte mit hilfe der Schadowmap und der modifizierten Beleuchtungmap beleuchten.
+
* Alle undurchsichtigen Objekte mit Hilfe der Schadowmap und der modifizierten Beleuchtungmap beleuchten.
 
* Alle Transparenten Objekte rendern, unter Berücksichtigung, dass die Lichtquelle bereits das gefilterte Licht aussendet. Dieses Blending sollte rückgängig gemacht werden.
 
* Alle Transparenten Objekte rendern, unter Berücksichtigung, dass die Lichtquelle bereits das gefilterte Licht aussendet. Dieses Blending sollte rückgängig gemacht werden.
  
Bei diesem Algoritmus bleibt noch das Alphablendingproblem mit der mehrfachen Überdeckung. Beim Modifizieren der Beleuchtungsmap gegebenfalls den Stencilbuffer zur Hilfe nehmen (Die Alphaproblematik würde hier jetzt aber den Rahmen sprengen)
+
Bei diesem Algorithmus bleibt noch das Alphablendingproblem mit der mehrfachen Überdeckung. Beim Modifizieren der Beleuchtungsmap gegebenfalls den Stencilbuffer zur Hilfe nehmen. Hier wäre ein denkbarer Algorithmus:
 +
 
 +
* Alle transparenten Polygone in den Zbuffer rendern. Dabei für alle Polygone, deren  doppelte Filterung zu Fehlern führen kann mit verschiedenen Stencilwerte schreiben.
 +
* Zbuffer und Colorbuffer Löschen.
 +
* Helligkeitsmap in den Colorbuffer kopieren.
 +
* Alle nicht transparenten Objekte in den Zbuffer Rendern.
 +
* Alle transparenten Polygone mit den gleichen Stencilwerten wie im erstem Pass rendern. Die Bedingung ist, dass nur Polygone in den Zbuffer geschrieben werden, bei den Stencilwerte nicht mit den Stencilbuffer übereinstimmen. Die vordersten Polygone werden so aussortiert und nur die zweite Schicht landet im Zbuffer.
 +
* Im letztem Rendervorgang müssen die gefilterten Lichtfarben der Transparenten Polygone in den Colorbuffer geschrieben werden. Die Bedingung ist das die Stencilwerte übereinstimmen müssen.
 +
 
 +
Auch wenn es wie ein großer Mehraufwand aussieht, ist der Anteil der transparenten Polygone doch eher gering. So das deren dreifacher Overdraw kaum eine Rolle spielt. Im Finalem Renderdurchgang gibt es bei den transparenten Polygonen noch eine Besonderheit: Stimmt die Entfernung aus der Shadowmap mit der Entfernung der Lichtquellen (fast) überein, so wird das Polygon mit dem Licht aus der Colormap beleuchtet. Ist die Entfernung aus der Shadowmap jedoch größer als die Entfernung zu Lichtquellen, enthält die Colormap die gefilterte Farbe des Lichtes hinter dem Polygon. Für die Beleuchtung des Polygon muss also das Licht der Lichtquelle benutzt werden. Sinnvoll ist es die Helligkeit für diesen Fall im Alphawert der Colormap zu Speichern.
 +
 
 +
Ansonsten noch ein paar Vorschläge:
 +
* Objekte die aus dem gleichem Glas sind und auch einfarbig. Sollten mit dem gleichem Stencilwert verarbeitet werden. So sieht z.B. das gefärbte Licht einer zweifach durchstrahlten leicht gefärbten Glaskuppel natürlicher aus als ein Vollschatten. Auch wenn das Licht korrekter weise zwei mal hätte gefiltert werden müssen.
 +
* Eventuell macht es sind mehrfarbige Objekte in mehrfache einfarbige Objekte zu zerlegen.
 +
* Komplexe Mehrfachfilterungen sollten vermieden werden oder so gewählt werden, das sie natürlich aussehen. Gute Filter wären Komplementabfärben, da dort ein echter Schatten erzeugt wird. Schlechte Kombinationen wären unter anderem Rot+Gelb, Blau+Gelb, usw...
 +
* Um von den Farbfehlern abzulenken, farbige Objekte in einer rot verschobenen Farbe fluoreszieren lassen. (Violet-> Blau -> Grün -> Gleb -> Organge -> Rot) Durch blau gefärbte Fenster, blau gefilterte Licht welches Dinge grün aufleuchten lässt nimmt einem jeder PC Modder ab...
  
 
===Schattenmap durch eine Heightmap modifizieren===
 
===Schattenmap durch eine Heightmap modifizieren===
Auch wennich hierzu noch kein Beispiel haben, ist es möglich gl_FragDepth mit hilfe einer Heightmap zu modifizieren. Prinzipiell entspricht dies einem einfachem Offsetmapping. Damit wäre durchaus eine Selbstschattierung von Bumpmaps möglich.
+
Auch wennich hierzu noch kein Beispiel haben, ist es möglich gl_FragDepth mit Hilfe einer Heightmap zu modifizieren. Prinzipiell entspricht dies einem einfachem Offsetmapping. Damit wäre durchaus eine Selbstschattierung von Bumpmaps möglich.
  
 
===Zweite parabolische Map vermeiden===
 
===Zweite parabolische Map vermeiden===
Lichtquellen, die nur in eine Richtung Licht werfen können, wie z.B. Spotlichter oder Lichter, die in Bodenähe oder Wandnähe befestigt sind, benötigen keinen vollständig erfassten Tiefenraum. Die Parabolische Map kann problemlos herein oder herausskaliert werden, so das Öffnungswinkel von 0 bis ca 240 Grad möglich sind.
+
Lichtquellen, die nur in eine Richtung Licht werfen können, wie z.B. Spotlichter oder Lichter, die in Bodennähe oder Wandnähe befestigt sind, benötigen keinen vollständig erfassten Tiefenraum. Die Parabolische Map kann problemlos herein oder herausskaliert werden, so das Öffnungswinkel von 0 bis ca 240 Grad möglich sind.
  
  
Zeile 469: Zeile 481:
  
 
===Texturelookups vermeiden===
 
===Texturelookups vermeiden===
Wenn das Skalarprodukt von normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und Berechnungen für die Lichquelle können komplett übersprungen werden. Diverse Multiplikationen und vorallem Texturelookups können so übersprungen werden. Besondere aufwendige Algorithmen wie selbstschattierende Bumpmaps, können so deutlich Beschleunigt werden.  
+
Wenn das Skalarprodukt von normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und Berechnungen für die Lichquelle können komplett übersprungen werden. Diverse Multiplikationen und vorallem Texturelookups können so übersprungen werden. Besondere aufwendige Algorithmen wie selbstschattierende Bumpmaps, können deutlich Beschleunigt werden.  
  
 
Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Skalarprodukt von vor der if Abfrage aber umbedingt wiederverwerten!)  
 
Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Skalarprodukt von vor der if Abfrage aber umbedingt wiederverwerten!)  
<cpp>
+
<source lang="glsl">
 
   light += gl_LightSource[0].diffuse * max(dot(normalvec, -lightdir), 0.0);
 
   light += gl_LightSource[0].diffuse * max(dot(normalvec, -lightdir), 0.0);
</cpp>
+
</source>
  
 
===Reichweite der Lichtquellen===  
 
===Reichweite der Lichtquellen===  
Zeile 482: Zeile 494:
 
Die Qualität lässt sich dadurch mehrere Samples auf der Schattenmap verbessern. Das folgende Codefragment nimmt 3 statt des einem Samples und glättet die Ränder des Schattens.  
 
Die Qualität lässt sich dadurch mehrere Samples auf der Schattenmap verbessern. Das folgende Codefragment nimmt 3 statt des einem Samples und glättet die Ränder des Schattens.  
  
<cpp>const float pof =1.0 /4096.0 *0.7;
+
<source lang="glsl">const float pof =1.0 /4096.0 *0.7;
 
vec4 shadow = step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol +vec2(pof,0)).r );
 
vec4 shadow = step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol +vec2(pof,0)).r );
 
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,-pof/1.2) ).r );
 
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,-pof/1.2) ).r );
 
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,pof/1.2)).r );
 
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,pof/1.2)).r );
light += shadow * 0.33 * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));</cpp>
+
light += shadow * 0.33 * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));</source>
 
Durch 9x Oversampling lässt sich schon eine sehr gute Qualität ereichen, sorgt aber bei 8 Lichtquellen durch die 72 Texturelookups für einen Zusammenbruch der Framerate.
 
Durch 9x Oversampling lässt sich schon eine sehr gute Qualität ereichen, sorgt aber bei 8 Lichtquellen durch die 72 Texturelookups für einen Zusammenbruch der Framerate.
Wichtig ist auch, dass man beachtet, dass die dualparaboliden Maps an Ihren Ränden undefiniert sind und so zu Artefakten kommen kann wenn man außerhalb des gültigem Bereichs sampled. Eine Möglichkeit wäre einen Bereich zu erfassen, der etwas größer als 180 Grad ist, um am Rand zusätzliche Texel zu schaffen.
+
Wichtig ist auch, dass man beachtet, dass die dualparaboloiden Maps an Ihren Ränden undefiniert sind und so zu Artefakten kommen kann wenn man außerhalb des gültigem Bereichs sampled. Eine Möglichkeit wäre einen Bereich zu erfassen, der etwas größer als 180 Grad ist, um am Rand zusätzliche Texel zu schaffen.
  
 
===Ein zusätzlicher Renderpass===
 
===Ein zusätzlicher Renderpass===
Nach dem die Tiefenmap gerendert würde, wäre es möglich in einem weiterem Framebufferobjekt eine zusätzliche Map zu rendern, in der mit Hilfe einer Kantenerkennung die Helligkeit der Softshadows vorberechnet wird. Auch hier muss berücksichtigt werden, dass die dualparaboliden Maps einen Übergang haben.
+
Nach dem die Tiefenmap gerendert würde, wäre es möglich in einem weiterem Framebufferobjekt eine zusätzliche Map zu rendern, in der mit Hilfe einer Kantenerkennung die Helligkeit der Softshadows vorberechnet wird. Auch hier muss berücksichtigt werden, dass die dualparaboloiden Maps einen Übergang haben.
  
 
===Dynamische Lichter in statische Lightmaps Rendern===
 
===Dynamische Lichter in statische Lightmaps Rendern===
Zeile 498: Zeile 510:
  
 
===Statische Anteile in den Schattenmaps===
 
===Statische Anteile in den Schattenmaps===
Es ist möglich mit einer zusätzlichen Matrix die Tiefenmaps zu den Weltkoordinaten auszurichten. Statische Anteile können dann aus einer zweiten Tiefenmap kopiert werden. Ein wenig problematisch ist das Entfernen der Rotation aus der gl_ModelViewMatrix. Das größte Problem ist, das die Positionen der Lichquellen bereits mit der gl_ModelViewMatrix multipliziert sind. Die einfachste Lösung ist, aus den den Rotationswinkeln eine neue Matrix zu erechnen, mit dessen Hilfe die Schattenvektoren für die Texturkoordinatenberechnung zurückgedreht werden. Diese Matrix sollte dann als Uniformvariable übergeben werden.
+
Es ist möglich mit einer zusätzlichen Matrix die Tiefenmaps zu den Weltkoordinaten auszurichten. Statische Anteile können dann aus einer zweiten Tiefenmap kopiert werden. Ein wenig problematisch ist das Entfernen der Rotation aus der gl_ModelViewMatrix. Das größte Problem ist, das die Positionen der Lichtquellen bereits mit der gl_ModelViewMatrix multipliziert sind. Die einfachste Lösung ist, aus den den Rotationswinkeln eine neue Matrix zu erechnen, mit dessen Hilfe die Schattenvektoren für die Texturkoordinatenberechnung zurückgedreht werden. Diese Matrix sollte dann als Uniformvariable übergeben werden.
Wenn die Schatten durch einen zusätzlichen Renderdurchgang gefiltert werden. Ist es sinvoll die Maps erst hier zu vereinen. Von beiden Tiefenmaps muss nur immer der kleinere Wert genommen werden.
+
Wenn die Schatten durch einen zusätzlichen Renderdurchgang gefiltert werden. Ist es sinnvoll die Maps hier erst zu vereinen. Von beiden Tiefenmaps muss nur immer der kleinere Wert genommen werden.

Aktuelle Version vom 18. März 2012, 16:52 Uhr

Vorwort

Willkommen zu meinem erstem Tutorial. Schatten können sowohl Traum als auch Albtraum eines jeden OpenGL-Programmierers werden. Im Gegensatz zur einfachen Beleuchtung mit Lichtquellen, steigt der Rechenaufwand hier extrem an und ohne Optimierung zwingen die Schatten selbst die modernste Hardware in die Knie. Für Schatten gibt es zwei praktikable Algorithmen: Den Stencilschatten und den projizierten Schatten. Hier möchte ich mich auf den projizierten Schatten beschränken, er bietet gegenüber dem Stencilschatten einige Vorteile:

  • Die komplette Berechnung kann von der Grafikkarte übernommen werden
  • Es ist möglich Softshadows zu realisieren
  • Die Shadowmaps können unter Umständen für mehr als ein Frame verwendet werden

Um den Rechenaufwand zu verringern werde ich hier zusätzliche Projektionstechniken zeigen, die über den Vertexshader realisiert werden: Die perspektivische Map und das paraboloide Mapping.

Für sehr weit entfernte Lichtquellen und offene Szenen ist der Einsatz von perspektivisch angepassten Maps sinnvoll. Da das Licht quasi parallel ausgestrahlt wird. Ist ein winkelabhängiger Cube oder dual parabolische Map kaum möglich. Auch eine einfache quadratische Shadowmap zeigt deutliche Schwächen bei der Auflösung im Nahbereich und dem zu hohem Oversampling in der Entfernung.

Das paraboloide Mapping ermöglicht nicht nur die Simulation eine Fischaugenoptik, sondern kann auch die Cubemap vollständig ersetzen. Durch einen geringfügig höheren Rechen- und Programieraufwand genügen zwei Renderpasses um eine vollständige Tiefen- oder Reflektionsmap zu erstellen. Was gegenüber der klassischen Cubemap den Prozessor und GPU um den Faktor 3 entlastet.

Vorkenntnisse

Dieses Tutorial basiert auf den grundlegenden Techniken, die erst in den letzten Jahren entwickelt wurden. Jeder der hiermit anfängt, sollte die zwei anderen GLSL-Tutorials gelesen haben und einfache Fragmentshader schreiben können. Auch das Laden und Einbinden von Texturen in die Shader sollte kein Problem mehr darstellen.

Framebufferobjekte

Für das Erstellen von dual paraboloiden Tiefentexturen ist noch das Rendern in Framebufferobjekten Vorraussetzung (Tutorial Framebufferobject). Damit der Einstieg nicht zu schwer wird sollte dieser Code helfen das FBO zu initialsisieren:

Als Erstes müssen die nötigen Extensions initialisiert werden Dann sollten die benötigten Modelle als VBO in die Grafikkarte hochgeladen werden. Nun muss das Framebufferobjekt (FBO) für die Schattenmaps erzeugt werden. So könnte die Initialsierung des FBOs aussehen. C code:

GLuint shadow_map = 0; // 
GLuint shadow_fbo = 0; // the shadow texture
GLuint shadow_size = 4096; //muss kleiner/gleich maximaler 2D-Texturgröße sein

glGenTextures (1, &shadow_map); //In Pascal das & durch ein @ ersetzten
glBindTexture (GL_TEXTURE_2D, shadow_map);
glTexImage2D (GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT16, shadow_size, shadow_size, 0,GL_DEPTH_COMPONENT , GL_UNSIGNED_BYTE, NULL);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glGenFramebuffersEXT (1, &shadow_fbo); //In Pascal das & durch ein @ ersetzten
glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, shadow_fbo);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_DEPTH_ATTACHMENT_EXT,GL_TEXTURE_2D,shadow_map, 0);
glDrawBuffer (GL_FALSE);
glReadBuffer (GL_FALSE);
GLenum status = glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.

Pascal :

var 
  shadow_map,
  shadow_fbo,
  shadow_size : TGLuint;
  status      : TGLenum; 
begin
  shadow_size := 4096; // muss kleiner/gleich maximaler 2D-Texturgröße sein

  glGenTextures(1, @shadow_map);
  glBindTexture(GL_TEXTURE_2D, shadow_map);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, shadow_size, shadow_size, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NIL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

  glGenFramebuffersEXT(1, @shadow_fbo);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, shadow_fbo);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, shadow_map, 0);
  glDrawBuffer (GL_FALSE);
  glReadBuffer(GL_FALSE);
  Status := glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.
end;

Jetzt sollten die Shader und weiter Texturen geladen werden.

Beschleunigtes Rendern

Ich empfehle auch die zu rendernden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird. Da es hierzu bereits ein Tutorial gibt werde ich hier nichts mehr darüber schrieben. Alternativ sollte alles was zwischen glBegin und glEnd in Displaylisten gespeichter werden. Das Speichern von Texturwechseln usw. ist nicht sinnvoll, da diese nicht zum Rendern der Shadowmaps benötig werden.

Bei mehrfachen Lichtquellen kann es wiederrum Sinn machen den ersten Rendervorgang inklusive Shaderwechsel in einer Displayliste zwischenzuspeichen und die zusätzlichen Schadowmaps mit dieser zu rendern.

Algemeines zu GLSL

Ich möchte nocheinmal darauf hinweisen, das sich Uniformvariablen nur setzten lassen, wenn der entsprechnde Shader gebunden ist. (Es funktionier nicht wenn der shader nicht gebunden ist. Warum man den shader dabei allerdings angeben muss ist mir nicht so ganz klar) In den Beispielen werden nur die Uniformvariablen während des Rendervorgangs neu gesetzt die sich ändern. Die Zuweisung der TMU zu einem Sampler gehöhrt in der Regel nicht dazu.

Grundlegendes zu den Koordinatensystemen in den Shadern

Im Vertexshader müssen alle Komponenten von gl_Position in einem Bereich von -1.0 bis 1.0 gebracht werden. Besonders beim Z-Wert könnte es verwirrend, dass der Bereich von gl_FragDepth im Fragmentshader von 0.0 bis 1.0 geht. Auch muss beachtet werden, dass Texturkoordinaten den Bereich von 0.0 bis 1.0 nutzen, während die Rendertargets im Bereich von -1.0 bis 1.0 arbeiten. In den Shadern werden daher öfters Multiplikationen mit 0.5 und einer anschließenden Addition von 0.5 auftreten.

Perspektivische Maps

In dem erstem Teil dieses Artikels geht es um perspektivisch angepasste Schattenmaps. Normale projizierte Schatten haben das Problem, das sie im Nahenbereich besonders stark verpixeln und in der Entfernung durch ein viel zu hohes Oversampling Bandbreite verschwenden.

Für eine globale Lichtquelle wird nur ein Vektor gegeben, der die Richtung des Lichtes beschreibt. Die gedachte Lichtquelle ist quasi unendlich weit weg. Da eine Entfernungsberechnung zur Lichtquelle unmöglich ist, muss die Schattenberechnung relativ zu einer Referenzebene durchgeführt werden. Diese Ebene kann sowohl Senkrecht zum Lichtvektor, als auch parallel zum gedachtem Boden ausgerichtet werden. Die Ausrichtung der Referenzebene beeinflusst die später sichtbare Auflösung der Schatten.

Nach dem ein Vertex auf die Ebene projiziert wurde und der Abstand ein einen Bereich von 0.0...1.0 gebracht wurde, muss diese unendlich große Ebene noch auf eine endliche Größe projiziert werden, die auf eine Textur passt und die entfernungabhängige Detaillierung beachtet.

Wenn wir unsere Referenzebene einfachhalber den Horizont schneidet (und auch den Bildschirm in obere und untere Hälfte teilt) Könnte ein Algorithmus in etwa so aussehen (Dieser lässt sich später in ein Vertexshaderprogramm umsetzten):

Projiziere den Vertex in die Modelview (Diese Schritt wurde auch bei den dual paraboloiden Maps durchgeführt) Ermittle den Schnittpunkt des vom Vertex ausgehendem gedachtem Lichtstrahl und der Referenzebene. Benutze den Abstand zwischen Schnittpunkt und Vertex um den Z-Wert zu ermitteln. (Es sind zwei zusätzlich Referenzwerte nötig die die Funktion Farclip und Nearclip übernehmen) Projiziere die Ebene auf eine Ebene die in eine Textur passt pos/=abs(pos)+1.0 passt recht gut. Anschließend wird noch die Schadowmap noch gestreckt und der Bereich hinter der Kamera entfernt.

Das wichtigste ist, dass beim auswerten der shadowmaps der gleiche Projektionsalgorithmus verwendet wird wie beim generieren.

Prinzipiell sollte sich der Algorithmus schon auf einer Geforce 3 oder Radeon 8500 implementieren lassen. Da da der Code hier in GLSL geschrieben ist, sollte in etwa Shadermodel 2.0 unterstützt werden.

Geschwindigkeitsmäßig ist dieser Algorithmus den Stencilshadows deutlich überlegen. Auch bei Stencilshadows sind mehrere Renderpasses nötig. Vorallem das extrudieren der Silhouetten kostet einiges an Bandbreite. Der geschätzte Aufwand ist etwa 20 (Aufwendige Shader im Finalem Renderdurchgang)bis 80% (CPU limitiert Geometrieübergabe) Mehraufwand gegenüber einer Schattenlos gerenderten Scene. Auch die Qualität ist nicht schlechter. Bei der Verwendung von weichen Schatten wirken Shadowmaps natürlicher als die extrem scharfen Stencilshadows.

Hauptprogramm

In der Hauptschleife könnte folgender Code verwendet werden:

	glBindTexture(GL_TEXTURE_2D, 0 ); //Entfernt alle Texturen aus der ersten TMU
	glUseProgramObjectARB(shadow); //lädt den Schattenshader
	glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, shadow_fb); //Bindet das Framebufferobjekt
	
	glViewport (0, 0,shadow_sz,shadow_sz); //Anpassen des Viewportes 
	glClear(GL_DEPTH_BUFFER_BIT); //Z-Buffer löschen
	
	render(); //Erster Renderdurchgang

	glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0); //Normalen Framebuffer binden
	glBindTexture(GL_TEXTURE_2D, shadow_tx ); //Tiefenmap an die erste TMU binden
	glUseProgramObjectARB(final); //finalen Shader binden
	
	glViewport(0, 0, screen_size_x, screen_size_y);
	
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //Color und Z-Buffer löschen
	render(); //rendern

Shader

Als erstes der Vertexshader zum generieren der Schadowmap. shadow.vert:

void main(void){

	vec3 lightdir = gl_NormalMatrix * normalize(vec3(0.1,-1.0,0.0)); //Lichtvektor in den Modelsprace rotieren
	vec3 mvertex =  vec3 (gl_ModelViewMatrix * gl_Vertex); //Vertex in den Modelspace projizieren
	vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y; //Schnittpunkt mit der XZ-Referenzebene
	pos = pos / (abs(pos)+1.0); //Projektion der Ebene auf ein Quadrat
	pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kamera
	gl_Position.xy = pos;
	gl_Position.z = -mvertex.y ; // Der Bereich von +-1 über der Referenzebene wird erfasst
	gl_TexCoord[0] = gl_MultiTexCoord0;
 	}

Die Lichtrichtung wird vorläufig noch fest übergeben. Eine Auswertung einer OpenGL Lichtquelle oder einer Uniformvariable wäre ebenfalls möglich. Anschließend wird der Vertex in die Modelview projiziert und der Schnittpunkt des vom Vertexausgehenden Lichstrahls mit der Referenzebene berechnet. Als letztes wird noch der Abstand zur Referenzebene in einem Bereich 0..1 umgerechnet und die Texturkoordinaten für eventuelles Alphamasking oder Heightmapping durchgeschleift. Soll Alphamasking verwendet werden, muss ein entsprechender Shader geschrieben werden der bei den freizulassenden pixeln discard(); aufruft. Dies sollte jedoch nur für die betroffenen Polygone statt finden, da die Grafikkarte dann keine Early-Z Optimierungen verwenden kann. Ansonsten ist der Fragmentshader ist sehr einfach: shadow.frag:

void main(void){}

Die für die Auswertung müssen wir im finalem Renderdurchgang exakt die gleichen Texturkoordinaten für die Shadowmap generieren. Allerdings gibt es eine Besonderheit: Die Koordinaten einer Textur reichen von 0;0 bis 1;1. Die Koordinaten eines Rendertargets reichen jedoch von -1;-1 bis 1;1, so das eine zusätzliche Korrektor nötig ist.

varying vec3 spos;

void main(void){
        vec3 lightdir = gl_NormalMatrix * normalize(vec3(0.1,-1.0,0.0));
        vec3 mvertex =  vec3 (gl_ModelViewMatrix * gl_Vertex);
        vec2 pos = mvertex.xz + lightdir.xz * -mvertex.y/lightdir.y;
        pos = pos / (abs(pos)+1.0);
        pos = pos * vec2(1.0,1.8) + vec2(0.0,0.8); //Abschneiden des Bereiches hinter der Kamera
        pos = pos * 0.5 + 0.5; //auf Texturkoordinaten umrechnen
 
        spos.xy = pos; //Daten in einer Varying verpacken
        spos.z = -mvertex.y * 0.5 + 0.5 - 0.005; // far/near-clamping und Anti-Z-Fightingoffset 
 	
        gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //Die echte Transformation
 
        gl_TexCoord[0] = gl_MultiTexCoord0;
        gl_TexCoord[1] = gl_MultiTexCoord1;
        }

Die ersten 5 Zeilen der main() sollten exakt das gleiche tun wie der Vertexshader zum generieren der Map.

Nun fehlt noch ein Fragmentshader für den letzten Durchgang:

Es muss noch der Normalvektor berücksichtigt werden!!!

uniform sampler2D Shadowmap;
varying vec3 spos;
 
void main(void){
        if (texture2D(Shadowmap, spos.xy).r > spos.z ){
              //Licht
              gl_FragColor = vec4(1.0,1.0,1.0,1.0);
              }
        else{
              //Schatten
              gl_FragColor = vec4(0.5,0.5,0.5,1.0);
              }
        }

Paraboloide Maps

Einfache paraboloide Maps können für alle Punktlichtquellen verwendet werden, deren Licht nicht in alle Richtungen abgestrahlt werden soll. Darunter fallen vorallem Spots und an Wänden oder Decken befestigte Lampen. Im Gegensatz zumr Parallelen Licht oder einer Punktlichquelle bringt eine solche Lichtquelle häufig eine Helligkeitsmap mit. Für die Helligkeitsmap und die Schattenmap können die gleichen Texturkoordinaten verwendet werden.

Bei kleinen Öffnungswinkeln hat die paraboloide Map keinen Vorteil gegenüber einer normalen Projektion. Der Vorteil ist jedoch das der Code sich für Öffnungswinkel von bis zu ~240 Grad eignet.

Um eine solche Lichquelle zu beschreiben weden nicht weniger als drei Vektoren benötigt:

  • Position
  • Richtung
  • Tangente

Während die Funktion der ersten beiden Vektoren klar ist, wird die Tangente dazu benötigt die Schattenmap und Helligkeitsmap auszurichten. Er ist Senkrecht zur Richtung tangential zur Textur ausgerichtet Beschreiben lässt sich diese Funktion am ehesten durch das Verdrehen einer Taschenlampe. Wenn die Schattenmap gegenüber den schattenwerfenden Objekten verdreht wird, sieht das Bild auf keinen Fall mehr natürlich aus.

Es ist sowohl möglich die Berechnungen im Vertexshader durchzuführen, als auch eine entsprechnede Matrix in den Uniformvariablen abzulegen. Wenn die Berechnungen per Matrix durchgefühert werden lässt sich das Prinzip der paraboloiden Maps zuindest als Reflektionsmap nutzen. Ein weiterer Vorteil an einer vorberechneten Matrix ist, dass diese auch für Shadowmap verwendet werden kann die nicht jeden Frame neu berechnet werden muss.

Dual Paraboloide Maps

Von OpenGL kennen wir zwei Projektionsmöglichkeiten: Orthografische und perspektivische Projektion. Beide Projektionen arbeiten ohne Verzerrung. Jede Gerade bleibt beim Transformieren eine Gerade. Wenn man dagegen das Bild eines Fischeyeobjektives betrachtet, allen einem sofort die zu Kurven verzerrten Geraden auf. Der entscheidende Vorteil an einer Fisheyeaufnahme ist, dass ein Öffnungswinkel von 180 Grad erfasst werden kann. Zwei entgegensetzte Aufnahmen können so problemlos den kompletten Raum um die Kamera erfassen.

Für das Erstellen der Tiefenmap ist es notwendig den Raum um das reflektierende Objekt in eine Textur zu rendern. Meistens wird hier eine Cubemap verwendet. Soll diese dynamisch generiert werden, ist es auffällig, dass die Scene ganze 6 mal gerendert werden muss. Mit der dual paraboloiden Map sind nur noch zwei Rendervorgänge nötig, es wird zwar keine Füllrate eingespart, dafür müssen nun nur noch 1/3 der Daten transformiert werden, so das die Vertexshader und die CPU entlastet werden.

Eine weitere Optimierung ist unter Umständen möglich: Wenn bei einer Cubemap auf eine der Flächen verzichte werden kann. Z.B. wird die Reflexion im Lack eines Autos so gut wie nie verdeckte Straße zeigen. Auch ist für eine an einer Wand befestigten Lampe nur eine halbkugelförmige Shadowmap nötig. Wenn sie einen Abstand zur Wand hat, genügt es auch hier den Blickwinkel von 180 Grad etwas zu erweitern.

Die Projektion der dualparaboloiden Map kann man sich am besten Bild eines Fisheyobjektives vorstellen. Mathematisch etwas genauer ist das Reflexionsbild der umgebenden Welt in einem Rotationsparaboloiden.

Die Hardwareanforderungen an den Pixelshader sind nicht gerade gering. Der Pixelshader für die 8fachen Lichtquellen kommt nach dem Compilieren auf über 170 Instruktionen. Eine Shader 2.0 Karte wie z.B. die Radeon 9x00 sind hier hoffnungslos überfordert da sie nur an 3 Stellen im Programm auf die Textureinheiten zugreifen können und der Assembler nicht mehr in der Lage ist die Instruktionen passend umzusortieren. Noch fataler wird es wenn die Texturelookups durch dynamisches Branching übersprungen werder sollen: Die Karte hat keine Chance mehr diese umzusortieren.

Wer dualparboloide Maps auf SM2.0 Karten einsetzten will sollte damit rechnen, dass das Limit bei 2 bis 4 Maps liegen sollte.

Hauptprogramm

So kann in der Hauptschleife das Rendern der 16 paraboloiden Maps und der anschließende Finale Renderdurchgang durchgeführt werden:

//Shader für Dualparabolische Maps aktivieren
glUseProgramObjectARB(parabol);

//Framebufferobjekt aktivieren
glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, shadow_fb);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//Die 8 Lichtquellen rendern
for (int light;light<8;light++){
        glViewport (shadow_size/2*(lights/4),shadow_size/4 *(light%4),shadow_size/4,shadow_size/4);
        glUniform1iARB(glGetUniformLocationARB(parabol, "light"),light);
        glUniform1iARB(glGetUniformLocationARB(parabol, "renderpass"),0);
        render();
        glViewport (shadow_size/4+shadow_size/2*(lights/4), shadow_size/4*(light%4),shadow_size/4 ,shadow_size/4);
        glUniform1iARB(glGetUniformLocationARB(parabol, "renderpass"),1);
        render();
        }

// Framebufferobjekt deaktivieren
glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0);

//Richtigen Shader aktivieren
glUseProgramObjectARB(final);
//Vieport an Fenstergröße anpassen
glViewport(0, 0,Windowsize_X,Windowsize_Y);

//Frame rendern
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
render();

ungetesteter Pascalcode:

//Shader für Dualparabolische Maps aktivieren
glUseProgramObjectARB(parabol);

//Framebufferobjekt aktivieren
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, shadow_fb);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

//Die 8 Lichtquellen rendern
for light := 0 to 7 do
begin
  glViewport(shadow_size/2*(lights/4), shadow_size/4 *(light mod 4), shadow_size/4, shadow_size/4);
  glUniform1iARB(glGetUniformLocationARB(parabol, 'light'), light);
  glUniform1iARB(glGetUniformLocationARB(parabol, 'renderpass'), 0);
  render();
  glViewport(shadow_size/4+shadow_size/2*(lights/4), shadow_size/4*(light mod 4), shadow_size/4, shadow_size/4);
  glUniform1iARB(glGetUniformLocationARB(parabol, 'renderpass'),1);
  render();
end;

// Framebufferobjekt deaktivieren
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

//Richtigen Shader aktivieren
glUseProgramObjectARB(final);
//Viewport an Fenstergröße anpassen
glViewport(0, 0, Windowsize_X, Windowsize_Y);

//Frame rendern
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
render();

Die Shader

Shader zum Rendern der Schattenmaps

parabol.vert

  uniform int renderpass;
  uniform int light;
  varying vec3 normal;
  varying vec3 pos;

  void main(void){

	gl_Position = gl_ModelViewMatrix * gl_Vertex - gl_LightSource[light].position ;

	float L= length (gl_Position.xyz);
	gl_Position /= -L;
	if (renderpass == 1) gl_Position.z *=-1.0;
	gl_Position.z += 1.0;
	gl_Position.xy /= gl_Position.z;
	if (gl_Position.z >= 0.01){
		gl_Position.z = L / 15.0;//Todo: optimieren
		gl_Position.w = 1.0;
		}
	else{
		gl_Position.z = -1.0;
		gl_Position.w = -1.0;
		}
        
        gl_Position.z = 2.0 * gl_Position.z -1.0; //Todo: optimieren
	pos=gl_Position.xyz;
	}

parabol.frag

varying vec3 pos;
void main(void){
	if (length(pos.xy)>1.005)discard; //Diese Zeile kann durchaus entfallen. Kosten/Nutzen unbekannt.
	}

Es gibt zwei Uniformvariablen, die gesetzt werden müssen: Eine ist die Nummer der aktuellen Lichtquelle und die zweite der Renderpass, der angibt ob die Vor- oder Rückseite der parabolischen Map gerendert werden soll. Eine automatische Berechnung der Texturkoordinaten wäre zwar möglich, jedoch ist die Änderung des Viewports deutlich weniger Rechenaufwendig.

Statt einer Multplikation des gl_Vertex mit der gl_ModelViewProjektionMatrix wird hier nur mit der gl_ModelViewMatrix multipliziert. Dadurch werden nur die Transformationen durchgeführt und die Projektion übersprungen. Die Division durch -L entspricht weitgehend einer Normalisierung, spiegelt die Welt jedoch in die richtige Lage. L enthält jetzt die Tiefeninformation, gl_Position einen Vektor, der von der Kamera auf den Vertex zeigt. Mit den zwei folgenden Zeilen wird der Vektor parabolisch auf die Bildschirmkoordinaten projeziert (Intervall von -1.0 bis 1.0). Mit Hilfe der folgenden if-Abfrage werden alle Vertices hinter der Kamera geclipt. Nur sichtbare Vertices bekommen gültige z Werte für den Zbuffer im Intervall von -1.0 bis 1.0.

Der Fragmentshader ist extrem einfach, da wir aufgrund des nicht vorhandem Farbbuffers keinen Farbwert benötigen lassen wir diesen wie bei den perspektivischen Maps einfach undefiniert. Zudem verlassen wir den Shader mit discard, wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die Voteil ist, das nur der kreisförmige Bereich der Map verwendet wird, der Nachteil, das durchaus eine Early-Z optimierung nicht mehr möglich ist. Sehr Sinnvoll sollte dies sein, wenn die Shadowmaps dynamisch nach benötigter Größe angeordnet werden. Da sich Kreise besser als Quadrate packen lassen.

Shader für den finalen Renderdurchgang

final.vert

varying vec3 Normal;
varying vec3 ModelVertex;

void main(void){
	Normal = gl_NormalMatrix * gl_Normal;
 	ModelVertex  = vec3 (gl_ModelViewMatrix * gl_Vertex);
	gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
	gl_TexCoord[0] = gl_MultiTexCoord0;
 	}

final.frag

varying vec3 Normal;
varying vec3 ModelVertex;

uniform sampler2D Texture0; // Eine normale Textur 
uniform sampler2D Shadowmap;  //Damit ist auch die 4. TMU belegt..
uniform int MaxLights;

//Achtung folgende Zeile ist nicht GLSL-Konform. 
//Workaround: Array als Uniform übergeben oder durch sehr aufwendige Berechnung erstzten
//Sollte es auch auf ATI-Karten funktionieren, frage ich mich warum es nicht erlaubt ist...
const vec2 texofset[8] = {vec2 (0.125,0.125), vec2 (0.125,0.375), vec2 (0.125,0.625), vec2 (0.125,0.875),
			  vec2 (0.625,0.125), vec2 (0.625,0.375), vec2 (0.625,0.625), vec2 (0.625,0.875)};
void main(void){
	vec4 light=vec4 (0.2, 0.2, 0.2, 0.0); //Emmitiertes Licht
	vec3 normalvec =normalize(Normal.xyz);
	for (int LightNum = 0;LightNum < MaxLights; LightNum++){
		vec3 lightvec = ModelVertex - gl_LightSource[LightNum].position.xyz;
		vec3 lightdir = normalize( lightvec );
		vec2 parabol = texofset[LightNum];
		if (lightdir.z > 0.0){
			parabol.t += 0.25;
			}
                // parabolische Projektion für subtextur
		parabol -=  lightdir.xy * 0.125/ (abs (lightdir.z) + 1.0); 
		vec4 shadow = step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol).r );
		light += shadow * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));
		}
	vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0]));	//Textur auslesen; 
	gl_FragColor = light * color ;
	}

Möglicherweise für ATI taugliche Variante:

varying vec3 Normal;
varying vec3 ModelVertex;
 
uniform sampler2D Texture0; // Eine normale Textur 
uniform sampler2D Shadowmap;  //Damit ist auch die 4. TMU belegt..
const int MaxLights=8;
 
vec2 texofset[8]; 
void main(void){
	texofset[0] = vec2 (0.125,0.125);
	texofset[1] = vec2 (0.125,0.375);
	texofset[2] = vec2 (0.125,0.625);
	texofset[3] = vec2 (0.125,0.875);
	texofset[4] = vec2 (0.625,0.125);
	texofset[5] = vec2 (0.625,0.375);
	texofset[6] = vec2 (0.625,0.625);
	texofset[7] = vec2 (0.625,0.875);

        vec4 light=vec4 (0.2, 0.2, 0.2, 0.0); //Emmitiertes Licht
        vec3 normalvec =normalize(Normal.xyz);
        for (int LightNum = 0;LightNum < MaxLights; LightNum++){
                vec3 lightvec = ModelVertex - gl_LightSource[LightNum].position.xyz;
                vec3 lightdir = normalize( lightvec );
                vec2 parabol = texofset[LightNum];
                if (lightdir.z > 0.0){
                        parabol.t += 0.25;
                        }
                // parabolische Projektion für subtextur
                parabol -=  lightdir.xy * 0.125/ (abs (lightdir.z) + 1.0); 
                float shadow = step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol).r );
                light += shadow * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));
                }
        vec4 color = texture2D(Texture0, vec2(gl_TexCoord[0])); //Textur auslesen; 
        gl_FragColor = light * color ;
        }


Diese Shader besitzen 3 Uniformvariablen, die unbedingt mit den richtigen Werten gefüllt werden müssen. Zwei davon sind Texturen, die dritte die Anzahl der aktiven Lichtquellen. Es ist wichtig zu wissen, dass der Shader nicht den OpenGL-Status der Lichtquellen berücksichtigt und nur die Daten der Ersten bis MaxLights holt. Während bei den Schattenmaps die meiste Arbeit im Vertexshader erledigt werden könnte, ist dieser sehr Fragmentshader lastig. Die Texturkoordinatenberechnung ist sehr ähnlich zu der im shadow.vert. Die größte Änderung ist, dass hier abhängig von der Lichtquellennummer ein Offset aufaddiert wird um die Untertextur auszuwählen. Ein zusätzlicher Offset wird dazuaddiert, wenn auf die zweite parabolide Map einer Lichtquelle zugegriffen wird. Um ein dynamisches Branching zu vermeiden, wird mit der Stepfunktion ermittelt ob sich das Fragment im Schatten befindet.

Optimierungen und Verbesserungen

Hier sind noch einige Vorschläge, die helfen können um besser Qualität oder Leistungen im eigenem Programm zu bekommen. Eine universelle Lösung lässt sich nur auf kosten von Performance schreiben. Viel besser ist es, wenn man die Shader an die jeweilige Situation anpasst.


Schatten durch Alphatest

Der bisherige Schattenshader kann nur ganze Polygone einen Schatten werfen lassen. Wenn shadow.vert so ergänzt wird, das die Texturkoordinaten in den Fragmentshader weitergereicht werden, dann ist es mit folgendem Fragmentshader möglich Schatten in Abhängigkeit einer Alphatextur zu rendern:

shadow.frag

uniform sampler2D Texture0;
void main(void)
{
        if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
}

Der Shader wird verlassen wenn das Texel einen Alphawert von kleiner als 50% hat. Damit lassen sich Schatten von Pflanzen wesentlich realistischer darstellen. Dieser Shader sollte natürlich nur dann verwendet werden, wenn eine Alphakanal in der Textur vorhanden ist. Auch Shader mit Parallax- oder Displacementmapping arbeiten kann man so um einen Schatten ergänzen, wenn die Polygone transparente Teile enthalten. Es ist auch möglich den Shader zu verlassen, wenn eine bestimmte Farbe in der Textur gefunden wird, dann nimmt die Alphamaske keinen Speicherplatz mehr weg.

Farbiges Glas

Schatten hat jetzt ja schon fast jeder. Aber wie wäre es mal mit einfärben von dynamischen Lichtquellen? Der Algorithmus ist nicht schwer:

  • Alle undurchsichtigen Sceneteile in die Tiefenmaps gerendern.
  • Zusätzlich wird eine Colormap gebunden und das Schreiben in den Zbuffer unterbunden.
  • Die Colormap wird mit Weiß oder einer Helligkeitsmap initialisiert.
  • Rendern aller transparenten Objekten
  • Auf finales Rendern umschalten
  • Alle undurchsichtigen Objekte mit Hilfe der Schadowmap und der modifizierten Beleuchtungmap beleuchten.
  • Alle Transparenten Objekte rendern, unter Berücksichtigung, dass die Lichtquelle bereits das gefilterte Licht aussendet. Dieses Blending sollte rückgängig gemacht werden.

Bei diesem Algorithmus bleibt noch das Alphablendingproblem mit der mehrfachen Überdeckung. Beim Modifizieren der Beleuchtungsmap gegebenfalls den Stencilbuffer zur Hilfe nehmen. Hier wäre ein denkbarer Algorithmus:

  • Alle transparenten Polygone in den Zbuffer rendern. Dabei für alle Polygone, deren doppelte Filterung zu Fehlern führen kann mit verschiedenen Stencilwerte schreiben.
  • Zbuffer und Colorbuffer Löschen.
  • Helligkeitsmap in den Colorbuffer kopieren.
  • Alle nicht transparenten Objekte in den Zbuffer Rendern.
  • Alle transparenten Polygone mit den gleichen Stencilwerten wie im erstem Pass rendern. Die Bedingung ist, dass nur Polygone in den Zbuffer geschrieben werden, bei den Stencilwerte nicht mit den Stencilbuffer übereinstimmen. Die vordersten Polygone werden so aussortiert und nur die zweite Schicht landet im Zbuffer.
  • Im letztem Rendervorgang müssen die gefilterten Lichtfarben der Transparenten Polygone in den Colorbuffer geschrieben werden. Die Bedingung ist das die Stencilwerte übereinstimmen müssen.

Auch wenn es wie ein großer Mehraufwand aussieht, ist der Anteil der transparenten Polygone doch eher gering. So das deren dreifacher Overdraw kaum eine Rolle spielt. Im Finalem Renderdurchgang gibt es bei den transparenten Polygonen noch eine Besonderheit: Stimmt die Entfernung aus der Shadowmap mit der Entfernung der Lichtquellen (fast) überein, so wird das Polygon mit dem Licht aus der Colormap beleuchtet. Ist die Entfernung aus der Shadowmap jedoch größer als die Entfernung zu Lichtquellen, enthält die Colormap die gefilterte Farbe des Lichtes hinter dem Polygon. Für die Beleuchtung des Polygon muss also das Licht der Lichtquelle benutzt werden. Sinnvoll ist es die Helligkeit für diesen Fall im Alphawert der Colormap zu Speichern.

Ansonsten noch ein paar Vorschläge:

  • Objekte die aus dem gleichem Glas sind und auch einfarbig. Sollten mit dem gleichem Stencilwert verarbeitet werden. So sieht z.B. das gefärbte Licht einer zweifach durchstrahlten leicht gefärbten Glaskuppel natürlicher aus als ein Vollschatten. Auch wenn das Licht korrekter weise zwei mal hätte gefiltert werden müssen.
  • Eventuell macht es sind mehrfarbige Objekte in mehrfache einfarbige Objekte zu zerlegen.
  • Komplexe Mehrfachfilterungen sollten vermieden werden oder so gewählt werden, das sie natürlich aussehen. Gute Filter wären Komplementabfärben, da dort ein echter Schatten erzeugt wird. Schlechte Kombinationen wären unter anderem Rot+Gelb, Blau+Gelb, usw...
  • Um von den Farbfehlern abzulenken, farbige Objekte in einer rot verschobenen Farbe fluoreszieren lassen. (Violet-> Blau -> Grün -> Gleb -> Organge -> Rot) Durch blau gefärbte Fenster, blau gefilterte Licht welches Dinge grün aufleuchten lässt nimmt einem jeder PC Modder ab...

Schattenmap durch eine Heightmap modifizieren

Auch wennich hierzu noch kein Beispiel haben, ist es möglich gl_FragDepth mit Hilfe einer Heightmap zu modifizieren. Prinzipiell entspricht dies einem einfachem Offsetmapping. Damit wäre durchaus eine Selbstschattierung von Bumpmaps möglich.

Zweite parabolische Map vermeiden

Lichtquellen, die nur in eine Richtung Licht werfen können, wie z.B. Spotlichter oder Lichter, die in Bodennähe oder Wandnähe befestigt sind, benötigen keinen vollständig erfassten Tiefenraum. Die Parabolische Map kann problemlos herein oder herausskaliert werden, so das Öffnungswinkel von 0 bis ca 240 Grad möglich sind.


Das verkleinern des Öffnungswinkels für zusätzliche Spotlichter ermöglicht auch kleinere Tiefenmaps, die in die 9 Zwischenräume der großen gepackt werden können. Die Zwischenräume können noch Maps mit einem Durchmesser von 40% der großen Maps aufnehmen.


Texturelookups vermeiden

Wenn das Skalarprodukt von normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und Berechnungen für die Lichquelle können komplett übersprungen werden. Diverse Multiplikationen und vorallem Texturelookups können so übersprungen werden. Besondere aufwendige Algorithmen wie selbstschattierende Bumpmaps, können deutlich Beschleunigt werden.

Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Skalarprodukt von vor der if Abfrage aber umbedingt wiederverwerten!)

  light += gl_LightSource[0].diffuse * max(dot(normalvec, -lightdir), 0.0);

Reichweite der Lichtquellen

Leider bietet OpenGL nicht direkt die Möglichkeit die maximale Reichweite einer Lichquelle anzugeben. In den Shadern wird zur Zeit der Wert 15.0 als maximale Entfernung zur Lichtquelle verwendet. Für ein einfaches Programm ist es sicher ausreichend, ansonsten macht es Sinn diesen Wert durch eine Unformvariable zu ersetzten, oder um mehr Flexibiltät zu bekommen durch ein Array aus 8 Uniformfloats.

Oversampling der Schattenmap

Die Qualität lässt sich dadurch mehrere Samples auf der Schattenmap verbessern. Das folgende Codefragment nimmt 3 statt des einem Samples und glättet die Ränder des Schattens.

const float pof =1.0 /4096.0 *0.7;
vec4 shadow = step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol +vec2(pof,0)).r );
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,-pof/1.2) ).r );
shadow += step (length(lightvec)/15.0 -0.05, texture2D(Shadowmap, parabol+vec2(-pof/2.0,pof/1.2)).r );
light += shadow * 0.33 * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));

Durch 9x Oversampling lässt sich schon eine sehr gute Qualität ereichen, sorgt aber bei 8 Lichtquellen durch die 72 Texturelookups für einen Zusammenbruch der Framerate. Wichtig ist auch, dass man beachtet, dass die dualparaboloiden Maps an Ihren Ränden undefiniert sind und so zu Artefakten kommen kann wenn man außerhalb des gültigem Bereichs sampled. Eine Möglichkeit wäre einen Bereich zu erfassen, der etwas größer als 180 Grad ist, um am Rand zusätzliche Texel zu schaffen.

Ein zusätzlicher Renderpass

Nach dem die Tiefenmap gerendert würde, wäre es möglich in einem weiterem Framebufferobjekt eine zusätzliche Map zu rendern, in der mit Hilfe einer Kantenerkennung die Helligkeit der Softshadows vorberechnet wird. Auch hier muss berücksichtigt werden, dass die dualparaboloiden Maps einen Übergang haben.

Dynamische Lichter in statische Lightmaps Rendern

Wenn sich Lichtquellen nicht relativ zur Umgebung nicht bewegen, ist es möglich sie in eine Lightmap zu rendern. Unter der Annahme, dass Lightmapkoordinaten vorhanden sind (ohne geht es echt nicht gut), müssen diese nur per Vertexshader an den Pixelshader weitergegeben werden. Im Pixelshader kann dann wie im final.frag die Helligkeit berechnet werden und in der Lightmap abgespeichert werden. Es ist immer noch möglich, das dynamische Objekte einen Schatten werfen: Es werden nur noch die beweglichen Objekte in die Tiefenmap gerendert. Im Schatten wird dann einfach das Licht der im Schatten liegenden Lichtquelle subtraiert.

Statische Anteile in den Schattenmaps

Es ist möglich mit einer zusätzlichen Matrix die Tiefenmaps zu den Weltkoordinaten auszurichten. Statische Anteile können dann aus einer zweiten Tiefenmap kopiert werden. Ein wenig problematisch ist das Entfernen der Rotation aus der gl_ModelViewMatrix. Das größte Problem ist, das die Positionen der Lichtquellen bereits mit der gl_ModelViewMatrix multipliziert sind. Die einfachste Lösung ist, aus den den Rotationswinkeln eine neue Matrix zu erechnen, mit dessen Hilfe die Schattenvektoren für die Texturkoordinatenberechnung zurückgedreht werden. Diese Matrix sollte dann als Uniformvariable übergeben werden. Wenn die Schatten durch einen zusätzlichen Renderdurchgang gefiltert werden. Ist es sinnvoll die Maps hier erst zu vereinen. Von beiden Tiefenmaps muss nur immer der kleinere Wert genommen werden.