GLSL Licht und Schatten: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Statische Anteile in den Schattenmaps)
(Rechtschreibung (fast schlimmer als effe :o) ) und C -> Pascal (zu überprüfen, daher invisible))
Zeile 3: Zeile 3:
 
==Vorwort==
 
==Vorwort==
  
Willkommen zu meinem erstem Tutorial. Schatten können sowohl Traum als auch Albtraum eines jeden OpenGLprogramieres 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-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.
 
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 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:
  
Die komplette Berechnung kann von der Grakfikkarte übernommen werden
+
*Die komplette Berechnung kann von der Grakfikkarte ü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 eine zusätzliche Projektionstechnik zeigen, die über den Vertexshader realisiert wird: Das parabolide Mapping. Es 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 Vertexpipline um den Faktor 3 entlastet.
+
Um den Rechenaufwand zu veringern werde ich hier eine zusätzliche Projektionstechnik zeigen, die über den Vertexshader realisiert wird: Das parabolide Mapping. Es 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 Vertexpipeline um den Faktor 3 entlastet.
  
==Vorkentnisse==
+
==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 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.
  
Für das Erstellen von dual paraboliden Tiefentexturen ist noch das Rendern in Framebufferobjekten Voraussetztung. (zu denen es leider noch kein Tutorial gibt.)
+
Für das Erstellen von dual paraboliden Tiefentexturen ist noch das Rendern in Framebufferobjekten Vorraussetzung (zu denen es leider noch kein Tutorial gibt).
  
Ich empfehle auch die zu Renderden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird.
+
Ich empfehle auch die zu rendernden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird.
  
 
==Grundlagen==
 
==Grundlagen==
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 verzerten Geraden auf. Der entscheidene Vorteil an einer Fisheyeaufnahme ist, dass ein Öffnungswinkel von 180Grad erfasst werden kann. Zwei entgegensetzte Aufnahmen können so problemlos den kompletten Raum um die Kammera 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 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.
  
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älig, 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 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.
  
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 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.
  
  
 
==Hauptprogramm==
 
==Hauptprogramm==
  
Achtung hier ist C code stat Pascal geschrieben. Minimale Stolpersteine könnten vorhanden sein...
+
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 werdern.
 
So könnte die Initialsierung des FBOs aussehen:
 
So könnte die Initialsierung des FBOs aussehen:
<cpp>
+
<cpp>GLuint shadow_map = 0; //  
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 sein als die maximale Texturgröße sein
  
 
glGenTextures (1, &shadow_map); //In Pascal das & durch ein @ ersetzten
 
glGenTextures (1, &shadow_map); //In Pascal das & durch ein @ ersetzten
Zeile 50: Zeile 47:
 
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.
+
GLenum status = glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.</cpp>
</cpp>
+
<!-- ungetesteter Pascalcode
 +
<pascal>var shadow_map,
 +
    shadow_fbo,
 +
    shadow_size : GLuint;
 +
    status      : GLenum;
 +
begin
 +
  shadow_map  := 0; 
 +
  shadow_fbo  := 0;    // the shadow texture
 +
  shadow_size := 4096; // muss kleiner sein als die maximale Texturgröße sein
  
Jetzt Sollten die Shader und weiter Texturen geladen werden.
+
  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, NULL);
 +
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  
Nach diesem Prinzip muss dann in der Haupschleife jeder Rendervorgang abgearbeitet werden der Code dürfte in Pascal und C sehr ähnlich sein.
+
  glGenFramebuffersEXT(1, @shadow_fbo);
<cpp>
+
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, shadow_fbo);
//Shader für Dualparabolische Mpas aktivieren
+
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, shadow_map, 0);
 +
  glDrawBuffe (GL_FALSE);
 +
  glReadBuffer(GL_FALSE);
 +
  status := glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); //noch ein wenig Code anfügen um den Status zu überprüfen.</pascal>
 +
-->
 +
Jetzt sollten die Shader und weiter Texturen geladen werden.
 +
 
 +
Nach diesem Prinzip muss dann in der Haupschleife jeder Rendervorgang abgearbeitet werden.
 +
