Tutorial 13: Perturbed Clouds

This OpenGL 4.0 cloud tutorial is based on the previous bitmap clouds tutorial. We will be doing just some slight modifications to the previous tutorial code to implement perturbed clouds. Note that if you have already looked over and understand the fire tutorial in the OpenGL 4.0 section then you already understand how to implement perturbed clouds. I recommend reading that tutorial if you want a more in-depth understanding of how the shader works.

In the previous tutorial we translated a cloud texture along the sky plane to simulate a basic cloud system. However as clouds move through the sky their formation modifies slightly. So for this tutorial we will perturb the sampling of the texture by a secondary noise texture. By slightly offsetting where we sample the clouds from it gives the effect of the edges and insides of the clouds expanding and collapsing as they move over the sky plane.

The bitmap cloud texture that we will use is the following:

Then we will use a perlin style noise texture for the perturbed sampling:

By offsetting where we sample the cloud texture from using the noise texture as the perturbed coordinates we will get the desired effect of clouds that change their formation as they scroll over the sky plane:

We will start the code section by looking at how we modified the SkyPlaneClass:


Skyplaneclass.h

The SkyPlane class has been modified to support a perturb texture and a scale value.

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplaneclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SKYPLANECLASS_H_
#define _SKYPLANECLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "textureclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: SkyPlaneClass
////////////////////////////////////////////////////////////////////////////////
class SkyPlaneClass
{
private:
    struct VertexType
    {
        float x, y, z;
        float tu, tv;
    };

public:
    SkyPlaneClass();
    SkyPlaneClass(const SkyPlaneClass&);
    ~SkyPlaneClass();

    bool Initialize(OpenGLClass*);
    void Shutdown(OpenGLClass*);
    void Render(OpenGLClass*);
    void Frame(float);

    void SetCloudTexture(OpenGLClass*, unsigned int);
    void SetPerturbTexture(OpenGLClass*, unsigned int);
    void SetFadeTexture(OpenGLClass*, unsigned int);

    float GetTranslation(int);
    float GetScale();

private:
    bool InitializeSkyPlane(int, float, float, float, int);
    void ShutdownSkyPlane();

    bool InitializeBuffers(OpenGLClass*, int);
    void ShutdownBuffers(OpenGLClass*);
    void RenderBuffers(OpenGLClass*);

    bool LoadTextures(OpenGLClass*, char*, char*, char*);
    void ReleaseTextures();

private:
    VertexType* m_skyPlane;
    TextureClass *m_CloudTexture, *m_PerturbTexture, *m_FadeTexture;
    int m_vertexCount, m_indexCount;
    unsigned int m_vertexArrayId, m_vertexBufferId, m_indexBufferId;
    float m_scale;
    float m_translationSpeed[2];
    float m_textureTranslation[2];
};

#endif

Skyplaneclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplaneclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "skyplaneclass.h"


SkyPlaneClass::SkyPlaneClass()
{
    m_skyPlane = 0;
    m_CloudTexture = 0;
    m_PerturbTexture = 0;
    m_FadeTexture = 0;
}


SkyPlaneClass::SkyPlaneClass(const SkyPlaneClass& other)
{
}


SkyPlaneClass::~SkyPlaneClass()
{
}


bool SkyPlaneClass::Initialize(OpenGLClass* OpenGL)
{
    char filename1[256], filename2[256], filename3[256];
    float skyPlaneWidth, skyPlaneTop, skyPlaneBottom;
    int skyPlaneResolution, textureRepeat;
    bool result;

We set the file name of the second texture to the new perturb texture.

    // Set the names of the textures.
    strcpy(filename1, "../Engine/data/textures/cloud001.tga");
    strcpy(filename2, "../Engine/data/textures/perturb001.tga");
    strcpy(filename3, "../Engine/data/textures/fade002.tga");

    // Set the sky plane parameters.
    skyPlaneResolution = 10;
    skyPlaneWidth = 10.0f;
    skyPlaneTop = 0.5f;
    skyPlaneBottom = 0.0f;
    textureRepeat = 1;

The scale value for perturbing the texture in the pixel shader is set here.

    // Set the perturb scale.
    m_scale = 0.3f;

    // Setup the cloud translation speed increments.
    m_translationSpeed[0] = 0.00003f;  // Texture X translation speed.
    m_translationSpeed[1] = 0.0f;      // Texture Z translation speed.

    // Initialize the texture translation values.
    m_textureTranslation[0] = 0.0f;
    m_textureTranslation[1] = 0.0f;

    // Create the sky plane.
    result = InitializeSkyPlane(skyPlaneResolution, skyPlaneWidth, skyPlaneTop, skyPlaneBottom, textureRepeat);
    if(!result)
    {
        return false;
    }

    // Create the vertex and index buffer for the sky plane.
    result = InitializeBuffers(OpenGL, skyPlaneResolution);
    if(!result)
    {
        return false;
    }

    // Release the sky plane array now that the buffers have been loaded.
    ShutdownSkyPlane();

    // Load the sky plane textures.
    result = LoadTextures(OpenGL, filename1, filename2, filename3);
    if(!result)
    {
        return false;
    }

    return true;
}


void SkyPlaneClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the sky plane textures.
    ReleaseTextures();

    // Release the vertex and index buffer that were used for rendering the sky plane.
    ShutdownBuffers(OpenGL);

    // Release the sky plane array.
    ShutdownSkyPlane();

    return;
}


void SkyPlaneClass::Render(OpenGLClass* OpenGL)
{
    // Render the sky plane.
    RenderBuffers(OpenGL);

    return;
}


void SkyPlaneClass::Frame(float frameTime)
{
    // Increment the translation values to simulate the moving clouds.
    m_textureTranslation[0] += m_translationSpeed[0] * frameTime * 100.0f;
    m_textureTranslation[1] += m_translationSpeed[1] * frameTime * 100.0f;

    // Keep the values in the zero to one range.
    if(m_textureTranslation[0] > 1.0f)  {  m_textureTranslation[0] -= 1.0f;  }
    if(m_textureTranslation[1] > 1.0f)  {  m_textureTranslation[1] -= 1.0f;  }

    return;
}


void SkyPlaneClass::SetCloudTexture(OpenGLClass* OpenGL, unsigned int textureUnit)
{
    m_CloudTexture->SetTexture(OpenGL, textureUnit);
    return;
}

We add a new function for setting the perturb texture in the pixel shader.

void SkyPlaneClass::SetPerturbTexture(OpenGLClass* OpenGL, unsigned int textureUnit)
{
    m_PerturbTexture->SetTexture(OpenGL, textureUnit);
    return;
}


void SkyPlaneClass::SetFadeTexture(OpenGLClass* OpenGL, unsigned int textureUnit)
{
    m_FadeTexture->SetTexture(OpenGL, textureUnit);
    return;
}


float SkyPlaneClass::GetTranslation(int index)
{
    return m_textureTranslation[index];
}

We add a new function to return the scale value for the sky plane noise perturbing.

float SkyPlaneClass::GetScale()
{
    return m_scale;
}


bool SkyPlaneClass::InitializeSkyPlane(int skyPlaneResolution, float skyPlaneWidth, float skyPlaneTop, float skyPlaneBottom, int textureRepeat)
{
    float quadSize, radius, constant, textureDelta;
    int i, j, index;
    float positionX, positionY, positionZ, tu, tv;


    // Create the array to hold the sky plane coordinates.
    m_skyPlane = new VertexType[(skyPlaneResolution + 1) * (skyPlaneResolution + 1)];

    // Determine the size of each quad on the sky plane.
    quadSize = skyPlaneWidth / (float)skyPlaneResolution;

    // Calculate the radius of the sky plane based on the width.
    radius = skyPlaneWidth / 2.0f;

    // Calculate the height constant to increment by.
    constant = (skyPlaneTop - skyPlaneBottom) / (radius * radius);

    // Calculate the texture coordinate increment value.
    textureDelta = (float)textureRepeat / (float)skyPlaneResolution;

    // Loop through the sky plane and build the coordinates based on the increment values given.
    for(j=0; j<=skyPlaneResolution; j++)
    {
        for(i=0; i<=skyPlaneResolution; i++)
        {
            // Calculate the vertex coordinates.
            positionX = (-0.5f * skyPlaneWidth) + ((float)i * quadSize);
            positionZ = (-0.5f * skyPlaneWidth) + ((float)j * quadSize);
            positionY = skyPlaneTop - (constant * ((positionX * positionX) + (positionZ * positionZ)));

            // Calculate the texture coordinates.
            tu = (float)i * textureDelta;
            tv = (float)j * textureDelta;

            // Calculate the index into the sky plane array to add this coordinate.
            index = j * (skyPlaneResolution + 1) + i;

            // Add the coordinates to the sky plane array.
            m_skyPlane[index].x = positionX;
            m_skyPlane[index].y = positionY;
            m_skyPlane[index].z = positionZ;
            m_skyPlane[index].tu = tu;
            m_skyPlane[index].tv = tv;
        }
    }

    return true;
}


