Tutorial Wassereffekt

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Einleitung

Erstmal willkommen zu meinem zweiten Tutorial. Ich möchte Euch hier einen Einblick in die „hohe Kunst“ der Wassereffekte geben. Hierbei wird erst einmal nur ein relativ stehendes Gewässer, also keines, welches in einer Rinne fließt beschrieben. Auch werde ich hier nicht auf die Technik für Wasser an Abhängen (abwärts fließende Flüsse) oder Wasserfälle eingehen. Hier geht es schlicht und einfach um eine Wasserebene, wie man sie zum Beispiel für ein Meer verwenden könnte.

Voraussetzung für dieses Tutorial ist eigentlich nur ein Delphi/FP-Kompiler, einige grundlegende Kenntnisse in OpenGL und in GLSL. Falls letztere fehlen, kann ich nur die beiden Tutorials zum Thema GLSL hier im Wiki empfehlen: Tutorial GLSL und Tutorial GLSL 2.

Noch etwas, dass man wissen sollte ist, dass die Kamera in die GL_PROJECTION_MATRIX geschrieben wird. Dies wird später für die Shader wichtig. Das ist kaum mehr Arbeit mit großer Wirkung, da sich dieses Tutorial an fortgeschrittene Benutzer richtet, werde ich hier allerdings nicht näher drauf eingehen. Durch diese Trennung haben wir dann den Vorteil, dass die eigene Rotation der Modelle von den Operationen der Kamera getrennt ist. Das wird wie erwähnt bei den Shadern wichtig.

In dem Sinne: glBegin(GL_WATER_TUTORIAL) ;-)


Die Wasserebene

Das Erste, das wir brauchen ist eine Ebene - am besten Quads - von mir aus auch zwei Triangles. Wichtig ist, dass es eine Ebene ist. Es darf keine Höhenunterschiede geben. Die Z-Achse sollte hierbei die Höhe darstellen, X und Y dienen als... als X und Y eben ;) Natürlich fehlt noch so ziemlich alles. Diese Ebene allein macht nicht einen Wassereffekt aus. Jetzt brauchen wir erst einmal ein paar Texturen. Diese sollten der "Power of Two"-Regel gemäß und quadratisch sein. Dabei muss man beachten, dass sie keinesfalls größer als der maximal beschreibbare Viewport sein sollten, weil die Texturen komplett gefüllt werden müssen. Der Grund dafür ist, dass man nicht über den Rand des Framebuffers (des Fensters) zeichnen kann, zuminest nicht ohne Erweiterungen, also muss die Textur kleiner oder genauso groß wie der Framebuffer sein. Diese Texturen sollten bereits zu Anfang des Programms erstellt und initialisiert werden. Genau genommen brauchen wir zwei Stück. Jeweils eine für die Reflexions- und Refraktionsmap. Wenn wir ohne Shader arbeiten, sollten sie nur als RGB initialisiert werden. Wenn wir später den Shader zuschalten, brauchen wir auch den Alphakanal. Die Größe sollte sich am originalen Viewport ausrichten. Die Texturen lassen sich übrigens am Besten mit glCopyTexImage2D initialisieren, da man dort keinen Puffer mit Pixeldaten braucht, den man übergeben muss.

Texturen füllen

Jetzt geht es darum, wie wir die Szene in die Texturen und dann auf die Wasserebene bekommen. Hier kann ich einen Trick empfehlen, den ich aus einer Demo von Delphi3d.net habe- aber dazu gleich noch mehr.