<cpp>//Shader für Dualparabolische Mpas aktivieren
 
glUseProgramObjectARB(shadow);
 
glUseProgramObjectARB(shadow);
  
Zeile 85: Zeile 101:
 
//Frame rendern
 
//Frame rendern
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
render();
+
render();</cpp>
</cpp>
+
<!--ungetesteter Pascalcode
 +
<pascal>
 +
//Shader für Dualparabolische Mpas aktivieren
 +
glUseProgramObjectARB(shadow);
 +
 
 +
//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();</pascal>
 +
-->
  
 
==Die Shader==
 
==Die Shader==
Zeile 92: Zeile 140:
 
===Shader zum Rendern der Schattenmaps===
 
===Shader zum Rendern der Schattenmaps===
 
parabol.vert
 
parabol.vert
<cpp>
+
<cpp> uniform int renderpass;
  uniform int renderpass;
 
 
   uniform int light;
 
   uniform int light;
 
   varying vec3 normal;
 
   varying vec3 normal;
Zeile 116: Zeile 163:
 
}
 
}
 
pos=gl_Position.xyz;
 
pos=gl_Position.xyz;
}
+
}</cpp>
</cpp>
 
  
 
parabol.frag
 
parabol.frag
<cpp>
+
<cpp>varying vec3 pos;
varying vec3 pos;
 
 
void main(void)
 
void main(void)
 
{
 
{
 
if (length(pos.xy)>1.005)discard;
 
if (length(pos.xy)>1.005)discard;
 
gl_FragDepth  =  pos.z;
 
gl_FragDepth  =  pos.z;
}
+
}</cpp>
</cpp>
 
  
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 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.
  
Stat 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 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.
Die Division durch -L entspricht weitgehend einer Normalsierung, speigelt 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 2 Folgenden Zeilen wird dieser wird der Vektor parabolisch auf die Bildschirmkoordinaten projeziert (Interwalle von -1.0 bis 1.0) . Mit hilfe der folgenden if abfrage werden alle Vertices hinter der Kammera geclipt. Nur sichtbare Verices bekommen gültige z Werte für den Zbugffer im Interwall von 0.0 bis 1.0. Da die komplette Projektionsmatrix von OpenGL irgnoriert wird, sind die Werte in gl_FragCoord nicht brauchbar und enthalten Teilweise Werteaus der festen Pipeline. Damit die eigenen Werte zuverlässig an den Fragmentshader übergeben wird die varying vec3 pos benutzt.
+
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 0.0 bis 1.0. Da die komplette Projektionsmatrix von OpenGL irgnoriert wird, sind die Werte in gl_FragCoord nicht brauchbar und enthalten teilweise Werte aus der festen Pipeline. Damit die eigenen Werte zuverlässig an den Fragmentshader übergeben wird die varying vec3 pos benutzt.
  
Der Fragmentshader ist extrem einfach, da wir Aufgrund des nicht vorhandem Colorbuffers keinen Frabwert benötigen lassen wir diesen einfach undeffiniert.
+
Der Fragmentshader ist extrem einfach, da wir aufgrund des nicht vorhandem Colorbuffers keinen Farbwert benötigen lassen wir diesen einfach undefiniert.
 
Zudem verlassen wir den Shader mit discard,  wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die nachfolgende Zeile ersetzt den für uns inkorrekten Z-Bufferwert aus der festen Pipeline. Dadurch können wir in späteren Shadern die Tiefenmap wie eine normale Textur auslesen.
 
Zudem verlassen wir den Shader mit discard,  wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die nachfolgende Zeile ersetzt den für uns inkorrekten Z-Bufferwert aus der festen Pipeline. Dadurch können wir in späteren Shadern die Tiefenmap wie eine normale Textur auslesen.
  
Zeile 141: Zeile 185:
  
 
final.vert
 
final.vert
 
+
<cpp>varying vec3 Normal;
<cpp>
 
varying vec3 Normal;
 
 
varying vec3 ModelVertex;
 
varying vec3 ModelVertex;
  