void SkyPlaneClass::ShutdownSkyPlane()
{
    // Release the sky plane array.
    if(m_skyPlane)
    {
        delete [] m_skyPlane;
        m_skyPlane = 0;
    }

    return;
}


bool SkyPlaneClass::InitializeBuffers(OpenGLClass* OpenGL, int skyPlaneResolution)
{
    VertexType* vertices;
    unsigned int* indices;
    int i, j, index, index1, index2, index3, index4;
    int width, height;


    // Set the dimensions.
    width = skyPlaneResolution + 1;
    height = skyPlaneResolution + 1;

    // Calculate the number of vertices in the sky plane mesh.
    m_vertexCount = (width-1) * (height-1) * 6;

    // Set the index count to the same as the vertex count.
    m_indexCount = m_vertexCount;

    // Create the vertex array.
    vertices = new VertexType[m_vertexCount];

    // Create the index array.
    indices = new unsigned int[m_indexCount];

    // Initialize the index into the vertex array.
    index = 0;

    // Load the vertex and index array with the sky plane array data.
    for(j=0; j<(height-1); j++)
    {
        for(i=0; i<(width-1); i++)
        {
            index1 = (width * j) + i;
            index2 = (width * j) + (i+1);
            index3 = (width * (j+1)) + i;
            index4 = (width * (j+1)) + (i+1);

            // Triangle 1 - Upper Left
            vertices[index].x = m_skyPlane[index1].x;
            vertices[index].y = m_skyPlane[index1].y;
            vertices[index].z = m_skyPlane[index1].z;
            vertices[index].tu = m_skyPlane[index1].tu;
            vertices[index].tv = m_skyPlane[index1].tv;
            indices[index] = index;
            index++;

            // Triangle 1 - Upper Right
            vertices[index].x = m_skyPlane[index2].x;
            vertices[index].y = m_skyPlane[index2].y;
            vertices[index].z = m_skyPlane[index2].z;
            vertices[index].tu = m_skyPlane[index2].tu;
            vertices[index].tv = m_skyPlane[index2].tv;
            indices[index] = index;
            index++;

            // Triangle 1 - Bottom Left
            vertices[index].x = m_skyPlane[index3].x;
            vertices[index].y = m_skyPlane[index3].y;
            vertices[index].z = m_skyPlane[index3].z;
            vertices[index].tu = m_skyPlane[index3].tu;
            vertices[index].tv = m_skyPlane[index3].tv;
            indices[index] = index;
            index++;

            // Triangle 2 - Bottom Left
            vertices[index].x = m_skyPlane[index3].x;
            vertices[index].y = m_skyPlane[index3].y;
            vertices[index].z = m_skyPlane[index3].z;
            vertices[index].tu = m_skyPlane[index3].tu;
            vertices[index].tv = m_skyPlane[index3].tv;
            indices[index] = index;
            index++;

            // Triangle 2 - Upper Right
            vertices[index].x = m_skyPlane[index2].x;
            vertices[index].y = m_skyPlane[index2].y;
            vertices[index].z = m_skyPlane[index2].z;
            vertices[index].tu = m_skyPlane[index2].tu;
            vertices[index].tv = m_skyPlane[index2].tv;
            indices[index] = index;
            index++;

            // Triangle 2 - Bottom Right
            vertices[index].x = m_skyPlane[index4].x;
            vertices[index].y = m_skyPlane[index4].y;
            vertices[index].z = m_skyPlane[index4].z;
            vertices[index].tu = m_skyPlane[index4].tu;
            vertices[index].tv = m_skyPlane[index4].tv;
            indices[index] = index;
            index++;
        }
    }

    // Allocate an OpenGL vertex array object.
    OpenGL->glGenVertexArrays(1, &m_vertexArrayId);

    // Bind the vertex array object to store all the buffers and vertex attributes we create here.
    OpenGL->glBindVertexArray(m_vertexArrayId);

    // Generate an ID for the vertex buffer.
    OpenGL->glGenBuffers(1, &m_vertexBufferId);

    // Bind the vertex buffer and load the vertex data into the vertex buffer.
    OpenGL->glBindBuffer(GL_ARRAY_BUFFER, m_vertexBufferId);
    OpenGL->glBufferData(GL_ARRAY_BUFFER, m_vertexCount * sizeof(VertexType), vertices, GL_STATIC_DRAW);

    // Enable the vertex array attribute.
    OpenGL->glEnableVertexAttribArray(0);  // Vertex position.
    OpenGL->glEnableVertexAttribArray(1);  // Texture coordinates.

    // Specify the location and format of the position portion of the vertex buffer.
    OpenGL->glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(VertexType), 0);

    // Specify the location and format of the texture coordinate portion of the vertex buffer.
    OpenGL->glVertexAttribPointer(1, 2, GL_FLOAT, false, sizeof(VertexType), (unsigned char*)NULL + (3 * sizeof(float)));

    // Generate an ID for the index buffer.
    OpenGL->glGenBuffers(1, &m_indexBufferId);

    // Bind the index buffer and load the index data into it.
    OpenGL->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexBufferId);
    OpenGL->glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexCount* sizeof(unsigned int), indices, GL_STATIC_DRAW);

    // Release the arrays now that the vertex and index buffers have been created and loaded.
    delete [] vertices;
    vertices = 0;

    delete [] indices;
    indices = 0;

    return true;
}