Wichtig ist vor allem, das wir uns erst einmal Gedanken machen, was die Texturen eigentlich beinhalten sollen. Refract sollte - wie der Name bereits sagt - die Refraktionsinformationen, also alles, was unter dem Wasser ist, darstellen, Reflect, daher das, was über dem Wasser ist. Damit keine Darstellungsfehler entstehen, wenn die Kamera über dem Wasser ist, müssen wir beim Erstellen der Texturen die Parameter GL_TEXTURE_WRAP_S und GL_TEXTURE_WRAP_T auf GL_CLAMP_TO_EDGE setzen. Wer schon etwas häufiger mit OpenGL gearbeitet hat, der kann sich vielleicht schon denken, wie man die Texturen am Besten füllen kann: natürlich mit ClipPlanes. Diese praktischen Dinger sorgen dafür, dass alles, was über bzw. unter ihnen liegt, abgeschnitten und nicht gezeichnet wird. Am besten für die Performance wäre es natürlich, wenn wir in der Anwendung noch einmal prüfen, ob das Objekt nicht vielleicht ganz unter/über Wasser ist und man es deshalb in diesem Renderpass weglassen könnte, aber das würde den Rahmen des Tutorials sprengen. Außerdem müssen wir dann noch entsprechend der aktuell zu füllenden Textur die Z-Achse invertieren je nachdem , ob wir den Teil über oder den Teil unter der Wasseroberfläche rendern wollen.

procedure DoRenderPass(InvertZ: Boolean);
const
  // Deklaration der ClipPlane
  CP: array [0..3] of Double = (0.0, 0.0, 1.0, 0.0);
begin
  glEnable(GL_CLIP_PLANE0);
  glDisable(GL_CULL_FACE);

  glLoadIdentity;
  glClipPlane(GL_CLIP_PLANE0, @CP);
  
  if InvertZ then
    glScalef(1, 1, -1);
  RenderGameScene;
  
  glDisable(GL_CLIP_PLANE0);
  glEnable(GL_CULL_FACE);
end;

Das sollte soweit klar sein. Jetzt zu dem eigentlichen Inhalt von RenderWaterMap.

Dort muss zunächst einmal der komplette Viewport geleert werden. Danach sollte der Viewport auf die Größe von WaterTexSize verkleinert werden, damit das Gerenderte letztendlich auch auf die Texturen passt. Jetzt kommen wir langsam zu dem Trick, den ich weiter oben erwähnte: Erst einmal muss die Texturmatrix auf die Identitätsmatrix zurückgesetzt werden. Weiter geht es mit dem ersten Renderpass für die Reflexion. Das Ergebnis sollte in die Reflect-Textur gespeichert werden. Danach die Szene noch einmal rendern (nicht vergessen, vorher die Textur wieder zu unbinden), diesmal jedoch ohne Invertierung der Z-Achse. Das kommt dann logischerweise in die Refract-Textur, weil die ClipPlane hier alles unter der Wasseroberfläche da lässt und den Rest abschneidet. Zu guter Vorletzt den Viewport wieder auf den Zustand zurücksetzen, auf dem er vor dem Rendern war und nochmal den Framebuffer leeren.

Jetzt wird es interessant: In der Texturmatrix müssen folgende Operationen durchgeführt werden: Verschieben um (0.5, 0.5, 0.0) und skalieren um (0.5, 0.5, 0.0). Dann eine Perspektive mit dem Seitenverhältnis, welches wir auch in unserer „echten“ Szene haben erzeugen und danach die Kameraoperationen durchführen.

Und jetzt der Code dazu:

  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

  glViewport(0, 0, WaterTexSize, WaterTexSize);

  glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
  glMatrixMode(GL_MODELVIEW);

  DoRenderPass(True);

  glBindTexture(GL_TEXTURE_2D, Reflect);
  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, WaterTexSize, WaterTexSize);
  glBindTexture(GL_TEXTURE_2D, 0);

  if UseWaterShader then
    glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

  DoRenderPass(False);

  glBindTexture(GL_TEXTURE_2D, Refract);
  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, WaterTexSize, WaterTexSize);
  glBindTexture(GL_TEXTURE_2D, 0);

  // Hier bitte die eigenen Werte einsetzen
  glViewport(0, 0, Settings.ScreenWidth, Settings.ScreenHeight); 
  
  glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT);

  glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
    glTranslatef(0.5, 0.5, 0);
    glScalef(0.5, 0.5, 0);
    
    gluPerspective(PERSPECTIVE_FOV, Settings.ScreenWidth/Settings.ScreenHeight, 0.01, 100000);
    DoCamTranslate;
    DoCamRotate;
  glMatrixMode(GL_MODELVIEW);
  
  glBindTexture(GL_TEXTURE_2D, 0);

Wasser marsch

