Home » Advanced » Lesson 15: Water effect with OpenGL (on the GPU with GLSL)

Lesson 15: Water effect with OpenGL (on the GPU with GLSL)

With the newer versions of OpenGL we are able to manipulate the OpenGL Rendering Pipeline. This gives a lot of power to the developers to do graphical manipulations and visual effects.  So far we have used the classical method to display 3D graphics.This (simplified) OpenGL pipeline is:

OpenGL Pipeline

OpenGL Pipeline

Essentially what we will do is upload small programs into the graphics card chip (GPU).  These small programs are called ‘shaders’.  For example, we will have a ‘vertex shader’ to manipulate vertexes and a ‘fragment shader’ to manipulate the image data.   Both will directly manipulate the OpenGL pipeline.  These programs are written in the language GLSL (abrev. OpenGL Shading Language).

Our fragment shader looks like this:

/* Fragment shader */

uniform sampler2D color_texture;

void main() {
        // Set the output color of our current pixel
        gl_FragColor = texture2D(color_texture, gl_TexCoord[0].st);
}

It tells the GPU to use the texture with the given texture coordinates, such that we can use texture mapping as we were used to. Then, we will create the vertex shader. As we want to create ocean waves we want to manipulate the vertexes in real time on the GPU. We will create a map of connected quads  and manipulate the height coordinate of each vertex in the quad the map. (In this case the z-coordinate). In this example we use the formula:

v.z = sin(waveWidth * v.x + waveTime) * cos(waveWidth * v.y + waveTime) * waveHeight;

Thus, we move the y-coordinate of each vertex according to a simple cosine/sine based function. You could define different functions here for different vertex effects.  :)

We can manipulate all of the parameters (waveWidth, waveTime, waveHeight) in our C program.

/* Vertex shader */
uniform float waveTime;
uniform float waveWidth;
uniform float waveHeight;

void main(void)
{
        vec4 v = vec4(gl_Vertex);
        v.z = sin(waveWidth * v.x + waveTime) * cos(waveWidth * v.y + waveTime) * waveHeight;
        gl_Position = gl_ModelViewProjectionMatrix * v;
        gl_TexCoord[0] = gl_MultiTexCoord0;
}

To draw the field we simply use

/* Draw here a plain surface */
float TS = 1.0 / 40; //0.025;

glBegin(GL_QUADS);
for (i = -20; i 
              

We use the variables startX,startY and TS to fit the texture to the field.We scale the texture over the entire field, but you may want to repeat the texture on a smaller field.  TS is the width of the total field, which is a variable we use to scale the texture. startX and startY are the offset of the texture map.   We will draw the entire field using our vertex shader and fragment shader defined above.  The output should be like:

OpenGL Water

OpenGL Water

The complete program is (don’t forget to copy the vertex and fragment shader & to get a texture):

/*
* OpenGL Water Effect. 
* It uses GLSL to create a wavey effect.
*
* https://talkera.org/opengl/
* http://openglsamples.sf.net/
*
*/
#include 
#include 
 
#include 
#include 
 
void printLog(GLuint obj)
{
    int infologLength = 0;
    char infoLog[1024];
 
        if (glIsShader(obj))
                glGetShaderInfoLog(obj, 1024, &infologLength, infoLog);
        else
                glGetProgramInfoLog(obj, 1024, &infologLength, infoLog);
 
    if (infologLength > 0)
                printf("%s\n", infoLog);
}
 
char *file2string(const char *path)
{
        FILE *fd;
        long len,
                 r;
        char *str;
 
        if (!(fd = fopen(path, "r")))
        {
                fprintf(stderr, "Can't open file '%s' for reading\n", path);
                return NULL;
        }
 
        fseek(fd, 0, SEEK_END);
        len = ftell(fd);
 
        printf("File '%s' is %ld long\n", path, len);
 
        fseek(fd, 0, SEEK_SET);
 
        if (!(str = malloc(len * sizeof(char))))
        {
                fprintf(stderr, "Can't malloc space for '%s'\n", path);
                return NULL;
        }
 
        r = fread(str, sizeof(char), len, fd);
 
        str[r - 1] = '\0'; /* Shader sources have to term with null */
 
        fclose(fd);
 
        return str;
}

/* Storage For Textures */
GLuint texture[3]; 


/* Loads in a bitmap as a GL texture */
int LoadGLTextures( ) {
    int Status = 0;
 
    /* Create storage space for the texture */
    SDL_Surface *TextureImage[1]; 
 
    /* Load The Bitmap into Memory */
    if ((TextureImage[0] = IMG_Load("texcube.bmp"))) {
        Status = 1;
        glGenTextures( 1, &texture[0] );
        glBindTexture( GL_TEXTURE_2D, texture[0] );
        glTexImage2D( GL_TEXTURE_2D, 0, 3, TextureImage[0]->w,
              TextureImage[0]->h, 0, GL_BGR,
              GL_UNSIGNED_BYTE, TextureImage[0]->pixels );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

        }
 
    /* Free up some memory */
    if ( TextureImage[0] )
        SDL_FreeSurface( TextureImage[0] );
 
    return Status;
}
 
int main(int argc, char **argv)
{
        SDL_Event sdlEv;
        Uint32 sdlVideoFlags = SDL_OPENGL;
        Uint8 quit;
        char *extensions;
 
        int i, j;
 
        /* Initialize */
        if (SDL_Init(SDL_INIT_VIDEO) = nextUpdate)
                {
                        fprintf(stderr, "FPS: %d\n", frameCount);
                        frameCount = 0;
                        nextUpdate = SDL_GetTicks() + 1000;
                }
        }
 
        glDeleteShader(vs);
        glDeleteShader(fs);
        glDeleteProgram(sp);
 
        return EXIT_SUCCESS;
}