void SkyPlaneClass::ShutdownBuffers(OpenGLClass* OpenGL)
{
    // Release the vertex array object.
    OpenGL->glBindVertexArray(0);
    OpenGL->glDeleteVertexArrays(1, &m_vertexArrayId);

    // Release the vertex buffer.
    OpenGL->glBindBuffer(GL_ARRAY_BUFFER, 0);
    OpenGL->glDeleteBuffers(1, &m_vertexBufferId);

    // Release the index buffer.
    OpenGL->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    OpenGL->glDeleteBuffers(1, &m_indexBufferId);

    return;
}


void SkyPlaneClass::RenderBuffers(OpenGLClass* OpenGL)
{
    // Bind the vertex array object that stored all the information about the vertex and index buffers.
    OpenGL->glBindVertexArray(m_vertexArrayId);

    // Render the vertex buffer using the index buffer.
    glDrawElements(GL_TRIANGLES, m_indexCount, GL_UNSIGNED_INT, 0);

    return;
}

The LoadTextures function now supports loading the noise based perturb texture.

bool SkyPlaneClass::LoadTextures(OpenGLClass* OpenGL, char* filename1, char* filename2, char* filename3)
{
    bool result;


    // Create and initialize the first cloud texture object.
    m_CloudTexture = new TextureClass;

    result = m_CloudTexture->Initialize(OpenGL, filename1, true);
    if(!result)
    {
        return false;
    }

    // Create and initialize the perturb texture object.
    m_PerturbTexture = new TextureClass;

    result = m_PerturbTexture->Initialize(OpenGL, filename2, true);
    if(!result)
    {
        return false;
    }

    // Create and initialize the fade texture object.
    m_FadeTexture = new TextureClass;

    result = m_FadeTexture->Initialize(OpenGL, filename3, true);
    if(!result)
    {
        return false;
    }

    return true;
}

The new perturb texture is released the in ReleaseTextures function.

void SkyPlaneClass::ReleaseTextures()
{
    // Release the fade texture.
    if(m_FadeTexture)
    {
        m_FadeTexture->Shutdown();
        delete m_FadeTexture;
        m_FadeTexture = 0;
    }

    // Release the perturb texture.
    if(m_PerturbTexture)
    {
        m_PerturbTexture->Shutdown();
        delete m_PerturbTexture;
        m_PerturbTexture = 0;
    }

    // Release the cloud texture.
    if(m_CloudTexture)
    {
        m_CloudTexture->Shutdown();
        delete m_CloudTexture;
        m_CloudTexture = 0;
    }

    return;
}

Skyplane.vs

The sky plane vertex shader remains the same as the previous tutorial.

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplane.vs
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec2 inputTexCoord;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec2 texCoord;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;


////////////////////////////////////////////////////////////////////////////////
// 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;
}

Skyplane.ps

The pixel shader is where we do all the work in this tutorial to create the perturbed cloud effect. The shader now takes a noise texture and scale as new inputs.

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplane.ps
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec2 texCoord;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;