Jetzt zu der Wasserebene. Sicher kann man sich denken, dass aus dem Weiß noch mehr wird. Jetzt kommen wir zu der Sache mit der Texturmatrix. Die ist dank unserer Routine jetzt so aufgebaut, dass die übergebenen Texturkoordinaten wie in der normalen Projektionsmatrix verarbeitet werden. Das ermöglicht es als Texturkoordinaten genau die Werte zu übergeben, die auch glVertex3f bekommt. Das ist eine große Erleichterung, was vor allem nerviges hin- und hergerechne erspart. Da wir uns erst einmal mit dem non-shader-way befassen wollen, sieht unser Code für die Wasserebene jetzt so aus:

glEnable(GL_TEXTURE_2D);

glColor4f(1.0, 1.0, 1.0, 1.0);
glBindTexture(GL_TEXTURE_2D, Reflect);

glDepthMask(GL_FALSE);
glBegin(GL_QUADS);
  glTexCoord3f(-100.0, -100.0, 0.0);
  glVertex3f(-100.0, -100.0, 0.0);
  glTexCoord3f(-100.0, 100.0, 0.0);
  glVertex3f(-100.0, 100.0, 0.0);
  glTexCoord3f(100.0, 100.0, 0.0);
  glVertex3f(100.0, 100.0, 0.0);
  glTexCoord3f(100.0, -100.0, 0.0);
  glVertex3f(100.0, -100.0, 0.0);
glEnd;

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

glDepthMask(GL_TRUE);
glBindTexture(GL_TEXTURE_2D, Refract);
glColor4f(1.0, 1.0, 1.0, RefractAlpha);
glBegin(GL_QUADS);
  glTexCoord3f(-100.0, -100.0, 0.0);
  glVertex3f(-100.0, -100.0, 0.0);
  glTexCoord3f(-100.0, 100.0, 0.0);
  glVertex3f(-100.0, 100.0, 0.0);
  glTexCoord3f(100.0, 100.0, 0.0);
  glVertex3f(100.0, 100.0, 0.0);
  glTexCoord3f(100.0, -100.0, 0.0);
  glVertex3f(100.0, -100.0, 0.0);
glEnd;

glBindTexture(GL_TEXTURE_2D, 0);

glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
Die Texturkoordinaten sollten ja jetzt klar sein. Zuerst wird die Reflexionstextur gerendert, danach folgt die Refraktionstextur. Dabei wird erstere mit vollem Alpha gerendert und die andere darüber geblendet. Bei den Shadern werden beide Texturen gebunden und an den Shadern übergeben, dazu jedoch gleich mehr.
Die einfache Wasserebene ohne Wellen (von littleDave)
Rechts könnt ihr euch von eurem Ergebnis überzeugen.

Auf zu den Shadern

Für die Shader müssen wir eigentlich kaum Änderungen vornehmen, zumindest solange wir beim simplen Einbauen von Wellen bleiben. Gleich werde ich noch auf eine Technik eingehen, die nervige Kanten an Grenzen zu Objekten vermeidet. Die Änderungen betreffen zunächst allein den Code, der die Ebenen selber rendert. Außerdem brauchen wir noch eine weitere Textur, die die Bumpmap der Wasseroberfläche darstellt und für die Wellen verwendet wird. Jetzt werden im Rendercode für die Wasserebene noch einige Änderungen durchgeführt. Zunächst brauchen wir nur noch ein Quad, da der Shader das Überblenden übernimmt. Vor dem Rendern des Quads sollte der Shader gebunden werden, sowie die drei Texturen in drei Textureinheiten geschoben werden, in der Reihenfolge Reflect-Refract-WaterBumpmap. Dann muss noch ein Haufen Parameter an den Shader übergeben werden. Erstmal der Standardkram, die Zuweisung der Texturen. Der Uniform-Integer refractTex wird 1, reflectTex wird 0 und bumpMap wird 2. Dann gibt es noch einige Variablen, die die Darstellung und die Stärke des Wassereffektes beeinflussen: reflectRatio und refractRatio sind ein Faktor, mit dem die Bumpmap multipliziert wird und der die Stärke des Effektes beeinflusst. Ich empfehle für beides einen Wert um 1/20.0. Dann gibt es noch mappingRatio und extendedBlending, die erkläre ich später beim Shader selbst.