Zeile 151: Zeile 193:
 
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 
gl_TexCoord[0] = gl_MultiTexCoord0;
 
gl_TexCoord[0] = gl_MultiTexCoord0;
  }
+
  }</cpp>
</cpp>
 
  
 
final.frag
 
final.frag
<cpp>
+
<cpp>varying vec3 Normal;
varying vec3 Normal;
 
 
varying vec3 ModelVertex;
 
varying vec3 ModelVertex;
  
Zeile 185: Zeile 225:
 
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>
</cpp>
 
  
Diese Shader besitzen 3 Uniformvariablen, die umbedingt 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 OpenGLstatus der Lichquellen berücksichtigt und nur die Daten der Ersten bis MaxLights hohlt.
+
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.
Wärend bei den Schattenmaps die meiste Arbeit im Vertexshader erledigt werden könnte, ist dieser sehr Fragmentshaderlastig. 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.
  
Zeile 196: Zeile 235:
  
 
===Schatten durch Alphatest===
 
===Schatten durch Alphatest===
Der bisherige Schattenshader kann nur ganze Poligone einen Schatten werfen lassen. Wenn shadow.vert so ergänst wird, das die Texturkoordinaten in den Fragmentshader  weitergereich werden, dann es es mit folgendem Fragmentshader möglich Shatten ini 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 Shatten in Abhängikeit einer Alphatextur zu rendern:
 +
 
 
shadow.frag
 
shadow.frag
<cpp>
+
<cpp>varying vec3 pos;
varying vec3 pos;
 
 
uniform sampler2D Texture0;
 
uniform sampler2D Texture0;
 
void main(void)
 
void main(void)
Zeile 206: Zeile 245:
 
         if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
 
         if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
 
         gl_FragDepth  =  pos.z;
 
         gl_FragDepth  =  pos.z;
}
+
}</cpp>
</cpp>
+
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 wesendlich 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.
 
  
 
===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 heraussakiert werden, so das Öffnungs Winkel von 0 Grad 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 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.
Das verfkleinern des Öffnungswinkels für zusätzliche Spotlicher 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.
+
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===
 
===Texturelookups vermeiden===
Wenn das Kreutzprodukt zwischen normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und kann komplett übersprungen werden. Diverse Multiplikationen und Texturelookups könne so überspungen werden. Besonders Algoritmen wie Selbstschattierende Bumpmaps, können so beschleunigt werden. Realistisch betrachtet sind es meistens deulich weniger weniger als 50% lichquellenabgewante Fragmente.
+
Wenn das Kreutzprodukt zwischen normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und kann komplett übersprungen werden. Diverse Multiplikationen und Texturelookups könne so übersprungen werden. Besonders Algorithmen wie selbstschattierende Bumpmaps, können so beschleunigt werden. Realistisch betrachtet sind es meistens deulich weniger weniger als 50% lichtquellenabgewante Fragmente.
  
Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Kreutsprodukt von vor der if Abfrage aber umbedingt wiederverwerten!)  
+
Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Kreutzprodukt von vor der if Abfrage aber umbedingt wiederverwerten!)  
  
 
   light += gl_LightSource[0].diffuse * max(dot(normalvec, -lightdir), 0.0);
 
   light += gl_LightSource[0].diffuse * max(dot(normalvec, -lightdir), 0.0);
Zeile 224: Zeile 262:
  
 
===Reichweite der Lichtquellen===  
 
===Reichweite der Lichtquellen===  
Leider bietet QpenGL 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.
+
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===
 
===Oversampling der Schattenmap===
Die Qualität lässt sich dadurch mehere Samples auf der Schattenmap verbessern. Das folgende Codefragment nimmt 3 stat 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;
+
<cpp>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));
+
light += shadow * 0.33 * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));</cpp>
</cpp>
+
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 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 erfassten 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 dualparaboliden Maps einen Übergang haben.
  
 
===Dynamische Lichter in statische Lightmaps Rendern===
 
===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 Lightmapcoordinaten 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.
+
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.
 
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===
 
