This tutorial will cover how to implement water in OpenGL 4.0 using GLSL and C++.
The code in this tutorial is based and builds upon the previous tutorials.
There are many different types of water implementations, each of which has advantages and disadvantages.
The type of water we will cover in this tutorial is reflective and refractive water.
It is one of the best-looking techniques but it does have edge problems if the wave height is too large.
Therefore, reflective and refractive water works best for smaller water bodies that have very small wave heights such as rivers, ponds, and pools.
You can extend this technique to lakes and oceans provided they are semi-still.
To start the tutorial, I will build a simple scene to facilitate the reflective and refractive water technique.
The scene will have a flat ground and a marble water bath to hold the water.
It will also have a stone wall so there is an object for the water to reflect.
The basic scene looks like the following:
Then we create a flat-water quad made from two triangles and translate it so it sits inside the bath object.
We will not give the water object a texture but instead we will use a reflection of the scene as the water texture.
To build the reflection texture we use the reflection technique from the reflection tutorial and create a reflection render to texture.
We render the reflection of the scene from the height of the water based on our camera position/angle and it produces a water that reflects everything above it:
Now that we have reflective water, we can improve the technique by adding refractive water.
Refraction is basically the inverse of reflection.
In reflection we render everything above the water line, but in refraction we render everything below the water line.
Since this is just a simple modification of the reflection technique, we can quickly produce a refraction to a render to texture in the same way and then map that texture to the water plane.
Also note that you should not render the ground and pillar when creating the refraction since the only object that can be seen beneath the water will be the marble bath.
The following is the refraction texture highlighted in yellow and I have purposely not rendered the bath in this image so you can clearly see what the refraction should look like:
Now that we have a reflection texture and a refraction texture mapped to the water quad, we will combine the two textures using a linear interpolation to produce the reflective and refractive water effect:
To enhance this reflective and refractive water effect we will add a normal map to simulate ripples in the water.
The normal mapping tutorial explained the use of normal maps to create bumps and in this tutorial will we use the normal maps to create water ripples in a similar fashion.
However, in normal mapping we used the light direction with the bump normal to determine the per pixel lighting.
But with water we use the normal to distort the texture sampling location the same way a wave distorts what we see beneath it in water.
We will also translate the normal map along the Y axis in this tutorial to move the ripples and simulate moving water.
As a side note the way I created a normal map for this tutorial was using Photoshop and the Nvidia texture tool (available on Nvidia's website).
In Photoshop create a new image (I created a 256x256 image).
Then with just the default black and white colors I did a Filter->Render->Clouds which gives a Perlin noise style cloud formation.
Then I did a Filter->Distort->Ocean Ripple to give a water ripple look to the cloud image.
Finally, I did the Filter->NVIDIA Tools->Normal Map Filter and set the scale to 10 or so.
It then produces the final image which I save into a targa format:
One final thing to note is that some graphics engines only update the reflection and refraction texture once every 15-30 frames or so to gain some speed by removing the expensive render to textures every single frame.
Framework
The frame work was updated to include the new RefractionShaderClass and WaterShaderClass.
We will start the code section of the tutorial by examining the water shader first.
Water.vs
////////////////////////////////////////////////////////////////////////////////
// Filename: water.vs
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;
This shader has two extra output positions for use by the reflection and refraction texture sampling coordinates in the pixel shader.
out vec4 reflectionPosition;
out vec4 refractionPosition;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
Just like the reflection tutorial, the water shader will require a reflection matrix.
uniform mat4 reflectionMatrix;
////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
mat4 reflectProjectWorld;
mat4 viewProjectWorld;
// Calculate the position of the vertex against the world, view, and projection matrices.
gl_Position = vec4(inputPosition, 1.0f) * worldMatrix;
gl_Position = gl_Position * viewMatrix;
gl_Position = gl_Position * projectionMatrix;
// Store the texture coordinates for the pixel shader.
texCoord = inputTexCoord;
Create the reflection projection world matrix, just like the reflection tutorial, and then calculate the reflection position coordinates using it.
// Create the reflection projection world matrix.
reflectProjectWorld = reflectionMatrix * projectionMatrix;
reflectProjectWorld = worldMatrix * reflectProjectWorld;
// Calculate the input position against the reflectProjectWorld matrix.
reflectionPosition = vec4(inputPosition, 1.0f) * reflectProjectWorld;
Refraction coordinates are calculated in the same way as the reflection coordinates except that we use a view projection world matrix for them.
// Create the view projection world matrix for refraction.
viewProjectWorld = viewMatrix * projectionMatrix;
viewProjectWorld = worldMatrix * viewProjectWorld;
// Calculate the input position against the viewProjectWorld matrix.
refractionPosition = vec4(inputPosition, 1.0f) * viewProjectWorld;
}
Water.ps
////////////////////////////////////////////////////////////////////////////////
// Filename: water.ps
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec4 reflectionPosition;
in vec4 refractionPosition;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
The water shader will need three textures.
A reflection texture for the scene reflection.
A refraction texture for the refraction of the scene.
And finally, a normal map texture for simulating water ripples.
uniform sampler2D normalTexture;
uniform sampler2D refractionTexture;
uniform sampler2D reflectionTexture;
The water translation variable will be used for simulating water motion by translating the texture sampling coordinates each frame.
The reflectRefractScale variable is used for controlling the size of the water ripples in relation to the normal map.
Some normal maps will be slightly different in how drastic the normals rise and fall.
Having a variable to control this becomes very useful.
uniform float waterTranslation;
uniform float reflectRefractScale;
////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
vec2 tex;
vec2 reflectTexCoord;
vec2 refractTexCoord;
vec4 normalMap;
vec3 normal;
vec4 reflectionColor;
vec4 refractionColor;
Just like the translate shader tutorial we use a translation variable updated each frame to move the water normal map texture along the Y axis to simulate motion.
// Copy the texture coordinates from a read only variable into a variable we can modify.
tex.x = texCoord.x;
tex.y = texCoord.y;
// Move the position the water normal is sampled from to simulate moving water.
tex.y += waterTranslation;
Convert both the reflection and refraction position coordinates into texture coordinates in the -1 to +1 range.
// Calculate the projected reflection texture coordinates.
reflectTexCoord.x = reflectionPosition.x / reflectionPosition.w / 2.0f + 0.5f;
reflectTexCoord.y = reflectionPosition.y / reflectionPosition.w / 2.0f + 0.5f;
// Calculate the projected refraction texture coordinates.
refractTexCoord.x = refractionPosition.x / refractionPosition.w / 2.0f + 0.5f;
refractTexCoord.y = refractionPosition.y / refractionPosition.w / 2.0f + 0.5f;
Sample the normal for this pixel from the normal map and expand the range to be in the -1 to +1 range.
// Sample the normal from the normal map texture.
normalMap = texture(normalTexture, tex);
// Expand the range of the normal from (0,1) to (-1,+1).
normal = (normalMap.xyz * 2.0f) - 1.0f;
Now distort the reflection and refraction coordinates by the normal map value.
This creates the rippling effect by using the normal transitioning from -1 to +1 to distort our view just as water waves distort light.
The normal map value is multiplied by the reflectRefractScale to make the ripples less pronounced and more natural looking.
// Re-position the texture coordinate sampling position by the normal map value to simulate the rippling wave effect.
reflectTexCoord = reflectTexCoord + (normal.xy * reflectRefractScale);
refractTexCoord = refractTexCoord + (normal.xy * reflectRefractScale);
Next sample the reflection and refraction pixel based on the updated texture sampling coordinates.
// Sample the texture pixels from the textures using the updated texture coordinates.
reflectionColor = texture(reflectionTexture, reflectTexCoord);
refractionColor = texture(refractionTexture, refractTexCoord);
Finally combine the sampled reflection and refraction pixels using a linear interpolation.
// Combine the reflection and refraction results for the final color.
outputColor = mix(reflectionColor, refractionColor, 0.6f);
}
Watershaderclass.h
The WaterShaderClass handles shading water models using the water.vs and water.ps GLSL shaders.
This shader will be similar to all other shaders with some minor changes to accommodate the water rendering.
////////////////////////////////////////////////////////////////////////////////
// Filename: watershaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _WATERSHADERCLASS_H_
#define _WATERSHADERCLASS_H_
//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class name: WaterShaderClass
////////////////////////////////////////////////////////////////////////////////
class WaterShaderClass
{
public:
WaterShaderClass();
WaterShaderClass(const WaterShaderClass&);
~WaterShaderClass();
bool Initialize(OpenGLClass*);
void Shutdown();
bool SetShaderParameters(float*, float*, float*, float*, float, float);
private:
bool InitializeShader(char*, char*);
void ShutdownShader();
char* LoadShaderSourceFile(char*);
void OutputShaderErrorMessage(unsigned int, char*);
void OutputLinkerErrorMessage(unsigned int);
private:
OpenGLClass* m_OpenGLPtr;
unsigned int m_vertexShader;
unsigned int m_fragmentShader;
unsigned int m_shaderProgram;
};
#endif
Watershaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: watershaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "watershaderclass.h"
WaterShaderClass::WaterShaderClass()
{
m_OpenGLPtr = 0;
}
WaterShaderClass::WaterShaderClass(const WaterShaderClass& other)
{
}
WaterShaderClass::~WaterShaderClass()
{
}
bool WaterShaderClass::Initialize(OpenGLClass* OpenGL)
{
char vsFilename[128];
char psFilename[128];
bool result;
// Store the pointer to the OpenGL object.
m_OpenGLPtr = OpenGL;
We load the water.vs and water.ps GLSL shader files here.
// Set the location and names of the shader files.
strcpy(vsFilename, "../Engine/water.vs");
strcpy(psFilename, "../Engine/water.ps");
// Initialize the vertex and pixel shaders.
result = InitializeShader(vsFilename, psFilename);
if(!result)
{
return false;
}
return true;
}
void WaterShaderClass::Shutdown()
{
// Shutdown the shader.
ShutdownShader();
// Release the pointer to the OpenGL object.
m_OpenGLPtr = 0;
return;
}
bool WaterShaderClass::InitializeShader(char* vsFilename, char* fsFilename)
{
const char* vertexShaderBuffer;
const char* fragmentShaderBuffer;
int status;
// Load the vertex shader source file into a text buffer.
vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
if(!vertexShaderBuffer)
{
return false;
}
// Load the fragment shader source file into a text buffer.
fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
if(!fragmentShaderBuffer)
{
return false;
}
// Create a vertex and fragment shader object.
m_vertexShader = m_OpenGLPtr->glCreateShader(GL_VERTEX_SHADER);
m_fragmentShader = m_OpenGLPtr->glCreateShader(GL_FRAGMENT_SHADER);
// Copy the shader source code strings into the vertex and fragment shader objects.
m_OpenGLPtr->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
m_OpenGLPtr->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);
// Release the vertex and fragment shader buffers.
delete [] vertexShaderBuffer;
vertexShaderBuffer = 0;
delete [] fragmentShaderBuffer;
fragmentShaderBuffer = 0;
// Compile the shaders.
m_OpenGLPtr->glCompileShader(m_vertexShader);
m_OpenGLPtr->glCompileShader(m_fragmentShader);
// Check to see if the vertex shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_vertexShader, vsFilename);
return false;
}
// Check to see if the fragment shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_fragmentShader, fsFilename);
return false;
}
// Create a shader program object.
m_shaderProgram = m_OpenGLPtr->glCreateProgram();
// Attach the vertex and fragment shader to the program object.
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_fragmentShader);
// Bind the shader input variables.
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 1, "inputTexCoord");
// Link the shader program.
m_OpenGLPtr->glLinkProgram(m_shaderProgram);
// Check the status of the link.
m_OpenGLPtr->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
if(status != 1)
{
// If it did not link then write the syntax error message out to a text file for review.
OutputLinkerErrorMessage(m_shaderProgram);
return false;
}
return true;
}
void WaterShaderClass::ShutdownShader()
{
// Detach the vertex and fragment shaders from the program.
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_fragmentShader);
// Delete the vertex and fragment shaders.
m_OpenGLPtr->glDeleteShader(m_vertexShader);
m_OpenGLPtr->glDeleteShader(m_fragmentShader);
// Delete the shader program.
m_OpenGLPtr->glDeleteProgram(m_shaderProgram);
return;
}
char* WaterShaderClass::LoadShaderSourceFile(char* filename)
{
FILE* filePtr;
char* buffer;
long fileSize, count;
int error;
// Open the shader file for reading in text modee.
filePtr = fopen(filename, "r");
if(filePtr == NULL)
{
return 0;
}
// Go to the end of the file and get the size of the file.
fseek(filePtr, 0, SEEK_END);
fileSize = ftell(filePtr);
// Initialize the buffer to read the shader source file into, adding 1 for an extra null terminator.
buffer = new char[fileSize + 1];
// Return the file pointer back to the beginning of the file.
fseek(filePtr, 0, SEEK_SET);
// Read the shader text file into the buffer.
count = fread(buffer, 1, fileSize, filePtr);
if(count != fileSize)
{
return 0;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
return 0;
}
// Null terminate the buffer.
buffer[fileSize] = '\0';
return buffer;
}
void WaterShaderClass::OutputShaderErrorMessage(unsigned int shaderId, char* shaderFilename)
{
long count;
int logSize, error;
char* infoLog;
FILE* filePtr;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);
// Open a text file to write the error message to.
filePtr = fopen("shader-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening shader error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing shader error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing shader error message output file." << endl;
return;
}
// Notify the user to check the text file for compile errors.
cout << "Error compiling shader. Check shader-error.txt for error message. Shader filename: " << shaderFilename << endl;
return;
}
void WaterShaderClass::OutputLinkerErrorMessage(unsigned int programId)
{
long count;
FILE* filePtr;
int logSize, error;
char* infoLog;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetProgramInfoLog(programId, logSize, NULL, infoLog);
// Open a file to write the error message to.
filePtr = fopen("linker-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening linker error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing linker error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing linker error message output file." << endl;
return;
}
// Pop a message up on the screen to notify the user to check the text file for linker errors.
cout << "Error linking shader program. Check linker-error.txt for message." << endl;
return;
}
The SetShaderParameters function takes in all the new water parameters that will be set inside the shader before rendering the water model.
This includes setting the reflection matrix, the reflection texture, the refraction texture, the normal map texture, the water translation, and the reflect refract scale.
bool WaterShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix,
float* reflectionMatrix, float waterTranslation, float reflectRefractScale)
{
float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16], tpReflectionMatrix[16];
int location;
Transpose and set the four matrices (including reflection) for the vertex shader first.
// Transpose the matrices to prepare them for the shader.
m_OpenGLPtr->MatrixTranspose(tpWorldMatrix, worldMatrix);
m_OpenGLPtr->MatrixTranspose(tpViewMatrix, viewMatrix);
m_OpenGLPtr->MatrixTranspose(tpProjectionMatrix, projectionMatrix);
m_OpenGLPtr->MatrixTranspose(tpReflectionMatrix, reflectionMatrix);
// Install the shader program as part of the current rendering state.
m_OpenGLPtr->glUseProgram(m_shaderProgram);
// Set the world matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "worldMatrix");
if(location == -1)
{
cout << "World matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpWorldMatrix);
// Set the view matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "viewMatrix");
if(location == -1)
{
cout << "View matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpViewMatrix);
// Set the projection matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
if(location == -1)
{
cout << "Projection matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpProjectionMatrix);
// Set the reflection matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "reflectionMatrix");
if(location == -1)
{
cout << "Reflection matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpReflectionMatrix);
Set the normal map, refraction, and reflection textures in the pixel shader.
// Set the texture in the pixel shader to use the data from the first texture unit.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "normalTexture");
if(location == -1)
{
cout << "Normal texture not set." << endl;
}
m_OpenGLPtr->glUniform1i(location, 0);
// Set the texture in the pixel shader to use the data from the second texture unit.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "refractionTexture");
if(location == -1)
{
cout << "Refraction texture not set." << endl;
}
m_OpenGLPtr->glUniform1i(location, 1);
// Set the texture in the pixel shader to use the data from the third texture unit.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "reflectionTexture");
if(location == -1)
{
cout << "Reflection texture not set." << endl;
}
m_OpenGLPtr->glUniform1i(location, 2);
Set the water translation and the reflect refract scale in the pixel shader.
// Set the water translation in the pixel shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "waterTranslation");
if(location == -1)
{
cout << "Water translation not set." << endl;
}
m_OpenGLPtr->glUniform1f(location, waterTranslation);
// Set the reflect refract scale in the pixel shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "reflectRefractScale");
if(location == -1)
{
cout << "Reflect refract scale not set." << endl;
}
m_OpenGLPtr->glUniform1f(location, reflectRefractScale);
return true;
}
Refraction.vs
The refraction shaders are just the light shaders re-written with a clip plane.
As the refraction renders the scene normally but only renders what is underneath the water, we use the clip plane with the light shader to achieve this effect.
////////////////////////////////////////////////////////////////////////////////
// Filename: refraction.vs
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;
in vec3 inputNormal;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;
out vec3 normal;
The clip distance variable is added as an output to achieve the clipping effect needed for the refraction shader.
out float gl_ClipDistance[1];
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
We add the clip plane uniform variable to the light shader to make it a refraction shader.
uniform vec4 clipPlane;
////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
// Calculate the position of the vertex against the world, view, and projection matrices.
gl_Position = vec4(inputPosition, 1.0f) * worldMatrix;
gl_Position = gl_Position * viewMatrix;
gl_Position = gl_Position * projectionMatrix;
// Store the texture coordinates for the pixel shader.
texCoord = inputTexCoord;
// Calculate the normal vector against the world matrix only.
normal = inputNormal * mat3(worldMatrix);
// Normalize the normal vector.
normal = normalize(normal);
Here we setup the clipping plane, otherwise the rest of the shader is the same as the light vertex shader originally was.
// Set the clipping plane.
gl_ClipDistance[0] = dot(vec4(inputPosition, 1.0f) * worldMatrix, clipPlane);
}
Refraction.ps
The refraction pixel shader is exactly the same as the light pixel shader.
There are no changes needed in the pixel shader to achieve the refraction effect since clipping was already implemented in the vertex shader.
////////////////////////////////////////////////////////////////////////////////
// Filename: refraction.ps
////////////////////////////////////////////////////////////////////////////////
#version 400
/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;
in vec3 normal;
//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;
///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D shaderTexture;
uniform vec3 lightDirection;
uniform vec4 diffuseLightColor;
uniform vec4 ambientLight;
////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
vec4 textureColor;
vec4 color;
vec3 lightDir;
float lightIntensity;
// Sample the pixel color from the texture using the sampler at this texture coordinate location.
textureColor = texture(shaderTexture, texCoord);
// Set the default output color to the ambient light value for all pixels.
color = ambientLight;
// Invert the light direction for calculations.
lightDir = -lightDirection;
// Calculate the amount of light on this pixel.
lightIntensity = clamp(dot(normal, lightDir), 0.0f, 1.0f);
if(lightIntensity > 0.0f)
{
// Determine the final diffuse color based on the diffuse color and the amount of light intensity.
color += (diffuseLightColor * lightIntensity);
}
// Clamp the final light color.
color = clamp(color, 0.0f, 1.0f);
// Multiply the texture pixel and the final diffuse color to get the final pixel color result.
outputColor = color * textureColor;
}
Refractionshaderclass.h
The RefractionShaderClass is just the LightShaderClass with a clip plane added.
////////////////////////////////////////////////////////////////////////////////
// Filename: refractionshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _REFRACTIONSHADERCLASS_H_
#define _REFRACTIONSHADERCLASS_H_
//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class name: RefractionShaderClass
////////////////////////////////////////////////////////////////////////////////
class RefractionShaderClass
{
public:
RefractionShaderClass();
RefractionShaderClass(const RefractionShaderClass&);
~RefractionShaderClass();
bool Initialize(OpenGLClass*);
void Shutdown();
bool SetShaderParameters(float*, float*, float*, float*, float*, float*, float*);
private:
bool InitializeShader(char*, char*);
void ShutdownShader();
char* LoadShaderSourceFile(char*);
void OutputShaderErrorMessage(unsigned int, char*);
void OutputLinkerErrorMessage(unsigned int);
private:
OpenGLClass* m_OpenGLPtr;
unsigned int m_vertexShader;
unsigned int m_fragmentShader;
unsigned int m_shaderProgram;
};
#endif
Refractionshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: refractionshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "refractionshaderclass.h"
RefractionShaderClass::RefractionShaderClass()
{
m_OpenGLPtr = 0;
}
RefractionShaderClass::RefractionShaderClass(const RefractionShaderClass& other)
{
}
RefractionShaderClass::~RefractionShaderClass()
{
}
bool RefractionShaderClass::Initialize(OpenGLClass* OpenGL)
{
char vsFilename[128];
char psFilename[128];
bool result;
// Store the pointer to the OpenGL object.
m_OpenGLPtr = OpenGL;
We load the refraction.vs and refraction.ps GLSL shader files here.
// Set the location and names of the shader files.
strcpy(vsFilename, "../Engine/refraction.vs");
strcpy(psFilename, "../Engine/refraction.ps");
// Initialize the vertex and pixel shaders.
result = InitializeShader(vsFilename, psFilename);
if(!result)
{
return false;
}
return true;
}
void RefractionShaderClass::Shutdown()
{
// Shutdown the shader.
ShutdownShader();
// Release the pointer to the OpenGL object.
m_OpenGLPtr = 0;
return;
}
bool RefractionShaderClass::InitializeShader(char* vsFilename, char* fsFilename)
{
const char* vertexShaderBuffer;
const char* fragmentShaderBuffer;
int status;
// Load the vertex shader source file into a text buffer.
vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
if(!vertexShaderBuffer)
{
return false;
}
// Load the fragment shader source file into a text buffer.
fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
if(!fragmentShaderBuffer)
{
return false;
}
// Create a vertex and fragment shader object.
m_vertexShader = m_OpenGLPtr->glCreateShader(GL_VERTEX_SHADER);
m_fragmentShader = m_OpenGLPtr->glCreateShader(GL_FRAGMENT_SHADER);
// Copy the shader source code strings into the vertex and fragment shader objects.
m_OpenGLPtr->glShaderSource(m_vertexShader, 1, &vertexShaderBuffer, NULL);
m_OpenGLPtr->glShaderSource(m_fragmentShader, 1, &fragmentShaderBuffer, NULL);
// Release the vertex and fragment shader buffers.
delete [] vertexShaderBuffer;
vertexShaderBuffer = 0;
delete [] fragmentShaderBuffer;
fragmentShaderBuffer = 0;
// Compile the shaders.
m_OpenGLPtr->glCompileShader(m_vertexShader);
m_OpenGLPtr->glCompileShader(m_fragmentShader);
// Check to see if the vertex shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_vertexShader, vsFilename);
return false;
}
// Check to see if the fragment shader compiled successfully.
m_OpenGLPtr->glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &status);
if(status != 1)
{
// If it did not compile then write the syntax error message out to a text file for review.
OutputShaderErrorMessage(m_fragmentShader, fsFilename);
return false;
}
// Create a shader program object.
m_shaderProgram = m_OpenGLPtr->glCreateProgram();
// Attach the vertex and fragment shader to the program object.
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glAttachShader(m_shaderProgram, m_fragmentShader);
// Bind the shader input variables.
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 0, "inputPosition");
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 1, "inputTexCoord");
m_OpenGLPtr->glBindAttribLocation(m_shaderProgram, 2, "inputNormal");
// Link the shader program.
m_OpenGLPtr->glLinkProgram(m_shaderProgram);
// Check the status of the link.
m_OpenGLPtr->glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, &status);
if(status != 1)
{
// If it did not link then write the syntax error message out to a text file for review.
OutputLinkerErrorMessage(m_shaderProgram);
return false;
}
return true;
}
void RefractionShaderClass::ShutdownShader()
{
// Detach the vertex and fragment shaders from the program.
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_vertexShader);
m_OpenGLPtr->glDetachShader(m_shaderProgram, m_fragmentShader);
// Delete the vertex and fragment shaders.
m_OpenGLPtr->glDeleteShader(m_vertexShader);
m_OpenGLPtr->glDeleteShader(m_fragmentShader);
// Delete the shader program.
m_OpenGLPtr->glDeleteProgram(m_shaderProgram);
return;
}
char* RefractionShaderClass::LoadShaderSourceFile(char* filename)
{
FILE* filePtr;
char* buffer;
long fileSize, count;
int error;
// Open the shader file for reading in text modee.
filePtr = fopen(filename, "r");
if(filePtr == NULL)
{
return 0;
}
// Go to the end of the file and get the size of the file.
fseek(filePtr, 0, SEEK_END);
fileSize = ftell(filePtr);
// Initialize the buffer to read the shader source file into, adding 1 for an extra null terminator.
buffer = new char[fileSize + 1];
// Return the file pointer back to the beginning of the file.
fseek(filePtr, 0, SEEK_SET);
// Read the shader text file into the buffer.
count = fread(buffer, 1, fileSize, filePtr);
if(count != fileSize)
{
return 0;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
return 0;
}
// Null terminate the buffer.
buffer[fileSize] = '\0';
return buffer;
}
void RefractionShaderClass::OutputShaderErrorMessage(unsigned int shaderId, char* shaderFilename)
{
long count;
int logSize, error;
char* infoLog;
FILE* filePtr;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);
// Open a text file to write the error message to.
filePtr = fopen("shader-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening shader error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing shader error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing shader error message output file." << endl;
return;
}
// Notify the user to check the text file for compile errors.
cout << "Error compiling shader. Check shader-error.txt for error message. Shader filename: " << shaderFilename << endl;
return;
}
void RefractionShaderClass::OutputLinkerErrorMessage(unsigned int programId)
{
long count;
FILE* filePtr;
int logSize, error;
char* infoLog;
// Get the size of the string containing the information log for the failed shader compilation message.
m_OpenGLPtr->glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);
// Increment the size by one to handle also the null terminator.
logSize++;
// Create a char buffer to hold the info log.
infoLog = new char[logSize];
// Now retrieve the info log.
m_OpenGLPtr->glGetProgramInfoLog(programId, logSize, NULL, infoLog);
// Open a file to write the error message to.
filePtr = fopen("linker-error.txt", "w");
if(filePtr == NULL)
{
cout << "Error opening linker error message output file." << endl;
return;
}
// Write out the error message.
count = fwrite(infoLog, sizeof(char), logSize, filePtr);
if(count != logSize)
{
cout << "Error writing linker error message output file." << endl;
return;
}
// Close the file.
error = fclose(filePtr);
if(error != 0)
{
cout << "Error closing linker error message output file." << endl;
return;
}
// Pop a message up on the screen to notify the user to check the text file for linker errors.
cout << "Error linking shader program. Check linker-error.txt for message." << endl;
return;
}
bool RefractionShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix,
float* lightDirection, float* diffuseLightColor, float* ambientLight, float* clipPlane)
{
float tpWorldMatrix[16], tpViewMatrix[16], tpProjectionMatrix[16];
int location;
// Transpose the matrices to prepare them for the shader.
m_OpenGLPtr->MatrixTranspose(tpWorldMatrix, worldMatrix);
m_OpenGLPtr->MatrixTranspose(tpViewMatrix, viewMatrix);
m_OpenGLPtr->MatrixTranspose(tpProjectionMatrix, projectionMatrix);
// Install the shader program as part of the current rendering state.
m_OpenGLPtr->glUseProgram(m_shaderProgram);
// Set the world matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "worldMatrix");
if(location == -1)
{
cout << "World matrix not set." << endl;
}
m_OpenGLPtr ->glUniformMatrix4fv(location, 1, false, tpWorldMatrix);
// Set the view matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "viewMatrix");
if(location == -1)
{
cout << "View matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpViewMatrix);
// Set the projection matrix in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "projectionMatrix");
if(location == -1)
{
cout << "Projection matrix not set." << endl;
}
m_OpenGLPtr->glUniformMatrix4fv(location, 1, false, tpProjectionMatrix);
Set the clip plane here, otherwise all the other variables set are the same as the light shader.
// Set the clip plane in the vertex shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "clipPlane");
if(location == -1)
{
cout << "Clip plane not set." << endl;
}
m_OpenGLPtr->glUniform4fv(location, 1, clipPlane);
// Set the texture in the pixel shader to use the data from the first texture unit.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "shaderTexture");
if(location == -1)
{
cout << "Shader texture not set." << endl;
}
m_OpenGLPtr->glUniform1i(location, 0);
// Set the light direction in the pixel shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "lightDirection");
if(location == -1)
{
cout << "Light direction not set." << endl;
}
m_OpenGLPtr->glUniform3fv(location, 1, lightDirection);
// Set the diffuse light color in the pixel shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "diffuseLightColor");
if(location == -1)
{
cout << "Diffuse light color not set." << endl;
}
m_OpenGLPtr->glUniform4fv(location, 1, diffuseLightColor);
// Set the ambient light in the pixel shader.
location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "ambientLight");
if(location == -1)
{
cout << "Ambient light not set." << endl;
}
m_OpenGLPtr->glUniform4fv(location, 1, ambientLight);
return true;
}
Applicationclass.h
The ApplicationClass is where we are going to do the work of setting up and rendering the 3D scene and water.
////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_
/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_NEAR = 0.3f;
const float SCREEN_DEPTH = 1000.0f;
///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "openglclass.h"
#include "cameraclass.h"
#include "modelclass.h"
#include "lightclass.h"
#include "rendertextureclass.h"
#include "lightshaderclass.h"
#include "refractionshaderclass.h"
#include "watershaderclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class Name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
ApplicationClass();
ApplicationClass(const ApplicationClass&);
~ApplicationClass();
bool Initialize(Display*, Window, int, int);
void Shutdown();
bool Frame(InputClass*);
private:
bool RenderRefractionToTexture();
bool RenderReflectionToTexture();
bool Render();
private:
OpenGLClass* m_OpenGL;
CameraClass* m_Camera;
We will need four different models for this tutorial.
A model for the ground, a model for the wall, and model for the bath, and a model for the water.
ModelClass *m_GroundModel, *m_WallModel, *m_BathModel, *m_WaterModel;
LightClass* m_Light;
We will also need two render to texture objects for the reflection texture and the refraction texture.
RenderTextureClass *m_RefractionTexture, *m_ReflectionTexture;
The light shader, refraction shader, and water shader are needed for this tutorial.
LightShaderClass* m_LightShader;
RefractionShaderClass* m_RefractionShader;
WaterShaderClass* m_WaterShader;
We also need a couple variables to keep track of the water position and water height.
float m_waterHeight, m_waterTranslation;
};
#endif
Applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "applicationclass.h"
ApplicationClass::ApplicationClass()
{
m_OpenGL = 0;
m_Camera = 0;
m_GroundModel = 0;
m_WallModel = 0;
m_BathModel = 0;
m_WaterModel = 0;
m_Light = 0;
m_RefractionTexture = 0;
m_ReflectionTexture = 0;
m_LightShader = 0;
m_RefractionShader = 0;
m_WaterShader = 0;
}
ApplicationClass::ApplicationClass(const ApplicationClass& other)
{
}
ApplicationClass::~ApplicationClass()
{
}
bool ApplicationClass::Initialize(Display* display, Window win, int screenWidth, int screenHeight)
{
char modelFilename[128];
char textureFilename[128];
bool result;
// Create and initialize the OpenGL object.
m_OpenGL = new OpenGLClass;
result = m_OpenGL->Initialize(display, win, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, VSYNC_ENABLED);
if(!result)
{
cout << "Error: Could not initialize the OpenGL object." << endl;
return false;
}
// Create the camera object.
m_Camera = new CameraClass;
// Set the position and rotation of the camera.
m_Camera->SetPosition(-10.0f, 6.0f, -10.0f);
m_Camera->SetRotation(0.0f, 45.0f, 0.0f);
m_Camera->Render();
Load in the ground, wall, bath, and water models here.
// Set the file names of the ground model.
strcpy(modelFilename, "../Engine/data/ground.txt");
strcpy(textureFilename, "../Engine/data/ground01.tga");
// Create and initialize the ground model object.
m_GroundModel = new ModelClass;
result = m_GroundModel->Initialize(m_OpenGL, modelFilename, textureFilename, true, NULL, true, NULL, true);
if(!result)
{
cout << "Error: Could not initialize the ground model object." << endl;
return false;
}
// Set the file names of the wall model.
strcpy(modelFilename, "../Engine/data/wall.txt");
strcpy(textureFilename, "../Engine/data/wall01.tga");
// Create and initialize the wall model object.
m_WallModel = new ModelClass;
result = m_WallModel->Initialize(m_OpenGL, modelFilename, textureFilename, true, NULL, true, NULL, true);
if(!result)
{
cout << "Error: Could not initialize the wall model object." << endl;
return false;
}
// Set the file names of the bath model.
strcpy(modelFilename, "../Engine/data/bath.txt");
strcpy(textureFilename, "../Engine/data/marble01.tga");
// Create and initialize the bath model object.
m_BathModel = new ModelClass;
result = m_BathModel->Initialize(m_OpenGL, modelFilename, textureFilename, true, NULL, true, NULL, true);
if(!result)
{
cout << "Error: Could not initialize the bath model object." << endl;
return false;
}
// Set the file names of the water model.
strcpy(modelFilename, "../Engine/data/water.txt");
strcpy(textureFilename, "../Engine/data/water01.tga");
// Create and initialize the water model object.
m_WaterModel = new ModelClass;
result = m_WaterModel->Initialize(m_OpenGL, modelFilename, textureFilename, true, NULL, true, NULL, true);
if(!result)
{
cout << "Error: Could not initialize the water model object." << endl;
return false;
}
// Create and initialize the light object.
m_Light = new LightClass;
m_Light->SetAmbientLight(0.15f, 0.15f, 0.15f, 1.0f);
m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
m_Light->SetDirection(0.0f, -1.0f, 0.5f);
Setup the two render to textures for the scene refraction and reflection.
// Create and initialize the refraction render to texture object.
m_RefractionTexture = new RenderTextureClass;
result = m_RefractionTexture->Initialize(m_OpenGL, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, 0);
if(!result)
{
cout << "Error: Could not initialize the refraction texture object." << endl;
return false;
}
// Create and initialize the reflection render to texture object.
m_ReflectionTexture = new RenderTextureClass;
result = m_ReflectionTexture->Initialize(m_OpenGL, screenWidth, screenHeight, SCREEN_NEAR, SCREEN_DEPTH, 0, 2);
if(!result)
{
cout << "Error: Could not initialize the reflection texture object." << endl;
return false;
}
Setup the three shaders that we will be using.
// Create and initialize the light shader object.
m_LightShader = new LightShaderClass;
result = m_LightShader->Initialize(m_OpenGL);
if(!result)
{
cout << "Error: Could not initialize the light shader object." << endl;
return false;
}
// Create and initialize the refraction shader object.
m_RefractionShader = new RefractionShaderClass;
result = m_RefractionShader->Initialize(m_OpenGL);
if(!result)
{
cout << "Error: Could not initialize the refraction shader object." << endl;
return false;
}
// Create and initialize the water shader object.
m_WaterShader = new WaterShaderClass;
result = m_WaterShader->Initialize(m_OpenGL);
if(!result)
{
cout << "Error: Could not initialize the water shader object." << endl;
return false;
}
Set the height of the water plane and initialize the position of the water translation here.
// Set the height of the water.
m_waterHeight = 2.75f;
// Initialize the position of the water.
m_waterTranslation = 0.0f;
return true;
}
void ApplicationClass::Shutdown()
{
// Release the water shader object.
if(m_WaterShader)
{
m_WaterShader->Shutdown();
delete m_WaterShader;
m_WaterShader = 0;
}
// Release the refraction shader object.
if(m_RefractionShader)
{
m_RefractionShader->Shutdown();
delete m_RefractionShader;
m_RefractionShader = 0;
}
// Release the light shader object.
if(m_LightShader)
{
m_LightShader->Shutdown();
delete m_LightShader;
m_LightShader = 0;
}
// Release the reflection render to texture object.
if(m_ReflectionTexture)
{
m_ReflectionTexture->Shutdown();
delete m_ReflectionTexture;
m_ReflectionTexture = 0;
}
// Release the refraction render to texture object.
if(m_RefractionTexture)
{
m_RefractionTexture->Shutdown();
delete m_RefractionTexture;
m_RefractionTexture = 0;
}
// Release the light object.
if(m_Light)
{
delete m_Light;
m_Light = 0;
}
// Release the water model object.
if(m_WaterModel)
{
m_WaterModel->Shutdown();
delete m_WaterModel;
m_WaterModel = 0;
}
// Release the bath model object.
if(m_BathModel)
{
m_BathModel->Shutdown();
delete m_BathModel;
m_BathModel = 0;
}
// Release the wall model object.
if(m_WallModel)
{
m_WallModel->Shutdown();
delete m_WallModel;
m_WallModel = 0;
}
// Release the ground model object.
if(m_GroundModel)
{
m_GroundModel->Shutdown();
delete m_GroundModel;
m_GroundModel = 0;
}
// Release the camera object.
if(m_Camera)
{
delete m_Camera;
m_Camera = 0;
}
// Release the OpenGL object.
if(m_OpenGL)
{
m_OpenGL->Shutdown();
delete m_OpenGL;
m_OpenGL = 0;
}
return;
}
bool ApplicationClass::Frame(InputClass* Input)
{
bool result;
// Check if the escape key has been pressed, if so quit.
if(Input->IsEscapePressed() == true)
{
return false;
}
Each frame updates the position of the water to simulate motion.
// Update the position of the water to simulate motion.
m_waterTranslation += 0.001f;
if(m_waterTranslation > 1.0f)
{
m_waterTranslation -= 1.0f;
}
First, we render the refraction of the scene to a texture.
Then we render the reflection of the scene to a texture.
And then finally we render the scene normally and use the reflection and refraction texture to create the water effect.
Remember if you want to reduce the cost of rendering to texture all the time you can instead only do so every 15-30 frames and reuse the previous textures.
// Render the refraction of the scene to a texture.
result = RenderRefractionToTexture();
if(!result)
{
return false;
}
// Render the reflection of the scene to a texture.
result = RenderReflectionToTexture();
if(!result)
{
return false;
}
// Render the final graphics scene to the back buffer.
result = Render();
if(!result)
{
return false;
}
return true;
}
The RenderRefractionToTexture function renders the refraction of the scene to a render to texture.
As the refraction of the scene is everything beneath the water and only the bath model is visible underneath the model, we can be selective and just render the bath model.
bool ApplicationClass::RenderRefractionToTexture()
{
float worldMatrix[16], viewMatrix[16], projectionMatrix[16];
float clipPlane[4];
float diffuseLightColor[4], lightDirection[3], ambientLight[4];
bool result;
Now as expected we use a clip plane to clip everything above the water plane and only render what is beneath it.
However, you will notice that I am translating the clip plane up just slightly above the water height.
The reason being is that this technique has edge issues and to reduce the visibility of the dark edges that appear once translated by the normal map sampling location we just force the sampling to occur in a range where
there is more color data available (by moving the clip plane slightly up).
When you run this program try just using the regular water height so you can see the artifacting that does occur normally.
// Setup a clipping plane based on the height of the water to clip everything above it.
clipPlane[0] = 0.0f;
clipPlane[1] = -1.0f;
clipPlane[2] = 0.0f;
clipPlane[3] = m_waterHeight + 0.1f;
// Set the render target to be the refraction render texture and clear it.
m_RefractionTexture->SetRenderTarget();
m_RefractionTexture->ClearRenderTarget(0.0f, 0.0f, 0.0f, 1.0f);
// Get the world, view, and projection matrices from the opengl and camera objects.
m_OpenGL->GetWorldMatrix(worldMatrix);
m_Camera->GetViewMatrix(viewMatrix);
m_RefractionTexture->GetProjectionMatrix(projectionMatrix);
// Get the light properties.
m_Light->GetDirection(lightDirection);
m_Light->GetDiffuseColor(diffuseLightColor);
m_Light->GetAmbientLight(ambientLight);
// Enable clip planes in OpenGL.
m_OpenGL->EnableClipping();
// Translate to where the bath model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 2.0f, 0.0f);
// Set the refraction shader as the current shader program and set the variables that it will use for rendering.
result = m_RefractionShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor, ambientLight, clipPlane);
if(!result)
{
return false;
}
// Render the bath model to the refraction render texture using the refraction shader.
m_BathModel->SetTexture1(0);
m_BathModel->Render();
// Disable clip planes in OpenGL.
m_OpenGL->DisableClipping();
// Reset the render target back to the original back buffer and not the render to texture anymore. And reset the viewport back to the original.
m_OpenGL->SetBackBufferRenderTarget();
m_OpenGL->ResetViewport();
return true;
}
RenderReflectionToTexture renders the reflection of the scene to a render to texture.
As the reflection of the scene is everything above the water, we only need to render the wall model since it is the only thing that can reflect in the water.
bool ApplicationClass::RenderReflectionToTexture()
{
float worldMatrix[16], reflectionViewMatrix[16], projectionMatrix[16];
float diffuseLightColor[4], lightDirection[3], ambientLight[4];
bool result;
// Set the render target to be the reflection render texture and clear it.
m_ReflectionTexture->SetRenderTarget();
m_ReflectionTexture->ClearRenderTarget(0.0f, 0.0f, 0.0f, 1.0f);
The reflection matrix is setup using the water height.
// Use the camera to render the reflection and create a reflection view matrix. Then get the camera reflection view matrix instead of the normal view matrix.
m_Camera->RenderReflection(m_waterHeight);
m_Camera->GetReflectionViewMatrix(reflectionViewMatrix);
// Get the world and projection matrices from the opengl and reflection render texture objects.
m_OpenGL->GetWorldMatrix(worldMatrix);
m_ReflectionTexture->GetProjectionMatrix(projectionMatrix);
// Get the light properties.
m_Light->GetDirection(lightDirection);
m_Light->GetDiffuseColor(diffuseLightColor);
m_Light->GetAmbientLight(ambientLight);
// Translate to where the wall model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 6.0f, 8.0f);
// Set the light shader as the current shader program and set the variables that it will use for rendering.
result = m_LightShader->SetShaderParameters(worldMatrix, reflectionViewMatrix, projectionMatrix, lightDirection, diffuseLightColor, ambientLight);
if(!result)
{
return false;
}
// Render the wall model using the regular light shader and the reflection view matrix to create a reflection of just the wall.
m_WallModel->SetTexture1(0);
m_WallModel->Render();
// Reset the render target back to the original back buffer and not the render to texture anymore. And reset the viewport back to the original.
m_OpenGL->SetBackBufferRenderTarget();
m_OpenGL->ResetViewport();
return true;
}
The Render function is where we render the scene to the back buffer and use the reflection and refraction render to textures to render the water.
bool ApplicationClass::Render()
{
float worldMatrix[16], viewMatrix[16], projectionMatrix[16], reflectionViewMatrix[16];
float diffuseLightColor[4], lightDirection[3], ambientLight[4];
bool result;
// Clear the buffers to begin the scene.
m_OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);
// Get the world, view, and projection matrices from the opengl and camera objects.
m_OpenGL->GetWorldMatrix(worldMatrix);
m_Camera->GetViewMatrix(viewMatrix);
m_OpenGL->GetProjectionMatrix(projectionMatrix);
// Get the light properties.
m_Light->GetDirection(lightDirection);
m_Light->GetDiffuseColor(diffuseLightColor);
m_Light->GetAmbientLight(ambientLight);
Render the ground model first.
// Translate to where the ground model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 1.0f, 0.0f);
// Set the light shader as the current shader program and set the matrices that it will use for rendering.
result = m_LightShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor, ambientLight);
if(!result)
{
return false;
}
// Render the ground model using the light shader.
m_GroundModel->SetTexture1(0);
m_GroundModel->Render();
Render the wall model next.
// Translate to where the wall model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 6.0f, 8.0f);
// Set the light shader as the current shader program and set the matrices that it will use for rendering.
result = m_LightShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor, ambientLight);
if(!result)
{
return false;
}
// Render the wall model using the light shader.
m_WallModel->SetTexture1(0);
m_WallModel->Render();
Render the bath model.
// Translate to where the bath model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, 2.0f, 0.0f);
// Set the light shader as the current shader program and set the variables that it will use for rendering.
result = m_LightShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, lightDirection, diffuseLightColor, ambientLight);
if(!result)
{
return false;
}
// Render the bath model using the light shader.
m_BathModel->SetTexture1(0);
m_BathModel->Render();
Finally render the water model using the reflection matrix, render to textures, translation value, and the reflectRefractScale (0.01f).
// Get the camera reflection view matrix for the water shader.
m_Camera->GetReflectionViewMatrix(reflectionViewMatrix);
// Translate to where the water model will be rendered.
m_OpenGL->MatrixTranslation(worldMatrix, 0.0f, m_waterHeight, 0.0f);
// Set the water shader as the current shader program and set the variables that it will use for rendering.
result = m_WaterShader->SetShaderParameters(worldMatrix, viewMatrix, projectionMatrix, reflectionViewMatrix, m_waterTranslation, 0.01f);
if(!result)
{
return false;
}
// Set the refraction render texture as the texture to be used in texture unit 1 for the water shader.
m_RefractionTexture->SetTexture(1);
// Set the reflection render texture as the texture to be used in texture unit 2 for the water shader.
m_ReflectionTexture->SetTexture(2);
// Render the water model using the water shader.
m_WaterModel->SetTexture1(0);
m_WaterModel->Render();
// Present the rendered scene to the screen.
m_OpenGL->EndScene();
return true;
}
Summary
The reflective and refractive water technique paired with translated normal maps provides a very realistic water effect that can be extended into many different related effects (glass, ice, etc.).
To Do Exercises
1. Recompile and run the program. You should get animated reflective/refractive water.
2. Modify how the reflection and refraction are coming by changing the 0.6f value in the linear interpolation at the end of the pixel shader.
3. Modify reflectRefractScale value (it is the last value sent into the m_WaterShader->Render function).
4. Create your own normal map and see the difference it makes in the water effect.
5. Change the clip plane to be the exact height of the water in the RenderRefractionToTexture() function.
Source Code
Source Code and Data Files: gl4linuxtut31_src.tar.gz