Da wir jetzt zwei verschiedene Texturarten verwenden, einmal die projizierten Reflect- und Refracttexturen und die BumpMaptexturen, sollten wir auch noch eine zweite Texturkoordinate übergeben, und zwar für die Bumpmaps. Welchen Wert Ihr hier einsetzt, ist Eure Sache.

Jetzt zu dem Shader, dem wir die Werte übergeben wollen. Erst einmal ein kurzes Brainstorming. Was brauchen wir? Zunächst müssen die Texturen entsprechend der Matrizen transformiert werden. Danach müssen sie wiederum entsprechend der Bumpmaps verschoben werden, so dass ein Welleneffekt entsteht. Dies alles passiert im Fragmentshader. Der Vertexshader dagegen sieht recht harmlos aus:

varying mat4 texMat;

void main(void)
{
  gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
  gl_TexCoord[0]  = gl_MultiTexCoord0;
  texMat = gl_TextureMatrix[0];
  gl_TexCoord[1]  = gl_MultiTexCoord1;
  gl_FrontColor   = gl_Color;
}

Was im Vertexshader passiert, sollte wohl ziemlich klar sein. In Zeile 5 wird erst einmal der Vertex anhand der normalen Matrizen projiziert, so dass es die gleiche Wirkung hat, als wenn man es durch die normale Renderpipe jagen würde. Zeilen Sechs und Acht sind dafür zuständig, dass die Texturkoordinaten auch im Fragmentshader verwendet werden können. Zeile Sieben speichert die Texturmatrix in einer Varying, so dass wir sie ebenfalls im Fragmentshader verwenden können. Die neunte Zeile speichert noch die Farbe - eher unwichtig und man könnte es auch weglassen, aber ich fand es schön ;). Genug der Scherze, weiter mit dem Fragmentshader:

uniform sampler2D refractTex;
uniform sampler2D reflectTex;
uniform sampler2D bumpMap;

uniform float refractRatio;
uniform float reflectRatio;

uniform float mappingRatio;

uniform int extendedBlending;

varying mat4 texMat;

void main(void)
{
  vec4 refractcoord;
  vec4 reflectcoord;
  vec4 offsetColor = (texture2D(bumpMap, vec2(gl_TexCoord[1])) + 
    texture2D(bumpMap, vec2(gl_TexCoord[1]) * 4.0)) / 2.0;
  vec4 origOffset = offsetColor;
  vec4 color;
  vec4 reflectColor = vec4(1.0, 1.0, 1.0, 1.0);
  vec4 refractColor = vec4(1.0, 1.0, 1.0, 1.0);
  vec4 blendedColor;
  
  offsetColor -= 0.5;
  offsetColor *= 2.0;
  
  
  refractcoord = gl_TexCoord[0];
  refractcoord.x += offsetColor[0] * refractRatio;
  refractcoord.z += offsetColor[1] * refractRatio;
  refractcoord = texMat * refractcoord;
  refractColor = texture2DProj(refractTex, refractcoord); 

  reflectcoord = gl_TexCoord[0];
  reflectcoord.x += offsetColor[0] * reflectRatio;
  reflectcoord.z += offsetColor[1] * reflectRatio;
  reflectcoord = texMat * reflectcoord;
  reflectColor = texture2DProj(reflectTex, reflectcoord); 

  reflectColor[3] = 1.0;
  refractColor[3] = 1.0;
  
  if (extendedBlending == 0)
  {
    float mappingRefract, mappingReflect;
    mappingRefract = mappingRatio * 255.0;
    mappingReflect = 255.0 - mappingRefract;
    mappingRefract /= 255.0;
    mappingReflect /= 255.0;
    blendedColor = refractColor * mappingRefract + reflectColor * mappingReflect;
    blendedColor.a = 1.0;
  }
  else
  {
    float Alpha, reflectAlpha, refractAlpha;
    Alpha = (refractColor.r + refractColor.g + refractColor.b) / 3.0;
    if (Alpha > 1.0)
      Alpha = 1.0;
    if (Alpha < 0.0)
      Alpha = 0.0;
      
    refractAlpha = Alpha * 255.0;
    reflectAlpha = 255.0 - refractAlpha;
    refractAlpha /= 255.0;
    reflectAlpha /= 255.0;
    blendedColor = reflectColor * reflectAlpha + refractColor * refractAlpha;
  }
  
  gl_FragColor = blendedColor;
}