===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 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.
Wenn die Schatten durch einen zusätzlichen Renderdurchgang gefiltert werden. Ist es sinvoll die Maps erst hier zu vereinen. Von beiden Tiefen Maps muss nur immer der kleinere Wert genommen 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.
  
 
==Todo==
 
==Todo==

Version vom 23. Juni 2006, 17:52 Uhr

Hinweis: Dieser Artikel wird gerade Offline bearbeitet!

Bitte haben Sie etwas Geduld und nehmen Sie keine Änderungen vor, bis der Artikel hochgeladen wurde.

(weitere Artikel)
WIP Offline.jpg

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. 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:

  • Die komplette Berechnung kann von der Grakfikkarte ü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 veringern werde ich hier eine zusätzliche Projektionstechnik zeigen, die über den Vertexshader realisiert wird: Das parabolide Mapping. Es 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 Vertexpipeline um den Faktor 3 entlastet.

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.

Für das Erstellen von dual paraboliden Tiefentexturen ist noch das Rendern in Framebufferobjekten Vorraussetzung (zu denen es leider noch kein Tutorial gibt).

Ich empfehle auch die zu rendernden Daten als Vertexbufferobjekte zu übergeben, da sonst die Mehrfachverwendung der Daten zu einer extremen Bremse wird.

Grundlagen

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.

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.

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.


Hauptprogramm

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 werdern. So könnte die Initialsierung des FBOs aussehen:

GLuint shadow_map = 0; // 
GLuint shadow_fbo = 0; // the shadow texture
GLuint shadow_size = 4096; //muss kleiner sein als die maximale 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.

Jetzt sollten die Shader und weiter Texturen geladen werden.

Nach diesem Prinzip muss dann in der Haupschleife jeder Rendervorgang abgearbeitet werden.

//Shader für Dualparabolische Mpas aktivieren
glUseProgramObjectARB(shadow);

//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();

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;
		gl_Position.w = 1.0;
		}
	else{
		gl_Position.z = -1.0;
		gl_Position.w = -1.0;
		}
	pos=gl_Position.xyz;
	}

parabol.frag

varying vec3 pos;
void main(void)
{
	if (length(pos.xy)>1.005)discard;
	gl_FragDepth  =  pos.z;
}

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.

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. 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 0.0 bis 1.0. Da die komplette Projektionsmatrix von OpenGL irgnoriert wird, sind die Werte in gl_FragCoord nicht brauchbar und enthalten teilweise Werte aus der festen Pipeline. Damit die eigenen Werte zuverlässig an den Fragmentshader übergeben wird die varying vec3 pos benutzt.

Der Fragmentshader ist extrem einfach, da wir aufgrund des nicht vorhandem Colorbuffers keinen Farbwert benötigen lassen wir diesen einfach undefiniert. Zudem verlassen wir den Shader mit discard, wenn der der Pixel außerhalb der aktuellen paraboliden Map liegt. Die nachfolgende Zeile ersetzt den für uns inkorrekten Z-Bufferwert aus der festen Pipeline. Dadurch können wir in späteren Shadern die Tiefenmap wie eine normale Textur auslesen.

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 funktinieren, 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 ;
	}

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. 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.

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:

shadow.frag

varying vec3 pos;
uniform sampler2D Texture0;
void main(void)
{
        if (length(pos.xy)>1.005)discard;
        if (texture2D(Texture0,vec2(gl_TexCoord[0])).a < 0.5) discard;
        gl_FragDepth  =  pos.z;
}

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.

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. 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 Kreutzprodukt zwischen normalvec und -lightdir negativ ist, dann ist die Oberfläche von der Lichtquelle abgewandt und kann komplett übersprungen werden. Diverse Multiplikationen und Texturelookups könne so übersprungen werden. Besonders Algorithmen wie selbstschattierende Bumpmaps, können so beschleunigt werden. Realistisch betrachtet sind es meistens deulich weniger weniger als 50% lichtquellenabgewante Fragmente.

Für die Beleuchtung sollte dann allerdings auch folgende Zeile verwendet werden. (Das Kreutzprodukt 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 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.

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.

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 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. 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.

Todo

Bilder Grafik für Parabilisches Mapping