///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform sampler2D cloudTexture;
uniform sampler2D perturbTexture;
uniform sampler2D fadeTexture;
uniform float translationX;
uniform float translationZ;
uniform float scale;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
    vec2 sampleLocation;
    vec4 perturbValue;
    vec4 cloudColor;
    vec4 fadeValue;


    // Translate the position where we sample the pixel from using the texture translation values.
    sampleLocation.x = texCoord.x + translationX;
    sampleLocation.y = texCoord.y + translationZ;

In the shader itself we start by sampling the noise texture to get the base perturb value that we will use to offset the sampling of the cloud texture. We then multiply it by the perturb scale and add the texture coordinates. This gives us the perturbed coordinate that we then sample the cloud texture from. Note that both are translated by the translation values to create the scrolling clouds effect.

    // Sample the texture value from the perturb texture using the translated texture coordinates.
    perturbValue = texture(perturbTexture, texCoord);

    // Multiply the perturb value by the perturb scale.
    perturbValue = perturbValue * scale;

    // Add the texture coordinates as well as the translation value to get the perturbed texture coordinate sampling location.
    perturbValue.xy = perturbValue.xy + sampleLocation.xy;

    // Sample the pixel color from the first cloud texture using the sampler at this texture coordinate location.
    cloudColor = texture(cloudTexture, perturbValue.xy);

    // Sample the fade texture at the regular texture coordinates.
    fadeValue = texture(fadeTexture, texCoord);

    // Combine the cloud with the fade value.
    outputColor = cloudColor * fadeValue;
}

Skyplaneshaderclass.h

The SkyPlaneShaderClass header has been modified a bit to handle the slightly different inputs into the pixel shader.

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplaneshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SKYPLANESHADERCLASS_H_
#define _SKYPLANESHADERCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <iostream>
using namespace std;


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: SkyPlaneShaderClass
////////////////////////////////////////////////////////////////////////////////
class SkyPlaneShaderClass
{
public:
    SkyPlaneShaderClass();
    SkyPlaneShaderClass(const SkyPlaneShaderClass&);
    ~SkyPlaneShaderClass();

    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

Skyplaneshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: skyplaneshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "skyplaneshaderclass.h"


SkyPlaneShaderClass::SkyPlaneShaderClass()
{
    m_OpenGLPtr = 0;
}


SkyPlaneShaderClass::SkyPlaneShaderClass(const SkyPlaneShaderClass& other)
{
}


SkyPlaneShaderClass::~SkyPlaneShaderClass()
{
}


bool SkyPlaneShaderClass::Initialize(OpenGLClass* OpenGL)
{
    char vsFilename[128];
    char psFilename[128];
    bool result;


    // Store the pointer to the OpenGL object.
    m_OpenGLPtr = OpenGL;

    // Set the location and names of the shader files.
    strcpy(vsFilename, "../Engine/skyplane.vs");
    strcpy(psFilename, "../Engine/skyplane.ps");

    // Initialize the vertex and pixel shaders.
    result = InitializeShader(vsFilename, psFilename);
    if(!result)
    {
        return false;
    }

    return true;
}


void SkyPlaneShaderClass::Shutdown()
{
    // Shutdown the shader.
    ShutdownShader();

    // Release the pointer to the OpenGL object.
    m_OpenGLPtr = 0;

    return;
}


bool SkyPlaneShaderClass::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 SkyPlaneShaderClass::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* SkyPlaneShaderClass::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 SkyPlaneShaderClass::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 SkyPlaneShaderClass::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;
}

SetShaderParameters now takes the perturbing scale as a new input.

bool SkyPlaneShaderClass::SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix, float translationX, float translationZ, float scale)
{
    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 texture in the pixel shader to use the data from the first texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "cloudTexture");
    if(location == -1)
    {
        cout << "Cloud texture not set." << endl;
    }
    m_OpenGLPtr->glUniform1i(location, 0);

We set the perturb texture here.

    // Set the texture in the pixel shader to use the data from the second texture unit.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "perturbTexture");
    if(location == -1)
    {
        cout << "Perturb 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, "fadeTexture");
    if(location == -1)
    {
        cout << "Fade texture not set." << endl;
    }
    m_OpenGLPtr->glUniform1i(location, 2);

    // Set the first translation X in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "translationX");
    if(location == -1)
    {
        cout << "Translation X not set." << endl;
    }
    m_OpenGLPtr->glUniform1f(location, translationX);

    // Set the first translation Z in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "translationZ");
    if(location == -1)
    {
        cout << "Translation Z not set." << endl;
    }
    m_OpenGLPtr->glUniform1f(location, translationZ);

We set the scale value here.

    // Set the scale in the pixel shader.
    location = m_OpenGLPtr->glGetUniformLocation(m_shaderProgram, "scale");
    if(location == -1)
    {
        cout << "Scale not set." << endl;
    }
    m_OpenGLPtr->glUniform1f(location, scale);

    return true;
}