Das ist recht viel Quelltext auf einmal. Aber wir schauen ihn uns nun stückchenweise an. Die ersten Zeilen deklarieren die verwendeten Uniforms. Dann wird noch das texMat-Varying aus dem Vertexshader übernommen. Wie wir bereits vorhin gelernt haben, ist die Texturmatrix unerlässlich, um die Textur einfacher auf die Ebene zu projizieren. Im Hauptcode haben wir dann einen ganzen Haufen Variablendeklarationen. Hervor sticht offsetColor, für das zwei Texturzugriffe verwendet werden, um die Farbe aus der Bumpmap zu lesen. Zwei Zugriffe deshalb, um das Wasser ein wenig detaillierter zu machen. Danach wird offsetColor skaliert: Eine normale Bitmap hat nun einmal nicht die Möglichkeit, negative Werte zu speichern, aber es würde ohne diese sehr eintönig aussehen. Danach wird das Offset für Reflexion und Refraktion berechnet. Das läuft bei beiden ähnlich ab: Die Texturkoordinate wird mit der Matrix multipliziert und erst einmal in einer Variable gespeichert, dann wird der Wert aus der Map gelesen und das Alpha auf 1.0 gesetzt.

Der zweite Schritt: Die Shader-Wellen sind deutlich sichtbar (von littleDave)
Weiter geht es mit dem Schalter zwischen erweitertem Blending und normalem Blending. Das erweiterte Blending versucht eine Wasseroberfläche von der Lichtdurchlässigkeit möglichst realistisch darzustellen, indem die Sichtbarkeit der Objekte unterhalb der Oberfläche, also dem Teil in der Refraktionsmap, der Helligkeit der Reflexion angepasst: Wenn man mit einer Lampe auf eine Wasseroberfläche leuchtet, kann man schlechter durch die von der Lampe getroffene Stelle blicken, weil die Reflexion der Lampe das, was darunter liegt, überstrahlt. Zu guter Letzt wird im Shader noch die berechnete Farbe an OpenGL zurückgegeben, sodass sie dann in den Framebuffer geschrieben werden kann. mappingRatio legt übrigens das Verhältnis zwischen Reflexion und Refraktion fest, falls erweitertes Blending deaktiviert ist (also extendedBlending = 0). Ein Wert von 1.0 würde bedeuten, dass nur die Refraktionsmap gerendert wird, ein Blending von 0.0 bedeutet, dass nur die Reflexionsmap durchkommt. Diese Multiplikationen da oben mache ich übrigens, um ein Problem mit der Genauigkeit des Floats im Shader zu umgehen. Rechts nochmal ein Bild zum aktuellen Status. Da erkennt man schon deutlich die Wellen und, wenn man genauer hinschaut, erkennt man auch eine unschöne Linie beim Übergang von der Wasserebene zum Objekt.

Feintuning

