GLSL Licht und Schatten: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Die Shader)
(Shader für den finalen Renderdurchgang)
Zeile 145: Zeile 145:
 
parabol.t += 0.25;
 
parabol.t += 0.25;
 
}
 
}
parabol -=  lightdir.xy * 0.125/ (abs (lightdir.z) + 1.0); // parabolische Projektion für subtextur
+
                // parabolische Projektion für subtextur
vec4 shadow =   step (length(lightvec)/15.0 +0.005,shadow2D(Shadowmap, vec3(parabol,1.0)));
+
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));
 
light += shadow * gl_LightSource[LightNum].diffuse * abs(dot(normalvec, lightdir));
 
}
 
}

Version vom 4. Juni 2006, 16:51 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 OpenGlprogramieres werden. Im gegensatz zur einfachen Beleuchtung mit Lichtquellen, steigt der Rechenaufwand 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 Vertexpipline um den Faktor 3 entlastet.

Vorkentnisse

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

Ich empfehle auch die zu Renderden 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 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.

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.

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.


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

Die Hauptschleife wird weiter unten erklärt...

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.

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

Der Fragmentshader ist extrem einfach, da wir Aufgrund des nicht vorhandem Colorbuffers keinen Frabwert benötigen lassen wir diesen einfach undeffiniert. 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 Depthmap 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 ;
	}