Zoneclass.h

The ZoneClass header has not changes since the previous tutorial.

///////////////////////////////////////////////////////////////////////////////
// Filename: zoneclass.h
///////////////////////////////////////////////////////////////////////////////
#ifndef _ZONECLASS_H_
#define _ZONECLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "openglclass.h"
#include "inputclass.h"
#include "userinterfaceclass.h"
#include "cameraclass.h"
#include "positionclass.h"
#include "terrainclass.h"
#include "skydomeclass.h"
#include "skyplaneclass.h"


////////////////////////////////////////////////////////////////////////////////
// Class name: ZoneClass
////////////////////////////////////////////////////////////////////////////////
class ZoneClass
{
public:
    ZoneClass();
    ZoneClass(const ZoneClass&);
    ~ZoneClass();

    bool Initialize(OpenGLClass*);
    void Shutdown(OpenGLClass*);
    bool Frame(OpenGLClass*, ShaderManagerClass*, FontClass*, UserInterfaceClass*, InputClass*, float);

private:
    bool Render(OpenGLClass*, ShaderManagerClass*, FontClass*, UserInterfaceClass*);
    void HandleMovementInput(InputClass*, float);
    bool RenderSkyDome(OpenGLClass*, ShaderManagerClass*, float*, float*);
    bool RenderSkyPlane(OpenGLClass*, ShaderManagerClass*, float*, float*);

private:
    CameraClass* m_Camera;
    PositionClass* m_Position;
    TerrainClass* m_Terrain;
    LightClass* m_Light;
    FrustumClass* m_Frustum;
    SkyDomeClass* m_SkyDome;
    SkyPlaneClass* m_SkyPlane;
};

#endif

Zoneclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: zoneclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "zoneclass.h"


ZoneClass::ZoneClass()
{
    m_Camera = 0;
    m_Position = 0;
    m_Terrain = 0;
    m_Light = 0;
    m_Frustum = 0;
    m_SkyDome = 0;
    m_SkyPlane = 0;
}


ZoneClass::ZoneClass(const ZoneClass& other)
{
}


ZoneClass::~ZoneClass()
{
}


bool ZoneClass::Initialize(OpenGLClass* OpenGL)
{
    char configFilename[256];
    bool result;


    // Create and initialize the camera object.
    m_Camera = new CameraClass;

    m_Camera->SetPosition(0.0f, 0.0f, -10.0f);
    m_Camera->Render();
    m_Camera->RenderBaseViewMatrix();

    // Create and initialize the position object.
    m_Position = new PositionClass;

    m_Position->SetPosition(500.0f, 50.0f, -10.0f);
    m_Position->SetRotation(0.0f, 0.0f, 0.0f);

    // Create and initialize the terrain object.
    m_Terrain = new TerrainClass;

    strcpy(configFilename, "../Engine/data/setup.txt");

    result = m_Terrain->Initialize(OpenGL, configFilename);
    if(!result)
    {
        cout << "Error: Could not initialize the terrain object." << endl;
        return false;
    }

    // Create and initialize the light object.
    m_Light = new LightClass;

    m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
    m_Light->SetDirection(-0.5f, -1.0f, -0.5f);

    // Create the frustum object.
    m_Frustum = new FrustumClass;

    // Create and initialize the sky dome object.
    m_SkyDome = new SkyDomeClass;

    result = m_SkyDome->Initialize(OpenGL);
    if(!result)
    {
        cout << "Error: Could not initialize the sky dome object." << endl;
        return false;
    }

    // Create and initialize the sky plane object.
    m_SkyPlane = new SkyPlaneClass;

    result = m_SkyPlane->Initialize(OpenGL);
    if(!result)
    {
        cout << "Error: Could not initialize the sky plane object." << endl;
        return false;
    }

    return true;
}