Wenn man jetzt einfach einen Würfel in die Wasserfläche setzt, der auf der Vorder- und der Rückseite verschiedene Farben hat, dann wird sichtbar, dass durch die Wellen unschöne Kanten entstehen, weil die Refraktions-/Reflexionsmap keine Objektgrenzen kennt. Doch dafür gibt es eine simple aber dennoch wirkungsvolle Methode, die ich jetzt beschreibe. Hierzu müssen wir den Code für das Rendern der Texturen leicht anpassen:

  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
  glViewport(0, 0, WaterTexSize, WaterTexSize);

  glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
  glMatrixMode(GL_MODELVIEW);

  DoRenderPass(True);

  if UseDepthShader then
  begin
    glColorMask(False, False, False, True);
    glClear(GL_DEPTH_BUFFER_BIT);

    glUseProgramObjectARB(DepthShader);
    glUniform1fARB(glGetUniformLocationARB(DepthShader, 'waterplaneZ'), 0.0);
    DoRenderPass(True);
    glUseProgramObjectARB(0);
    glColorMask(True, True, True, True);
  end;

  glBindTexture(GL_TEXTURE_2D, Reflect);
  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, WaterTexSize, WaterTexSize);
  glBindTexture(GL_TEXTURE_2D, 0);

  if UseWaterShader then
    glClearColor(0.0, 0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

  if UseDepthShader then
  begin
    glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
    glColorMask(True, True, True, True);

    glUseProgramObjectARB(DepthShader);
    DoRenderPass(False);
    glUseProgramObjectARB(0);
    
    glColorMask(True, True, True, False);
    glClear(GL_DEPTH_BUFFER_BIT);
  end;

  DoRenderPass(False);

  glColorMask(True, True, True, True);   

  glBindTexture(GL_TEXTURE_2D, Refract);
  glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, WaterTexSize, WaterTexSize);
  glBindTexture(GL_TEXTURE_2D, 0);

  // Hier bitte die eigenen Werte einsetzen
  glViewport(0, 0, Settings.ScreenWidth, Settings.ScreenHeight); 
  
  glClear(GL_DEPTH_BUFFER_BIT or GL_COLOR_BUFFER_BIT);

  glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
    glTranslatef(0.5, 0.5, 0);
    glScalef(0.5, 0.5, 0);
    
    gluPerspective(PERSPECTIVE_FOV, Settings.ScreenWidth/Settings.ScreenHeight, 0.01, 100000);
    DoCamTranslate;
    DoCamRotate;
  glMatrixMode(GL_MODELVIEW);
  
  glBindTexture(GL_TEXTURE_2D, 0);

Wieder ein riesiger Codeblock, aber das meiste kennen wir ja bereits. Das einzige, was dazugekommen ist, sind die if UseDepthShader-Blöcke. UseDepthShader ist mal wieder eine Variable, die Ihr ruhig deklarieren solltet, um die Verwendung des DepthShaders einzuschränken. DepthShader sollte wieder mal eine Variable für den Namen des Shaderprogramms sein. Ich missbrauche hier den Alpha-Kanal der Texturen für Tiefeninformationen. Dies ist insofern sinnvoll, dass man nicht extra eine Textur braucht und daher auch die Shader relativ einfach zu erweitern sind. Aber jetzt erst einmal zum Tiefenshader. Hier haben wir wieder einen recht unspektakulären Vertexshader:

varying vec4 vpos;
varying vec4 ppos;

void main(void)
{
  vpos = gl_ModelViewMatrix * gl_Vertex;
  ppos = gl_ModelViewProjectionMatrix * gl_Vertex;
  gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
  gl_FrontColor   = gl_Color;
  gl_ClipVertex = vpos;
}

Obwohl es eher wie ein normaler Shader aussieht, ist es doch interessanter als man glaubt. Jetzt kommt uns der Vorteil zugute, dass wir die Kamera in der Projektionsmatrix haben. Wir können einfach die Kameraoptionen außen vor lassen und nur die Operationen, die in der Modelview-Matrix stehen auf den Vertex anwenden. Das erleichtert das Errechnen des Z-Wertes des Vertex ungemein. Jetzt zum Fragmentshader:

uniform float waterplaneZ;

varying vec4 vpos;
varying vec4 ppos;

void main(void)
{
  float y, z;
  y = waterplaneZ - vpos.z;
  z = ppos.z;
  
  gl_FragColor = vec4(z, z, z, y);
}

Hier werden die im Vertexshader berechneten Werte verwendet, um die Höhe des Vertices in den Alphakanal zu schreiben. Angemerkt werden sollte, dass die RGB-Informationen dank des glColorMask in RenderWaterMap verworfen werden. WaterplaneZ ist übrigens der Z-Wert der Wasserebene, in unserem Beispiel also 0.0, wie aus dem obigen Code hervorgeht.

Jetzt müssen wir nur noch den Wassershader so verändern, dass er die jetzt im Alphakanal vorhandenen Informationen auch sinnvoll nutzt. Der Vertexshader bleibt dabei, wie er ist, aber der Fragmentshader sieht jetzt so aus:

uniform sampler2D refractTex;
uniform sampler2D reflectTex;
uniform sampler2D bumpMap;

uniform float refractRatio;

uniform float reflectRatio;

uniform float mappingRatio;

uniform int extendedBlending;

varying mat4 texMat;

void main(void)
{
  vec4 refractcoord;
  vec4 reflectcoord;
  vec4 offsetColor = (texture2D(bumpMap, vec2(gl_TexCoord[1])) + 
    texture2D(bumpMap, vec2(gl_TexCoord[1]) * 4.0)) / 2.0;
  vec4 origOffset = offsetColor;
  vec4 color;
  vec4 reflectColor = vec4(1.0, 1.0, 1.0, 1.0);
  vec4 refractColor = vec4(1.0, 1.0, 1.0, 1.0);
  vec4 blendedColor;
  
  offsetColor -= 0.5;
  offsetColor *= 2.0;

  refractcoord = texMat * gl_TexCoord[0];
  refractColor = texture2DProj(refractTex, refractcoord);
  refractcoord = gl_TexCoord[0];
  refractcoord.x += offsetColor[0] * refractColor[3] * refractRatio;
  refractcoord.z += offsetColor[1] * refractColor[3] * refractRatio;
  refractcoord = texMat * refractcoord;

  reflectcoord = texMat * gl_TexCoord[0];
  reflectColor = texture2DProj(reflectTex, reflectcoord);
  reflectcoord = gl_TexCoord[0];
  reflectcoord.x -= offsetColor[0] * reflectColor[3] * reflectRatio;
  reflectcoord.z -= offsetColor[1] * reflectColor[3] * reflectRatio;
  reflectcoord = texMat * reflectcoord;
  
  reflectColor = texture2DProj(reflectTex, reflectcoord);
  refractColor = texture2DProj(refractTex, refractcoord);

  reflectColor[3] = 1.0;
  refractColor[3] = 1.0;
  
  if (extendedBlending == 0)
  {
    float mappingRefract, mappingReflect;
    mappingRefract = mappingRatio * 255.0;
    mappingReflect = 255.0 - mappingRefract;
    mappingRefract /= 255.0;
    mappingReflect /= 255.0;
    blendedColor = refractColor * mappingRefract + reflectColor * mappingReflect;
    blendedColor.a = 1.0;
  }
  else
  {
    float Alpha, reflectAlpha, refractAlpha;
    Alpha = (refractColor.r + refractColor.g + refractColor.b) / 3.0;
    if (Alpha > 1.0)
      Alpha = 1.0;
    if (Alpha < 0.0)
      Alpha = 0.0;
      
    refractAlpha = Alpha * 255.0;
    reflectAlpha = 255.0 - refractAlpha;
    refractAlpha /= 255.0;
    reflectAlpha /= 255.0;
    blendedColor = reflectColor * reflectAlpha + refractColor * refractAlpha;
  }
  
  gl_FragColor = blendedColor;
}
Der finale Wassereffekt mit Wellen und "Kantenerkennung" (von littleDave)
Der neue Code multipliziert das Offset nochmal mit dem Alpha-Wert, in unserem Falle also die Distanz zwischen der Wasserebene und dem Pixel. Dadurch werden die Wellen je näher der Pixel an der Wasseroberfläche ist, immer niedriger. So werden an der direkten Kante zu dem Objekt, also da, wo der Alpha-Wert 0.0 ist, gar keine Wellen mehr erzeugt. Damit ist die nervige Kante weg. So sieht das ganze zum Schluss aus.

In the end...

Das war es eigentlich. Wenn alles so läuft, wie es soll, dann habt ihr jetzt eine schöne Ebene mit Wassereffekt. Die Shader sowie der andere Code dürfen natürlich beliebig weiterverwendet werden. Verbesserungsvorschläge, Kritik, Feedback jeglicher Art und Morddrohungen wie immer einfach an mich im Forum wenden.

glEnd;

Gruss Lord Horazont


Vorhergehendes Tutorial:
Tutorial Alphamasking
Nächstes Tutorial:
-

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.