void ZoneClass::Shutdown(OpenGLClass* OpenGL)
{
    // Release the sky plane object.
    if(m_SkyPlane)
    {
        m_SkyPlane->Shutdown(OpenGL);
        delete m_SkyPlane;
        m_SkyPlane = 0;
    }

    // Release the sky dome object.
    if(m_SkyDome)
    {
        m_SkyDome->Shutdown(OpenGL);
        delete m_SkyDome;
        m_SkyDome = 0;
    }

    // Release the frustum object.
    if(m_Frustum)
    {
        delete m_Frustum;
        m_Frustum = 0;
    }

    // Release the light object.
    if(m_Light)
    {
        delete m_Light;
        m_Light = 0;
    }

    // Release the terrain object.
    if(m_Terrain)
    {
        m_Terrain->Shutdown(OpenGL);
        delete m_Terrain;
        m_Terrain = 0;
    }

    // Release the position object.
    if(m_Position)
    {
        delete m_Position;
        m_Position = 0;
    }

    // Release the camera object.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    return;
}


bool ZoneClass::Frame(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, FontClass* Font, UserInterfaceClass* UserInterface, InputClass* Input, float frameTime)
{
    float posX, posY, posZ, rotX, rotY, rotZ, height;
    bool result, foundHeight, heightLocked;


    // Set whether we lock to the terrain height or not.
    heightLocked = false;

    // Do the sky plane frame processing.
    m_SkyPlane->Frame(frameTime);

    // Do the frame input processing for the movement.
    HandleMovementInput(Input, frameTime);

    // Get the view point position/rotation.
    m_Position->GetPosition(posX, posY, posZ);
    m_Position->GetRotation(rotX, rotY, rotZ);

    // Get the height of the triangle that is directly underneath the given camera position.
    foundHeight =  m_Terrain->GetHeightAtPosition(posX, posZ, height);
    if(foundHeight)
    {
        // If there was a triangle under the camera then position the camera just above it by two units.
        if(heightLocked)
        {
            posY = height + 2.0f;
        }
    }

    // Set the position of the camera and update the camera view matrix for rendering.
    m_Camera->SetPosition(posX, posY, posZ);
    m_Camera->SetRotation(rotX, rotY, rotZ);
    m_Camera->Render();

    // Update the UI with the position.
    UserInterface->UpdatePositonStrings(Font, posX, posY, posZ, rotX, rotY, rotZ);

    // Render the zone.
    result = Render(OpenGL, ShaderManager, Font, UserInterface);
    if(!result)
    {
        return false;
    }

    return true;
}


bool ZoneClass::Render(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, FontClass* Font, UserInterfaceClass* UserInterface)
{
    float worldMatrix[16], viewMatrix[16], projectionMatrix[16], baseViewMatrix[16], orthoMatrix[16];
    int nodesDrawn, nodesCulled;
    bool result;


    // Get the world, view, and ortho matrices from the opengl and camera objects.
    OpenGL->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    OpenGL->GetProjectionMatrix(projectionMatrix);
    m_Camera->GetBaseViewMatrix(baseViewMatrix);
    OpenGL->GetOrthoMatrix(orthoMatrix);

    // Construct the frustum.
    m_Frustum->ConstructFrustum(OpenGL, viewMatrix, projectionMatrix);

    // Clear the scene.
    OpenGL->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // Render the sky dome.
    result = RenderSkyDome(OpenGL, ShaderManager, viewMatrix, projectionMatrix);
    if(!result)
    {
        return false;
    }

    // Render the sky plane.
    result = RenderSkyPlane(OpenGL, ShaderManager, viewMatrix, projectionMatrix);
    if(!result)
    {
        return false;
    }

    // Render the terrain using the terrain shader.
    result = m_Terrain->Render(OpenGL, ShaderManager, m_Light, m_Frustum, worldMatrix, viewMatrix, projectionMatrix);
    if(!result)
    {
        return false;
    }

    // Update with nodes drawn/culled.
    m_Terrain->GetNodesDrawn(nodesDrawn, nodesCulled);

    result = UserInterface->UpdateNodeStrings(Font, nodesDrawn, nodesCulled);
    if(!result)
    {
        return false;
    }

    // Render the user interface.
    result = UserInterface->Render(OpenGL, ShaderManager, Font, worldMatrix, baseViewMatrix, orthoMatrix);
    if(!result)
    {
        return false;
    }

    // Present the rendered scene to the screen.
    OpenGL->EndScene();

    return true;
}


void ZoneClass::HandleMovementInput(InputClass* Input, float frameTime)
{
    bool keyDown;


    // Set the frame time for calculating the updated position.
    m_Position->SetFrameTime(frameTime);

    // Check if the input keys have been pressed.  If so, then update the position accordingly.
    keyDown = Input->IsLeftPressed();
    m_Position->TurnLeft(keyDown);

    keyDown = Input->IsRightPressed();
    m_Position->TurnRight(keyDown);

    keyDown = Input->IsUpPressed();
    m_Position->MoveForward(keyDown);

    keyDown = Input->IsDownPressed();
    m_Position->MoveBackward(keyDown);

    keyDown = Input->IsAPressed();
    m_Position->MoveUpward(keyDown);

    keyDown = Input->IsZPressed();
    m_Position->MoveDownward(keyDown);

    keyDown = Input->IsPgUpPressed();
    m_Position->LookUpward(keyDown);

    keyDown = Input->IsPgDownPressed();
    m_Position->LookDownward(keyDown);

    keyDown = Input->IsQPressed();
    m_Position->StrafeLeft(keyDown);

    keyDown = Input->IsEPressed();
    m_Position->StrafeRight(keyDown);

    return;
}


bool ZoneClass::RenderSkyDome(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, float* viewMatrix, float* projectionMatrix)
{
    float translateMatrix[16];
    float apexColor[4], centerColor[4];
    float cameraPosition[3];
    bool result;


    // Get the sky dome colors.
    m_SkyDome->GetApexColor(apexColor);
    m_SkyDome->GetCenterColor(centerColor);

    // Get the position of the camera.
    m_Camera->GetPosition(cameraPosition);

    // Translate the sky dome to be centered around the camera position.
    OpenGL->MatrixTranslation(translateMatrix, cameraPosition[0], cameraPosition[1], cameraPosition[2]);

    // Turn off back face culling and turn off the Z buffer.
    OpenGL->TurnOffCulling();
    OpenGL->TurnZBufferOff();

    // Render the sky dome using the sky dome shader.
    result = ShaderManager->RenderSkyDomeShader(translateMatrix, viewMatrix, projectionMatrix, apexColor, centerColor);
    if(!result)
    {
        return false;
    }

    m_SkyDome->Render(OpenGL);

    // Turn back face culling back on and turn the Z buffer back on.
    OpenGL->TurnOnCulling();
    OpenGL->TurnZBufferOn();

    return true;
}

The RenderSkyPlane function now uses the perturb scale value and perturb noise texture.

bool ZoneClass::RenderSkyPlane(OpenGLClass* OpenGL, ShaderManagerClass* ShaderManager, float* viewMatrix, float* projectionMatrix)
{
    float translateMatrix[16];
    float cameraPosition[3];
    float translationX, translationZ, scale;
    bool result;


    // Get the position of the camera.
    m_Camera->GetPosition(cameraPosition);

    // Translate the sky dome to be centered around the camera position.
    OpenGL->MatrixTranslation(translateMatrix, cameraPosition[0], cameraPosition[1], cameraPosition[2]);

    translationX = m_SkyPlane->GetTranslation(0);
    translationZ = m_SkyPlane->GetTranslation(1);
    scale = m_SkyPlane->GetScale();

    // Turn Z buffer off and enable additive blending so the clouds blend with the sky dome color.
    OpenGL->TurnZBufferOff();
    OpenGL->EnableUIAlphaBlending();

    // Render the sky plane using the sky plane shader.
    result = ShaderManager->RenderSkyPlaneShader(translateMatrix, viewMatrix, projectionMatrix, translationX, translationZ, scale);
    if(!result)
    {
        return false;
    }

    m_SkyPlane->SetCloudTexture(OpenGL, 0);
    m_SkyPlane->SetPerturbTexture(OpenGL, 1);
    m_SkyPlane->SetFadeTexture(OpenGL, 2);

    m_SkyPlane->Render(OpenGL);

    // Turn Z buffer back on and disable blending.
    OpenGL->TurnZBufferOn();
    OpenGL->DisableAlphaBlending();

    return true;
}

Summary

We now have clouds that transform as they move across the sky plane.


To Do Exercises

1. Recompile the code and run the program. Use the PgUp keys to look up into the sky to see the effect. Press escape to quit when done.

2. Speed up the translation so the effect is very clear.

3. Modify the scale value in the SkyPlane class to see the effect it has on the pixel shader.

4. Modify the noise texture to see its effect on the sampling.

5. Add a second layer of perturbed clouds, make sure they have their own translation and perturb scale also.

6. Use a higher resolution texture for the clouds and the noise to see the improved look you get.


Source Code

Source Code and Data Files: gl4terlinux13.tar.gz

Back to Tutorial Index